Move stub resources into its own module

Stop relying on internal AGP intermediate paths in the build directory.
Use standard AGP classes to achieve the same result
This commit is contained in:
topjohnwu
2026-04-07 03:14:37 -07:00
committed by John Wu
parent fb8e5b569e
commit 240b6db1cc
59 changed files with 70 additions and 64 deletions

View File

@@ -38,7 +38,7 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
Default string resources for the Magisk app and its stub APK are located here:
- `app/core/src/main/res/values/strings.xml`
- `app/stub/src/main/res/values/strings.xml`
- `app/stub-res/src/main/res/values/strings.xml`
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).

View File

@@ -170,7 +170,7 @@ fun Project.setupCoreLib() {
it.addGeneratedSourceDirectory(syncResources, SyncWithDir::outputFolder)
}
val stubTask = tasks.getByPath(":stub:comment$variantCapped")
val stubTask = tasks.getByPath(":stub:transform${variantCapped}Apk")
val syncAssets = tasks.register("sync${variantCapped}Assets", SyncWithDir::class) {
outputFolder.set(layout.buildDirectory.dir("$variantName/assets"))
into(outputFolder)
@@ -261,20 +261,23 @@ fun Project.setupAppCommon() {
androidAppComponents {
onVariants { variant ->
val commentTask = tasks.register(
"comment${variant.name.replaceFirstChar { it.uppercase() }}",
AddCommentTask::class.java
"transform${variant.name.replaceFirstChar { it.uppercase() }}Apk",
TransformApkTask::class.java
)
val transformationRequest = variant.artifacts.use(commentTask)
.wiredWithDirectories(AddCommentTask::apkFolder, AddCommentTask::outFolder)
.wiredWithDirectories(TransformApkTask::apkFolder, TransformApkTask::outFolder)
.toTransformMany(SingleArtifact.APK)
val signingConfig = androidApp.buildTypes.getByName(variant.buildType!!).signingConfig
commentTask.configure {
this.transformationRequest = transformationRequest
this.signingConfig = signingConfig
this.comment = "version=${Config.version}\n" +
"versionCode=${Config.versionCode}\n" +
"stubVersion=${Config.stubVersion}\n"
this.outFolder.set(layout.buildDirectory.dir("outputs/apk/${variant.name}"))
// Always add a transformation to set comments on the APK
this.transformations.add {
it.eocdComment = ("version=${Config.version}\n" +
"versionCode=${Config.versionCode}\n" +
"stubVersion=${Config.stubVersion}\n").toByteArray()
}
}
}

View File

@@ -5,7 +5,6 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
@@ -14,8 +13,8 @@ import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
@@ -24,9 +23,7 @@ import java.security.SecureRandom
import java.util.Random
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
@@ -290,56 +287,37 @@ fun Project.setupStubApk() {
ManifestUpdater::outputManifest)
.toTransform(SingleArtifact.MERGED_MANIFEST)
val aapt = sdkComponents.aapt2.get().executable.get().asFile
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
"${variantLowered}/process${variantCapped}Resources/" +
"linked-resources-binary-format-${variantLowered}.ap_").get().asFile
val resTask = tasks.getByPath(":stub-res:package$variantCapped")
val genResourcesTask = tasks.register("generate${variantCapped}BundledResources", TaskWithDir::class) {
dependsOn("process${variantCapped}Resources")
dependsOn(resTask)
outputFolder.set(layout.buildDirectory.dir("generated/${variantLowered}/resources"))
doLast {
val apkTmp = File("${apk}.tmp")
providers.exec {
commandLine(aapt, "optimize", "-o", apkTmp, "--collapse-resource-names", apk)
}.result.get()
val apk = resTask.outputs.files.asFileTree
.filter { it.name.endsWith(".apk") }.files.first()
val bos = ByteArrayOutputStream()
ZipFile(apkTmp).use { src ->
ZipOutputStream(apk.outputStream()).use {
it.setLevel(Deflater.BEST_COMPRESSION)
it.putNextEntry(ZipEntry("AndroidManifest.xml"))
src.getInputStream(src.getEntry("AndroidManifest.xml")).transferTo(it)
it.closeEntry()
}
ZipFile(apk).use { src ->
DeflaterOutputStream(bos, Deflater(Deflater.BEST_COMPRESSION)).use {
src.getInputStream(src.getEntry("resources.arsc")).transferTo(it)
}
}
apkTmp.delete()
genEncryptedResources(bos.toByteArray(), outputFolder.get().asFile)
}
}
tasks.withType(TransformApkTask::class) {
transformations.add {
// Always delete resources.arsc from the APK
// to ensure that external resources can be loaded
it.get("resources.arsc")?.delete()
}
}
variant.sources.java?.let {
it.addStaticSourceDirectory(componentJavaOutDir.path)
it.addGeneratedSourceDirectory(genResourcesTask, TaskWithDir::outputFolder)
}
}
}
// Override optimizeReleaseResources task
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
"release/processReleaseResources/linked-resources-binary-format-release.ap_").get().asFile
val optRes = layout.buildDirectory.file("intermediates/optimized_processed_res/" +
"release/optimizeReleaseResources/resources-release-optimize.ap_").get().asFile
afterEvaluate {
tasks.named("optimizeReleaseResources") {
doLast { apk.copyTo(optRes, true) }
}
}
tasks.named<Delete>("clean") {
delete.addAll(listOf("src/debug/AndroidManifest.xml", "src/release/AndroidManifest.xml"))
}
}

