support metamodule, remove built-in overlayfs mount
Co-authored-by: weishu <twsxtd@gmail.com> Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com> Co-authored-by: Ylarod <me@ylarod.cn>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,15 +1,13 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use clap::Parser;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::{
|
||||
apk_sign, assets, debug, defs, defs::KSUD_VERBOSE_LOG_FILE, init_event, ksucalls, module, utils,
|
||||
};
|
||||
use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils};
|
||||
|
||||
/// KernelSU userspace cli
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -17,9 +15,6 @@ use crate::{
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
#[arg(short, long, default_value_t = cfg!(debug_assertions))]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
@@ -209,8 +204,6 @@ enum Debug {
|
||||
/// Get kernel version
|
||||
Version,
|
||||
|
||||
Mount,
|
||||
|
||||
/// For testing
|
||||
Test,
|
||||
|
||||
@@ -277,14 +270,14 @@ enum Module {
|
||||
zip: String,
|
||||
},
|
||||
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// Undo module uninstall mark <id>
|
||||
UndoUninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Restore module <id>
|
||||
Restore {
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
@@ -498,10 +491,6 @@ pub fn run() -> Result<()> {
|
||||
|
||||
let cli = Args::parse();
|
||||
|
||||
if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() {
|
||||
log::set_max_level(LevelFilter::Info);
|
||||
}
|
||||
|
||||
log::info!("command: {:?}", cli.command);
|
||||
|
||||
let result = match cli.command {
|
||||
@@ -515,8 +504,8 @@ pub fn run() -> Result<()> {
|
||||
}
|
||||
match command {
|
||||
Module::Install { zip } => module::install_module(&zip),
|
||||
Module::UndoUninstall { id } => module::undo_uninstall_module(&id),
|
||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Restore { id } => module::restore_uninstall_module(&id),
|
||||
Module::Enable { id } => module::enable_module(&id),
|
||||
Module::Disable { id } => module::disable_module(&id),
|
||||
Module::Action { id } => module::run_action(&id),
|
||||
@@ -563,7 +552,6 @@ pub fn run() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Debug::Su { global_mnt } => crate::su::grant_root(global_mnt),
|
||||
Debug::Mount => init_event::mount_modules_systemlessly(),
|
||||
Debug::Test => assets::ensure_binaries(false),
|
||||
Debug::Mark { command } => match command {
|
||||
MarkCommand::Get { pid } => debug::mark_get(pid),
|
||||
@@ -672,11 +660,7 @@ pub fn run() -> Result<()> {
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
for c in e.chain() {
|
||||
log::error!("{c:#?}");
|
||||
}
|
||||
|
||||
log::error!("{:#?}", e.backtrace());
|
||||
log::error!("Error: {e:?}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/");
|
||||
pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/");
|
||||
|
||||
pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc");
|
||||
pub const KSU_MOUNT_SOURCE: &str = "KSU";
|
||||
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud");
|
||||
pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
|
||||
|
||||
@@ -18,18 +17,19 @@ pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
|
||||
pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud");
|
||||
|
||||
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
|
||||
|
||||
// warning: this directory should not change, or you need to change the code in module_installer.sh!!!
|
||||
pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
|
||||
pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose");
|
||||
pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/");
|
||||
|
||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const UPDATE_FILE_NAME: &str = "update";
|
||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
// Metamodule support
|
||||
pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh";
|
||||
pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh";
|
||||
pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh";
|
||||
|
||||
pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE"));
|
||||
pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME"));
|
||||
@@ -37,6 +37,3 @@ pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_N
|
||||
pub const KSU_BACKUP_DIR: &str = WORKING_DIR;
|
||||
pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";
|
||||
pub const BACKUP_FILENAME: &str = "stock_image.sha1";
|
||||
|
||||
pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs");
|
||||
pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount");
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use crate::kpm;
|
||||
use crate::utils::is_safe_mode;
|
||||
use crate::{
|
||||
assets, defs,
|
||||
defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH},
|
||||
ksucalls,
|
||||
module::{handle_updated_modules, prune_modules},
|
||||
restorecon, uid_scanner, utils,
|
||||
utils::find_tmp_path,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use log::{info, warn};
|
||||
use rustix::fs::{MountFlags, mount};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
crate::magic_mount::magic_mount(&find_tmp_path())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use crate::kpm;
|
||||
use crate::module::{handle_updated_modules, prune_modules};
|
||||
use crate::utils::is_safe_mode;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule, restorecon,
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
pub fn on_post_data_fs() -> Result<()> {
|
||||
ksucalls::report_post_fs_data();
|
||||
@@ -39,9 +25,11 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let safe_mode = utils::is_safe_mode();
|
||||
let safe_mode = crate::utils::is_safe_mode();
|
||||
|
||||
if safe_mode {
|
||||
// we should still ensure module directory exists in safe mode
|
||||
// because we may need to operate the module dir in safe mode
|
||||
warn!("safe mode, skip common post-fs-data.d scripts");
|
||||
} else {
|
||||
// Then exec common post-fs-data scripts
|
||||
@@ -50,18 +38,18 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let module_dir = defs::MODULE_DIR;
|
||||
|
||||
assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?;
|
||||
|
||||
// Start UID scanner daemon with highest priority
|
||||
uid_scanner::start_uid_scanner_daemon()?;
|
||||
crate::uid_scanner::uid_scanner::start_uid_scanner_daemon()?;
|
||||
|
||||
if is_safe_mode() {
|
||||
warn!("safe mode, skip load feature config");
|
||||
} else if let Err(e) = crate::umount_manager::load_and_apply_config() {
|
||||
warn!("Failed to load umount config: {e}");
|
||||
}
|
||||
// tell kernel that we've mount the module, so that it can do some optimization
|
||||
ksucalls::report_module_mounted();
|
||||
|
||||
// if we are in safe mode, we should disable all modules
|
||||
if safe_mode {
|
||||
@@ -72,14 +60,14 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = prune_modules() {
|
||||
warn!("prune modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = handle_updated_modules() {
|
||||
warn!("handle updated modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = prune_modules() {
|
||||
warn!("prune modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = restorecon::restorecon() {
|
||||
warn!("restorecon failed: {e}");
|
||||
}
|
||||
@@ -110,23 +98,9 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
warn!("KPM: Failed to load KPM modules: {e}");
|
||||
}
|
||||
|
||||
let tmpfs_path = find_tmp_path();
|
||||
// for compatibility
|
||||
let no_mount = Path::new(NO_TMPFS_PATH).exists() || Path::new(NO_MOUNT_PATH).exists();
|
||||
|
||||
// mount temp dir
|
||||
if !no_mount {
|
||||
if let Err(e) = mount(
|
||||
KSU_MOUNT_SOURCE,
|
||||
&tmpfs_path,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
) {
|
||||
warn!("do temp dir mount failed: {e}");
|
||||
}
|
||||
} else {
|
||||
info!("no tmpfs requested");
|
||||
// execute metamodule post-fs-data script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec metamodule post-fs-data script failed: {e}");
|
||||
}
|
||||
|
||||
// exec modules post-fs-data scripts
|
||||
@@ -140,18 +114,15 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
warn!("load system.prop failed: {e}");
|
||||
}
|
||||
|
||||
// mount module systemlessly by magic mount
|
||||
#[cfg(target_os = "android")]
|
||||
if !no_mount {
|
||||
if let Err(e) = crate::magic_mount::magic_mount(&tmpfs_path) {
|
||||
warn!("do systemless mount failed: {e}");
|
||||
}
|
||||
} else {
|
||||
info!("no mount requested");
|
||||
// execute metamodule mount script
|
||||
if let Err(e) = metamodule::exec_mount_script(module_dir) {
|
||||
warn!("execute metamodule mount failed: {e}");
|
||||
}
|
||||
|
||||
run_stage("post-mount", true);
|
||||
|
||||
std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -171,6 +142,13 @@ fn run_stage(stage: &str, block: bool) {
|
||||
if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) {
|
||||
warn!("Failed to exec common {stage} scripts: {e}");
|
||||
}
|
||||
|
||||
// execute metamodule stage script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec metamodule {stage} script: {e}");
|
||||
}
|
||||
|
||||
// execute regular modules stage scripts
|
||||
if let Err(e) = crate::module::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec {stage} scripts: {e}");
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ setup_flashable() {
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in /proc/$$/fd/*; do
|
||||
for FD in `ls /proc/$$/fd`; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
@@ -313,14 +313,6 @@ mark_remove() {
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
@@ -338,16 +330,19 @@ is_legacy_script() {
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
PARTITION="$1"
|
||||
REQUIRE_SYMLINK="$2"
|
||||
if [ ! -e "$MODPATH/system/$PARTITION" ]; then
|
||||
# if /system/vendor is a symlink, we need to move it out of $MODPATH/system
|
||||
# if /system/vendor is a normal directory, no special handling is needed.
|
||||
if [ ! -e $MODPATH/system/$1 ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then
|
||||
ui_print "- Handle partition /$PARTITION"
|
||||
ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION"
|
||||
# we move the folder to / only if it is a native folder that is not a symlink
|
||||
if [ -d "/$1" ] && [ ! -L "/$1" ]; then
|
||||
ui_print "- Handle partition /$1"
|
||||
# we create a symlink if module want to access $MODPATH/system/$1
|
||||
# but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)
|
||||
mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -428,23 +423,23 @@ install_module() {
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
handle_partition vendor true
|
||||
handle_partition system_ext true
|
||||
handle_partition product true
|
||||
handle_partition odm false
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace "$MODPATH$TARGET"
|
||||
mark_replace $MODPATH$TARGET
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove "$MODPATH$TARGET"
|
||||
mark_remove $MODPATH$TARGET
|
||||
done
|
||||
|
||||
handle_partition vendor
|
||||
handle_partition system_ext
|
||||
handle_partition product
|
||||
handle_partition odm
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
fs::{self, DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link},
|
||||
os::unix::fs::{FileTypeExt, symlink},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use extattr::lgetxattr;
|
||||
use rustix::{
|
||||
fs::{
|
||||
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
|
||||
chmod, chown, mount, move_mount, remount, unmount,
|
||||
},
|
||||
mount::mount_change,
|
||||
path::Arg,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME},
|
||||
magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout},
|
||||
restorecon::{lgetfilecon, lsetfilecon},
|
||||
utils::ensure_dir_exists,
|
||||
};
|
||||
|
||||
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
enum NodeFileType {
|
||||
RegularFile,
|
||||
Directory,
|
||||
Symlink,
|
||||
Whiteout,
|
||||
}
|
||||
|
||||
impl NodeFileType {
|
||||
fn from_file_type(file_type: FileType) -> Option<Self> {
|
||||
if file_type.is_file() {
|
||||
Some(RegularFile)
|
||||
} else if file_type.is_dir() {
|
||||
Some(Directory)
|
||||
} else if file_type.is_symlink() {
|
||||
Some(Symlink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
file_type: NodeFileType,
|
||||
children: HashMap<String, Node>,
|
||||
// the module that owned this node
|
||||
module_path: Option<PathBuf>,
|
||||
replace: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn collect_module_files<P>(&mut self, module_dir: P) -> Result<bool>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let dir = module_dir.as_ref();
|
||||
let mut has_file = false;
|
||||
for entry in dir.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let node = match self.children.entry(name.clone()) {
|
||||
Entry::Occupied(o) => Some(o.into_mut()),
|
||||
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
|
||||
};
|
||||
|
||||
if let Some(node) = node {
|
||||
has_file |= if node.file_type == Directory {
|
||||
node.collect_module_files(dir.join(&node.name))? || node.replace
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(has_file)
|
||||
}
|
||||
|
||||
fn new_root<T>(name: T) -> Self
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T>(name: T, entry: &DirEntry) -> Option<Self>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let path = entry.path();
|
||||
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
|
||||
Some(Whiteout)
|
||||
} else {
|
||||
NodeFileType::from_file_type(metadata.file_type())
|
||||
};
|
||||
if let Some(file_type) = file_type {
|
||||
let mut replace = false;
|
||||
if file_type == Directory
|
||||
&& let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR)
|
||||
&& String::from_utf8_lossy(&v) == "y"
|
||||
{
|
||||
replace = true;
|
||||
}
|
||||
return Some(Node {
|
||||
name: name.to_string(),
|
||||
file_type,
|
||||
children: Default::default(),
|
||||
module_path: Some(path),
|
||||
replace,
|
||||
skip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_module_files() -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = Path::new(MODULE_DIR);
|
||||
let mut has_file = false;
|
||||
for entry in module_root.read_dir()?.flatten() {
|
||||
if !entry.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.path().join(DISABLE_FILE_NAME).exists()
|
||||
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_system = entry.path().join("system");
|
||||
if !mod_system.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("collecting {}", entry.path().display());
|
||||
|
||||
has_file |= system.collect_module_files(&mod_system)?;
|
||||
}
|
||||
|
||||
if has_file {
|
||||
for (partition, require_symlink) in [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
] {
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
|
||||
let name = partition.to_string();
|
||||
if let Some(node) = system.children.remove(&name) {
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<P>(src: P, dst: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P>(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P, WP>(path: P, work_dir_path: WP, current: Node, has_tmpfs: bool) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
WP: AsRef<Path>,
|
||||
{
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path).with_context(|| {
|
||||
format!("mount module file {module_path:?} -> {work_dir_path:?}")
|
||||
})?;
|
||||
// we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags
|
||||
if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") {
|
||||
log::warn!("make file {target_path:?} ro: {e:#?}");
|
||||
}
|
||||
} else {
|
||||
bail!("cannot mount root file {}!", path.display());
|
||||
}
|
||||
}
|
||||
Symlink => {
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"create module symlink {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(module_path, &work_dir_path).with_context(|| {
|
||||
format!("create module symlink {module_path:?} -> {work_dir_path:?}")
|
||||
})?;
|
||||
} else {
|
||||
bail!("cannot mount root symlink {}!", path.display());
|
||||
}
|
||||
}
|
||||
Directory => {
|
||||
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
|
||||
if !has_tmpfs && !create_tmpfs {
|
||||
for it in &mut current.children {
|
||||
let (name, node) = it;
|
||||
let real_path = path.join(name);
|
||||
let need = match node.file_type {
|
||||
Symlink => true,
|
||||
Whiteout => real_path.exists(),
|
||||
_ => {
|
||||
if let Ok(metadata) = real_path.symlink_metadata() {
|
||||
let file_type = NodeFileType::from_file_type(metadata.file_type())
|
||||
.unwrap_or(Whiteout);
|
||||
file_type != node.file_type || file_type == Symlink
|
||||
} else {
|
||||
// real path not exists
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if need {
|
||||
if current.module_path.is_none() {
|
||||
log::error!(
|
||||
"cannot create tmpfs on {}, ignore: {name}",
|
||||
path.display()
|
||||
);
|
||||
node.skip = true;
|
||||
continue;
|
||||
}
|
||||
create_tmpfs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_tmpfs = has_tmpfs || create_tmpfs;
|
||||
|
||||
if has_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs skeleton for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir_all(&work_dir_path)?;
|
||||
let (metadata, path) = if path.exists() {
|
||||
(path.metadata()?, &path)
|
||||
} else if let Some(module_path) = ¤t.module_path {
|
||||
(module_path.metadata()?, module_path)
|
||||
} else {
|
||||
bail!("cannot mount root dir {}!", path.display());
|
||||
};
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(&work_dir_path, &work_dir_path)
|
||||
.context("bind self")
|
||||
.with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?;
|
||||
}
|
||||
|
||||
if path.exists() && !current.replace {
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let result = if let Some(node) = current.children.remove(&name) {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
} else if has_tmpfs {
|
||||
mount_mirror(&path, &work_dir_path, &entry)
|
||||
.with_context(|| format!("mount mirror {}/{name}", path.display()))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.replace {
|
||||
if current.module_path.is_none() {
|
||||
bail!(
|
||||
"dir {} is declared as replaced but it is root!",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
log::debug!("dir {} is replaced", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, node) in current.children.into_iter() {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
{
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"moving tmpfs {} -> {}",
|
||||
work_dir_path.display(),
|
||||
path.display()
|
||||
);
|
||||
if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") {
|
||||
log::warn!("make dir {path:?} ro: {e:#?}");
|
||||
}
|
||||
move_mount(&work_dir_path, &path)
|
||||
.context("move self")
|
||||
.with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?;
|
||||
// make private to reduce peer group count
|
||||
if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) {
|
||||
log::warn!("make dir {path:?} private: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Whiteout => {
|
||||
log::debug!("file {} is removed", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magic_mount(tmp_path: &String) -> Result<()> {
|
||||
if let Some(root) = collect_module_files()? {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
let tmp_dir = Path::new(tmp_path).join("workdir");
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let result = do_magic_mount("/", &tmp_dir, root, false);
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
fs::remove_dir(tmp_dir).ok();
|
||||
result
|
||||
} else {
|
||||
log::info!("no modules to mount, skipping!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ mod init_event;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod kpm;
|
||||
mod ksucalls;
|
||||
#[cfg(target_os = "android")]
|
||||
mod magic_mount;
|
||||
mod metamodule;
|
||||
mod module;
|
||||
mod profile;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod su;
|
||||
#[cfg(target_os = "android")]
|
||||
mod uid_scanner;
|
||||
mod umount_manager;
|
||||
mod utils;
|
||||
|
||||
287
userspace/ksud/src/metamodule.rs
Normal file
287
userspace/ksud/src/metamodule.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
//! Metamodule management
|
||||
//!
|
||||
//! This module handles all metamodule-related functionality.
|
||||
//! Metamodules are special modules that manage how regular modules are mounted
|
||||
//! and provide hooks for module installation/uninstallation.
|
||||
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use log::{info, warn};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::module::ModuleType::All;
|
||||
use crate::{assets, defs};
|
||||
|
||||
/// Determine whether the provided module properties mark it as a metamodule
|
||||
pub fn is_metamodule(props: &HashMap<String, String>) -> bool {
|
||||
props
|
||||
.get("metamodule")
|
||||
.map(|s| {
|
||||
let trimmed = s.trim();
|
||||
trimmed == "1" || trimmed.eq_ignore_ascii_case("true")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get metamodule path if it exists
|
||||
/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule
|
||||
pub fn get_metamodule_path() -> Option<PathBuf> {
|
||||
let path = Path::new(defs::METAMODULE_DIR);
|
||||
|
||||
// Check if symlink exists and resolve it
|
||||
if path.is_symlink()
|
||||
&& let Ok(target) = std::fs::read_link(path)
|
||||
{
|
||||
// If target is relative, resolve it
|
||||
let resolved = if target.is_absolute() {
|
||||
target
|
||||
} else {
|
||||
path.parent()?.join(target)
|
||||
};
|
||||
|
||||
if resolved.exists() && resolved.is_dir() {
|
||||
return Some(resolved);
|
||||
} else {
|
||||
warn!(
|
||||
"Metamodule symlink points to non-existent path: {:?}",
|
||||
resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search for metamodule=1 in modules directory
|
||||
let mut result = None;
|
||||
let _ = crate::module::foreach_module(All, |module_path| {
|
||||
if let Ok(props) = crate::module::read_module_prop(module_path)
|
||||
&& is_metamodule(&props)
|
||||
{
|
||||
info!("Found metamodule in modules directory: {:?}", module_path);
|
||||
result = Some(module_path.to_path_buf());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if metamodule exists
|
||||
pub fn has_metamodule() -> bool {
|
||||
get_metamodule_path().is_some()
|
||||
}
|
||||
|
||||
/// Check if it's safe to install a regular module
|
||||
/// Returns Ok(()) if safe, Err(is_disabled) if blocked
|
||||
/// - Err(true) means metamodule is disabled
|
||||
/// - Err(false) means metamodule is in other unstable state
|
||||
pub fn check_install_safety() -> Result<(), bool> {
|
||||
// No metamodule → safe
|
||||
let Some(metamodule_path) = get_metamodule_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// No metainstall.sh → safe (uses default installer)
|
||||
// The staged update directory may contain the latest scripts, so check both locations
|
||||
let has_metainstall = metamodule_path
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
|| metamodule_path.file_name().is_some_and(|module_id| {
|
||||
Path::new(defs::MODULE_UPDATE_DIR)
|
||||
.join(module_id)
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
});
|
||||
if !has_metainstall {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check for marker files
|
||||
let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
|
||||
// Stable state (no markers) → safe
|
||||
if !has_update && !has_remove && !has_disable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Return true if disabled, false for other unstable states
|
||||
Err(has_disable && !has_update && !has_remove)
|
||||
}
|
||||
|
||||
/// Create or update the metamodule symlink
|
||||
/// Points /data/adb/metamodule -> /data/adb/modules/{module_id}
|
||||
pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> {
|
||||
// METAMODULE_DIR might have trailing slash, so we need to trim it
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
info!(
|
||||
"Creating metamodule symlink: {:?} -> {:?}",
|
||||
symlink_path, module_path
|
||||
);
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if symlink_path.exists() || symlink_path.is_symlink() {
|
||||
info!("Removing old metamodule symlink/path");
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?;
|
||||
} else {
|
||||
// Could be a directory, remove it
|
||||
std::fs::remove_dir_all(symlink_path)
|
||||
.with_context(|| "Failed to remove old directory")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(module_path, symlink_path)
|
||||
.with_context(|| format!("Failed to create symlink to {:?}", module_path))?;
|
||||
|
||||
info!("Metamodule symlink created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the metamodule symlink
|
||||
pub(crate) fn remove_symlink() -> Result<()> {
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path)
|
||||
.with_context(|| "Failed to remove metamodule symlink")?;
|
||||
info!("Metamodule symlink removed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the install script content, using metainstall.sh from metamodule if available
|
||||
/// Returns the script content to be executed
|
||||
pub(crate) fn get_install_script(
|
||||
is_metamodule: bool,
|
||||
installer_content: &str,
|
||||
install_module_script: &str,
|
||||
) -> Result<String> {
|
||||
// Check if there's a metamodule with metainstall.sh
|
||||
// Only apply this logic for regular modules (not when installing metamodule itself)
|
||||
let install_script = if !is_metamodule {
|
||||
if let Some(metamodule_path) = get_metamodule_path() {
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, using default installer");
|
||||
install_module_script.to_string()
|
||||
} else {
|
||||
let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT);
|
||||
|
||||
if metainstall_path.exists() {
|
||||
info!("Using metainstall.sh from metamodule");
|
||||
let metamodule_content = std::fs::read_to_string(&metainstall_path)
|
||||
.with_context(|| "Failed to read metamodule metainstall.sh")?;
|
||||
format!("{}\n{}\nexit 0\n", installer_content, metamodule_content)
|
||||
} else {
|
||||
info!("Metamodule exists but has no metainstall.sh, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No metamodule found, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
} else {
|
||||
info!("Installing metamodule, using default installer");
|
||||
install_module_script.to_string()
|
||||
};
|
||||
|
||||
Ok(install_script)
|
||||
}
|
||||
|
||||
/// Check if metamodule script exists and is ready to execute
|
||||
/// Returns None if metamodule doesn't exist, is disabled, or script is missing
|
||||
/// Returns Some(script_path) if script is ready to execute
|
||||
fn check_metamodule_script(script_name: &str) -> Option<PathBuf> {
|
||||
// Check if metamodule exists
|
||||
let metamodule_path = get_metamodule_path()?;
|
||||
|
||||
// Check if metamodule is disabled
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, skipping {}", script_name);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if script exists
|
||||
let script_path = metamodule_path.join(script_name);
|
||||
if !script_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(script_path)
|
||||
}
|
||||
|
||||
/// Execute metamodule's metauninstall.sh for a specific module
|
||||
pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> {
|
||||
let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(
|
||||
"Executing metamodule metauninstall.sh for module: {}",
|
||||
module_id
|
||||
);
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", metauninstall_path.to_str().unwrap()])
|
||||
.current_dir(metauninstall_path.parent().unwrap())
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_ID", module_id)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule metauninstall.sh failed for module {}: {:?}",
|
||||
module_id,
|
||||
result
|
||||
);
|
||||
|
||||
info!(
|
||||
"Metamodule metauninstall.sh executed successfully for {}",
|
||||
module_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule mount script
|
||||
pub fn exec_mount_script(module_dir: &str) -> Result<()> {
|
||||
let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing mount script for metamodule");
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", mount_script.to_str().unwrap()])
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_DIR", module_dir)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule mount script failed with status: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
info!("Metamodule mount script executed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule script for a specific stage
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing metamodule {}.sh", stage);
|
||||
crate::module::exec_script(&script_path, block)?;
|
||||
info!("Metamodule {}.sh executed successfully", stage);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
use std::fs::{copy, rename};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, remove_dir_all, remove_file, set_permissions},
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::FromStr,
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule,
|
||||
restorecon::{restore_syscon, setsyscon},
|
||||
sepolicy,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
@@ -16,17 +11,23 @@ use const_format::concatcp;
|
||||
use is_executable::is_executable;
|
||||
use java_properties::PropertiesIter;
|
||||
use log::{info, warn};
|
||||
|
||||
use std::fs::{copy, rename};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, remove_dir_all, set_permissions},
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::FromStr,
|
||||
};
|
||||
use zip_extensions::zip_extract_file_to_memory;
|
||||
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::{
|
||||
assets,
|
||||
defs::{self, MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME},
|
||||
ksucalls,
|
||||
restorecon::{restore_syscon, setsyscon},
|
||||
sepolicy,
|
||||
utils::*,
|
||||
};
|
||||
use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME};
|
||||
use crate::module::ModuleType::{Active, All};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
|
||||
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
|
||||
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
@@ -38,27 +39,36 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
"\n"
|
||||
);
|
||||
|
||||
fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", INSTALL_MODULE_SCRIPT])
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env(
|
||||
/// Get common environment variables for script execution
|
||||
pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
("ASH_STANDALONE", "1".to_string()),
|
||||
("KSU", "true".to_string()),
|
||||
("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()),
|
||||
("KSU_VER_CODE", defs::VERSION_CODE.to_string()),
|
||||
("KSU_VER", defs::VERSION_NAME.to_string()),
|
||||
(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
env_var("PATH").unwrap_or_default(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
)
|
||||
.env("KSU", "true")
|
||||
.env("KSU_SUKISU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
// Get install script from metamodule module
|
||||
let install_script =
|
||||
metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", &install_script])
|
||||
.envs(get_common_script_envs())
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
@@ -66,10 +76,7 @@ fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// becuase we use something like A-B update
|
||||
// we need to update the module state after the boot_completed
|
||||
// if someone(such as the module) install a module before the boot_completed
|
||||
// then it may cause some problems, just forbid it
|
||||
// Check if Android boot is completed before installing modules
|
||||
fn ensure_boot_completed() -> Result<()> {
|
||||
// ensure getprop sys.boot_completed == 1
|
||||
if getprop("sys.boot_completed").as_deref() != Some("1") {
|
||||
@@ -78,34 +85,21 @@ fn ensure_boot_completed() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> {
|
||||
let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file);
|
||||
if create {
|
||||
ensure_file_exists(module_state_file)
|
||||
} else {
|
||||
if module_state_file.exists() {
|
||||
remove_file(module_state_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum ModuleType {
|
||||
pub(crate) enum ModuleType {
|
||||
All,
|
||||
Active,
|
||||
Updated,
|
||||
}
|
||||
|
||||
fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
pub(crate) fn foreach_module(
|
||||
module_type: ModuleType,
|
||||
mut f: impl FnMut(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let modules_dir = Path::new(match module_type {
|
||||
ModuleType::Updated => MODULE_UPDATE_DIR,
|
||||
_ => defs::MODULE_DIR,
|
||||
});
|
||||
if !modules_dir.is_dir() {
|
||||
warn!("{} is not a directory, skip", modules_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
let dir = std::fs::read_dir(modules_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
@@ -114,11 +108,11 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()
|
||||
continue;
|
||||
}
|
||||
|
||||
if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
warn!("{} is removed, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
@@ -130,7 +124,7 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()
|
||||
}
|
||||
|
||||
fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
foreach_module(ModuleType::Active, f)
|
||||
foreach_module(Active, f)
|
||||
}
|
||||
|
||||
pub fn load_sepolicy_rule() -> Result<()> {
|
||||
@@ -150,7 +144,7 @@ pub fn load_sepolicy_rule() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
|
||||
@@ -169,21 +163,7 @@ fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
.current_dir(path.as_ref().parent().unwrap())
|
||||
.arg("sh")
|
||||
.arg(path.as_ref())
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env("KSU", "true")
|
||||
.env("KSU_SUKISU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
);
|
||||
.envs(get_common_script_envs());
|
||||
|
||||
let result = if wait {
|
||||
command.status().map(|_| ())
|
||||
@@ -251,45 +231,89 @@ pub fn load_system_prop() -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn prune_modules() -> Result<()> {
|
||||
foreach_module(ModuleType::All, |module| {
|
||||
if module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists()
|
||||
&& let Err(e) = exec_script(uninstaller, true)
|
||||
{
|
||||
warn!("Failed to exec uninstaller: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
} else {
|
||||
remove_file(module.join(defs::UPDATE_FILE_NAME)).ok();
|
||||
foreach_module(All, |module| {
|
||||
if !module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
// Execute metamodule's metauninstall.sh first
|
||||
let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
// Check if this is a metamodule
|
||||
let is_metamodule = read_module_prop(module)
|
||||
.map(|props| metamodule::is_metamodule(&props))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Removing metamodule symlink");
|
||||
if let Err(e) = metamodule::remove_symlink() {
|
||||
warn!("Failed to remove metamodule symlink: {}", e);
|
||||
}
|
||||
} else if let Err(e) = metamodule::exec_metauninstall_script(module_id) {
|
||||
warn!(
|
||||
"Failed to exec metamodule uninstall for {}: {}",
|
||||
module_id, e
|
||||
);
|
||||
}
|
||||
|
||||
// Then execute module's own uninstall.sh
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists()
|
||||
&& let Err(e) = exec_script(uninstaller, true)
|
||||
{
|
||||
warn!("Failed to exec uninstaller: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// collect remaining modules, if none, clean up metamodule record
|
||||
let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().join("module.prop").exists())
|
||||
.collect();
|
||||
|
||||
if remaining_modules.is_empty() {
|
||||
info!("no remaining modules.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_updated_modules() -> Result<()> {
|
||||
let modules_root = Path::new(MODULE_DIR);
|
||||
foreach_module(ModuleType::Updated, |module| {
|
||||
if !module.is_dir() {
|
||||
foreach_module(ModuleType::Updated, |updated_module| {
|
||||
if !updated_module.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(name) = module.file_name() {
|
||||
let old_dir = modules_root.join(name);
|
||||
if old_dir.exists()
|
||||
&& let Err(e) = remove_dir_all(&old_dir)
|
||||
{
|
||||
log::error!("Failed to remove old {}: {}", old_dir.display(), e);
|
||||
if let Some(name) = updated_module.file_name() {
|
||||
let module_dir = modules_root.join(name);
|
||||
let mut disabled = false;
|
||||
let mut removed = false;
|
||||
if module_dir.exists() {
|
||||
// If the old module is disabled, we need to also disable the new one
|
||||
disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists();
|
||||
removed = module_dir.join(defs::REMOVE_FILE_NAME).exists();
|
||||
remove_dir_all(&module_dir)?;
|
||||
}
|
||||
if let Err(e) = rename(module, &old_dir) {
|
||||
log::error!("Failed to move new module {}: {}", module.display(), e);
|
||||
rename(updated_module, &module_dir)?;
|
||||
if removed {
|
||||
let path = module_dir.join(defs::REMOVE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
} else if disabled {
|
||||
let path = module_dir.join(defs::DISABLE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -297,107 +321,182 @@ pub fn handle_updated_modules() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
fn inner(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
fn _install_module(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
|
||||
// first check if working dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
// first check if working dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
|
||||
// read the module_id from zip, if failed it will return early.
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
// read the module_id from zip, if failed it will return early.
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {:?}", module_prop);
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {module_prop:?}");
|
||||
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
let module_id = module_id.trim();
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
let module_id = module_id.trim();
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
// Check if this module is a metamodule
|
||||
let is_metamodule = metamodule::is_metamodule(&module_prop);
|
||||
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
println!("- Preparing Zip");
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// ensure modules_update exists
|
||||
ensure_dir_exists(MODULE_UPDATE_DIR)?;
|
||||
setsyscon(MODULE_UPDATE_DIR)?;
|
||||
|
||||
let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id);
|
||||
ensure_clean_dir(&update_module_dir)?;
|
||||
info!("module dir: {}", update_module_dir.display());
|
||||
|
||||
let do_install = || -> Result<()> {
|
||||
// unzip the image and move it to modules_update/<id> dir
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&update_module_dir)?;
|
||||
|
||||
// set permission and selinux context for $MOD/system
|
||||
let module_system_dir = update_module_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
exec_install_script(zip)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
update_module_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
info!("Module install successfully!");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
let result = do_install();
|
||||
if result.is_err() {
|
||||
remove_dir_all(&update_module_dir).ok();
|
||||
// Check if it's safe to install regular module
|
||||
if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() {
|
||||
println!("\n❌ Installation Blocked");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule with custom installer is active");
|
||||
println!("│");
|
||||
if is_disabled {
|
||||
println!("│ Current state: Disabled");
|
||||
println!("│ Action required: Re-enable or uninstall it, then reboot");
|
||||
} else {
|
||||
println!("│ Current state: Pending changes");
|
||||
println!("│ Action required: Reboot to apply changes first");
|
||||
}
|
||||
result
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Metamodule installation blocked");
|
||||
}
|
||||
let result = inner(zip);
|
||||
|
||||
// All modules (including metamodules) are installed to MODULE_UPDATE_DIR
|
||||
let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Installing metamodule: {}", module_id);
|
||||
|
||||
// Check if there's already a metamodule installed
|
||||
if metamodule::has_metamodule()
|
||||
&& let Some(existing_path) = metamodule::get_metamodule_path()
|
||||
{
|
||||
let existing_id = read_module_prop(&existing_path)
|
||||
.ok()
|
||||
.and_then(|m| m.get("id").cloned())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
if existing_id != module_id {
|
||||
println!("\n❌ Installation Failed");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule is already installed");
|
||||
println!("│ Current metamodule: {}", existing_id);
|
||||
println!("│");
|
||||
println!("│ Only one metamodule can be active at a time.");
|
||||
println!("│");
|
||||
println!("│ To install this metamodule:");
|
||||
println!("│ 1. Uninstall the current metamodule");
|
||||
println!("│ 2. Reboot your device");
|
||||
println!("│ 3. Install the new metamodule");
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Cannot install multiple metamodules");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// Ensure module directory exists and set SELinux context
|
||||
ensure_dir_exists(defs::MODULE_UPDATE_DIR)?;
|
||||
setsyscon(defs::MODULE_UPDATE_DIR)?;
|
||||
|
||||
// Prepare target directory
|
||||
println!("- Installing to {}", updated_dir.display());
|
||||
ensure_clean_dir(&updated_dir)?;
|
||||
info!("target dir: {}", updated_dir.display());
|
||||
|
||||
// Extract zip to target directory
|
||||
println!("- Extracting module files");
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&updated_dir)?;
|
||||
|
||||
// Set permission and selinux context for $MOD/system
|
||||
let module_system_dir = updated_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
// Execute install script
|
||||
println!("- Running module installer");
|
||||
exec_install_script(zip, is_metamodule)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
updated_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
// Create symlink for metamodule
|
||||
if is_metamodule {
|
||||
println!("- Creating metamodule symlink");
|
||||
metamodule::ensure_symlink(&module_dir)?;
|
||||
}
|
||||
|
||||
println!("- Module installed successfully!");
|
||||
info!("Module {} installed successfully!", module_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
let result = _install_module(zip);
|
||||
if let Err(ref e) = result {
|
||||
println!("- Error: {e}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, true)
|
||||
pub fn undo_uninstall_module(id: &str) -> Result<()> {
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Remove the remove mark
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
if remove_file.exists() {
|
||||
std::fs::remove_file(&remove_file)
|
||||
.with_context(|| format!("Failed to delete remove file for module '{}'", id))?;
|
||||
info!("Removed the remove mark for module {}", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, false)
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Mark for removal
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
File::create(remove_file).with_context(|| "Failed to create remove file")?;
|
||||
|
||||
info!("Module {} marked for removal", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
@@ -406,11 +505,30 @@ pub fn run_action(id: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, false)
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
if disable_path.exists() {
|
||||
std::fs::remove_file(&disable_path).with_context(|| {
|
||||
format!("Failed to remove disable file: {}", disable_path.display())
|
||||
})?;
|
||||
info!("Module {} enabled", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, true)
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
ensure_file_exists(disable_path)?;
|
||||
|
||||
info!("Module {} disabled", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
@@ -418,11 +536,13 @@ pub fn disable_all_modules() -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn uninstall_all_modules() -> Result<()> {
|
||||
info!("Uninstalling all modules");
|
||||
mark_all_modules(defs::REMOVE_FILE_NAME)
|
||||
}
|
||||
|
||||
fn mark_all_modules(flag_file: &str) -> Result<()> {
|
||||
let dir = std::fs::read_dir(MODULE_DIR)?;
|
||||
// we assume the module dir is already mounted
|
||||
let dir = std::fs::read_dir(defs::MODULE_DIR)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let flag = path.join(flag_file);
|
||||
@@ -472,6 +592,7 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
if !path.join("module.prop").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut module_prop_map = match read_module_prop(&path) {
|
||||
Ok(prop) => prop,
|
||||
Err(e) => {
|
||||
@@ -481,26 +602,33 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
};
|
||||
|
||||
// If id is missing or empty, use directory name as fallback
|
||||
let dir_id = entry.file_name().to_string_lossy().to_string();
|
||||
module_prop_map.insert("dir_id".to_owned(), dir_id.clone());
|
||||
|
||||
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
|
||||
info!("Use dir name as module id: {dir_id}");
|
||||
module_prop_map.insert("id".to_owned(), dir_id.clone());
|
||||
match entry.file_name().to_str() {
|
||||
Some(id) => {
|
||||
info!("Use dir name as module id: {id}");
|
||||
module_prop_map.insert("id".to_owned(), id.to_owned());
|
||||
}
|
||||
_ => {
|
||||
info!("Failed to get module id from dir name");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enabled, update, remove flags
|
||||
// Add enabled, update, remove, web, action flags
|
||||
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let web = path.join(defs::MODULE_WEB_DIR).exists();
|
||||
let action = path.join(defs::MODULE_ACTION_SH).exists();
|
||||
let need_mount = path.join("system").exists() && !path.join("skip_mount").exists();
|
||||
|
||||
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
|
||||
module_prop_map.insert("update".to_owned(), update.to_string());
|
||||
module_prop_map.insert("remove".to_owned(), remove.to_string());
|
||||
module_prop_map.insert("web".to_owned(), web.to_string());
|
||||
module_prop_map.insert("action".to_owned(), action.to_string());
|
||||
module_prop_map.insert("mount".to_owned(), need_mount.to_string());
|
||||
|
||||
modules.push(module_prop_map);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path())
|
||||
&& let Result::Ok(con) = lgetfilecon(&path)
|
||||
&& (con == ADB_CON || con == UNLABEL_CON || con.is_empty())
|
||||
&& (con == UNLABEL_CON || con.is_empty())
|
||||
{
|
||||
lsetfilecon(&path, SYSTEM_CON)?;
|
||||
}
|
||||
@@ -76,6 +76,6 @@ fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
|
||||
pub fn restorecon() -> Result<()> {
|
||||
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
|
||||
restore_modules_con(defs::MODULE_DIR)?;
|
||||
restore_syscon_if_unlabeled(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions, create_dir_all, remove_file, write},
|
||||
fs::{Permissions, set_permissions},
|
||||
fs::{File, OpenOptions, create_dir_all, remove_file, write},
|
||||
io::{
|
||||
ErrorKind::{AlreadyExists, NotFound},
|
||||
Write,
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{
|
||||
process,
|
||||
thread::{LinkNameSpaceType, move_into_link_name_space},
|
||||
};
|
||||
|
||||
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
|
||||
|
||||
pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
let path = dir.as_ref();
|
||||
log::debug!("ensure_clean_dir: {}", path.display());
|
||||
@@ -32,7 +35,7 @@ pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
|
||||
pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
|
||||
match File::options().write(true).create_new(true).open(&file) {
|
||||
Result::Ok(_) => Ok(()),
|
||||
std::result::Result::Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == AlreadyExists && file.as_ref().is_file() {
|
||||
Ok(())
|
||||
@@ -172,27 +175,6 @@ pub fn has_magisk() -> bool {
|
||||
which::which("magisk").is_ok()
|
||||
}
|
||||
|
||||
fn is_ok_empty(dir: &str) -> bool {
|
||||
use std::result::Result::Ok;
|
||||
|
||||
match fs::read_dir(dir) {
|
||||
Ok(mut entries) => entries.next().is_none(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_tmp_path() -> String {
|
||||
let dirs = ["/debug_ramdisk", "/patch_hw", "/oem", "/root", "/sbin"];
|
||||
|
||||
// find empty directory
|
||||
for dir in dirs {
|
||||
if is_ok_empty(dir) {
|
||||
return dir.to_string();
|
||||
}
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn link_ksud_to_bin() -> Result<()> {
|
||||
let ksu_bin = PathBuf::from(defs::DAEMON_PATH);
|
||||
|
||||
4
userspace/meta-overlayfs/.gitignore
vendored
Normal file
4
userspace/meta-overlayfs/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
/out
|
||||
Cargo.lock
|
||||
*.log
|
||||
23
userspace/meta-overlayfs/Cargo.toml
Normal file
23
userspace/meta-overlayfs/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "meta-overlayfs"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
authors = ["KernelSU Developers"]
|
||||
description = "An implementation of a metamodule using OverlayFS for KernelSU"
|
||||
license = "GPL-3.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = ["all-apis"] }
|
||||
procfs = "0.17"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z" # Minimize binary size
|
||||
lto = true # Link-time optimization
|
||||
codegen-units = 1 # Maximum optimization
|
||||
panic = "abort" # Reduce binary size
|
||||
58
userspace/meta-overlayfs/README.md
Normal file
58
userspace/meta-overlayfs/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# meta-overlayfs
|
||||
|
||||
Official overlayfs mount handler for KernelSU metamodules.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
adb push meta-overlayfs-v1.0.0.zip /sdcard/
|
||||
adb shell su -c 'ksud module install /sdcard/meta-overlayfs-v1.0.0.zip'
|
||||
adb reboot
|
||||
```
|
||||
|
||||
Or install via KernelSU Manager → Modules.
|
||||
|
||||
**Note**: The metamodule is now installed as a regular module to `/data/adb/modules/meta-overlay/`, with a symlink created at `/data/adb/metamodule` pointing to it.
|
||||
|
||||
## How It Works
|
||||
|
||||
Uses dual-directory architecture for ext4 image support:
|
||||
|
||||
- **Metadata**: `/data/adb/modules/` - Contains `module.prop`, `disable`, `skip_mount` markers
|
||||
- **Content**: `/data/adb/metamodule/mnt/` - Contains `system/`, `vendor/` etc. directories from ext4 images
|
||||
|
||||
Scans metadata directory for enabled modules, then mounts their content directories as overlayfs layers.
|
||||
|
||||
### Supported Partitions
|
||||
|
||||
system, vendor, product, system_ext, odm, oem
|
||||
|
||||
### Read-Write Layer
|
||||
|
||||
Optional upperdir/workdir support via `/data/adb/modules/.rw/`:
|
||||
|
||||
```bash
|
||||
mkdir -p /data/adb/modules/.rw/system/{upperdir,workdir}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODULE_METADATA_DIR` - Metadata location (default: `/data/adb/modules/`)
|
||||
- `MODULE_CONTENT_DIR` - Content location (default: `/data/adb/metamodule/mnt/`)
|
||||
- `RUST_LOG` - Log level (debug, info, warn, error)
|
||||
|
||||
## Architecture
|
||||
|
||||
Automatically selects aarch64 or x86_64 binary during installation (~500KB).
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Output: `target/meta-overlayfs-v1.0.0.zip`
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0
|
||||
92
userspace/meta-overlayfs/build.sh
Normal file
92
userspace/meta-overlayfs/build.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
OUTPUT_DIR="target"
|
||||
METAMODULE_DIR="metamodule"
|
||||
MODULE_OUTPUT_DIR="$OUTPUT_DIR/module"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Building meta-overlayfs v${VERSION}"
|
||||
echo "=========================================="
|
||||
|
||||
# Detect build tool
|
||||
if command -v cross >/dev/null 2>&1; then
|
||||
BUILD_TOOL="cross"
|
||||
echo "Using cross for compilation"
|
||||
else
|
||||
BUILD_TOOL="cargo-ndk"
|
||||
echo "Using cargo ndk for compilation"
|
||||
if ! command -v cargo-ndk >/dev/null 2>&1; then
|
||||
echo "Error: Neither cross nor cargo-ndk found!"
|
||||
echo "Please install one of them:"
|
||||
echo " - cross: cargo install cross"
|
||||
echo " - cargo-ndk: cargo install cargo-ndk"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean output directory
|
||||
echo "Cleaning output directory..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$MODULE_OUTPUT_DIR"
|
||||
|
||||
# Build for multiple architectures
|
||||
echo ""
|
||||
echo "Building for aarch64-linux-android..."
|
||||
if [ "$BUILD_TOOL" = "cross" ]; then
|
||||
cross build --release --target aarch64-linux-android
|
||||
else
|
||||
cargo ndk build -t arm64-v8a --release
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Building for x86_64-linux-android..."
|
||||
if [ "$BUILD_TOOL" = "cross" ]; then
|
||||
cross build --release --target x86_64-linux-android
|
||||
else
|
||||
cargo ndk build -t x86_64 --release
|
||||
fi
|
||||
|
||||
# Copy binaries
|
||||
echo ""
|
||||
echo "Copying binaries..."
|
||||
cp target/aarch64-linux-android/release/meta-overlayfs \
|
||||
"$MODULE_OUTPUT_DIR/meta-overlayfs-aarch64"
|
||||
cp target/x86_64-linux-android/release/meta-overlayfs \
|
||||
"$MODULE_OUTPUT_DIR/meta-overlayfs-x86_64"
|
||||
|
||||
# Copy metamodule files
|
||||
echo "Copying metamodule files..."
|
||||
cp "$METAMODULE_DIR"/module.prop "$MODULE_OUTPUT_DIR/"
|
||||
cp "$METAMODULE_DIR"/*.sh "$MODULE_OUTPUT_DIR/"
|
||||
|
||||
# Set permissions
|
||||
echo "Setting permissions..."
|
||||
chmod 755 "$MODULE_OUTPUT_DIR"/*.sh
|
||||
chmod 755 "$MODULE_OUTPUT_DIR"/meta-overlayfs-*
|
||||
|
||||
# Display binary sizes
|
||||
echo ""
|
||||
echo "Binary sizes:"
|
||||
echo " aarch64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-aarch64 | awk '{print $1}')"
|
||||
echo " x86_64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-x86_64 | awk '{print $1}')"
|
||||
|
||||
# Package
|
||||
echo ""
|
||||
echo "Packaging..."
|
||||
cd "$MODULE_OUTPUT_DIR"
|
||||
ZIP_NAME="meta-overlayfs-v${VERSION}.zip"
|
||||
zip -r "../$ZIP_NAME" .
|
||||
cd ../..
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Build completed successfully!"
|
||||
echo "Output: $OUTPUT_DIR/$ZIP_NAME"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "To install:"
|
||||
echo " adb push $OUTPUT_DIR/$ZIP_NAME /sdcard/"
|
||||
echo " adb shell su -c 'ksud module install /sdcard/$ZIP_NAME'"
|
||||
0
userspace/meta-overlayfs/metamodule/.gitkeep
Normal file
0
userspace/meta-overlayfs/metamodule/.gitkeep
Normal file
77
userspace/meta-overlayfs/metamodule/customize.sh
Normal file
77
userspace/meta-overlayfs/metamodule/customize.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
ui_print "- Detecting device architecture..."
|
||||
|
||||
# Detect architecture using ro.product.cpu.abi
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
ui_print "- Detected ABI: $ABI"
|
||||
|
||||
# Select the correct binary based on architecture
|
||||
case "$ABI" in
|
||||
arm64-v8a)
|
||||
ARCH_BINARY="meta-overlayfs-aarch64"
|
||||
REMOVE_BINARY="meta-overlayfs-x86_64"
|
||||
ui_print "- Selected architecture: ARM64"
|
||||
;;
|
||||
x86_64)
|
||||
ARCH_BINARY="meta-overlayfs-x86_64"
|
||||
REMOVE_BINARY="meta-overlayfs-aarch64"
|
||||
ui_print "- Selected architecture: x86_64"
|
||||
;;
|
||||
*)
|
||||
abort "! Unsupported architecture: $ABI"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify the selected binary exists
|
||||
if [ ! -f "$MODPATH/$ARCH_BINARY" ]; then
|
||||
abort "! Binary not found: $ARCH_BINARY"
|
||||
fi
|
||||
|
||||
ui_print "- Installing $ARCH_BINARY as meta-overlayfs"
|
||||
|
||||
# Rename the selected binary to the generic name
|
||||
mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-overlayfs" || abort "! Failed to rename binary"
|
||||
|
||||
# Remove the unused binary
|
||||
rm -f "$MODPATH/$REMOVE_BINARY"
|
||||
|
||||
# Ensure the binary is executable
|
||||
chmod 755 "$MODPATH/meta-overlayfs" || abort "! Failed to set permissions"
|
||||
|
||||
ui_print "- Architecture-specific binary installed successfully"
|
||||
|
||||
# Create ext4 image for module content storage
|
||||
IMG_FILE="$MODPATH/modules.img"
|
||||
MNT_DIR="$MODPATH/mnt"
|
||||
IMG_SIZE_MB=2048
|
||||
|
||||
if [ ! -f "$IMG_FILE" ]; then
|
||||
ui_print "- Creating 2GB ext4 image for module storage"
|
||||
|
||||
# Create sparse file (2GB logical size, 0 bytes actual)
|
||||
truncate -s ${IMG_SIZE_MB}M "$IMG_FILE" || \
|
||||
abort "! Failed to create image file"
|
||||
|
||||
# Format as ext4 with small journal (8MB) for safety with minimal overhead
|
||||
/system/bin/mke2fs -t ext4 -J size=8 -F "$IMG_FILE" >/dev/null 2>&1 || \
|
||||
abort "! Failed to format ext4 image"
|
||||
|
||||
ui_print "- Image created successfully (sparse file)"
|
||||
else
|
||||
ui_print "- Existing image found, keeping it"
|
||||
fi
|
||||
|
||||
# Mount image immediately for use
|
||||
ui_print "- Mounting image for immediate use..."
|
||||
mkdir -p "$MNT_DIR"
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || \
|
||||
abort "! Failed to mount image"
|
||||
ui_print "- Image mounted successfully"
|
||||
else
|
||||
ui_print "- Image already mounted"
|
||||
fi
|
||||
|
||||
ui_print "- Installation complete"
|
||||
ui_print "- Image is ready for module installations"
|
||||
61
userspace/meta-overlayfs/metamodule/metainstall.sh
Normal file
61
userspace/meta-overlayfs/metamodule/metainstall.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/system/bin/sh
|
||||
############################################
|
||||
# meta-overlayfs metainstall.sh
|
||||
# Module installation hook for ext4 image support
|
||||
############################################
|
||||
|
||||
# Constants
|
||||
IMG_FILE="/data/adb/metamodule/modules.img"
|
||||
MNT_DIR="/data/adb/metamodule/mnt"
|
||||
|
||||
# Ensure ext4 image is mounted
|
||||
ensure_image_mounted() {
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
ui_print "- Mounting modules image"
|
||||
mkdir -p "$MNT_DIR"
|
||||
mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || {
|
||||
abort "! Failed to mount modules image"
|
||||
}
|
||||
ui_print "- Image mounted successfully"
|
||||
else
|
||||
ui_print "- Image already mounted"
|
||||
fi
|
||||
}
|
||||
|
||||
# Post-installation: move partition directories to ext4 image
|
||||
post_install_to_image() {
|
||||
ui_print "- Moving module content to image"
|
||||
|
||||
MOD_IMG_DIR="$MNT_DIR/$MODID"
|
||||
mkdir -p "$MOD_IMG_DIR"
|
||||
|
||||
# Move all partition directories
|
||||
for partition in system vendor product system_ext odm oem; do
|
||||
if [ -d "$MODPATH/$partition" ]; then
|
||||
ui_print " Moving $partition/"
|
||||
mv "$MODPATH/$partition" "$MOD_IMG_DIR/" || {
|
||||
ui_print "! Warning: Failed to move $partition"
|
||||
}
|
||||
fi
|
||||
done
|
||||
|
||||
# Set permissions
|
||||
chown -R 0:0 "$MOD_IMG_DIR" 2>/dev/null
|
||||
chmod -R 755 "$MOD_IMG_DIR" 2>/dev/null
|
||||
|
||||
ui_print "- Module content moved to image"
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
ui_print "- Using meta-overlayfs metainstall"
|
||||
|
||||
# 1. Ensure ext4 image is mounted
|
||||
ensure_image_mounted
|
||||
|
||||
# 2. Call standard install_module function (defined in installer.sh)
|
||||
install_module
|
||||
|
||||
# 3. Post-process: move content to image
|
||||
post_install_to_image
|
||||
|
||||
ui_print "- Installation complete"
|
||||
65
userspace/meta-overlayfs/metamodule/metamount.sh
Normal file
65
userspace/meta-overlayfs/metamodule/metamount.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/system/bin/sh
|
||||
# meta-overlayfs Module Mount Handler
|
||||
# This script is the entry point for dual-directory module mounting
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
IMG_FILE="$MODDIR/modules.img"
|
||||
MNT_DIR="$MODDIR/mnt"
|
||||
|
||||
# Log function
|
||||
log() {
|
||||
echo "[meta-overlayfs] $1"
|
||||
}
|
||||
|
||||
log "Starting module mount process"
|
||||
|
||||
# Ensure ext4 image is mounted
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
log "Image not mounted, mounting now..."
|
||||
|
||||
# Check if image file exists
|
||||
if [ ! -f "$IMG_FILE" ]; then
|
||||
log "ERROR: Image file not found at $IMG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create mount point
|
||||
mkdir -p "$MNT_DIR"
|
||||
|
||||
# Mount the ext4 image
|
||||
mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || {
|
||||
log "ERROR: Failed to mount image"
|
||||
exit 1
|
||||
}
|
||||
log "Image mounted successfully at $MNT_DIR"
|
||||
else
|
||||
log "Image already mounted at $MNT_DIR"
|
||||
fi
|
||||
|
||||
# Binary path (architecture-specific binary selected during installation)
|
||||
BINARY="$MODDIR/meta-overlayfs"
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
log "ERROR: Binary not found: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set dual-directory environment variables
|
||||
export MODULE_METADATA_DIR="/data/adb/modules"
|
||||
export MODULE_CONTENT_DIR="$MNT_DIR"
|
||||
|
||||
log "Metadata directory: $MODULE_METADATA_DIR"
|
||||
log "Content directory: $MODULE_CONTENT_DIR"
|
||||
log "Executing $BINARY"
|
||||
|
||||
# Execute the mount binary
|
||||
"$BINARY"
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
log "Mount failed with exit code $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
|
||||
log "Mount completed successfully"
|
||||
exit 0
|
||||
35
userspace/meta-overlayfs/metamodule/metauninstall.sh
Normal file
35
userspace/meta-overlayfs/metamodule/metauninstall.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-overlayfs metauninstall.sh
|
||||
# Module uninstallation hook for ext4 image cleanup
|
||||
############################################
|
||||
|
||||
# Constants
|
||||
MNT_DIR="/data/adb/metamodule/mnt"
|
||||
|
||||
if [ -z "$MODULE_ID" ]; then
|
||||
echo "! Error: MODULE_ID not provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "- Cleaning up module content from image: $MODULE_ID"
|
||||
|
||||
# Check if image is mounted
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
echo "! Warning: Image not mounted, skipping cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remove module content from image
|
||||
MOD_IMG_DIR="$MNT_DIR/$MODULE_ID"
|
||||
if [ -d "$MOD_IMG_DIR" ]; then
|
||||
echo " Removing $MOD_IMG_DIR"
|
||||
rm -rf "$MOD_IMG_DIR" || {
|
||||
echo "! Warning: Failed to remove module content from image"
|
||||
}
|
||||
echo "- Module content removed from image"
|
||||
else
|
||||
echo "- No module content found in image, skipping"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
8
userspace/meta-overlayfs/metamodule/module.prop
Normal file
8
userspace/meta-overlayfs/metamodule/module.prop
Normal file
@@ -0,0 +1,8 @@
|
||||
id=meta-overlayfs
|
||||
metamodule=1
|
||||
name=OverlayFS MetaModule
|
||||
version=1.0.0
|
||||
versionCode=1
|
||||
author=KernelSU Developers
|
||||
description=An implementation of a metamodule using OverlayFS for KernelSU
|
||||
updateJson=https://raw.githubusercontent.com/tiann/KernelSU/main/userspace/meta-overlayfs/update.json
|
||||
24
userspace/meta-overlayfs/metamodule/uninstall.sh
Normal file
24
userspace/meta-overlayfs/metamodule/uninstall.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-overlayfs uninstall.sh
|
||||
# Cleanup script for metamodule removal
|
||||
############################################
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
MNT_DIR="$MODDIR/mnt"
|
||||
|
||||
echo "- Uninstalling metamodule..."
|
||||
|
||||
# Unmount the ext4 image if mounted
|
||||
if mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
echo "- Unmounting image..."
|
||||
umount "$MNT_DIR" 2>/dev/null || {
|
||||
echo "- Warning: Failed to unmount cleanly"
|
||||
umount -l "$MNT_DIR" 2>/dev/null
|
||||
}
|
||||
echo "- Image unmounted"
|
||||
fi
|
||||
|
||||
echo "- Uninstall complete"
|
||||
|
||||
exit 0
|
||||
17
userspace/meta-overlayfs/src/defs.rs
Normal file
17
userspace/meta-overlayfs/src/defs.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Constants for KernelSU module mounting
|
||||
|
||||
// Dual-directory support for ext4 image
|
||||
pub const MODULE_METADATA_DIR: &str = "/data/adb/modules/";
|
||||
pub const MODULE_CONTENT_DIR: &str = "/data/adb/metamodule/mnt/";
|
||||
|
||||
// Legacy constant (for backwards compatibility)
|
||||
pub const _MODULE_DIR: &str = "/data/adb/modules/";
|
||||
|
||||
// Status marker files
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const _REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
// System directories
|
||||
pub const SYSTEM_RW_DIR: &str = "/data/adb/modules/.rw/";
|
||||
pub const KSU_OVERLAY_SOURCE: &str = "KSU";
|
||||
29
userspace/meta-overlayfs/src/main.rs
Normal file
29
userspace/meta-overlayfs/src/main.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
mod defs;
|
||||
mod mount;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Initialize logger
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.init();
|
||||
|
||||
info!("meta-overlayfs v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Dual-directory support: metadata + content
|
||||
let metadata_dir = std::env::var("MODULE_METADATA_DIR")
|
||||
.unwrap_or_else(|_| defs::MODULE_METADATA_DIR.to_string());
|
||||
let content_dir = std::env::var("MODULE_CONTENT_DIR")
|
||||
.unwrap_or_else(|_| defs::MODULE_CONTENT_DIR.to_string());
|
||||
|
||||
info!("Metadata directory: {}", metadata_dir);
|
||||
info!("Content directory: {}", content_dir);
|
||||
|
||||
// Execute dual-directory mounting
|
||||
mount::mount_modules_systemlessly(&metadata_dir, &content_dir)?;
|
||||
|
||||
info!("Mount completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
376
userspace/meta-overlayfs/src/mount.rs
Normal file
376
userspace/meta-overlayfs/src/mount.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
// Overlayfs mounting implementation
|
||||
// Migrated from ksud/src/mount.rs and ksud/src/init_event.rs
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use log::{info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use procfs::process::Process;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{fd::AsFd, fs::CWD, mount::*};
|
||||
|
||||
use crate::defs::{DISABLE_FILE_NAME, KSU_OVERLAY_SOURCE, SKIP_MOUNT_FILE_NAME, SYSTEM_RW_DIR};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlayfs(
|
||||
lower_dirs: &[String],
|
||||
lowest: &str,
|
||||
upperdir: Option<PathBuf>,
|
||||
workdir: Option<PathBuf>,
|
||||
dest: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let lowerdir_config = lower_dirs
|
||||
.iter()
|
||||
.map(|s| s.as_ref())
|
||||
.chain(std::iter::once(lowest))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":");
|
||||
info!(
|
||||
"mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}",
|
||||
dest.as_ref(),
|
||||
lowerdir_config,
|
||||
upperdir,
|
||||
workdir
|
||||
);
|
||||
|
||||
let upperdir = upperdir
|
||||
.filter(|up| up.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
let workdir = workdir
|
||||
.filter(|wd| wd.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
|
||||
let result = (|| {
|
||||
let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?;
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?;
|
||||
if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) {
|
||||
fsconfig_set_string(fs, "upperdir", upperdir)?;
|
||||
fsconfig_set_string(fs, "workdir", workdir)?;
|
||||
}
|
||||
fsconfig_set_string(fs, "source", KSU_OVERLAY_SOURCE)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
dest.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!("fsopen mount failed: {e:#}, fallback to mount");
|
||||
let mut data = format!("lowerdir={lowerdir_config}");
|
||||
if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) {
|
||||
data = format!("{data},upperdir={upperdir},workdir={workdir}");
|
||||
}
|
||||
mount(
|
||||
KSU_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"overlay",
|
||||
MountFlags::empty(),
|
||||
data,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn bind_mount(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
|
||||
info!(
|
||||
"bind mount {} -> {}",
|
||||
from.as_ref().display(),
|
||||
to.as_ref().display()
|
||||
);
|
||||
let tree = open_tree(
|
||||
CWD,
|
||||
from.as_ref(),
|
||||
OpenTreeFlags::OPEN_TREE_CLOEXEC
|
||||
| OpenTreeFlags::OPEN_TREE_CLONE
|
||||
| OpenTreeFlags::AT_RECURSIVE,
|
||||
)?;
|
||||
move_mount(
|
||||
tree.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
to.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn mount_overlay_child(
|
||||
mount_point: &str,
|
||||
relative: &String,
|
||||
module_roots: &Vec<String>,
|
||||
stock_root: &String,
|
||||
) -> Result<()> {
|
||||
if !module_roots
|
||||
.iter()
|
||||
.any(|lower| Path::new(&format!("{lower}{relative}")).exists())
|
||||
{
|
||||
return bind_mount(stock_root, mount_point);
|
||||
}
|
||||
if !Path::new(&stock_root).is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut lower_dirs: Vec<String> = vec![];
|
||||
for lower in module_roots {
|
||||
let lower_dir = format!("{lower}{relative}");
|
||||
let path = Path::new(&lower_dir);
|
||||
if path.is_dir() {
|
||||
lower_dirs.push(lower_dir);
|
||||
} else if path.exists() {
|
||||
// stock root has been blocked by this file
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if lower_dirs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// merge modules and stock
|
||||
if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) {
|
||||
warn!("failed: {e:#}, fallback to bind mount");
|
||||
bind_mount(stock_root, mount_point)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlay(
|
||||
root: &String,
|
||||
module_roots: &Vec<String>,
|
||||
workdir: Option<PathBuf>,
|
||||
upperdir: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
info!("mount overlay for {root}");
|
||||
std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?;
|
||||
let stock_root = ".";
|
||||
|
||||
// collect child mounts before mounting the root
|
||||
let mounts = Process::myself()?
|
||||
.mountinfo()
|
||||
.with_context(|| "get mountinfo")?;
|
||||
let mut mount_seq = mounts
|
||||
.0
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point)
|
||||
})
|
||||
.map(|m| m.mount_point.to_str())
|
||||
.collect::<Vec<_>>();
|
||||
mount_seq.sort();
|
||||
mount_seq.dedup();
|
||||
|
||||
mount_overlayfs(module_roots, root, upperdir, workdir, root)
|
||||
.with_context(|| "mount overlayfs for root failed")?;
|
||||
for mount_point in mount_seq.iter() {
|
||||
let Some(mount_point) = mount_point else {
|
||||
continue;
|
||||
};
|
||||
let relative = mount_point.replacen(root, "", 1);
|
||||
let stock_root: String = format!("{stock_root}{relative}");
|
||||
if !Path::new(&stock_root).exists() {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) {
|
||||
warn!("failed to mount overlay for child {mount_point}: {e:#}, revert");
|
||||
umount_dir(root).with_context(|| format!("failed to revert {root}"))?;
|
||||
bail!(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umount_dir(src: impl AsRef<Path>) -> Result<()> {
|
||||
unmount(src.as_ref(), UnmountFlags::empty())
|
||||
.with_context(|| format!("Failed to umount {}", src.as_ref().display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_overlay(
|
||||
_root: &String,
|
||||
_module_roots: &Vec<String>,
|
||||
_workdir: Option<PathBuf>,
|
||||
_upperdir: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
unimplemented!("mount_overlay is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_overlayfs(
|
||||
_lower_dirs: &[String],
|
||||
_lowest: &str,
|
||||
_upperdir: Option<PathBuf>,
|
||||
_workdir: Option<PathBuf>,
|
||||
_dest: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
unimplemented!("mount_overlayfs is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn bind_mount(_from: impl AsRef<Path>, _to: impl AsRef<Path>) -> Result<()> {
|
||||
unimplemented!("bind_mount is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
// ========== Mount coordination logic (from init_event.rs) ==========
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn mount_partition(partition_name: &str, lowerdir: &Vec<String>) -> Result<()> {
|
||||
if lowerdir.is_empty() {
|
||||
warn!("partition: {partition_name} lowerdir is empty");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = format!("/{partition_name}");
|
||||
|
||||
// if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately
|
||||
if Path::new(&partition).read_link().is_ok() {
|
||||
warn!("partition: {partition} is a symlink");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut workdir = None;
|
||||
let mut upperdir = None;
|
||||
let system_rw_dir = Path::new(SYSTEM_RW_DIR);
|
||||
if system_rw_dir.exists() {
|
||||
workdir = Some(system_rw_dir.join(partition_name).join("workdir"));
|
||||
upperdir = Some(system_rw_dir.join(partition_name).join("upperdir"));
|
||||
}
|
||||
|
||||
mount_overlay(&partition, lowerdir, workdir, upperdir)
|
||||
}
|
||||
|
||||
/// Collect enabled module IDs from metadata directory
|
||||
///
|
||||
/// Reads module list and status from metadata directory, returns enabled module IDs
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn collect_enabled_modules(metadata_dir: &str) -> Result<Vec<String>> {
|
||||
let dir = std::fs::read_dir(metadata_dir)
|
||||
.with_context(|| format!("Failed to read metadata directory: {}", metadata_dir))?;
|
||||
|
||||
let mut enabled = Vec::new();
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let module_id = match entry.file_name().to_str() {
|
||||
Some(id) => id.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Check status markers
|
||||
if path.join(DISABLE_FILE_NAME).exists() {
|
||||
info!("Module {} is disabled, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.join(SKIP_MOUNT_FILE_NAME).exists() {
|
||||
info!("Module {} has skip_mount, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional: verify module.prop exists
|
||||
if !path.join("module.prop").exists() {
|
||||
warn!("Module {} has no module.prop, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Module {} enabled", module_id);
|
||||
enabled.push(module_id);
|
||||
}
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Dual-directory version of mount_modules_systemlessly
|
||||
///
|
||||
/// Parameters:
|
||||
/// - metadata_dir: Metadata directory, stores module.prop, disable, skip_mount, etc.
|
||||
/// - content_dir: Content directory, stores system/, vendor/ and other partition content (ext4 image mount point)
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_modules_systemlessly(metadata_dir: &str, content_dir: &str) -> Result<()> {
|
||||
info!("Scanning modules (dual-directory mode)");
|
||||
info!(" Metadata: {}", metadata_dir);
|
||||
info!(" Content: {}", content_dir);
|
||||
|
||||
// 1. Traverse metadata directory, collect enabled module IDs
|
||||
let enabled_modules = collect_enabled_modules(metadata_dir)?;
|
||||
|
||||
if enabled_modules.is_empty() {
|
||||
info!("No enabled modules found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Found {} enabled module(s)", enabled_modules.len());
|
||||
|
||||
// 2. Initialize partition lowerdir lists
|
||||
let partition = vec!["vendor", "product", "system_ext", "odm", "oem"];
|
||||
let mut system_lowerdir: Vec<String> = Vec::new();
|
||||
let mut partition_lowerdir: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for part in &partition {
|
||||
partition_lowerdir.insert((*part).to_string(), Vec::new());
|
||||
}
|
||||
|
||||
// 3. Read module content from content directory
|
||||
for module_id in &enabled_modules {
|
||||
let module_content_path = Path::new(content_dir).join(module_id);
|
||||
|
||||
if !module_content_path.exists() {
|
||||
warn!("Module {} has no content directory, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Processing module: {}", module_id);
|
||||
|
||||
// Collect system partition
|
||||
let system_path = module_content_path.join("system");
|
||||
if system_path.is_dir() {
|
||||
system_lowerdir.push(system_path.display().to_string());
|
||||
info!(" + system/");
|
||||
}
|
||||
|
||||
// Collect other partitions
|
||||
for part in &partition {
|
||||
let part_path = module_content_path.join(part);
|
||||
if part_path.is_dir()
|
||||
&& let Some(v) = partition_lowerdir.get_mut(*part)
|
||||
{
|
||||
v.push(part_path.display().to_string());
|
||||
info!(" + {}/", part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Mount partitions
|
||||
info!("Mounting partitions...");
|
||||
|
||||
if let Err(e) = mount_partition("system", &system_lowerdir) {
|
||||
warn!("mount system failed: {e:#}");
|
||||
}
|
||||
|
||||
for (k, v) in partition_lowerdir {
|
||||
if let Err(e) = mount_partition(&k, &v) {
|
||||
warn!("mount {k} failed: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
info!("All partitions processed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_modules_systemlessly(_metadata_dir: &str, _content_dir: &str) -> Result<()> {
|
||||
unimplemented!("mount_modules_systemlessly is only supported on Linux/Android")
|
||||
}
|
||||
Reference in New Issue
Block a user