Skip to content

feat: Magisk module install#127

Open
secp192k1 wants to merge 48 commits intoReVanced:devfrom
secp192k1:feat/magisk-module
Open

feat: Magisk module install#127
secp192k1 wants to merge 48 commits intoReVanced:devfrom
secp192k1:feat/magisk-module

Conversation

@secp192k1
Copy link
Copy Markdown
Member

@secp192k1 secp192k1 commented Apr 7, 2026

Closes #127

Note: I had to rollback the AGP version during local development and testing to 8.13.2 since there is a version mismatch. But this shouldn't be an issue once they match

Introduce Magisk-based installation support: add an abstract MagiskInstaller implementing install/uninstall/getInstallation logic for deploying patched APKs as Magisk modules (writes module.prop, copies patched APK to module, restarts/kills apps). Add platform-specific installers: AdbMagiskInstaller (ADB root) and LocalMagiskInstaller (local root via LocalShellCommandRunner, Closeable). Add Magisk-related constants (module paths, COPY_APK_TO_MODULE command, MAGISK_MODULE_PROP template) used by the installer.
@secp192k1 secp192k1 marked this pull request as ready for review April 7, 2026 15:28
@secp192k1 secp192k1 marked this pull request as draft April 7, 2026 16:13
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/module.prop Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
rm -f "$DIR/log"

