This commit is contained in:
7a72 2025-11-20 22:17:43 +08:00
commit ddd4a55bf6
15 changed files with 1012 additions and 1120 deletions

301
build.sh
View file

@ -9,13 +9,25 @@ NC='\033[0m' # No Color
# Default values
VERSION=""
BUILD_TYPES=()
BASE_URL="https://github.com/7a72/meta-magic_mount/releases/download"
UPDATE_JSON_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/update.json"
CHANGELOG_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/changelog.md"
# Rust targets
RUST_TARGETS=(
"x86_64-unknown-linux-musl"
"armv7-unknown-linux-musleabihf"
"aarch64-unknown-linux-musl"
)
# Show usage
usage() {
echo "Usage: $0 [--release] [--debug]"
echo " --release Build release version"
echo " --debug Build debug version"
echo " (no option) Build both release and debug versions"
echo "Usage: $0 [--release] [--debug] [--url <base_url>] [--update-json-url <url>]"
echo " --release Build release version"
echo " --debug Build debug version"
echo " --url <url> Set base URL for downloads (default: $BASE_URL)"
echo " --update-json-url Set update JSON URL (default: $UPDATE_JSON_URL)"
echo " (no option) Build both release and debug versions"
echo ""
echo "Version number will be automatically obtained from git tag"
exit 1
@ -32,6 +44,14 @@ while [[ $# -gt 0 ]]; do
BUILD_TYPES+=("debug")
shift
;;
--url)
BASE_URL="$2"
shift 2
;;
--update-json-url)
UPDATE_JSON_URL="$2"
shift 2
;;
-h|--help)
usage
;;
@ -54,6 +74,19 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
exit 1
fi
# Check if cargo is installed
if ! command -v cargo &> /dev/null; then
echo -e "${RED}Error: cargo is not installed${NC}"
exit 1
fi
# Check if cargo-zigbuild is installed
if ! cargo zigbuild --help &> /dev/null 2>&1; then
echo -e "${RED}Error: cargo-zigbuild is not installed${NC}"
echo -e "${YELLOW}Install it with: cargo install cargo-zigbuild${NC}"
exit 1
fi
# Get git tag as version number
VERSION=$(git describe --tags --exact-match 2>/dev/null)
if [ -z "$VERSION" ]; then
@ -85,6 +118,161 @@ else
fi
fi
# Function to generate changelog from git commits
generate_changelog() {
local CHANGELOG_FILE="$1"
local PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null)
echo -e "${YELLOW}Generating changelog...${NC}"
# Create changelog header
cat > "$CHANGELOG_FILE" << EOF
# Changelog
## Version $VERSION ($(date +%Y-%m-%d))
### Changes
EOF
# Add commits since last tag
if [ -n "$PREV_TAG" ]; then
echo "Changes since $PREV_TAG:" >> "$CHANGELOG_FILE"
echo "" >> "$CHANGELOG_FILE"
git log --pretty=format:"- %s (%h)" "$PREV_TAG"..HEAD >> "$CHANGELOG_FILE"
else
echo "Initial release" >> "$CHANGELOG_FILE"
echo "" >> "$CHANGELOG_FILE"
git log --pretty=format:"- %s (%h)" >> "$CHANGELOG_FILE"
fi
echo "" >> "$CHANGELOG_FILE"
echo "" >> "$CHANGELOG_FILE"
echo "---" >> "$CHANGELOG_FILE"
echo "Full Changelog: https://github.com/7a72/meta-magic_mount/compare/${PREV_TAG}...${VERSION}" >> "$CHANGELOG_FILE"
echo -e "${GREEN}Changelog generated: $CHANGELOG_FILE${NC}"
}
# Function to generate main update JSON (for release version)
generate_main_update_json() {
local OUTPUT_NAME=$1
local VERSION_CODE=$2
local JSON_FILE="build/update.json"
echo -e "${YELLOW}Generating main update JSON...${NC}"
cat > "$JSON_FILE" << EOF
{
"versionCode": $VERSION_CODE,
"version": "$VERSION",
"zipUrl": "$BASE_URL/$VERSION/$OUTPUT_NAME",
"changelog": "$CHANGELOG_URL"
}
EOF
echo -e "${GREEN}Main update JSON generated: $JSON_FILE${NC}"
}
# Function to build Rust project for all targets
build_rust_binaries() {
local BUILD_TYPE=$1
local BIN_DIR="src/bin"
echo -e "\n${YELLOW}Building Rust binaries for all targets...${NC}"
# Create bin directory if it doesn't exist
if [ -d "$BIN_DIR" ]; then
rm -rf "$BIN_DIR"
fi
mkdir -p "$BIN_DIR"
# Change to src directory
if [ ! -d "src" ]; then
echo -e "${RED}Error: src directory does not exist${NC}"
return 1
fi
cd src || return 1
# Clean previous builds
cargo clean
# Build for each target
for target in "${RUST_TARGETS[@]}"; do
echo -e "${YELLOW}Building for target: $target${NC}"
# Determine build command based on build type
if [ "$BUILD_TYPE" = "release" ]; then
cargo zigbuild --release --target "$target"
else
cargo zigbuild --target "$target"
fi
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to build for target $target${NC}"
cd ..
return 1
fi
# Determine build output directory
if [ "$BUILD_TYPE" = "release" ]; then
BUILD_OUTPUT_DIR="target/$target/release"
else
BUILD_OUTPUT_DIR="target/$target/debug"
fi
# Get the binary name from Cargo.toml (assuming single binary project)
BINARY_NAME=$(cargo metadata --format-version 1 --no-deps 2>/dev/null | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$BINARY_NAME" ]; then
echo -e "${RED}Error: Could not determine binary name${NC}"
cd ..
return 1
fi
# Copy binary to bin directory with target-specific name
# Map target to Android ABI
case "$target" in
"x86_64-unknown-linux-musl")
ABI_DIR="x86_64"
;;
"armv7-unknown-linux-musleabihf")
ABI_DIR="armeabi-v7a"
;;
"aarch64-unknown-linux-musl")
ABI_DIR="arm64-v8a"
;;
*)
echo -e "${RED}Error: Unknown target $target${NC}"
cd ..
return 1
;;
esac
# Create ABI directory
mkdir -p "bin/$ABI_DIR"
# Copy binary
if [ -f "$BUILD_OUTPUT_DIR/$BINARY_NAME" ]; then
cp "$BUILD_OUTPUT_DIR/$BINARY_NAME" "bin/$ABI_DIR/$BINARY_NAME"
# Strip binary to reduce size (for release builds)
if [ "$BUILD_TYPE" = "release" ]; then
strip "bin/$ABI_DIR/$BINARY_NAME" 2>/dev/null || true
fi
echo -e "${GREEN}Binary copied to bin/$ABI_DIR/$BINARY_NAME${NC}"
else
echo -e "${RED}Error: Binary not found at $BUILD_OUTPUT_DIR/$BINARY_NAME${NC}"
cd ..
return 1
fi
done
cd ..
echo -e "${GREEN}All Rust binaries built successfully${NC}"
return 0
}
# Function to build for a specific type
build_for_type() {
local BUILD_TYPE=$1
@ -108,7 +296,7 @@ build_for_type() {
mkdir -p "$BUILD_DIR"
# Step 1: Copy template to build directory
echo -e "\n${YELLOW}[1/6] Copying template to build directory...${NC}"
echo -e "\n${YELLOW}[1/7] Copying template to build directory...${NC}"
if [ ! -d "template" ]; then
echo -e "${RED}Error: template directory does not exist${NC}"
return 1
@ -121,66 +309,48 @@ build_for_type() {
fi
echo -e "${GREEN}Template copied${NC}"
# Step 2: Compile source code in src directory
echo -e "\n${YELLOW}[2/6] Compiling source code...${NC}"
if [ ! -d "src" ]; then
echo -e "${RED}Error: src directory does not exist${NC}"
return 1
fi
cd src || return 1
make clean
make $BUILD_TYPE VERSION="$VERSION"
# Step 2: Build Rust binaries
echo -e "\n${YELLOW}[2/7] Building Rust binaries...${NC}"
build_rust_binaries "$BUILD_TYPE"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Compilation failed${NC}"
cd ..
echo -e "${RED}Error: Rust build failed${NC}"
return 1
fi
cd ..
echo -e "${GREEN}Compilation completed${NC}"
echo -e "${GREEN}Rust build completed${NC}"
# Step 3: Modify log level in metamount.sh (in build directory)
echo -e "\n${YELLOW}[3/6] Modifying metamount.sh configuration...${NC}"
echo -e "\n${YELLOW}[3/7] Modifying metamount.sh configuration...${NC}"
if [ ! -f "$BUILD_DIR/metamount.sh" ]; then
echo -e "${RED}Error: $BUILD_DIR/metamount.sh does not exist${NC}"
return 1
fi
if [ "$BUILD_TYPE" = "debug" ]; then
LOG_LEVEL=3
else
LOG_LEVEL=2
fi
# Use sed to modify log level
sed -i "s/MODULE_METADATA_LOGLEVEL=.*/MODULE_METADATA_LOGLEVEL=$LOG_LEVEL/" "$BUILD_DIR/metamount.sh"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to modify metamount.sh${NC}"
return 1
fi
echo -e "${GREEN}metamount.sh configuration completed (LOG_LEVEL=$LOG_LEVEL)${NC}"
# Step 4: Modify module.prop (in build directory)
echo -e "\n${YELLOW}[4/6] Modifying module.prop...${NC}"
echo -e "\n${YELLOW}[4/7] Modifying module.prop...${NC}"
if [ ! -f "$BUILD_DIR/module.prop" ]; then
echo -e "${RED}Error: $BUILD_DIR/module.prop does not exist${NC}"
return 1
fi
# Generate versionCode (format: yymmddhhmm)
VERSION_CODE=$(date +%y%m%d%H%M)
# Generate versionCode
VERSION_CODE=$(git rev-list --count HEAD)
# Modify version and versionCode
sed -i "s/^version=.*/version=$VERSION/" "$BUILD_DIR/module.prop"
sed -i "s/^versionCode=.*/versionCode=$VERSION_CODE/" "$BUILD_DIR/module.prop"
# Modify version, versionCode, and updateJson
sed -i "s|^version=.*|version=$VERSION|" "$BUILD_DIR/module.prop"
sed -i "s|^versionCode=.*|versionCode=$VERSION_CODE|" "$BUILD_DIR/module.prop"
sed -i "s|^updateJson=.*|updateJson=$UPDATE_JSON_URL|" "$BUILD_DIR/module.prop"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to modify module.prop${NC}"
return 1
fi
echo -e "${GREEN}module.prop configuration completed (version=$VERSION, versionCode=$VERSION_CODE)${NC}"
echo -e "${GREEN}module.prop configuration completed${NC}"
echo -e "${GREEN} version=$VERSION${NC}"
echo -e "${GREEN} versionCode=$VERSION_CODE${NC}"
echo -e "${GREEN} updateJson=$UPDATE_JSON_URL${NC}"
# Step 5: Copy bin directory to build directory
echo -e "\n${YELLOW}[5/6] Copying bin directory...${NC}"
echo -e "\n${YELLOW}[5/7] Copying bin directory...${NC}"
if [ ! -d "src/bin" ]; then
echo -e "${RED}Error: src/bin directory does not exist${NC}"
return 1
@ -194,14 +364,10 @@ build_for_type() {
echo -e "${GREEN}bin directory copied${NC}"
# Step 6: Package as zip
echo -e "\n${YELLOW}[6/6] Packaging module...${NC}"
echo -e "\n${YELLOW}[6/7] Packaging module...${NC}"
# Build filename
if [ -n "$GIT_SUFFIX" ]; then
OUTPUT_NAME="meta-magic_mount-${VERSION}-${BUILD_TYPE}-${VERSION_CODE}-${GIT_SUFFIX}.zip"
else
OUTPUT_NAME="meta-magic_mount-${VERSION}-${BUILD_TYPE}-${VERSION_CODE}.zip"
fi
OUTPUT_NAME="meta-magic_mount-${VERSION}-${VERSION_CODE}-${GIT_SUFFIX}-${BUILD_TYPE}.zip"
# Enter build directory and package
cd "$BUILD_DIR" || return 1
@ -213,6 +379,11 @@ build_for_type() {
fi
cd ../..
# Step 7: Generate update JSON for this build
if [ "$BUILD_TYPE" = "release" ]; then
generate_main_update_json "$OUTPUT_NAME" "$VERSION_CODE"
fi
# Clean up temporary build directory
rm -rf "$BUILD_DIR"
@ -224,8 +395,16 @@ build_for_type() {
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Build completed!${NC}"
echo -e "${GREEN}Output file: build/$OUTPUT_NAME${NC}"
echo -e "${GREEN}Update JSON: build/update_${BUILD_TYPE}.json${NC}"
if [ "$BUILD_TYPE" = "release" ]; then
echo -e "${GREEN}Main update JSON: build/update.json${NC}"
fi
echo -e "${GREEN}========================================${NC}"
# Export for summary
LAST_OUTPUT_NAME="$OUTPUT_NAME"
LAST_VERSION_CODE="$VERSION_CODE"
return 0
}
@ -234,9 +413,14 @@ if [ ! -d "build" ]; then
mkdir build
fi
# Generate changelog once for all builds
generate_changelog "build/changelog.md"
# Build for each specified type
SUCCESS_COUNT=0
FAIL_COUNT=0
LAST_OUTPUT_NAME=""
LAST_VERSION_CODE=""
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
build_for_type "$BUILD_TYPE"
@ -251,11 +435,30 @@ done
# Print summary
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Build Summary${NC}"
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Version: $VERSION${NC}"
echo -e "${GREEN}Total builds: ${#BUILD_TYPES[@]}${NC}"
echo -e "${GREEN}Successful: $SUCCESS_COUNT${NC}"
if [ $FAIL_COUNT -gt 0 ]; then
echo -e "${RED}Failed: $FAIL_COUNT${NC}"
fi
echo -e "${GREEN}----------------------------------------${NC}"
echo -e "${GREEN}Generated files:${NC}"
echo -e "${GREEN} - Changelog: build/changelog.md${NC}"
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
echo -e "${GREEN} - Update JSON (${BUILD_TYPE}): build/update_${BUILD_TYPE}.json${NC}"
done
# Check if release was built
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
if [ "$BUILD_TYPE" = "release" ]; then
echo -e "${GREEN} - Main update JSON: build/update.json${NC}"
break
fi
done
echo -e "${GREEN} - ZIP packages: build/*.zip${NC}"
echo -e "${GREEN}========================================${NC}"
echo -e "${YELLOW}Note: Update JSON URL in module.prop: $UPDATE_JSON_URL${NC}"
echo -e "${YELLOW}Make sure to upload update.json to the correct location!${NC}"
echo -e "${GREEN}========================================${NC}"
# Exit with error if any build failed
@ -263,4 +466,4 @@ if [ $FAIL_COUNT -gt 0 ]; then
exit 1
fi
exit 0
exit 0

5
src/.gitignore vendored
View file

@ -1 +1,4 @@
/bin
/target
/out
Cargo.lock
*.log

19
src/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "magic_mount_rs"
version = "0.1.1"
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

@ -1,87 +0,0 @@
PROJECT = magic_mount
VERSION ?= 0.1.0
# use zig cc
ZIG ?= zig
CC := $(ZIG) cc
STRIPPER := strip
# source files
SRCS := utils.c magic_mount.c main.c
# output directory
OUTDIR := bin
# common flags
CFLAGS_COMMON := -D_POSIX_C_SOURCE=200809L -std=c23 -Wall -Wextra -pedantic
CFLAGS_COMMON += -DVERSION=\"$(VERSION)\"
LDFLAGS_COMMON := -static
# build mode specific flags
CFLAGS_RELEASE := -O2 -DNDEBUG
CFLAGS_DEBUG := -O0 -g -DDEBUG
# zig target triples
TARGET_AMD64 ?= x86_64-linux-musl
TARGET_ARM64 ?= aarch64-linux-musl
TARGET_ARMV7 ?= arm-linux-musleabihf
# output binary names
BIN_AMD64 := $(OUTDIR)/mm_amd64
BIN_ARM64 := $(OUTDIR)/mm_arm64
BIN_ARMV7 := $(OUTDIR)/mm_armv7
BINS := $(BIN_AMD64) $(BIN_ARM64) $(BIN_ARMV7)
.PHONY: all clean release debug amd64 arm64 armv7 dirs help strip-bins
# default target
all: release
help:
@echo "Usage:"
@echo " make release [version=X.Y.Z] - Build release version (optimized, stripped)"
@echo " make debug [version=X.Y.Z] - Build debug version (with symbols)"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Individual targets:"
@echo " make amd64 - Build for x86_64"
@echo " make arm64 - Build for aarch64"
@echo " make armv7 - Build for armv7"
dirs:
mkdir -p $(OUTDIR)
# Release build (default)
release: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_RELEASE)
release: dirs $(BINS) strip-bins
@echo "Release build completed: $(VERSION)"
# Debug build
debug: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_DEBUG)
debug: dirs $(BINS)
@echo "Debug build completed: $(VERSION)"
strip-bins:
@echo "Stripping binaries..."
@for bin in $(BINS); do \
echo " Stripping $$bin"; \
$(STRIPPER) $$bin; \
done
amd64: $(BIN_AMD64)
arm64: $(BIN_ARM64)
armv7: $(BIN_ARMV7)
$(BIN_AMD64): $(SRCS)
$(CC) -target $(TARGET_AMD64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARM64): $(SRCS)
$(CC) -target $(TARGET_ARM64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARMV7): $(SRCS)
$(CC) -target $(TARGET_ARMV7) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
clean:
rm -rf $(OUTDIR)

View file

@ -1,702 +0,0 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/xattr.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "magic_mount.h"
#include "utils.h"
typedef enum {
NFT_REGULAR,
NFT_DIRECTORY,
NFT_SYMLINK,
NFT_WHITEOUT
} NodeType;
typedef struct Node {
char *name;
NodeType type;
struct Node **children;
size_t child_count;
char *module_path;
char *module_name;
bool replace;
bool skip;
bool done;
} Node;
typedef struct {
int modules_total;
int nodes_total;
int nodes_mounted;
int nodes_skipped;
int nodes_whiteout;
int nodes_fail;
} Stats;
static Stats g_stats = {0};
Config g_config = {
.module_dir = DEFAULT_MODULE_DIR,
.mount_source = MOUNT_SOURCE,
.log_file = NULL,
.log_level = 2,
.failed_modules = NULL,
.failed_count = 0,
.extra_parts = NULL,
.extra_count = 0
};
void magic_log_write(LogLevel level, const char *fmt, ...) {
if ((int)level > g_config.log_level) return;
static const char *level_str[] = {"ERROR", "WARN", "INFO", "DEBUG"};
FILE *out = g_config.log_file ? g_config.log_file : stderr;
fprintf(out, "[%s] ", level_str[level]);
va_list ap;
va_start(ap, fmt);
vfprintf(out, fmt, ap);
va_end(ap);
fputc('\n', out);
fflush(out);
}
static int set_selinux_context(const char *path, const char *ctx) {
if (!path || !ctx) return 0;
LOGD("set_selinux(%s, \"%s\")", path, ctx);
if (lsetxattr(path, XATTR_SELINUX, ctx, strlen(ctx), 0) < 0) {
LOGW("set_selinux %s: %s", path, strerror(errno));
return -1;
}
return 0;
}
static char *get_selinux_context(const char *path) {
if (!path) return NULL;
ssize_t sz = lgetxattr(path, XATTR_SELINUX, NULL, 0);
if (sz <= 0) return NULL;
char *buf = malloc((size_t)sz + 1);
if (!buf) return NULL;
if (lgetxattr(path, XATTR_SELINUX, buf, (size_t)sz) != sz) {
free(buf);
return NULL;
}
buf[sz] = '\0';
LOGD("get_selinux(%s) -> \"%s\"", path, buf);
return buf;
}
static void copy_selinux_context(const char *src, const char *dst) {
char *ctx = get_selinux_context(src);
if (ctx) {
set_selinux_context(dst, ctx);
free(ctx);
}
}
static Node *node_new(const char *name, NodeType type) {
Node *n = calloc(1, sizeof(Node));
if (!n) return NULL;
n->name = strdup(name ? name : "");
n->type = type;
return n;
}
static void node_free(Node *n) {
if (!n) return;
for (size_t i = 0; i < n->child_count; i++)
node_free(n->children[i]);
free(n->children);
free(n->name);
free(n->module_path);
free(n->module_name);
free(n);
}
static NodeType get_node_type(const struct stat *st) {
if (S_ISCHR(st->st_mode) && st->st_rdev == 0) return NFT_WHITEOUT;
if (S_ISREG(st->st_mode)) return NFT_REGULAR;
if (S_ISDIR(st->st_mode)) return NFT_DIRECTORY;
if (S_ISLNK(st->st_mode)) return NFT_SYMLINK;
return NFT_WHITEOUT;
}
static bool is_dir_opaque(const char *path) {
#ifdef __linux__
char buf[8];
ssize_t len = lgetxattr(path, XATTR_OPAQUE, buf, sizeof(buf) - 1);
return len > 0 && buf[0] == 'y';
#else
(void)path;
return false;
#endif
}
static int node_add_child(Node *parent, Node *child) {
Node **arr = realloc(parent->children, (parent->child_count + 1) * sizeof(Node*));
if (!arr) return -1;
parent->children = arr;
parent->children[parent->child_count++] = child;
return 0;
}
static Node *node_find_child(Node *parent, const char *name) {
for (size_t i = 0; i < parent->child_count; i++)
if (strcmp(parent->children[i]->name, name) == 0)
return parent->children[i];
return NULL;
}
static Node *node_take_child(Node *parent, const char *name) {
for (size_t i = 0; i < parent->child_count; i++) {
if (strcmp(parent->children[i]->name, name) == 0) {
Node *n = parent->children[i];
memmove(&parent->children[i], &parent->children[i + 1],
(parent->child_count - i - 1) * sizeof(Node*));
parent->child_count--;
return n;
}
}
return NULL;
}
static Node *create_node_from_path(const char *name, const char *path,
const char *module_name) {
struct stat st;
if (lstat(path, &st) < 0) return NULL;
if (!(S_ISCHR(st.st_mode) || S_ISREG(st.st_mode) ||
S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
return NULL;
NodeType type = get_node_type(&st);
Node *n = node_new(name, type);
if (!n) return NULL;
n->module_path = strdup(path);
n->module_name = module_name ? strdup(module_name) : NULL;
n->replace = (type == NFT_DIRECTORY) && is_dir_opaque(path);
g_stats.nodes_total++;
return n;
}
static bool is_module_disabled(const char *mod_dir) {
char buf[PATH_MAX];
const char *disable_files[] = {DISABLE_FILE, REMOVE_FILE, SKIP_MOUNT_FILE};
for (size_t i = 0; i < ARR_LEN(disable_files); i++) {
if (path_join(buf, sizeof(buf), mod_dir, disable_files[i]) == 0 &&
path_exists(buf))
return true;
}
return false;
}
static int collect_module_files(Node *node, const char *dir,
const char *module_name, bool *has_any) {
DIR *d = opendir(dir);
if (!d) {
LOGE("opendir %s: %s", dir, strerror(errno));
return -1;
}
char path[PATH_MAX];
struct dirent *de;
bool any = false;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (path_join(path, sizeof(path), dir, de->d_name) != 0) {
closedir(d);
return -1;
}
Node *child = node_find_child(node, de->d_name);
if (!child) {
child = create_node_from_path(de->d_name, path, module_name);
if (child && node_add_child(node, child) != 0) {
node_free(child);
closedir(d);
return -1;
}
}
if (child && child->type == NFT_DIRECTORY) {
bool sub = false;
if (collect_module_files(child, path, module_name, &sub) != 0) {
closedir(d);
return -1;
}
if (sub || child->replace) any = true;
} else if (child) {
any = true;
}
}
closedir(d);
*has_any = any;
return 0;
}
static int add_partition_nodes(Node *root, Node *system) {
struct { const char *name; bool need_symlink; } parts[] = {
{"vendor", true},
{"system_ext", true},
{"product", true},
{"odm", false}
};
char root_path[PATH_MAX], sys_path[PATH_MAX];
for (size_t i = 0; i < ARR_LEN(parts); i++) {
if (path_join(root_path, sizeof(root_path), "/", parts[i].name) != 0 ||
path_join(sys_path, sizeof(sys_path), "/system", parts[i].name) != 0)
return -1;
if (!is_dir(root_path)) continue;
if (parts[i].need_symlink && !is_symlink(sys_path)) continue;
Node *child = node_take_child(system, parts[i].name);
if (child && node_add_child(root, child) != 0) {
node_free(child);
return -1;
}
}
for (int i = 0; i < g_config.extra_count; i++) {
if (path_join(root_path, sizeof(root_path), "/", g_config.extra_parts[i]) != 0)
return -1;
if (!is_dir(root_path)) continue;
Node *child = node_take_child(system, g_config.extra_parts[i]);
if (child && node_add_child(root, child) != 0) {
node_free(child);
return -1;
}
}
return 0;
}
static Node *scan_modules(void) {
Node *root = node_new("", NFT_DIRECTORY);
Node *system = node_new("system", NFT_DIRECTORY);
if (!root || !system) {
node_free(root);
node_free(system);
return NULL;
}
DIR *d = opendir(g_config.module_dir);
if (!d) {
LOGE("opendir %s: %s", g_config.module_dir, strerror(errno));
node_free(root);
node_free(system);
return NULL;
}
char mod_path[PATH_MAX], sys_path[PATH_MAX];
struct dirent *de;
bool has_any = false;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (path_join(mod_path, sizeof(mod_path), g_config.module_dir, de->d_name) != 0)
continue;
if (!is_dir(mod_path) || is_module_disabled(mod_path)) continue;
if (path_join(sys_path, sizeof(sys_path), mod_path, "system") != 0)
continue;
if (!is_dir(sys_path)) continue;
LOGD("scanning module: %s", de->d_name);
g_stats.modules_total++;
bool sub = false;
if (collect_module_files(system, sys_path, de->d_name, &sub) == 0 && sub)
has_any = true;
}
closedir(d);
if (!has_any) {
node_free(root);
node_free(system);
return NULL;
}
g_stats.nodes_total += 2; /* root + system */
if (add_partition_nodes(root, system) != 0) {
node_free(root);
node_free(system);
return NULL;
}
if (node_add_child(root, system) != 0) {
node_free(root);
node_free(system);
return NULL;
}
return root;
}
static int clone_symlink(const char *src, const char *dst) {
char target[PATH_MAX];
ssize_t len = readlink(src, target, sizeof(target) - 1);
if (len < 0) {
LOGE("readlink %s: %s", src, strerror(errno));
return -1;
}
target[len] = '\0';
if (symlink(target, dst) < 0) {
LOGE("symlink %s->%s: %s", dst, target, strerror(errno));
return -1;
}
copy_selinux_context(src, dst);
LOGD("clone_symlink: %s -> %s", dst, target);
return 0;
}
static int mirror_file(const char *src, const char *dst, const struct stat *st) {
int fd = open(dst, O_CREAT | O_WRONLY, st->st_mode & 07777);
if (fd < 0) {
LOGE("create %s: %s", dst, strerror(errno));
return -1;
}
close(fd);
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", src, dst, strerror(errno));
return -1;
}
return 0;
}
static int mirror_dir(const char *src, const char *dst, const struct stat *st);
static int mirror_entry(const char *src_base, const char *dst_base, const char *name) {
char src[PATH_MAX], dst[PATH_MAX];
if (path_join(src, sizeof(src), src_base, name) != 0 ||
path_join(dst, sizeof(dst), dst_base, name) != 0)
return -1;
struct stat st;
if (lstat(src, &st) < 0) {
LOGW("lstat %s: %s", src, strerror(errno));
return 0;
}
if (S_ISREG(st.st_mode)) return mirror_file(src, dst, &st);
else if (S_ISDIR(st.st_mode)) return mirror_dir(src, dst, &st);
else if (S_ISLNK(st.st_mode)) return clone_symlink(src, dst);
return 0;
}
static int mirror_dir(const char *src, const char *dst, const struct stat *st) {
if (mkdir(dst, st->st_mode & 07777) < 0 && errno != EEXIST) {
LOGE("mkdir %s: %s", dst, strerror(errno));
return -1;
}
chmod(dst, st->st_mode & 07777);
chown(dst, st->st_uid, st->st_gid);
copy_selinux_context(src, dst);
DIR *d = opendir(src);
if (!d) {
LOGE("opendir %s: %s", src, strerror(errno));
return -1;
}
struct dirent *de;
int ret = 0;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (mirror_entry(src, dst, de->d_name) != 0) {
ret = -1;
break;
}
}
closedir(d);
return ret;
}
static int mount_regular_file(Node *node, const char *path,
const char *wpath, bool has_tmpfs) {
const char *target = has_tmpfs ? wpath : path;
if (has_tmpfs) {
int fd = open(wpath, O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
LOGE("create %s: %s", wpath, strerror(errno));
return -1;
}
close(fd);
}
if (!node->module_path) {
LOGE("no module_path for %s", path);
return -1;
}
LOGD("bind %s -> %s", node->module_path, target);
if (mount(node->module_path, target, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", node->module_path, target, strerror(errno));
return -1;
}
mount(NULL, target, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
g_stats.nodes_mounted++;
return 0;
}
static int mount_directory(Node *node, const char *path,
const char *wpath, bool has_tmpfs);
static int process_child(Node *child, const char *path,
const char *wpath, bool has_tmpfs) {
if (child->skip) {
child->done = true;
return 0;
}
child->done = true;
return mount_directory(child, path, wpath, has_tmpfs);
}
static bool need_tmpfs_for_child(Node *child, const char *real_path) {
char path[PATH_MAX];
if (path_join(path, sizeof(path), real_path, child->name) != 0)
return false;
if (child->type == NFT_SYMLINK) return true;
if (child->type == NFT_WHITEOUT) return path_exists(path);
struct stat st;
if (lstat(path, &st) < 0) return true;
NodeType real_type = get_node_type(&st);
return real_type != child->type || real_type == NFT_SYMLINK;
}
static bool check_need_tmpfs(Node *node, const char *path, bool has_tmpfs) {
if (!has_tmpfs && node->replace && node->module_path)
return true;
for (size_t i = 0; i < node->child_count; i++)
if (need_tmpfs_for_child(node->children[i], path))
return true;
return false;
}
static int setup_tmpfs_dir(const char *wpath, const char *path,
const char *module_path) {
if (mkdir_recursive(wpath) != 0) return -1;
struct stat st;
const char *meta = NULL;
if (stat(path, &st) == 0) meta = path;
else if (module_path && stat(module_path, &st) == 0) meta = module_path;
else {
LOGE("no metadata for %s", path);
return -1;
}
chmod(wpath, st.st_mode & 07777);
chown(wpath, st.st_uid, st.st_gid);
copy_selinux_context(meta, wpath);
return 0;
}
static int mount_directory(Node *node, const char *base_path,
const char *base_work, bool has_tmpfs) {
char path[PATH_MAX], wpath[PATH_MAX];
if (path_join(path, sizeof(path), base_path, node->name) != 0 ||
path_join(wpath, sizeof(wpath), base_work, node->name) != 0)
return -1;
bool create_tmpfs = check_need_tmpfs(node, path, has_tmpfs);
bool now_tmpfs = has_tmpfs || create_tmpfs;
if (now_tmpfs && setup_tmpfs_dir(wpath, path, node->module_path) != 0)
return -1;
if (create_tmpfs &&
mount(wpath, wpath, NULL, MS_BIND, NULL) < 0) {
LOGE("bind self %s: %s", wpath, strerror(errno));
return -1;
}
/* 处理原目录内容 */
if (path_exists(path) && !node->replace) {
DIR *d = opendir(path);
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
Node *child = node_find_child(node, de->d_name);
int ret = child
? process_child(child, path, wpath, now_tmpfs)
: (now_tmpfs ? mirror_entry(path, wpath, de->d_name) : 0);
if (ret != 0) {
const char *mod = (child && child->module_name)
? child->module_name : node->module_name;
if (mod) {
LOGE("child %s/%s failed (module: %s)", path, de->d_name, mod);
add_to_array(&g_config.failed_modules,
&g_config.failed_count, mod);
}
g_stats.nodes_fail++;
if (now_tmpfs) {
closedir(d);
return -1;
}
}
}
closedir(d);
} else if (now_tmpfs) {
LOGE("opendir %s: %s", path, strerror(errno));
return -1;
}
}
for (size_t i = 0; i < node->child_count; i++) {
Node *child = node->children[i];
if (child->skip || child->done) continue;
if (process_child(child, path, wpath, now_tmpfs) != 0) {
const char *mod = child->module_name ? child->module_name : node->module_name;
if (mod) {
LOGE("child %s/%s failed (module: %s)", path, child->name, mod);
add_to_array(&g_config.failed_modules, &g_config.failed_count, mod);
}
g_stats.nodes_fail++;
if (now_tmpfs) return -1;
}
}
if (create_tmpfs) {
mount(NULL, wpath, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
if (mount(wpath, path, NULL, MS_MOVE, NULL) < 0) {
LOGE("move %s->%s: %s", wpath, path, strerror(errno));
if (node->module_name)
add_to_array(&g_config.failed_modules,
&g_config.failed_count, node->module_name);
return -1;
}
LOGI("moved tmpfs: %s -> %s", wpath, path);
mount(NULL, path, NULL, MS_REC | MS_PRIVATE, NULL);
}
g_stats.nodes_mounted++;
return 0;
}
static int mount_node(Node *node, const char *path,
const char *wpath, bool has_tmpfs) {
switch (node->type) {
case NFT_REGULAR:
return mount_regular_file(node, path, wpath, has_tmpfs);
case NFT_SYMLINK:
if (!node->module_path) {
LOGE("no module_path for symlink %s", path);
return -1;
}
if (clone_symlink(node->module_path, wpath) == 0) {
g_stats.nodes_mounted++;
return 0;
}
return -1;
case NFT_WHITEOUT:
LOGD("whiteout: %s", path);
g_stats.nodes_whiteout++;
return 0;
case NFT_DIRECTORY:
return mount_directory(node, "/", wpath, has_tmpfs);
}
return 0;
}
const char *magic_mount_select_temp_dir(char *buf) {
const char *candidates[] = {"/mnt/vendor", "/mnt", "/debug_ramdisk"};
for (size_t i = 0; i < ARR_LEN(candidates); i++) {
if (!is_rw_tmpfs(candidates[i])) continue;
if (path_join(buf, PATH_MAX, candidates[i], ".magic_mount") == 0) {
LOGI("auto temp_dir: %s (from %s)", buf, candidates[i]);
return buf;
}
}
LOGW("no rw tmpfs, using fallback: %s", DEFAULT_TEMP_DIR);
return DEFAULT_TEMP_DIR;
}
int magic_mount_run(const char *temp_root) {
Node *root = scan_modules();
if (!root) {
LOGI("no modules found");
return 0;
}
char tmpfs_dir[PATH_MAX];
if (path_join(tmpfs_dir, sizeof(tmpfs_dir), temp_root, "workdir") != 0) {
node_free(root);
return -1;
}
if (mkdir_recursive(tmpfs_dir) != 0) {
node_free(root);
return -1;
}
LOGI("mounting tmpfs: %s (source=%s)", tmpfs_dir, g_config.mount_source);
if (mount(g_config.mount_source, tmpfs_dir, "tmpfs", 0, "") < 0) {
LOGE("mount tmpfs %s: %s", tmpfs_dir, strerror(errno));
node_free(root);
return -1;
}
mount(NULL, tmpfs_dir, NULL, MS_REC | MS_PRIVATE, NULL);
int ret = mount_node(root, "/", tmpfs_dir, false);
if (ret != 0) g_stats.nodes_fail++;
umount2(tmpfs_dir, MNT_DETACH);
rmdir(tmpfs_dir);
node_free(root);
return ret;
}
int magic_mount_enter_pid1_mntns(void) {
int fd = open("/proc/1/ns/mnt", O_RDONLY);
if (fd < 0) {
LOGE("open /proc/1/ns/mnt: %s", strerror(errno));
return -1;
}
if (setns(fd, 0) < 0) {
LOGE("setns: %s", strerror(errno));
close(fd);
return -1;
}
close(fd);
LOGI("entered pid1 mount namespace");
return 0;
}
void magic_mount_print_summary(void) {
LOGI("summary: modules=%d nodes=%d mounted=%d skipped=%d whiteouts=%d failures=%d",
g_stats.modules_total, g_stats.nodes_total, g_stats.nodes_mounted,
g_stats.nodes_skipped, g_stats.nodes_whiteout, g_stats.nodes_fail);
if (g_config.failed_count > 0) {
LOGW("failed modules (%d):", g_config.failed_count);
for (int i = 0; i < g_config.failed_count; i++)
LOGW(" - %s", g_config.failed_modules[i]);
}
}
void magic_mount_parse_partitions(const char *list) {
const char *p = list;
while (*p) {
const char *start = p;
while (*p && *p != ',') p++;
size_t len = (size_t)(p - start);
while (len > 0 && (start[0] == ' ' || start[0] == '\t')) {
start++;
len--;
}
while (len > 0 && (start[len-1] == ' ' || start[len-1] == '\t'))
len--;
if (len > 0) {
char *name = malloc(len + 1);
if (name) {
memcpy(name, start, len);
name[len] = '\0';
add_to_array(&g_config.extra_parts, &g_config.extra_count, name);
free(name);
}
}
if (*p == ',') p++;
}
}

View file

@ -1,55 +0,0 @@
#ifndef MAGIC_MOUNT_H
#define MAGIC_MOUNT_H
#include <stdio.h>
#define DEFAULT_MODULE_DIR "/data/adb/modules"
#define DEFAULT_TEMP_DIR "/dev/.magic_mount"
#define MOUNT_SOURCE "KSU"
#define DISABLE_FILE "disable"
#define REMOVE_FILE "remove"
#define SKIP_MOUNT_FILE "skip_mount"
#define XATTR_OPAQUE "trusted.overlay.opaque"
#define XATTR_SELINUX "security.selinux"
#define ARR_LEN(a) (sizeof(a) / sizeof((a)[0]))
typedef enum {
LOG_ERROR = 0,
LOG_WARN = 1,
LOG_INFO = 2,
LOG_DEBUG = 3
} LogLevel;
typedef struct {
const char *module_dir;
const char *mount_source;
FILE *log_file;
int log_level;
char **failed_modules;
int failed_count;
char **extra_parts;
int extra_count;
} Config;
extern Config g_config;
// shim
int setns(int fd, int nstype);
void magic_log_write(LogLevel level, const char *fmt, ...);
#define LOGE(...) magic_log_write(LOG_ERROR, __VA_ARGS__)
#define LOGW(...) magic_log_write(LOG_WARN, __VA_ARGS__)
#define LOGI(...) magic_log_write(LOG_INFO, __VA_ARGS__)
#define LOGD(...) magic_log_write(LOG_DEBUG, __VA_ARGS__)
const char *magic_mount_select_temp_dir(char *buf);
int magic_mount_run(const char *temp_root);
int magic_mount_enter_pid1_mntns(void);
void magic_mount_print_summary(void);
void magic_mount_parse_partitions(const char *list);
#endif /* MAGIC_MOUNT_H */

View file

@ -1,104 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "magic_mount.h"
#include "utils.h"
static void usage(const char *prog) {
fprintf(stderr,
"Magic Mount v%s\n"
"Usage: %s [options]\n"
" -m DIR module directory (default: %s)\n"
" -t DIR temp directory (default: auto)\n"
" -s SRC mount source (default: %s)\n"
" -p LIST extra partitions (-p mi_ext,my_stock)\n"
" -l FILE log file ('-' for stdout, default: stderr)\n"
" -v N log level (0-3, default: 2)\n"
" -h show this help\n",
VERSION, prog, DEFAULT_MODULE_DIR, MOUNT_SOURCE);
}
int main(int argc, char **argv) {
const char *temp_dir = NULL;
const char *log_path = NULL;
char auto_temp[PATH_MAX] = {0};
int ret = 1;
/* 解析参数 */
for (int i = 1; i < argc; i++) {
if ((strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--module-dir") == 0) &&
i + 1 < argc) {
g_config.module_dir = argv[++i];
} else if ((strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--temp-dir") == 0) &&
i + 1 < argc) {
temp_dir = argv[++i];
} else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--mount-source") == 0) &&
i + 1 < argc) {
g_config.mount_source = argv[++i];
} else if ((strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--log-file") == 0) &&
i + 1 < argc) {
log_path = argv[++i];
} else if ((strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) &&
i + 1 < argc) {
g_config.log_level = atoi(argv[++i]);
if (g_config.log_level < 0) g_config.log_level = 0;
if (g_config.log_level > 3) g_config.log_level = 3;
} else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--partitions") == 0) &&
i + 1 < argc) {
magic_mount_parse_partitions(argv[++i]);
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
usage(argv[0]);
return 0;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
usage(argv[0]);
return 1;
}
}
/* 打开日志文件 */
if (log_path) {
g_config.log_file = strcmp(log_path, "-") == 0 ?
stdout : fopen(log_path, "a");
if (!g_config.log_file && strcmp(log_path, "-") != 0) {
fprintf(stderr, "Failed to open log: %s\n", strerror(errno));
return 1;
}
}
/* 选择临时目录 */
if (!temp_dir)
temp_dir = magic_mount_select_temp_dir(auto_temp);
/* 权限检查 */
if (geteuid() != 0) {
fprintf(stderr, "must run as root\n");
goto cleanup;
}
/* 进入 PID1 命名空间 */
if (magic_mount_enter_pid1_mntns() != 0)
goto cleanup;
/* 执行挂载 */
fprintf(stderr, "starting: module_dir=%s temp_dir=%s source=%s level=%d\n",
g_config.module_dir, temp_dir, g_config.mount_source, g_config.log_level);
ret = magic_mount_run(temp_dir);
fprintf(stderr, "%s\n", ret == 0 ? "completed successfully" : "failed");
magic_mount_print_summary();
cleanup:
if (g_config.log_file && g_config.log_file != stdout &&
g_config.log_file != stderr)
fclose(g_config.log_file);
free_str_array(&g_config.failed_modules, &g_config.failed_count);
free_str_array(&g_config.extra_parts, &g_config.extra_count);
return ret == 0 ? 0 : 1;
}

489
src/src/magic_mount.rs Normal file
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
src/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
src/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());
}
}

