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
+1 -1
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: Default string resources for the Magisk app and its stub APK are located here:
- `app/core/src/main/res/values/strings.xml` - `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`). Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
+10 -7
View File
@@ -170,7 +170,7 @@ fun Project.setupCoreLib() {
it.addGeneratedSourceDirectory(syncResources, SyncWithDir::outputFolder) 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) { val syncAssets = tasks.register("sync${variantCapped}Assets", SyncWithDir::class) {
outputFolder.set(layout.buildDirectory.dir("$variantName/assets")) outputFolder.set(layout.buildDirectory.dir("$variantName/assets"))
into(outputFolder) into(outputFolder)
@@ -261,20 +261,23 @@ fun Project.setupAppCommon() {
androidAppComponents { androidAppComponents {
onVariants { variant -> onVariants { variant ->
val commentTask = tasks.register( val commentTask = tasks.register(
"comment${variant.name.replaceFirstChar { it.uppercase() }}", "transform${variant.name.replaceFirstChar { it.uppercase() }}Apk",
AddCommentTask::class.java TransformApkTask::class.java
) )
val transformationRequest = variant.artifacts.use(commentTask) val transformationRequest = variant.artifacts.use(commentTask)
.wiredWithDirectories(AddCommentTask::apkFolder, AddCommentTask::outFolder) .wiredWithDirectories(TransformApkTask::apkFolder, TransformApkTask::outFolder)
.toTransformMany(SingleArtifact.APK) .toTransformMany(SingleArtifact.APK)
val signingConfig = androidApp.buildTypes.getByName(variant.buildType!!).signingConfig val signingConfig = androidApp.buildTypes.getByName(variant.buildType!!).signingConfig
commentTask.configure { commentTask.configure {
this.transformationRequest = transformationRequest this.transformationRequest = transformationRequest
this.signingConfig = signingConfig 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}")) 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()
}
} }
} }
+14 -36
View File
@@ -5,7 +5,6 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory 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.PathSensitivity
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
@@ -24,9 +23,7 @@ import java.security.SecureRandom
import java.util.Random import java.util.Random
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream import java.util.zip.DeflaterOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@@ -290,56 +287,37 @@ fun Project.setupStubApk() {
ManifestUpdater::outputManifest) ManifestUpdater::outputManifest)
.toTransform(SingleArtifact.MERGED_MANIFEST) .toTransform(SingleArtifact.MERGED_MANIFEST)
val aapt = sdkComponents.aapt2.get().executable.get().asFile val resTask = tasks.getByPath(":stub-res:package$variantCapped")
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
"${variantLowered}/process${variantCapped}Resources/" +
"linked-resources-binary-format-${variantLowered}.ap_").get().asFile
val genResourcesTask = tasks.register("generate${variantCapped}BundledResources", TaskWithDir::class) { val genResourcesTask = tasks.register("generate${variantCapped}BundledResources", TaskWithDir::class) {
dependsOn("process${variantCapped}Resources") dependsOn(resTask)
outputFolder.set(layout.buildDirectory.dir("generated/${variantLowered}/resources")) outputFolder.set(layout.buildDirectory.dir("generated/${variantLowered}/resources"))
doLast { doLast {
val apkTmp = File("${apk}.tmp") val apk = resTask.outputs.files.asFileTree
providers.exec { .filter { it.name.endsWith(".apk") }.files.first()
commandLine(aapt, "optimize", "-o", apkTmp, "--collapse-resource-names", apk)
}.result.get()
val bos = ByteArrayOutputStream() val bos = ByteArrayOutputStream()
ZipFile(apkTmp).use { src -> ZipFile(apk).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()
}
DeflaterOutputStream(bos, Deflater(Deflater.BEST_COMPRESSION)).use { DeflaterOutputStream(bos, Deflater(Deflater.BEST_COMPRESSION)).use {
src.getInputStream(src.getEntry("resources.arsc")).transferTo(it) src.getInputStream(src.getEntry("resources.arsc")).transferTo(it)
} }
} }
apkTmp.delete()
genEncryptedResources(bos.toByteArray(), outputFolder.get().asFile) 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 { variant.sources.java?.let {
it.addStaticSourceDirectory(componentJavaOutDir.path) it.addStaticSourceDirectory(componentJavaOutDir.path)
it.addGeneratedSourceDirectory(genResourcesTask, TaskWithDir::outputFolder) 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"))
}
} }
@@ -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.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions import com.android.tools.build.apkzlib.sign.SigningOptions
import com.android.tools.build.apkzlib.zfile.ZFiles 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 com.android.tools.build.apkzlib.zip.ZFileOptions
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.InputFiles
@@ -17,10 +19,7 @@ import org.gradle.api.tasks.TaskAction
import java.io.File import java.io.File
import java.util.jar.JarFile import java.util.jar.JarFile
abstract class AddCommentTask: DefaultTask() { abstract class TransformApkTask : DefaultTask() {
@get:Input
abstract val comment: Property<String>
@get:Input @get:Input
abstract val signingConfig: Property<ApkSigningConfig> abstract val signingConfig: Property<ApkSigningConfig>
@@ -31,7 +30,10 @@ abstract class AddCommentTask: DefaultTask() {
abstract val outFolder: DirectoryProperty abstract val outFolder: DirectoryProperty
@get:Internal @get:Internal
abstract val transformationRequest: Property<ArtifactTransformationRequest<AddCommentTask>> abstract val transformations: ListProperty<(ZFile) -> Unit>
@get:Internal
abstract val transformationRequest: Property<ArtifactTransformationRequest<TransformApkTask>>
@TaskAction @TaskAction
fun taskAction() = transformationRequest.get().submit(this) { artifact -> fun taskAction() = transformationRequest.get().submit(this) { artifact ->
@@ -63,10 +65,10 @@ abstract class AddCommentTask: DefaultTask() {
inFile.copyTo(outFile, overwrite = true) inFile.copyTo(outFile, overwrite = true)
ZFiles.apk(outFile, options).use { ZFiles.apk(outFile, options).use {
SigningExtension(signingOptions).register(it) SigningExtension(signingOptions).register(it)
it.eocdComment = comment.get().toByteArray()
it.get(IncrementalPackager.APP_METADATA_ENTRY_PATH)?.delete() it.get(IncrementalPackager.APP_METADATA_ENTRY_PATH)?.delete()
it.get(IncrementalPackager.VERSION_CONTROL_INFO_ENTRY_PATH)?.delete() it.get(IncrementalPackager.VERSION_CONTROL_INFO_ENTRY_PATH)?.delete()
it.get(JarFile.MANIFEST_NAME)?.delete() it.get(JarFile.MANIFEST_NAME)?.delete()
transformations.get().forEach { transform -> transform(it) }
} }
outFile outFile
+1 -1
View File
@@ -17,4 +17,4 @@ pluginManagement {
} }
rootProject.name = "Magisk" rootProject.name = "Magisk"
include(":apk", ":apk-ng", ":core", ":shared", ":stub", ":test") include(":apk", ":apk-ng", ":core", ":shared", ":stub", ":stub-res", ":test")
+16
View File
@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.android.application)
}
setupCommon()
android {
namespace = "com.topjohnwu.magisk"
enableKotlin = false
buildTypes {
release {
isShrinkResources = false
}
}
}
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest><application /></manifest>
-1
View File
@@ -28,7 +28,6 @@ android {
release { release {
proguardFiles("proguard-rules.pro") proguardFiles("proguard-rules.pro")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = false
} }
} }
@@ -3,9 +3,6 @@ package com.topjohnwu.magisk;
import static android.R.string.no; import static android.R.string.no;
import static android.R.string.ok; import static android.R.string.ok;
import static android.R.string.yes; 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.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -46,14 +43,18 @@ import javax.crypto.spec.SecretKeySpec;
public class DownloadActivity extends Activity { public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk"; private static final String APP_NAME = "Magisk";
private static final String RES_PKG_NAME = "com.topjohnwu.magisk";
private Context themed;
private boolean dynLoad; private boolean dynLoad;
private int dling;
private int no_internet_msg;
private int upgrade_msg;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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 // Only download and dynamic load full APK if hidden
dynLoad = !getPackageName().equals(BuildConfig.APPLICATION_ID); dynLoad = !getPackageName().equals(BuildConfig.APPLICATION_ID);
@@ -63,6 +64,7 @@ public class DownloadActivity extends Activity {
loadResources(); loadResources();
} catch (Exception e) { } catch (Exception e) {
error(e); error(e);
return;
} }
ProviderInstaller.install(this); ProviderInstaller.install(this);
@@ -70,7 +72,7 @@ public class DownloadActivity extends Activity {
if (Networking.checkNetworkStatus(this)) { if (Networking.checkNetworkStatus(this)) {
showDialog(); showDialog();
} else { } else {
new AlertDialog.Builder(themed) new AlertDialog.Builder(this)
.setCancelable(false) .setCancelable(false)
.setTitle(APP_NAME) .setTitle(APP_NAME)
.setMessage(getString(no_internet_msg)) .setMessage(getString(no_internet_msg))
@@ -95,7 +97,7 @@ public class DownloadActivity extends Activity {
} }
private void showDialog() { private void showDialog() {
new AlertDialog.Builder(themed) new AlertDialog.Builder(this)
.setCancelable(false) .setCancelable(false)
.setTitle(APP_NAME) .setTitle(APP_NAME)
.setMessage(getString(upgrade_msg)) .setMessage(getString(upgrade_msg))
@@ -105,7 +107,7 @@ public class DownloadActivity extends Activity {
} }
private void dlAPK() { 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 // Download and upgrade the app
var request = request(BuildConfig.APK_URL).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR); var request = request(BuildConfig.APK_URL).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (dynLoad) { if (dynLoad) {
@@ -139,6 +141,7 @@ public class DownloadActivity extends Activity {
} }
private void loadResources() throws Exception { private void loadResources() throws Exception {
var res = getResources();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
var fd = Os.memfd_create("res", 0); var fd = Os.memfd_create("res", 0);
try { try {
@@ -147,14 +150,14 @@ public class DownloadActivity extends Activity {
var loader = new ResourcesLoader(); var loader = new ResourcesLoader();
try (var pfd = ParcelFileDescriptor.dup(fd)) { try (var pfd = ParcelFileDescriptor.dup(fd)) {
loader.addProvider(ResourcesProvider.loadFromTable(pfd, null)); loader.addProvider(ResourcesProvider.loadFromTable(pfd, null));
getResources().addLoaders(loader); res.addLoaders(loader);
} }
} finally { } finally {
Os.close(fd); Os.close(fd);
} }
} else { } else {
File res = new File(getCodeCacheDir(), "res.apk"); File apk = new File(getCodeCacheDir(), "res.apk");
try (var out = new ZipOutputStream(new FileOutputStream(res))) { try (var out = new ZipOutputStream(new FileOutputStream(apk))) {
// AndroidManifest.xml is required on Android 6-, and directory support is broken on Android 9-10 // AndroidManifest.xml is required on Android 6-, and directory support is broken on Android 9-10
out.putNextEntry(new ZipEntry("AndroidManifest.xml")); out.putNextEntry(new ZipEntry("AndroidManifest.xml"));
try (var stubApk = new ZipFile(getPackageCodePath())) { try (var stubApk = new ZipFile(getPackageCodePath())) {
@@ -163,7 +166,10 @@ public class DownloadActivity extends Activity {
out.putNextEntry(new ZipEntry("resources.arsc")); out.putNextEntry(new ZipEntry("resources.arsc"));
decryptResources(out); 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);
} }
} }