Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
85dd550
feat: Magisk module installer
secp192k1 Apr 7, 2026
9ba4e3e
fix: Ensure dex streams are closed to prevent resource leaks
secp192k1 Apr 7, 2026
a283be1
feat: Migration of magisk logic from `manager` to `library`
secp192k1 Apr 7, 2026
b180aea
docs: Explain boot wait loop and sync induction behavior
secp192k1 Apr 7, 2026
6299737
refactor: Improve mount safety and path detection
secp192k1 Apr 7, 2026
647e7d6
refactor: Better `service.sh` logging
secp192k1 Apr 7, 2026
bba8eb3
fix: Add missing check for already overlayed app
secp192k1 Apr 7, 2026
3e9dbc9
refactor: Proceed with remount if already mounted/overlayed
secp192k1 Apr 7, 2026
1d972f1
refactor: Human readable logging
secp192k1 Apr 7, 2026
f6e0783
feat: Add interactive volume key support and SELinux permission enfor…
secp192k1 Apr 7, 2026
fa4e883
refactor: Moved service to `Constants.kt`
secp192k1 Apr 7, 2026
b026668
refactor: Optimization and reuse of operations
secp192k1 Apr 7, 2026
c2d57b7
refactor: Removed redundant code
secp192k1 Apr 7, 2026
35ab7dc
refactor: Generalize induction constants and optimize installer to us…
secp192k1 Apr 7, 2026
2c4a8d6
refactor: Implement unified source-and-trigger architecture for root …
secp192k1 Apr 7, 2026
7b86534
refactor: Consolidate induction templates and synchronize Magisk inst…
secp192k1 Apr 7, 2026
08ac5dd
refactor: Simplify `ShellCommandRunner` supplier and improve naming c…
secp192k1 Apr 7, 2026
ed0c7a0
feat: Add Magisk module uninstall script template
secp192k1 Apr 7, 2026
dda483b
refactor: Align `MagiskInstaller` with unified source-and-trigger arc…
secp192k1 Apr 7, 2026
e022803
fix: Restore `LocalMagiskInstaller` build after supplier signature ch…
secp192k1 Apr 7, 2026
09b39f8
fix: Restore `LocalRootInstaller` build after supplier signature change
secp192k1 Apr 7, 2026
277e368
feat: Write `uninstall.sh` when provisioning Magisk and root modules
secp192k1 Apr 7, 2026
754bddf
fix: Module integration and migration impl
secp192k1 Apr 9, 2026
2e64ffd
feat: Better module logging
secp192k1 Apr 9, 2026
e4c06cb
Update library/src/commonMain/kotlin/app/revanced/library/installatio…
secp192k1 Apr 12, 2026
c229f11
refactor: Rename confusing and unused vars
secp192k1 Apr 12, 2026
3280af0
Refactor Magisk provisioning and watchdog
secp192k1 Apr 12, 2026
d27169d
revert: Use flat apk path instead of per-package subdirectory
secp192k1 Apr 12, 2026
3119114
refactor: Better descriptive module script
secp192k1 Apr 12, 2026
fe6f102
enhance: Disable instead of uninstall handler
secp192k1 Apr 12, 2026
650e952
refactor: Moved paths for grouping with other paths
secp192k1 Apr 12, 2026
e276095
refactor: Unicode fix
secp192k1 Apr 13, 2026
d0f432e
fix: Removed user param for uninstall
secp192k1 Apr 14, 2026
041519a
refactor: Standardize naming to "prepare"
secp192k1 Apr 14, 2026
84738bb
refactor: Inline single-use constants & clarify placeholders
secp192k1 Apr 14, 2026
6fded1d
refactor: Change 'MAGISK_' prefix in favor of Module
secp192k1 Apr 14, 2026
3d020fa
refactor: Accept `ShellCommandRunner` directly instead of a supplier
secp192k1 Apr 15, 2026
55f752c
fix: `pm uninstall` freshly-installed apps on module uninstall
secp192k1 Apr 15, 2026
b9d0a1b
refactor: Comment out unused `extractNativeLibraries`
secp192k1 Apr 15, 2026
995d155
refactor: Use current user instead of static value
secp192k1 Apr 18, 2026
b65883f
refactor: Remove obselete shell helper
secp192k1 Apr 18, 2026
ce3e0e6
refactor: Better comment wording
secp192k1 Apr 18, 2026
91c4b87
refactor: File renamings
secp192k1 Apr 18, 2026
86887b2
Merge branch 'feat/magisk-module' of https://github.com/secp192k1/rev…
secp192k1 Apr 18, 2026
dc438d2
refactor: Styling and wording
secp192k1 Apr 18, 2026
547764d
refactor: Rename `.boot_token` to `.last_boot_id`
secp192k1 Apr 18, 2026
d1f1076
refactor: Remove redundant step for path creation
secp192k1 Apr 18, 2026
2d19f45
fix: Hardcode `mounted=false` in MagiskRootInstaller
secp192k1 Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions library/src/androidMain/assets/root/module.prop
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
id=__PKG_NAME__-ReVanced
name=__LABEL__ ReVanced
version=__VERSION__
versionCode=0
author=ReVanced
description=Mounts the patched APK on top of the original one
Comment thread
oSumAtrIX marked this conversation as resolved.
Outdated
54 changes: 54 additions & 0 deletions library/src/androidMain/assets/root/service.sh
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/system/bin/sh
DIR=${0%/*}

package_name="__PKG_NAME__"
version="__VERSION__"
sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g')
Comment thread
secp192k1 marked this conversation as resolved.
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


until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
# Wait a bit more for package manager to settle
sleep 10
Comment thread
secp192k1 marked this conversation as resolved.
Outdated

base_path="$DIR/system/app/$sanitized_package_name/base.apk"
if [ ! -f "$base_path" ]; then
# Fallback to old path for compatibility during transition
base_path="$DIR/$package_name.apk"
fi

stock_path="$(pm path "$package_name" | grep base | sed 's/package://g' | head -n 1)"
stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2 | head -n 1 | sed 's/ //g')"

echo "base_path: $base_path"
echo "stock_path: $stock_path"
echo "base_version: $version"
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
echo "stock_version: $stock_version"

if [ -z "$stock_path" ]; then
echo "App $package_name is not installed. System app induction might have failed or still being processed."
exit 1
fi

if echo "$stock_path" | grep -q "^/system/"; then
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
echo "App is already running from system partition (likely our Magisk overlay). Skipping bind mount."
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
exit 0
fi

if mount | grep -q "$stock_path" ; then
echo "Not mounting as stock path is already mounted"
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
exit 1
fi

if [ "$version" != "$stock_version" ]; then
echo "Version mismatch: base=$version, stock=$stock_version. Attempting to mount anyway as it might be a minor diff."
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
# Optional: exit 1 if you want to be strict
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
fi

echo "Mounting $base_path over $stock_path"
mount -o bind "$base_path" "$stock_path"
Comment thread
secp192k1 marked this conversation as resolved.
Outdated

} >> "$DIR/log"
233 changes: 233 additions & 0 deletions library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt
Comment thread
secp192k1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package app.revanced.library

import android.content.res.AssetManager
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.FileSystemManager
import java.io.File
import java.util.zip.ZipFile

object MagiskUtils {
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.

can be internal no?

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.

Actually looks like these are APIs for callsite? Why are they needed?

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.

I got an error while testing patching with youtube, it failed to load since there were missing native libraries, so this basically includes the necessary libs

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.

And MagiskUtils is used directly by the manager, so it can't be internal

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.

APIs in library should only exist in library if they can/should be shared with cli too, but i cant see how this would apply for a couple of apis introduced here.

Copy link
Copy Markdown
Member Author

@secp192k1 secp192k1 Apr 14, 2026

Choose a reason for hiding this comment

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

can be internal no?

MagiskUtils is called directly from the manager's RootInstaller for live mount & unmount operations in a coroutine scope, so it must stay public

APIs in library should only exist in library if they can/should be shared with cli too, but i cant see how this would apply for a couple of apis introduced here.

Understandable but for android specific APIs like prepareMagiskModule & extractNativeLibraries don't make sense for CLI imo, but we could always move it into manager as a specialized internal helper if you want

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.

MagiskUtils is called directly from the manager's RootInstaller for live mount & unmount operations in a coroutine scope, so it must stay public

Manager should not require any live mount/unmount operations at all, this is business logic of the library installer that manager should use and not reimplement. So The APIs here should be designed to reflect that.

Understandable but for android specific APIs like prepareMagiskModule & extractNativeLibraries don't make sense for CLI imo, but we could always move it into manager as a specialized internal helper if you want

Why not? CLI can prepare a magisk module via ADB too?

const val MODULES_PATH = "/data/adb/modules"

fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false

fun isDeviceRooted() =
System.getenv("PATH")?.split(":")?.any { path -> File(path, "su").canExecute() } ?: false

fun isInstalled(packageName: String, remoteFS: FileSystemManager) =
remoteFS.getFile("$MODULES_PATH/$packageName-revanced").exists()

fun isInstalledAsMagiskModule(packageName: String, remoteFS: FileSystemManager) =
remoteFS.getFile("$MODULES_PATH/revanced_${packageName.replace('.', '_')}").exists()

fun mount(packageName: String, sourceDir: String) {
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 function needed? Installers already implement it

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 all these utils functions below

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.

These are called from the manager's own RootInstaller for live mounts outside the library's install lifecycle (re-mounting on app launch without reinstalling). The library installers handle their own mounting on the installation flow

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.

re-mounting on app launch without reinstalling

I dont think manager should have this feature, in which case the utils function should be removable no?

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.

re-mounting on app launch without reinstalling

I dont think manager should have this feature, in which case the utils function should be removable no?

Sorry but I dont get what you meant here, I guess you want to get rid of the mount & unmount from the manager and directly call those 2 functions on library?

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.

Sorry but I dont get what you meant here, I guess you want to get rid of the mount & unmount from the manager and directly call those 2 functions on library?

Basically, manager should not provide in-place mount & unmount options. Installation and uninstallation over mount is handled by installers which manager is supposed to call.

val modulePath = "$MODULES_PATH/$packageName-revanced"
val patchedApk = "$modulePath/$packageName.apk"
Shell.getShell().newJob()
.add("mount -o bind \"$patchedApk\" \"$sourceDir\"")
.exec()
.assertSuccess("Failed to mount APK")
}

fun unmount(sourceDir: String) {
Shell.getShell().newJob()
.add("umount -l \"$sourceDir\"")
.exec()
.assertSuccess("Failed to unmount APK")
}

fun uninstall(packageName: String, remoteFS: FileSystemManager) {
remoteFS.getFile("$MODULES_PATH/$packageName-revanced").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete files") }
}

fun uninstallMagiskModule(packageName: String, remoteFS: FileSystemManager) {
val sanitizedPackageName = packageName.replace('.', '_')
remoteFS.getFile("$MODULES_PATH/revanced_$sanitizedPackageName").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete Magisk module files") }
}

fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) {
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 needed?

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.

Its needed since it can raise an exception when installing/mounting the app, I've mentioned this exact issue somewhere else on the comments

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 cant find the comment, how/why does it raise an error?

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.

I cant find the comment, how/why does it raise an error?

#127 (comment)
Missing libs, here is an example of trying to start youtube without them

15:43:02.158 12378 AndroidRuntime E FATAL EXCEPTION: main
    java.lang.UnsatisfiedLinkError: dlopen failed: library "libelements.so" not found
        at java.lang.Runtime.loadLibrary0(Runtime.java:1087)
        at java.lang.Runtime.loadLibrary0(Runtime.java:1008)
        at java.lang.System.loadLibrary(System.java:1664)
        at com.google.android.libraries.youtube.elements.innercube.ElementsLib.load(ElementsLib.java:1)
        ...

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 that thrown when mounting? And how does extractNativeLibraries fix it

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.

With bind-mounting, that copy of native libs never happens, since well, its handled by the PM, so we have to manually copy them, and thats exactly what extractNativeLibraries does

The base APK already extracts the native libs, so when we mount the patched APK the patched APK would make use of those native libs, so why do we need to mount them in addition

Correct for libs that exist in both the unpatched/stock and patched APKs. The issue is when the patched APK introduces new .so file(s) that were never in the stock APK (i.e libelements.so in YT), PM never extracted those, so dlopen can't find them - only case extractNativeLibraries handles

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.

Shouldn't these be mounted as well? Otherwise, you may end up modifying the system partition if the target is a system app. Also, libraries are only extracted if the thing is set in the manifest.

For system apps yes but revanced targets user-installed apps (/data/app), not system apps, so this is safe

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.

Correct for libs that exist in both the unpatched/stock and patched APKs. The issue is when the patched APK introduces new .so file(s) that were never in the stock APK (i.e libelements.so in YT), PM never extracted those, so dlopen can't find them - only case extractNativeLibraries handles

Understandable, however i dont think youre exhausting everything that needs mounting no? Is there anything else besides the base apk and the libs we would have to handle in mounting? Granted this including the extractNativeLibraries function are not in scope of this PR, but if you want to introduce this feature here, this functionality shouldve been implemented in RootInstaller class on install/uninstall and not just MagiskUtils.

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.

For system apps yes but revanced targets user-installed apps (/data/app), not system apps, so this is safe

Actually we dont constrain any target. Mounting installation follows this:

  1. Get installed app location
  2. Mount to that location

Where the location is is not in our responsibility. It should work anywhere where the app is installed.

I could create a ROM which ships e.g. yt as a system app. So, if we patch and mount it, it should work correctly.

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.

For system apps yes but revanced targets user-installed apps (/data/app), not system apps, so this is safe

Actually we dont constrain any target. Mounting installation follows this:

  1. Get installed app location
  2. Mount to that location

Where the location is is not in our responsibility. It should work anywhere where the app is installed.

Correct, the mount path already does this, the manager reads applicationInfo.sourceDir at runtime, so it naturally follows wherever the app is installed, whether its /data/app or /system/app

The system-app caveat I mentioned was specifically about extractNativeLibraries() writing to the lib directory on a read-only system partition. But not longer an "issue" since 754bddf the module path uses pm install, so PM handles lib extraction automatically, and the function has been removed in b9d0a1b

Related: 3280af0 & fe6f102

val libPath = "$systemAppPath/lib"
remoteFS.getFile(libPath).apply {
if (exists()) deleteRecursively()
mkdirs()
}

ZipFile(apkFile).use { zip ->
zip.entries().asSequence()
.filter { it.name.startsWith("lib/") && it.name.endsWith(".so") }
.forEach { entry ->
val parts = entry.name.split("/")
if (parts.size < 3) return@forEach

val apkAbi = parts[1]
val libName = parts.last()
val systemAbi = when (apkAbi) {
"arm64-v8a" -> "arm64"
"armeabi-v7a" -> "arm"
"x86_64" -> "x86_64"
"x86" -> "x86"
else -> apkAbi
}

val targetDir = "$libPath/$systemAbi"
remoteFS.getFile(targetDir).apply { if (!exists()) mkdirs() }

val targetFile = "$targetDir/$libName"
zip.getInputStream(entry).use { inputStream ->
remoteFS.getFile(targetFile).newOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
}

fun provisionMagiskModule(
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.

stage/prepare/provision decide haha

(use prepare, simpler)

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.

The --user 0 should also be removed from that command above it, ReVanced/revanced-manager#3288

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.

The --user 0 should also be removed from that command above it, ReVanced/revanced-manager#3288

secp192k1@d0f432e

remoteFS: FileSystemManager,
assets: AssetManager,
packageName: String,
version: String,
label: String,
patchedApk: File
) {
val sanitizedPackageName = packageName.replace('.', '_')
val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName"
val systemAppPath = "$modulePath/system/app/$sanitizedPackageName"

Shell.getShell().newJob()
.add("mkdir -p \"$systemAppPath\"")
.exec()
.assertSuccess("Failed to create system app directory")

val moduleProp = buildString {
appendLine("id=revanced_$sanitizedPackageName")
appendLine("name=$label ReVanced")
appendLine("version=$version")
appendLine("versionCode=1")
appendLine("author=ReVanced")
append("description=Patched by ReVanced")
}
remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) }

assets.open("root/service.sh").use { inputStream ->
remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { outputStream ->
val content = String(inputStream.readBytes())
.replace("__PKG_NAME__", packageName)
.replace("__VERSION__", version)
.replace("__LABEL__", label)
.toByteArray()
outputStream.write(content)
}
}

val targetApkPath = "$systemAppPath/base.apk"
remoteFS.getFile(patchedApk.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream ->
remoteFS.getFile(targetApkPath).newOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}

extractNativeLibraries(patchedApk, systemAppPath, remoteFS)

Shell.getShell().newJob()
.add("chmod 644 \"$targetApkPath\"")
.add("chmod 755 \"$systemAppPath\"")
.add("chmod -R 755 \"$systemAppPath/lib\"")
.add("find \"$systemAppPath/lib\" -type f -name \"*.so\" -exec chmod 644 {} +")
.add("chown -R system:system \"$modulePath/system\"")
.add("chcon -R u:object_r:system_file:s0 \"$modulePath/system\"")
.add("chmod +x \"$modulePath/service.sh\"")
.exec()
.assertSuccess("Failed to set file permissions")
}

fun provisionRootFolder(
remoteFS: FileSystemManager,
assets: AssetManager,
packageName: String,
version: String,
label: String,
patchedApk: File
) {
val modulePath = "$MODULES_PATH/$packageName-revanced"
remoteFS.getFile(modulePath).apply {
if (!mkdirs() && !exists()) {
throw Exception("Failed to create module directory")
}
}

listOf(
"service.sh",
"module.prop",
).forEach { file ->
assets.open("root/$file").use { inputStream ->
remoteFS.getFile("$modulePath/$file").newOutputStream().use { outputStream ->
val content = String(inputStream.readBytes())
.replace("__PKG_NAME__", packageName)
.replace("__VERSION__", version)
.replace("__LABEL__", label)
.toByteArray()
outputStream.write(content)
}
}
}

val apkPath = "$modulePath/$packageName.apk"
remoteFS.getFile(patchedApk.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream ->
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}

Shell.getShell().newJob()
.add("chmod 644 \"$apkPath\"")
.add("chown system:system \"$apkPath\"")
.add("chcon u:object_r:apk_data_file:s0 \"$apkPath\"")
.add("chmod +x \"$modulePath/service.sh\"")
.exec()
.assertSuccess("Failed to set file permissions")
}

private fun Shell.Result.assertSuccess(errorMessage: String) {
if (!isSuccess) {
throw ShellCommandException(errorMessage, code, out, err)
}
}
}

class ShellCommandException(
val userMessage: String,
val exitCode: Int,
val stdout: List<String>,
val stderr: List<String>
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
companion object {
private fun format(
message: String,
exitCode: Int,
stdout: List<String>,
stderr: List<String>
): String = buildString {
appendLine(message)
appendLine("Exit code: $exitCode")

val output = stdout.filter { it.isNotBlank() }
val errors = stderr.filter { it.isNotBlank() }

if (output.isNotEmpty()) {
appendLine("stdout:")
output.forEach(::appendLine)
}
if (errors.isNotEmpty()) {
appendLine("stderr:")
errors.forEach(::appendLine)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package app.revanced.library.installation.installer

import android.content.Context
import app.revanced.library.installation.command.LocalShellCommandRunner
import com.topjohnwu.superuser.ipc.RootService
import java.io.Closeable

/**
* [LocalMagiskInstaller] for installing and uninstalling [Apk] files locally with root permissions via Magisk modules.
*
* @param context The [Context] to use for binding to the [RootService].
* @param onReady A callback to be invoked when [LocalMagiskInstaller] is ready to be used.
*
* @throws NoRootPermissionException If the device does not have root permission.
*
* @see Installer
* @see LocalShellCommandRunner
*/
@Suppress("unused")
class LocalMagiskInstaller(
context: Context,
onReady: LocalMagiskInstaller.() -> Unit = {},
) : MagiskInstaller(
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.

Hm, Is there any reason why this class does not extend LocalRootInstaller?

MagiskRootInstaller : RootInstaller
MagiskLocalRootInstaller : LocalRootInstaller

No?

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 from here #127 (comment)
But cant extend both LocalRootInstaller & MagiskRootInstaller due to single type and we need MagiskRootInstaller's install and uninstall overrides

{ installer ->
LocalShellCommandRunner(context) {
(installer as LocalMagiskInstaller).onReady()
}
},
),
Closeable {
override fun close() = (shellCommandRunner as LocalShellCommandRunner).close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ object ApkUtils {
fun PatchesResult.applyTo(apkFile: File) {
ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile ->
dexFiles.forEach { dexFile ->
targetApkZFile.add(dexFile.name, dexFile.stream)
dexFile.stream.close()
dexFile.stream.use { stream ->
targetApkZFile.add(dexFile.name, stream)
}
}

resources?.let { resources ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package app.revanced.library.installation.installer

import app.revanced.library.installation.command.AdbShellCommandRunner

/**
* [AdbMagiskInstaller] for installing and uninstalling [Apk] files using ADB root permissions via Magisk modules.
*
* @param deviceSerial The device serial. If null, the first connected device will be used.
*
* @throws NoRootPermissionException If the device does not have root permission.
*
* @see MagiskInstaller
* @see AdbShellCommandRunner
*/
class AdbMagiskInstaller(
deviceSerial: String? = null,
) : MagiskInstaller({ AdbShellCommandRunner(deviceSerial) }) {
init {
logger.fine("Connected to $deviceSerial")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ internal object Constants {
const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH"
const val GET_SDK_VERSION = "getprop ro.build.version.sdk"

const val MAGISK_MODULES_PATH = "/data/adb/modules/"
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
Comment thread
oSumAtrIX marked this conversation as resolved.
Outdated
const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER"
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
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.

Any reason why this isnt inlined, seems to be used in one place only

Copy link
Copy Markdown
Member Author

@secp192k1 secp192k1 Apr 14, 2026

Choose a reason for hiding this comment

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

const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID"

const val COPY_APK_TO_MODULE =
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
"cp $TMP_FILE_PATH $PLACEHOLDER && " +
"chmod 644 $PLACEHOLDER && " +
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
"chown system:system $PLACEHOLDER && " +
"chcon $SELINUX_CONTEXT $PLACEHOLDER"

val MAGISK_MODULE_PROP =
Comment thread
secp192k1 marked this conversation as resolved.
Outdated
$$"""
id=$$MAGISK_MODULE_ID
name=ReVanced $$PLACEHOLDER
version=1.0
versionCode=1
author=ReVanced
description=Patched by ReVanced
""".trimIndent()

const val MOUNT_APK =
"base_path=\"$MOUNTED_APK_PATH\" && " +
$$"mv $$TMP_FILE_PATH $base_path && " +
Expand Down
Loading