View file

@ -1,87 +0,0 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "utils.h"
int path_join(char *buf, size_t n, const char *base, const char *name) {
if (!base) return -1;
if (!name || !name[0]) return snprintf(buf, n, "%s", base) >= (int)n ? -1 : 0;
size_t len = strlen(base);
bool has_slash = (len > 0 && base[len-1] == '/');
return snprintf(buf, n, has_slash ? "%s%s" : "%s/%s", base, name) >= (int)n ? -1 : 0;
}
bool path_exists(const char *p) { return access(p, F_OK) == 0; }
bool is_dir(const char *p) {
struct stat st;
return stat(p, &st) == 0 && S_ISDIR(st.st_mode);
}
bool is_symlink(const char *p) {
struct stat st;
return lstat(p, &st) == 0 && S_ISLNK(st.st_mode);
}
int mkdir_recursive(const char *dir) {
if (!dir || !dir[0]) return -1;
if (is_dir(dir)) return 0;
char tmp[PATH_MAX];
if (snprintf(tmp, sizeof(tmp), "%s", dir) >= (int)sizeof(tmp)) return -1;
char *p = strrchr(tmp, '/');
if (p && p != tmp) {
*p = 0;
if (mkdir_recursive(tmp) != 0) return -1;
}
return (mkdir(dir, 0755) == 0 || errno == EEXIST) ? 0 : -1;
}
bool is_rw_tmpfs(const char *path) {
struct statfs s;
if (!is_dir(path) || statfs(path, &s) < 0) return false;
if ((unsigned long)s.f_type != TMPFS_MAGIC) return false;
char tmp[PATH_MAX];
if (path_join(tmp, sizeof(tmp), path, ".test_XXXXXX") != 0) return false;
int fd = mkstemp(tmp);
if (fd < 0) return false;
close(fd);
unlink(tmp);
return true;
}
void free_str_array(char ***arr, int *count) {
if (!arr || !*arr) return;
for (int i = 0; i < *count; i++) free((*arr)[i]);
free(*arr);
*arr = NULL;
*count = 0;
}
void add_to_array(char ***arr, int *count, const char *str) {
for (int i = 0; i < *count; i++)
if (strcmp((*arr)[i], str) == 0) return;
char **new_arr = realloc(*arr, (size_t)(*count + 1) * sizeof(char*));
if (!new_arr) return;
*arr = new_arr;
(*arr)[*count] = strdup(str);
if ((*arr)[*count]) (*count)++;
}
bool is_dot_or_dotdot(const char *n) {
return n && (!strcmp(n, ".") || !strcmp(n, ".."));
}