{
echo "Induction check for $package_name"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does induction check mean

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically making sure that the system is ready for the app to be injected/introduced into the system by overlaying on top of the original app

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why "induction"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a mix of both, right now that whole name has been replaced by the watchdog and STAGE_APK variables

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standardized naming on these commits
secp192k1@041519a
secp192k1/revanced-manager@8f8efa5

Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated
Comment thread library/src/androidMain/assets/root/service.sh Outdated

@Suppress("MemberVisibilityCanBePrivate")
internal object Constants {
object Constants {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isnt this internal anymore?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isnt this internal anymore?

Manager using it on RootInstaller.kt

}

/*
* When bind-mounting a patched APK over the unpatched/stock APK, Android's PM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out (this should be handled by root installer if not already)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out (this should be handled by root installer if not already)

First, no, its not being handled in by root installer (.install())

For the Magisk module path this is fine since pm install lets the PM extract native libs automatically, however, for the bind-mount path, if a patch introduces a new .so native lib that wasnt in the stock APK, it will crash with UnsatisfiedLinkError (logcat shown before)

I left that function commented-out as a reference for when that's addressed, but when it is, it will belong in root install. But before addressing any of this, remember that writing to the app's lib directory will fail on read-only system partitions (/system/app, /product/app, etc)

*
* @throws PackageNameRequiredException If the [Apk] does not have a package name.
*/
override suspend fun install(apk: Apk): RootInstallerResult {
Copy link
Copy Markdown
Member

@oSumAtrIX oSumAtrIX Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this doesnt handle native libs mounting, like the extractNativeLibs utils function from earlier?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this doesnt handle native libs mounting, like the extractNativeLibs utils function from earlier?

More detailed comment here #127 (comment)
But basically since we switched to PM for mounting, its not needed, it will be needed for bind-mounting though

CREATE_INSTALLATION_PATH().waitFor()
MOUNT_APK(packageName)().waitFor()
CREATE_INSTALLATION_PATH(packageName)().waitFor()
prepareApk(packageName)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cant prepare apk handle CREATE_INSTALLATION_PATH?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like, in the constant it moves a file to a folder, but then it can make sure to create the folder if it doesnt exist.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cant prepare apk handle CREATE_INSTALLATION_PATH?
Seems like it already does that?

Yes true my bad, its already running the whole creating dirs command before moving the file, so CREATE_INSTALLATION_PATH was useless here, removed on d1f1076

override suspend fun uninstall(packageName: String): RootInstallerResult {
logger.info("Uninstalling $packageName Magisk module")

val formattedPackageName = packageName.replace('.', '_')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case would . be unsafe to use here? Isnt

/my.pkg.name/ a valid folder name

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case would . be unsafe to use here? Isnt

/my.pkg.name/ a valid folder name

Yes its a valid folder name, but because formattedPackageName doubles as the Magisk module id in module.prop (id=revanced___FORMATTED_PKG__), which by convention should only contain alphanumeric characters and underscores
Since the module id must match the directory name under the modules dir (/data/adb/modules/), both are kept consistent and dot-free

val modulePath = MODULE_PATH(formattedPackageName)

// Read the flag before removing the module directory.
val newlyInstalled = EXISTS("$modulePath/.newly_installed")().exitCode == 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this val to where its used (above the if).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this val to where its used (above the if).

The val has to stay where it is, .newly_installed is inside modulePath, which is deleted a few lines later by DELETE(modulePath), moving the check to just above the if means the directory is already gone and EXISTS always returns false, so the app would never get uninstalled. The early read is intentional ("// Read the flag before removing the module directory.")

return RootInstallation(
INSTALLED_APK_PATH(packageName)().output.ifEmpty { null },
patchedApkPath,
MOUNT_GREP(patchedApkPath)().exitCode == 0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we still have the mounted boolean https://github.com/ReVanced/revanced-library/pull/127/changes#r3061197338

Lmao... 2d19f45
Also the val above as useless so I moved it inside the root installation call

#!/system/bin/sh
pm uninstall "__PATCHED_PKG__"
rm -f "/data/adb/revanced/__PKG_NAME__.apk"
rm -f "/data/adb/service.d/revanced_handle_disabled___FORMATTED_PKG__.sh"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt the module uninstall script delete itself too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt the module uninstall script delete itself too?

Nope. Magisk runs the uninstall script and then removes the entire module directory itself, so the script is cleaned up automatically. The explicit rm -f for the handle-disabled script is necessary precisely because that file lives outside the module directory at /data/adb/service.d/ : Magisk will never touch it

cp /proc/sys/kernel/random/boot_id "${DIR}/.boot_token"

LOG="${DIR}/log"
MAX_LOG_LINES=200
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need logging here? Is it to display stuff in magisk manager?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine to not have any logging code for the module script, like logging to magisk manager is fine, but no dump to a log file

Copy link
Copy Markdown
Member Author

@secp192k1 secp192k1 Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need logging here? Is it to display stuff in magisk manager?

No, this logging is strictly for the magisk module, since manager wouldn't be able to access the logs and to keep it organized by category (manager logs vs module logs) its also keeps the logs separated between multiple modules

I think its fine to not have any logging code for the module script, like logging to magisk manager is fine, but no dump to a log file

Any other alternative that comes to mind?
Also keep in mind, along side with my reply above, each log file has a MAX_LOG_LINES=200

# (install-create/write/commit) and is less prone to early-boot binder failures.
# NOTE: On Xiaomi devices (MIUI/HyperOS), pm install may still fail due to
# package verification restrictions. Manual install via ReVanced Manager
# may be required in that case.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside lol, how do we handle that in manager tbh or like how tf do you prompt the user to install the apk on Xiamoi programatically, ideally library can handle Xiomi installation business logic by itself

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside lol, how do we handle that in manager tbh or like how tf do you prompt the user to install the apk on Xiaomi programatically, ideally library can handle Xiomi installation business logic by itself

How basically everyone else solves this issue, by the phone props, for example:

[ro.fota.oem]: [Xiaomi]
[ro.product.bootimage.manufacturer]: [Xiaomi]
[ro.product.manufacturer]: [Xiaomi]
[ro.product.odm.manufacturer]: [Xiaomi]
[ro.product.product.brand]: [Xiaomi]
[ro.product.product.manufacturer]: [Xiaomi]
[ro.product.system.brand]: [Xiaomi]
[ro.product.system.manufacturer]: [Xiaomi]
[ro.product.system_ext.brand]: [Xiaomi]
[ro.product.system_ext.manufacturer]: [Xiaomi]
[ro.product.vendor.manufacturer]: [Xiaomi]
[ro.product.vendor_dlkm.manufacturer]: [Xiaomi]

Note: Remember that these are keys from my device with the value specifically being Xiaomi, some keys like ...vendor_dlkm... might not exist on other Xiaomi devices

Also this will almost always be a hassle to completely fix since there isnt a good and reliable way to tell MIUI and HyperOS apart (without a lot of checks)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants