diff --git a/.gitmodules b/.gitmodules index 0c4456f4f..bdf0d69ee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "zopfli"] path = native/jni/external/zopfli url = https://github.com/google/zopfli.git +[submodule "cxx-rs"] + path = native/jni/external/cxx-rs + url = https://github.com/topjohnwu/cxx.git diff --git a/README.MD b/README.MD index 7f8890aa6..a31f730ef 100644 --- a/README.MD +++ b/README.MD @@ -51,7 +51,7 @@ For Magisk app crashes, record and upload the logcat when the crash occurs. - Run `./build.py ndk` to let the script download and install NDK for you - To start building, run `build.py` to see your options. \ For each action, use `-h` to access help (e.g. `./build.py all -h`) -- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources. +- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources. - Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided. ## Signing and Distribution diff --git a/build.py b/build.py index 0fcdafb36..fad1e4225 100755 --- a/build.py +++ b/build.py @@ -37,10 +37,10 @@ def vprint(str): is_windows = os.name == 'nt' is_ci = 'CI' in os.environ and os.environ['CI'] == 'true' +EXE_EXT = '.exe' if is_windows else '' if not is_ci and is_windows: import colorama - colorama.init() # Environment checks @@ -58,16 +58,20 @@ except FileNotFoundError: cpu_count = multiprocessing.cpu_count() archs = ['armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64'] +triples = ['armv7a-linux-androideabi', 'i686-linux-android', 'aarch64-linux-android', 'x86_64-linux-android'] default_targets = ['magisk', 'magiskinit', 'magiskboot', 'magiskpolicy', 'busybox'] support_targets = default_targets + ['resetprop', 'test'] +rust_targets = ['magisk', 'magiskinit', 'magiskboot', 'magiskpolicy'] sdk_path = os.environ['ANDROID_SDK_ROOT'] ndk_root = op.join(sdk_path, 'ndk') ndk_path = op.join(ndk_root, 'magisk') ndk_build = op.join(ndk_path, 'ndk-build') +rust_bin = op.join(ndk_path, 'toolchains', 'rust', 'bin') +cargo = op.join(rust_bin, 'cargo' + EXE_EXT) gradlew = op.join('.', 'gradlew' + ('.bat' if is_windows else '')) -adb_path = op.join(sdk_path, 'platform-tools', 'adb' + ('.exe' if is_windows else '')) -native_gen_path = op.join('native', 'out', 'generated') +adb_path = op.join(sdk_path, 'platform-tools', 'adb' + EXE_EXT) +native_gen_path = op.realpath(op.join('native', 'out', 'generated')) # Global vars config = {} @@ -123,16 +127,17 @@ def mkdir_p(path, mode=0o755): os.makedirs(path, mode, exist_ok=True) -def execv(cmd): - return subprocess.run(cmd, stdout=STDOUT) +def execv(cmd, env=None): + return subprocess.run(cmd, stdout=STDOUT, env=env) def system(cmd): return subprocess.run(cmd, shell=True, stdout=STDOUT) -def cmd_out(cmd): - return subprocess.check_output(cmd).strip().decode('utf-8') +def cmd_out(cmd, env=None): + return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env) \ + .stdout.strip().decode('utf-8') def xz(data): @@ -180,15 +185,6 @@ def load_config(args): STDOUT = None if args.verbose else subprocess.DEVNULL -def collect_binary(): - for arch in archs: - mkdir_p(op.join('native', 'out', arch)) - for bin in support_targets + ['libpreload.so']: - source = op.join('native', 'libs', arch, bin) - target = op.join('native', 'out', arch, bin) - mv(source, target) - - def clean_elf(): if is_windows: elf_cleaner = op.join('tools', 'elf-cleaner.exe') @@ -203,48 +199,6 @@ def clean_elf(): execv(args) -def find_build_tools(): - global build_tools - if build_tools: - return build_tools - build_tools_root = op.join(os.environ['ANDROID_SDK_ROOT'], 'build-tools') - ls = os.listdir(build_tools_root) - # Use the latest build tools available - ls.sort() - build_tools = op.join(build_tools_root, ls[-1]) - return build_tools - - -# Unused but keep this code -def sign_zip(unsigned): - if 'keyStore' not in config: - return - - msg = '* Signing APK' - apksigner = op.join(find_build_tools(), 'apksigner' + ('.bat' if is_windows else '')) - - exec_args = [apksigner, 'sign', - '--ks', config['keyStore'], - '--ks-pass', f'pass:{config["keyStorePass"]}', - '--ks-key-alias', config['keyAlias'], - '--key-pass', f'pass:{config["keyPass"]}', - '--v1-signer-name', 'CERT', - '--v4-signing-enabled', 'false'] - - if unsigned.endswith('.zip'): - msg = '* Signing zip' - exec_args.extend(['--min-sdk-version', '17', - '--v2-signing-enabled', 'false', - '--v3-signing-enabled', 'false']) - - exec_args.append(unsigned) - - header(msg) - proc = execv(exec_args) - if proc.returncode != 0: - error('Signing failed!') - - def binary_dump(src, var_name): out_str = f'constexpr unsigned char {var_name}[] = {{' for i, c in enumerate(xz(src.read())): @@ -261,7 +215,70 @@ def run_ndk_build(flags): if proc.returncode != 0: error('Build binary failed!') os.chdir('..') - collect_binary() + for arch in archs: + for tgt in support_targets + ['libpreload.so']: + source = op.join('native', 'libs', arch, tgt) + target = op.join('native', 'out', arch, tgt) + mv(source, target) + + +def run_cargo_build(args): + os.chdir(op.join('native', 'rust')) + targets = set(args.target) & set(rust_targets) + + env = os.environ.copy() + env['PATH'] = f'{rust_bin}:{env["PATH"]}' + + # Install cxxbridge and generate C++ bindings + native_out = op.join('..', '..', 'native', 'out') + local_cargo_root = op.join(native_out, '.cargo') + mkdir_p(local_cargo_root) + cmds = [cargo, 'install', '--root', local_cargo_root, 'cxxbridge-cmd'] + if not args.verbose: + cmds.append('-q') + proc = execv(cmds, env) + if proc.returncode != 0: + error('cxxbridge-cmd installation failed!') + cxxbridge = op.join(local_cargo_root, 'bin', 'cxxbridge' + EXE_EXT) + mkdir(native_gen_path) + for p in ['base', 'boot', 'core', 'init', 'sepolicy']: + text = cmd_out([cxxbridge, op.join(p, 'src', 'lib.rs')]) + write_if_diff(op.join(native_gen_path, f'{p}-rs.cpp'), text) + text = cmd_out([cxxbridge, '--header', op.join(p, 'src', 'lib.rs')]) + write_if_diff(op.join(native_gen_path, f'{p}-rs.hpp'), text) + + # Start building the actual build commands + cmds = [cargo, 'build', '-Z', 'build-std=std,panic_abort', + '-Z', 'build-std-features=panic_immediate_abort'] + for target in targets: + cmds.append('-p') + cmds.append(target) + rust_out = 'debug' + if args.release: + cmds.append('-r') + rust_out = 'release' + if not args.verbose: + cmds.append('-q') + + os_name = platform.system().lower() + llvm_bin = op.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', f'{os_name}-x86_64', 'bin') + env['TARGET_CC'] = op.join(llvm_bin, 'clang' + EXE_EXT) + env['RUSTFLAGS'] = '-Clinker-plugin-lto' + for (arch, triple) in zip(archs, triples): + env['TARGET_CFLAGS'] = f'--target={triple}21' + rust_triple = 'thumbv7neon-linux-androideabi' if triple.startswith('armv7') else triple + proc = execv([*cmds, '--target', rust_triple], env) + if proc.returncode != 0: + error('Build binary failed!') + + arch_out = op.join(native_out, arch) + mkdir(arch_out) + for tgt in targets: + source = op.join('target', rust_triple, rust_out, f'lib{tgt}.a') + target = op.join(arch_out, f'lib{tgt}-rs.a') + mv(source, target) + + os.chdir(op.join('..', '..')) def write_if_diff(file_name, text): @@ -326,6 +343,8 @@ def build_binary(args): header('* Building binaries: ' + ' '.join(args.target)) + run_cargo_build(args) + dump_flag_header() flag = '' @@ -402,6 +421,7 @@ def cleanup(args): rm_rf(op.join('native', 'out')) rm_rf(op.join('native', 'libs')) rm_rf(op.join('native', 'obj')) + rm_rf(op.join('native', 'rust', 'target')) if 'java' in args.target: header('* Cleaning java') diff --git a/native/README.md b/native/README.md new file mode 100644 index 000000000..291ee6801 --- /dev/null +++ b/native/README.md @@ -0,0 +1,34 @@ +# Native Development + +## Prerequisite + +Install the NDK required to build and develop Magisk with `./build.py ndk`. The NDK will be installed to `$ANDROID_SDK_ROOT/ndk/magisk`. You don't need to manually install a Rust toolchain with `rustup`, as the NDK installed already has a Rust toolchain bundled. + +## Code Paths + +- `jni`: Magisk's code in C++ +- `jni/external`: external dependencies, mostly submodules +- `rust`: Magisk's code in Rust +- `src`: irrelevant, only exists to setup a native Android Studio project + +## Build Configs + +All C/C++ code and its dependencies are built with [`ndk-build`](https://developer.android.com/ndk/guides/ndk-build) and configured with several `*.mk` files scatterred in many places. + +The `rust` folder is a proper Cargo workspace, and all Rust code is built with `cargo` just like any other Rust projects. + +## Rust + C/C++ + +To reduce complexity involved in linking, all Rust code is built as `staticlib` and linked to C++ targets to ensure our final product is built with an officially supported NDK build system. Each C++ target can at most link to **one** Rust `staticlib` or else multiple definitions error will occur. + +We use the [`cxx`](https://cxx.rs) project for interop between Rust and C++. Although cxx supports interop in both directions, for Magisk, it is strongly advised to avoid calling C++ functions in Rust; if some functionality required in Rust is already implemented in C++, the desired solution is to port the C++ implementation into Rust and export the migrated function back to C++. + +## Development / IDE + +All C++ code should be recognized and properly indexed by Android Studio out of the box. For Rust: + +- Install the [Rust plugin](https://www.jetbrains.com/rust/) in Android Studio +- In Preferences > Languages & Frameworks > Rust, set `$ANDROID_SDK_ROOT/ndk/magisk/toolchains/rust/bin` as the toolchain location +- Open `native/rust/Cargo.toml`, and select "Attach" in the "No Cargo projects found" banner + +Note: run `./build.py binary` before developing to make sure generated code is created. diff --git a/native/jni/Android-rs.mk b/native/jni/Android-rs.mk new file mode 100644 index 000000000..51b8db2c6 --- /dev/null +++ b/native/jni/Android-rs.mk @@ -0,0 +1,25 @@ +LOCAL_PATH := $(call my-dir) + +########################### +# Rust compilation outputs +########################### + +include $(CLEAR_VARS) +LOCAL_MODULE := magisk-rs +LOCAL_SRC_FILES := ../out/$(TARGET_ARCH_ABI)/libmagisk-rs.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := boot-rs +LOCAL_SRC_FILES := ../out/$(TARGET_ARCH_ABI)/libmagiskboot-rs.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := init-rs +LOCAL_SRC_FILES := ../out/$(TARGET_ARCH_ABI)/libmagiskinit-rs.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := policy-rs +LOCAL_SRC_FILES := ../out/$(TARGET_ARCH_ABI)/libmagiskpolicy-rs.a +include $(PREBUILT_STATIC_LIBRARY) diff --git a/native/jni/Android.mk b/native/jni/Android.mk index eb9b54589..8e5341b38 100644 --- a/native/jni/Android.mk +++ b/native/jni/Android.mk @@ -14,7 +14,8 @@ LOCAL_STATIC_LIBRARIES := \ libsystemproperties \ libphmap \ libxhook \ - libmincrypt + libmincrypt \ + libmagisk-rs LOCAL_SRC_FILES := \ core/applets.cpp \ @@ -68,7 +69,8 @@ LOCAL_STATIC_LIBRARIES := \ libbase \ libcompat \ libpolicy \ - libxz + libxz \ + libinit-rs LOCAL_SRC_FILES := \ init/init.cpp \ @@ -95,7 +97,8 @@ LOCAL_STATIC_LIBRARIES := \ libbz2 \ libfdt \ libz \ - libzopfli + libzopfli \ + libboot-rs LOCAL_SRC_FILES := \ boot/main.cpp \ @@ -119,7 +122,8 @@ LOCAL_MODULE := magiskpolicy LOCAL_STATIC_LIBRARIES := \ libbase \ libbase \ - libpolicy + libpolicy \ + libpolicy-rs LOCAL_SRC_FILES := sepolicy/main.cpp @@ -135,7 +139,8 @@ LOCAL_STATIC_LIBRARIES := \ libbase \ libcompat \ libnanopb \ - libsystemproperties + libsystemproperties \ + libmagisk-rs LOCAL_SRC_FILES := \ core/applet_stub.cpp \ @@ -181,6 +186,7 @@ LOCAL_SRC_FILES := \ sepolicy/statement.cpp include $(BUILD_STATIC_LIBRARY) +include jni/Android-rs.mk include jni/base/Android.mk include jni/external/Android.mk diff --git a/native/jni/base/Android.mk b/native/jni/base/Android.mk index e5dcce192..71b515364 100644 --- a/native/jni/base/Android.mk +++ b/native/jni/base/Android.mk @@ -15,7 +15,8 @@ LOCAL_SRC_FILES := \ selinux.cpp \ logging.cpp \ xwrap.cpp \ - stream.cpp + stream.cpp \ + ../external/cxx-rs/src/cxx.cc include $(BUILD_STATIC_LIBRARY) # All static executables should link with libcompat diff --git a/native/jni/external/cxx-rs b/native/jni/external/cxx-rs new file mode 160000 index 000000000..650e64bc3 --- /dev/null +++ b/native/jni/external/cxx-rs @@ -0,0 +1 @@ +Subproject commit 650e64bc3970625f580a61ad9cfc65faa5cdd948 diff --git a/native/rust/.gitignore b/native/rust/.gitignore new file mode 100644 index 000000000..9f970225a --- /dev/null +++ b/native/rust/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/native/rust/Cargo.lock b/native/rust/Cargo.lock new file mode 100644 index 000000000..b24797e6b --- /dev/null +++ b/native/rust/Cargo.lock @@ -0,0 +1,99 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base" +version = "0.1.0" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cxx" +version = "1.0.69" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.69" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "magisk" +version = "0.1.0" +dependencies = [ + "base", + "cxx", +] + +[[package]] +name = "magiskboot" +version = "0.1.0" +dependencies = [ + "base", +] + +[[package]] +name = "magiskinit" +version = "0.1.0" +dependencies = [ + "base", +] + +[[package]] +name = "magiskpolicy" +version = "0.1.0" +dependencies = [ + "base", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml new file mode 100644 index 000000000..56fc4ad3d --- /dev/null +++ b/native/rust/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] + +members = [ + "base", + "boot", + "core", + "init", + "sepolicy", +] + +[profile.dev] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[patch.crates-io] +cxx = { path = "../jni/external/cxx-rs" } diff --git a/native/rust/base/Cargo.toml b/native/rust/base/Cargo.toml new file mode 100644 index 000000000..d683bee11 --- /dev/null +++ b/native/rust/base/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "base" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/native/rust/base/src/lib.rs b/native/rust/base/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/native/rust/boot/Cargo.toml b/native/rust/boot/Cargo.toml new file mode 100644 index 000000000..2e65df4af --- /dev/null +++ b/native/rust/boot/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "magiskboot" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +base = { path = "../base" } diff --git a/native/rust/boot/src/lib.rs b/native/rust/boot/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/native/rust/core/Cargo.toml b/native/rust/core/Cargo.toml new file mode 100644 index 000000000..ff2b982fc --- /dev/null +++ b/native/rust/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "magisk" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +base = { path = "../base" } +cxx = "1.0.69" diff --git a/native/rust/core/src/lib.rs b/native/rust/core/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/native/rust/init/Cargo.toml b/native/rust/init/Cargo.toml new file mode 100644 index 000000000..b0e6084dc --- /dev/null +++ b/native/rust/init/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "magiskinit" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +base = { path = "../base" } diff --git a/native/rust/init/src/lib.rs b/native/rust/init/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/native/rust/sepolicy/Cargo.toml b/native/rust/sepolicy/Cargo.toml new file mode 100644 index 000000000..1e4bea119 --- /dev/null +++ b/native/rust/sepolicy/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "magiskpolicy" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib", "rlib"] + +[dependencies] +base = { path = "../base" } diff --git a/native/rust/sepolicy/src/lib.rs b/native/rust/sepolicy/src/lib.rs new file mode 100644 index 000000000..e69de29bb