View File

@@ -5,9 +5,11 @@ import com.android.ide.common.signing.KeystoreHelper
import com.android.tools.build.apkzlib.sign.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions
import com.android.tools.build.apkzlib.zfile.ZFiles
import com.android.tools.build.apkzlib.zip.ZFile
import com.android.tools.build.apkzlib.zip.ZFileOptions
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
@@ -17,10 +19,7 @@ import org.gradle.api.tasks.TaskAction
import java.io.File
import java.util.jar.JarFile
abstract class AddCommentTask: DefaultTask() {
@get:Input
abstract val comment: Property<String>
abstract class TransformApkTask : DefaultTask() {
@get:Input
abstract val signingConfig: Property<ApkSigningConfig>
@@ -31,7 +30,10 @@ abstract class AddCommentTask: DefaultTask() {
abstract val outFolder: DirectoryProperty
@get:Internal
abstract val transformationRequest: Property<ArtifactTransformationRequest<AddCommentTask>>
abstract val transformations: ListProperty<(ZFile) -> Unit>
@get:Internal
abstract val transformationRequest: Property<ArtifactTransformationRequest<TransformApkTask>>
@TaskAction
fun taskAction() = transformationRequest.get().submit(this) { artifact ->
@@ -63,10 +65,10 @@ abstract class AddCommentTask: DefaultTask() {
inFile.copyTo(outFile, overwrite = true)
ZFiles.apk(outFile, options).use {
SigningExtension(signingOptions).register(it)
it.eocdComment = comment.get().toByteArray()
it.get(IncrementalPackager.APP_METADATA_ENTRY_PATH)?.delete()
it.get(IncrementalPackager.VERSION_CONTROL_INFO_ENTRY_PATH)?.delete()
it.get(JarFile.MANIFEST_NAME)?.delete()
transformations.get().forEach { transform -> transform(it) }
}
outFile

View File

@@ -17,4 +17,4 @@ pluginManagement {
}
rootProject.name = "Magisk"
include(":apk", ":apk-ng", ":core", ":shared", ":stub", ":test")
include(":apk", ":apk-ng", ":core", ":shared", ":stub", ":stub-res", ":test")

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.android.application)
}
setupCommon()
android {
namespace = "com.topjohnwu.magisk"
enableKotlin = false
buildTypes {
release {
isShrinkResources = false
}
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest><application /></manifest>

View File

@@ -28,7 +28,6 @@ android {
release {
proguardFiles("proguard-rules.pro")
isMinifyEnabled = true
isShrinkResources = false
}
}

View File

@@ -3,9 +3,6 @@ package com.topjohnwu.magisk;
import static android.R.string.no;
import static android.R.string.ok;
import static android.R.string.yes;
import static com.topjohnwu.magisk.R.string.dling;
import static com.topjohnwu.magisk.R.string.no_internet_msg;
import static com.topjohnwu.magisk.R.string.upgrade_msg;
import android.app.Activity;
import android.app.AlertDialog;
@@ -46,14 +43,18 @@ import javax.crypto.spec.SecretKeySpec;
public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk";
private static final String RES_PKG_NAME = "com.topjohnwu.magisk";
private Context themed;
private boolean dynLoad;
private int dling;
private int no_internet_msg;
private int upgrade_msg;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault);
getTheme().applyStyle(android.R.style.Theme_DeviceDefault_Dialog_NoActionBar, true);
// Only download and dynamic load full APK if hidden
dynLoad = !getPackageName().equals(BuildConfig.APPLICATION_ID);
@@ -63,6 +64,7 @@ public class DownloadActivity extends Activity {
loadResources();
} catch (Exception e) {
error(e);
return;
}
ProviderInstaller.install(this);
@@ -70,7 +72,7 @@ public class DownloadActivity extends Activity {
if (Networking.checkNetworkStatus(this)) {
showDialog();
} else {
new AlertDialog.Builder(themed)
new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle(APP_NAME)
.setMessage(getString(no_internet_msg))
@@ -95,7 +97,7 @@ public class DownloadActivity extends Activity {
}
private void showDialog() {
new AlertDialog.Builder(themed)
new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle(APP_NAME)
.setMessage(getString(upgrade_msg))
@@ -105,7 +107,7 @@ public class DownloadActivity extends Activity {
}
private void dlAPK() {
ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
ProgressDialog.show(this, getString(dling), getString(dling) + " " + APP_NAME, true);
// Download and upgrade the app
var request = request(BuildConfig.APK_URL).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (dynLoad) {
@@ -139,6 +141,7 @@ public class DownloadActivity extends Activity {
}
private void loadResources() throws Exception {
var res = getResources();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
var fd = Os.memfd_create("res", 0);
try {
@@ -147,14 +150,14 @@ public class DownloadActivity extends Activity {
var loader = new ResourcesLoader();
try (var pfd = ParcelFileDescriptor.dup(fd)) {
loader.addProvider(ResourcesProvider.loadFromTable(pfd, null));
getResources().addLoaders(loader);
res.addLoaders(loader);
}
} finally {
Os.close(fd);
}
} else {
File res = new File(getCodeCacheDir(), "res.apk");
try (var out = new ZipOutputStream(new FileOutputStream(res))) {
File apk = new File(getCodeCacheDir(), "res.apk");
try (var out = new ZipOutputStream(new FileOutputStream(apk))) {
// AndroidManifest.xml is required on Android 6-, and directory support is broken on Android 9-10
out.putNextEntry(new ZipEntry("AndroidManifest.xml"));
try (var stubApk = new ZipFile(getPackageCodePath())) {
@@ -163,7 +166,10 @@ public class DownloadActivity extends Activity {
out.putNextEntry(new ZipEntry("resources.arsc"));
decryptResources(out);
}
StubApk.addAssetPath(getResources(), res.getPath());
StubApk.addAssetPath(res, apk.getPath());
}
dling = res.getIdentifier("dling", "string", RES_PKG_NAME);
no_internet_msg = res.getIdentifier("no_internet_msg", "string", RES_PKG_NAME);
upgrade_msg = res.getIdentifier("upgrade_msg", "string", RES_PKG_NAME);
}
}