diff --git a/build.sh b/build.sh index f7c58b6..820affe 100644 --- a/build.sh +++ b/build.sh @@ -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 ] [--update-json-url ]" + echo " --release Build release version" + echo " --debug Build debug version" + echo " --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 \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore index 5e56e04..a6a11a6 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,4 @@ -/bin +/target +/out +Cargo.lock +*.log \ No newline at end of file diff --git a/src/Cargo.toml b/src/Cargo.toml new file mode 100644 index 0000000..479a6e5 --- /dev/null +++ b/src/Cargo.toml @@ -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 diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index f709dbb..0000000 --- a/src/Makefile +++ /dev/null @@ -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) diff --git a/src/magic_mount.c b/src/magic_mount.c deleted file mode 100644 index c1eab06..0000000 --- a/src/magic_mount.c +++ /dev/null @@ -1,702 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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++; - } -} diff --git a/src/magic_mount.h b/src/magic_mount.h deleted file mode 100644 index 7b836fc..0000000 --- a/src/magic_mount.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef MAGIC_MOUNT_H -#define MAGIC_MOUNT_H - -#include - -#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 */ diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 959cb73..0000000 --- a/src/main.c +++ /dev/null @@ -1,104 +0,0 @@ -#include -#include -#include -#include -#include - -#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; -} diff --git a/src/src/magic_mount.rs b/src/src/magic_mount.rs new file mode 100644 index 0000000..70c0326 --- /dev/null +++ b/src/src/magic_mount.rs @@ -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 { + 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, + // the module that owned this node + module_path: Option, + replace: bool, + skip: bool, +} + +impl Node { + fn collect_module_files>(&mut self, module_dir: T) -> Result { + 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(name: T) -> Self { + Node { + name: name.to_string(), + file_type: Directory, + children: Default::default(), + module_path: None, + replace: false, + skip: false, + } + } + + fn new_module(name: T, entry: &DirEntry) -> Option { + 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> { + 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, Dst: AsRef>(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, WP: AsRef>( + 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, WP: AsRef>( + path: P, + work_dir_path: WP, + current: Node, + has_tmpfs: bool, +) -> Result<()> { + 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: 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(()) + } +} diff --git a/src/src/main.rs b/src/src/main.rs new file mode 100644 index 0000000..558c1c4 --- /dev/null +++ b/src/src/main.rs @@ -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, + + /// 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, +} + +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) + } + } +} diff --git a/src/src/utils.rs b/src/src/utils.rs new file mode 100644 index 0000000..2403880 --- /dev/null +++ b/src/src/utils.rs @@ -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>(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>(path: P) -> Result { + 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>(path: P) -> Result { + unimplemented!() +} + +pub fn ensure_dir_exists>(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 { + 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()); + } +} diff --git a/src/utils.c b/src/utils.c deleted file mode 100644 index 91ba551..0000000 --- a/src/utils.c +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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, "..")); -} diff --git a/src/utils.h b/src/utils.h deleted file mode 100644 index d400db4..0000000 --- a/src/utils.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef UTILS_H -#define UTILS_H - -#include -#include - -#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 */ diff --git a/template/customize.sh b/template/customize.sh index fbee9f2..ddc6516 100644 --- a/template/customize.sh +++ b/template/customize.sh @@ -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" diff --git a/template/metamount.sh b/template/metamount.sh index 50f969a..65a32bf 100644 --- a/template/metamount.sh +++ b/template/metamount.sh @@ -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=$? diff --git a/template/module.prop b/template/module.prop index 79de0d0..55acd9b 100644 --- a/template/module.prop +++ b/template/module.prop @@ -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