View file

@ -1,27 +0,0 @@
#ifndef UTILS_H
#define UTILS_H
#include <stdbool.h>
#include <stddef.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#ifndef TMPFS_MAGIC
#define TMPFS_MAGIC 0x01021994
#endif
int path_join(char *buf, size_t n, const char *base, const char *name);
bool path_exists(const char *p);
bool is_dir(const char *p);
bool is_symlink(const char *p);
int mkdir_recursive(const char *dir);
bool is_rw_tmpfs(const char *path);
void free_str_array(char ***arr, int *count);
void add_to_array(char ***arr, int *count, const char *str);
bool is_dot_or_dotdot(const char *n);
#endif /* UTILS_H */

View file

@ -10,15 +10,15 @@ ui_print "- Detected ABI: $ABI"
case "$ABI" in
arm64-v8a)
ui_print "- Selected architecture: ARM64"
ARCH_BINARY="mm_arm64"
ARCH_BINARY="arm64-v8a/magic_mount_rs"
;;
armeabi-v7a)
ui_print "- Selected architecture: ARM"
ARCH_BINARY="mm_armv7"
ARCH_BINARY="armeabi-v7a/magic_mount_rs"
;;
x86_64)
ui_print "- Selected architecture: AMD64"
ARCH_BINARY="mm_amd64"
ARCH_BINARY="x86_64/magic_mount_rs"
;;
*)
abort "! Unsupported architecture: $ABI"

View file

@ -15,7 +15,6 @@ fi
# Set dual-directory environment variables
MODULE_METADATA_DIR="/data/adb/modules"
MODULE_METADATA_SOURCE="KSU"
MODULE_METADATA_LOGLEVEL=3
MODULE_METADATA_LOGFILE="/data/adb/mm.log"
if [ -f "$MODULE_METADATA_LOGFILE" ]; then
@ -23,7 +22,7 @@ if [ -f "$MODULE_METADATA_LOGFILE" ]; then
fi
# Execute the mount binary
"$BINARY" -s "$MODULE_METADATA_SOURCE" -m "$MODULE_METADATA_DIR" -v "$MODULE_METADATA_LOGLEVEL" -l "$MODULE_METADATA_LOGFILE"
"$BINARY" -s "$MODULE_METADATA_SOURCE" -m "$MODULE_METADATA_DIR" -v -l "$MODULE_METADATA_LOGFILE"
EXIT_CODE=$?

View file

@ -1,8 +1,8 @@
id=meta-mm
metamodule=1
name=Magic Mount MetaModule
version=v0.1.0
versionCode=2511201925
version=version
versionCode=versionCode
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
updateJson=updateJson