Initial commit

Based on 52f1f575ce/userspace/ksud/src/magic_mount.rs
This commit is contained in:
7a72 2025-11-20 16:37:57 +08:00
commit 156b452dae
13 changed files with 2138 additions and 0 deletions

3
README.md Normal file
View file

@ -0,0 +1,3 @@
## Magic Mount Metamodule
Based on https://github.com/5ec1cff/KernelSU/blob/52f1f575ce2bd0ca46ebf644fd00a838af9f344e/userspace/ksud/src/magic_mount.rs

2
magic_mount/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.cargo/

19
magic_mount/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "magic_mount_rs"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
extattr = "1"
log = "0.4"
simplelog = "0.12"
rustix = { version = "0.38", features = ["fs", "mount"] }
libc = "0.2"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1

View file

@ -0,0 +1,489 @@
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
use crate::utils::{lgetfilecon, lsetfilecon, ensure_dir_exists};
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,
};
use rustix::mount::mount_change;
use rustix::path::Arg;
use std::cmp::PartialEq;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs;
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
use std::os::unix::fs::{FileTypeExt, symlink};
use std::path::{Path, PathBuf};
const DISABLE_FILE_NAME: &str = "disable";
const REMOVE_FILE_NAME: &str = "remove";
const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
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<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
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: ToString>(name: T) -> Self {
Node {
name: name.to_string(),
file_type: Directory,
children: Default::default(),
module_path: None,
replace: false,
skip: false,
}
}
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
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(module_dir: &Path, extra_partitions: &[String]) -> Result<Option<Node>> {
let mut root = Node::new_root("");
let mut system = Node::new_root("system");
let module_root = 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(REMOVE_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 {
const BUILTIN_PARTITIONS: [(&str, bool); 4] = [
("vendor", true),
("system_ext", true),
("product", true),
("odm", false),
];
for (partition, require_symlink) in BUILTIN_PARTITIONS {
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);
}
}
}
for partition in extra_partitions {
if BUILTIN_PARTITIONS.iter().any(|(p, _)| p == partition) {
continue;
}
if partition == "system" {
continue;
}
let path_of_root = Path::new("/").join(partition);
let path_of_system = Path::new("/system").join(partition);
let require_symlink = false;
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) {
log::debug!("attach extra partition '{}' to root", name);
root.children.insert(name, node);
}
}
}
root.children.insert("system".to_string(), system);
Ok(Some(root))
} else {
Ok(None)
}
}
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
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: AsRef<Path>, WP: AsRef<Path>>(
path: P,
work_dir_path: WP,
entry: &DirEntry,
) -> Result<()> {
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: AsRef<Path>, WP: AsRef<Path>>(
path: P,
work_dir_path: WP,
current: Node,
has_tmpfs: bool,
) -> Result<()> {
let mut current = current;
let path = path.as_ref().join(&current.name);
let work_dir_path = work_dir_path.as_ref().join(&current.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) = &current.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) = &current.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) = &current.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<T: AsRef<Path>>(
tmp_path: T,
module_dir: &Path,
mount_source: &str,
extra_partitions: &[String],
) -> Result<()> {
if let Some(root) = collect_module_files(module_dir, extra_partitions)? {
log::debug!("collected: {:#?}", root);
let tmp_root = tmp_path.as_ref();
let tmp_dir = tmp_root.join("workdir");
ensure_dir_exists(&tmp_dir)?;
mount(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(())
}
}

82
magic_mount/src/main.rs Normal file
View file

@ -0,0 +1,82 @@
mod magic_mount;
mod utils;
use anyhow::{Context, Result};
use clap::Parser;
use std::path::PathBuf;
const MODULE_DIR_DEFAULT: &str = "/data/adb/modules/";
const MOUNT_SOURCE_DEFAULT: &str = "MaGIcMounT";
const LOG_FILE_DEFAULT: &str = "/data/adb/mm.log";
#[derive(Parser, Debug)]
#[command(name = "magic_mount", version, about = "Magic Mount Metamodule")]
struct Cli {
/// Module directory path
#[arg(short = 'm', long = "moduledir", default_value = MODULE_DIR_DEFAULT)]
moduledir: PathBuf,
/// Temporary directory path (auto-selected if not specified)
#[arg(short = 't', long = "tempdir")]
tempdir: Option<PathBuf>,
/// Mount source name
#[arg(short = 's', long = "mountsource", default_value = MOUNT_SOURCE_DEFAULT)]
mountsource: String,
/// Log file path
#[arg(short = 'l', long = "logfile", default_value = LOG_FILE_DEFAULT)]
logfile: PathBuf,
/// Enable verbose (debug) logging
#[arg(short = 'v', long = "verbose")]
verbose: bool,
/// Extra partitions, comma-separated, eg: -p mi_ext,my_stock
#[arg(short = 'p', long = "partitions", value_delimiter = ',')]
partitions: Vec<String>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
utils::init_logger(&cli.logfile, cli.verbose)?;
log::info!("Magic Mount Starting");
log::info!("module dir : {}", cli.moduledir.display());
let tempdir = if let Some(temp) = cli.tempdir {
log::info!("temp dir (user) : {}", temp.display());
temp
} else {
let temp = utils::select_temp_dir()
.context("failed to select temp dir automatically")?;
log::info!("temp dir (auto) : {}", temp.display());
temp
};
log::info!("mount source : {}", cli.mountsource);
log::info!("log file : {}", cli.logfile.display());
log::info!("verbose mode : {}", cli.verbose);
if !cli.partitions.is_empty() {
log::info!("extra partitions: {:?}", cli.partitions);
}
utils::ensure_temp_dir(&tempdir)?;
let result = magic_mount::magic_mount(&tempdir, &cli.moduledir, &cli.mountsource, &cli.partitions);
utils::cleanup_temp_dir(&tempdir);
match result {
Ok(_) => {
log::info!("Magic Mount Completed Successfully");
Ok(())
}
Err(e) => {
log::error!("Magic Mount Failed");
log::error!("error: {:#}", e);
Err(e)
}
}
}

159
magic_mount/src/utils.rs Normal file
View file

@ -0,0 +1,159 @@
use anyhow::{bail, Context, Result};
use log::LevelFilter;
use simplelog::{Config, WriteLogger};
use std::fs::{OpenOptions, write, remove_file, create_dir_all, remove_dir_all, read_to_string};
use std::path::{Path, PathBuf};
#[cfg(any(target_os = "linux", target_os = "android"))]
use extattr::{lsetxattr, Flags as XattrFlags};
const SELINUX_XATTR: &str = "security.selinux";
const TEMP_DIR_SUFFIX: &str = ".magic_mount";
const TMPFS_CANDIDATES: &[&str] = &["/mnt/vendor", "/mnt", "/debug_ramdisk"];
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "android"))]
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
format!(
"Failed to change SELinux context for {}",
path.as_ref().display()
)
})?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
format!(
"Failed to get SELinux context for {}",
path.as_ref().display()
)
})?;
let con = String::from_utf8_lossy(&con);
Ok(con.to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
unimplemented!()
}
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
let result = create_dir_all(&dir);
if dir.as_ref().is_dir() && result.is_ok() {
Ok(())
} else {
bail!("{} is not a regular directory", dir.as_ref().display())
}
}
pub fn init_logger(logfile: &Path, verbose: bool) -> Result<()> {
let level = if verbose {
LevelFilter::Debug
} else if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let file = OpenOptions::new()
.create(true)
.append(true)
.open(logfile)
.with_context(|| format!("failed to open log file {logfile:?}"))?;
WriteLogger::init(level, Config::default(), file)
.context("failed to init file logger")?;
log::info!("log level: {:?}", level);
Ok(())
}
fn is_writable_tmpfs(path: &Path) -> bool {
if !path.is_dir() {
log::debug!(" {} is not a directory", path.display());
return false;
}
if let Ok(mounts) = read_to_string("/proc/mounts") {
let path_str = path.to_string_lossy();
let is_tmpfs = mounts.lines().any(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
parts.len() >= 3 && parts[1] == path_str && parts[2] == "tmpfs"
});
if !is_tmpfs {
log::debug!(" {} is not a tmpfs", path.display());
return false;
}
log::debug!(" {} is a tmpfs", path.display());
} else {
log::debug!(" failed to read /proc/mounts");
}
let test_file = path.join(format!(".mm_test_{}", std::process::id()));
let writable = write(&test_file, b"test").is_ok();
if writable {
let _ = remove_file(&test_file);
log::debug!(" {} is writable", path.display());
} else {
log::debug!(" {} is not writable", path.display());
}
writable
}
pub fn select_temp_dir() -> Result<PathBuf> {
log::debug!("searching for suitable tmpfs mount point...");
for candidate in TMPFS_CANDIDATES {
let path = Path::new(candidate);
log::debug!("checking tmpfs candidate: {}", path.display());
if is_writable_tmpfs(path) {
let temp_dir = path.join(TEMP_DIR_SUFFIX);
log::info!(
"selected tmpfs: {} -> {}",
path.display(),
temp_dir.display()
);
return Ok(temp_dir);
}
}
bail!(
"no writable tmpfs found in candidates: {}",
TMPFS_CANDIDATES.join(", ")
)
}
pub fn ensure_temp_dir(temp_dir: &Path) -> Result<()> {
if temp_dir.exists() {
log::debug!("cleaning existing temp dir: {}", temp_dir.display());
remove_dir_all(temp_dir)
.with_context(|| format!("failed to clean temp dir {temp_dir:?}"))?;
}
create_dir_all(temp_dir)
.with_context(|| format!("failed to create temp dir {temp_dir:?}"))?;
log::debug!("temp dir ready: {}", temp_dir.display());
Ok(())
}
pub fn cleanup_temp_dir(temp_dir: &Path) {
if let Err(e) = remove_dir_all(temp_dir) {
log::warn!(
"failed to clean up temp dir {}: {:#}",
temp_dir.display(),
e
);
} else {
log::debug!("cleaned up temp dir: {}", temp_dir.display());
}
}

43
module/customize.sh Normal file
View file

@ -0,0 +1,43 @@
#!/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)
ui_print "- Selected architecture: ARM64"
ARCH_BINARY="mm_arm64"
;;
armeabi-v7a)
ui_print "- Selected architecture: ARM"
ARCH_BINARY="mm_arm"
;;
*)
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-mm"
# Rename the selected binary to the generic name
mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-mm" || abort "! Failed to rename binary"
# Remove the unused binary
# rm -f "$MODPATH/$REMOVE_BINARY"
# Ensure the binary is executable
chmod 755 "$MODPATH/meta-mm" || abort "! Failed to set permissions"
ui_print "- Architecture-specific binary installed successfully"
ui_print "- Installation complete"
ui_print "- Image is ready for module installations"

1271
module/magic_mount.c Normal file

File diff suppressed because it is too large Load diff

9
module/metainstall.sh Normal file
View file

@ -0,0 +1,9 @@
#!/system/bin/sh
############################################
# meta-mm metainstall.sh
############################################
# Main installation flow
ui_print "- Using meta-mm metainstall"
ui_print "- Installation complete"

35
module/metamount.sh Normal file
View file

@ -0,0 +1,35 @@
#!/system/bin/sh
# meta-overlayfs Module Mount Handler
# This script is the entry point for dual-directory module mounting
MODDIR="${0%/*}"
# Binary path (architecture-specific binary selected during installation)
BINARY="$MODDIR/meta-mm"
if [ ! -f "$BINARY" ]; then
log "ERROR: Binary not found: $BINARY"
exit 1
fi
# Set dual-directory environment variables
MODULE_METADATA_DIR="/data/adb/modules"
MODULE_SOURCE="KSU"
MODULE_METADATA_LOGFILE="/data/adb/mm.log"
if [ -f "$MODULE_METADATA_LOGFILE" ]; then
mv "$MODULE_METADATA_LOGFILE" "$MODULE_METADATA_LOGFILE".bak
fi
# Execute the mount binary
"$BINARY" -s "$MODULE_SOURCE" -m "$MODULE_METADATA_DIR" -v -l "$MODULE_METADATA_LOGFILE"
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

6
module/metauninstall.sh Normal file
View file

@ -0,0 +1,6 @@
#!/system/bin/sh
############################################
# mm-mm metauninstall.sh
############################################
exit 0

8
module/module.prop Normal file
View file

@ -0,0 +1,8 @@
id=meta-mm
metamodule=1
name=Magic Mount MetaModule
version=0.1.0
versionCode=2511190013
author=7a72
description=An implementation of a metamodule using Magic Mount, Based on MKSU, Template is from meta-overlay
updateJson=https://zrlab.org/api/m/k/mm/update.json

12
module/uninstall.sh Normal file
View file

@ -0,0 +1,12 @@
#!/system/bin/sh
############################################
# mm-mm uninstall.sh
# Cleanup script for metamodule removal
############################################
MODDIR="${0%/*}"
rm -rf /data/adb/mm.log
rm -rf /data/adb/mm.log.bak
exit 0