mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-12 22:32:10 +00:00
Move all gradle files into folder app
Decouple java and native projects
This commit is contained in:
1
app/buildSrc/.gitignore
vendored
Normal file
1
app/buildSrc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
34
app/buildSrc/build.gradle.kts
Normal file
34
app/buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,34 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("MagiskPlugin") {
|
||||
id = "MagiskPlugin"
|
||||
implementationClass = "MagiskPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
languageVersion = KotlinVersion.KOTLIN_2_0
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("gradle-plugin", libs.versions.kotlin.get()))
|
||||
implementation(libs.android.gradle.plugin)
|
||||
implementation(libs.ksp.plugin)
|
||||
implementation(libs.navigation.safe.args.plugin)
|
||||
implementation(libs.lsparanoid.plugin)
|
||||
implementation(libs.jgit)
|
||||
}
|
||||
7
app/buildSrc/settings.gradle.kts
Normal file
7
app/buildSrc/settings.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
261
app/buildSrc/src/main/java/Codegen.kt
Normal file
261
app/buildSrc/src/main/java/Codegen.kt
Normal file
@@ -0,0 +1,261 @@
|
||||
import org.gradle.api.DefaultTask
|
||||
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.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.security.SecureRandom
|
||||
import java.util.Random
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
// Set non-zero value here to fix the random seed for reproducible builds
|
||||
// CI builds are always reproducible
|
||||
val RAND_SEED = if (System.getenv("CI") != null) 42 else 0
|
||||
private lateinit var RANDOM: Random
|
||||
private val kRANDOM get() = RANDOM.asKotlinRandom()
|
||||
|
||||
private val c1 = mutableListOf<String>()
|
||||
private val c2 = mutableListOf<String>()
|
||||
private val c3 = mutableListOf<String>()
|
||||
|
||||
fun initRandom(dict: File) {
|
||||
RANDOM = if (RAND_SEED != 0) Random(RAND_SEED.toLong()) else SecureRandom()
|
||||
c1.clear()
|
||||
c2.clear()
|
||||
c3.clear()
|
||||
for (a in chain('a'..'z', 'A'..'Z')) {
|
||||
if (a != 'a' && a != 'A') {
|
||||
c1.add("$a")
|
||||
}
|
||||
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||
c2.add("$a$b")
|
||||
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||
c3.add("$a$b$c")
|
||||
}
|
||||
}
|
||||
}
|
||||
c1.shuffle(RANDOM)
|
||||
c2.shuffle(RANDOM)
|
||||
c3.shuffle(RANDOM)
|
||||
PrintStream(dict).use {
|
||||
for (c in chain(c1, c2, c3)) {
|
||||
it.println(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> chain(vararg iters: Iterable<T>) = sequence {
|
||||
iters.forEach { it.forEach { v -> yield(v) } }
|
||||
}
|
||||
|
||||
private fun PrintStream.byteField(name: String, bytes: ByteArray) {
|
||||
println("public static byte[] $name() {")
|
||||
print("byte[] buf = {")
|
||||
print(bytes.joinToString(",") { it.toString() })
|
||||
println("};")
|
||||
println("return buf;")
|
||||
println("}")
|
||||
}
|
||||
|
||||
@CacheableTask
|
||||
abstract class ManifestUpdater: DefaultTask() {
|
||||
@get:Input
|
||||
abstract val applicationId: Property<String>
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val mergedManifest: RegularFileProperty
|
||||
|
||||
@get:InputFiles
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val factoryClassDir: DirectoryProperty
|
||||
|
||||
@get:InputFiles
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val appClassDir: DirectoryProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val outputManifest: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun taskAction() {
|
||||
fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level))
|
||||
|
||||
val cmpList = mutableListOf<String>()
|
||||
|
||||
cmpList.add("""
|
||||
|<provider
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_0"
|
||||
| android:authorities="${'$'}{applicationId}.provider"
|
||||
| android:directBootAware="true"
|
||||
| android:exported="false"
|
||||
| android:grantUriPermissions="true" />""".ind(2)
|
||||
)
|
||||
|
||||
cmpList.add("""
|
||||
|<receiver
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_1"
|
||||
| android:exported="false">
|
||||
| <intent-filter>
|
||||
| <action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
| <action android:name="android.intent.action.UID_REMOVED" />
|
||||
| <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
| </intent-filter>
|
||||
| <intent-filter>
|
||||
| <action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
| <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
|
||||
|
|
||||
| <data android:scheme="package" />
|
||||
| </intent-filter>
|
||||
|</receiver>""".ind(2)
|
||||
)
|
||||
|
||||
cmpList.add("""
|
||||
|<activity
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_2"
|
||||
| android:exported="true">
|
||||
| <intent-filter>
|
||||
| <action android:name="android.intent.action.MAIN" />
|
||||
| <category android:name="android.intent.category.LAUNCHER" />
|
||||
| </intent-filter>
|
||||
|</activity>""".ind(2)
|
||||
)
|
||||
|
||||
cmpList.add("""
|
||||
|<activity
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_3"
|
||||
| android:directBootAware="true"
|
||||
| android:exported="false"
|
||||
| android:taskAffinity="">
|
||||
| <intent-filter>
|
||||
| <action android:name="android.intent.action.VIEW"/>
|
||||
| <category android:name="android.intent.category.DEFAULT"/>
|
||||
| </intent-filter>
|
||||
|</activity>""".ind(2)
|
||||
)
|
||||
|
||||
cmpList.add("""
|
||||
|<service
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_4"
|
||||
| android:exported="false"
|
||||
| android:foregroundServiceType="dataSync" />""".ind(2)
|
||||
)
|
||||
|
||||
cmpList.add("""
|
||||
|<service
|
||||
| android:name="x.COMPONENT_PLACEHOLDER_5"
|
||||
| android:exported="false"
|
||||
| android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2)
|
||||
)
|
||||
|
||||
// Shuffle the order of the components
|
||||
cmpList.shuffle(RANDOM)
|
||||
val (factoryPkg, factoryClass) = factoryClassDir.asFileTree.firstNotNullOf {
|
||||
it.parentFile!!.name to it.name.removeSuffix(".java")
|
||||
}
|
||||
val (appPkg, appClass) = appClassDir.asFileTree.firstNotNullOf {
|
||||
it.parentFile!!.name to it.name.removeSuffix(".java")
|
||||
}
|
||||
val components = cmpList.joinToString("\n\n")
|
||||
.replace("\${applicationId}", applicationId.get())
|
||||
val manifest = mergedManifest.asFile.get().readText().replace(Regex(".*\\<application"), """
|
||||
|<application
|
||||
| android:appComponentFactory="$factoryPkg.$factoryClass"
|
||||
| android:name="$appPkg.$appClass"""".ind(1)
|
||||
).replace(Regex(".*\\<\\/application"), "$components\n </application")
|
||||
outputManifest.get().asFile.writeText(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun genStubClasses(factoryOutDir: File, appOutDir: File) {
|
||||
fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level))
|
||||
|
||||
val classNameGenerator = sequence {
|
||||
fun notJavaKeyword(name: String) = when (name) {
|
||||
"do", "if", "for", "int", "new", "try" -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun List<String>.process() = asSequence()
|
||||
.filter(::notJavaKeyword)
|
||||
// Distinct by lower case to support case insensitive file systems
|
||||
.distinctBy { it.lowercase() }
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
names.addAll(c1)
|
||||
names.addAll(c2.process().take(30))
|
||||
names.addAll(c3.process().take(30))
|
||||
names.shuffle(RANDOM)
|
||||
|
||||
while (true) {
|
||||
val cls = StringBuilder()
|
||||
cls.append(names.random(kRANDOM))
|
||||
cls.append('.')
|
||||
cls.append(names.random(kRANDOM))
|
||||
// Old Android does not support capitalized package names
|
||||
// Check Android 7.0.0 PackageParser#buildClassName
|
||||
yield(cls.toString().replaceFirstChar { it.lowercase() })
|
||||
}
|
||||
}.distinct().iterator()
|
||||
|
||||
fun genClass(type: String, outDir: File) {
|
||||
val clzName = classNameGenerator.next()
|
||||
val (pkg, name) = clzName.split('.')
|
||||
val pkgDir = File(outDir, pkg)
|
||||
pkgDir.mkdirs()
|
||||
PrintStream(File(pkgDir, "$name.java")).use {
|
||||
it.println("package $pkg;")
|
||||
it.println("public class $name extends com.topjohnwu.magisk.$type {}")
|
||||
}
|
||||
}
|
||||
|
||||
genClass("DelegateComponentFactory", factoryOutDir)
|
||||
genClass("StubApplication", appOutDir)
|
||||
}
|
||||
|
||||
fun genEncryptedResources(res: ByteArray, outDir: File) {
|
||||
val mainPkgDir = File(outDir, "com/topjohnwu/magisk")
|
||||
mainPkgDir.mkdirs()
|
||||
|
||||
// Generate iv and key
|
||||
val iv = ByteArray(16)
|
||||
val key = ByteArray(32)
|
||||
RANDOM.nextBytes(iv)
|
||||
RANDOM.nextBytes(key)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
ByteArrayInputStream(res).use {
|
||||
CipherOutputStream(bos, cipher).use { os ->
|
||||
it.transferTo(os)
|
||||
}
|
||||
}
|
||||
|
||||
PrintStream(File(mainPkgDir, "Bytes.java")).use {
|
||||
it.println("package com.topjohnwu.magisk;")
|
||||
it.println("public final class Bytes {")
|
||||
|
||||
it.byteField("key", key)
|
||||
it.byteField("iv", iv)
|
||||
it.byteField("res", bos.toByteArray())
|
||||
|
||||
it.println("}")
|
||||
}
|
||||
}
|
||||
122
app/buildSrc/src/main/java/DesugarClassVisitorFactory.kt
Normal file
122
app/buildSrc/src/main/java/DesugarClassVisitorFactory.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Opcodes.ASM9
|
||||
|
||||
private const val DESUGAR_CLASS_NAME = "com.topjohnwu.magisk.core.utils.Desugar"
|
||||
private const val ZIP_ENTRY_CLASS_NAME = "java.util.zip.ZipEntry"
|
||||
private const val ZIP_OUT_STREAM_CLASS_NAME = "org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream"
|
||||
private const val ZIP_UTIL_CLASS_NAME = "org/apache/commons/compress/archivers/zip/ZipUtil"
|
||||
private const val ZIP_ENTRY_GET_TIME_DESC = "()Ljava/nio/file/attribute/FileTime;"
|
||||
private const val DESUGAR_GET_TIME_DESC =
|
||||
"(Ljava/util/zip/ZipEntry;)Ljava/nio/file/attribute/FileTime;"
|
||||
|
||||
private fun ClassData.isTypeOf(name: String) = className == name || superClasses.contains(name)
|
||||
|
||||
abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor
|
||||
): ClassVisitor {
|
||||
return if (classContext.currentClassData.className == ZIP_OUT_STREAM_CLASS_NAME) {
|
||||
ZipEntryPatcher(classContext, ZipOutputStreamPatcher(nextClassVisitor))
|
||||
} else {
|
||||
ZipEntryPatcher(classContext, nextClassVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData) = classData.className != DESUGAR_CLASS_NAME
|
||||
|
||||
// Patch ALL references to ZipEntry#getXXXTime
|
||||
class ZipEntryPatcher(
|
||||
private val classContext: ClassContext,
|
||||
cv: ClassVisitor
|
||||
) : ClassVisitor(ASM9, cv) {
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?
|
||||
) = MethodPatcher(super.visitMethod(access, name, descriptor, signature, exceptions))
|
||||
|
||||
inner class MethodPatcher(mv: MethodVisitor?) : MethodVisitor(ASM9, mv) {
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int,
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
isInterface: Boolean
|
||||
) {
|
||||
if (!process(owner, name, descriptor)) {
|
||||
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(owner: String, name: String, descriptor: String): Boolean {
|
||||
val classData = classContext.loadClassData(owner.replace("/", ".")) ?: return false
|
||||
if (!classData.isTypeOf(ZIP_ENTRY_CLASS_NAME))
|
||||
return false
|
||||
if (descriptor != ZIP_ENTRY_GET_TIME_DESC)
|
||||
return false
|
||||
return when (name) {
|
||||
"getLastModifiedTime", "getLastAccessTime", "getCreationTime" -> {
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
DESUGAR_CLASS_NAME.replace('.', '/'),
|
||||
name,
|
||||
DESUGAR_GET_TIME_DESC,
|
||||
false
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Patch ZipArchiveOutputStream#copyFromZipInputStream
|
||||
class ZipOutputStreamPatcher(cv: ClassVisitor) : ClassVisitor(ASM9, cv) {
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
signature: String?,
|
||||
exceptions: Array<out String?>?
|
||||
): MethodVisitor? {
|
||||
return if (name == "copyFromZipInputStream") {
|
||||
MethodPatcher(super.visitMethod(access, name, descriptor, signature, exceptions))
|
||||
} else {
|
||||
super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
}
|
||||
}
|
||||
|
||||
class MethodPatcher(mv: MethodVisitor?) : MethodVisitor(ASM9, mv) {
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int,
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String?,
|
||||
isInterface: Boolean
|
||||
) {
|
||||
if (owner == ZIP_UTIL_CLASS_NAME && name == "checkRequestedFeatures") {
|
||||
// Redirect
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
DESUGAR_CLASS_NAME.replace('.', '/'),
|
||||
name,
|
||||
descriptor,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/buildSrc/src/main/java/Plugin.kt
Normal file
49
app/buildSrc/src/main/java/Plugin.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import org.eclipse.jgit.internal.storage.file.FileRepository
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
|
||||
private val props = Properties()
|
||||
private var commitHash = ""
|
||||
private val supportAbis = setOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64", "riscv64")
|
||||
private val defaultAbis = setOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
||||
|
||||
object Config {
|
||||
operator fun get(key: String): String? {
|
||||
val v = props[key] as? String ?: return null
|
||||
return v.ifBlank { null }
|
||||
}
|
||||
|
||||
fun contains(key: String) = get(key) != null
|
||||
|
||||
val version: String get() = get("version") ?: commitHash
|
||||
val versionCode: Int get() = get("magisk.versionCode")!!.toInt()
|
||||
val stubVersion: String get() = get("magisk.stubVersion")!!
|
||||
val abiList: Set<String> get() {
|
||||
val abiList = get("abiList") ?: return defaultAbis
|
||||
return abiList.split(Regex("\\s*,\\s*")).toSet() intersect supportAbis
|
||||
}
|
||||
}
|
||||
|
||||
val Project.baseDir: File get() = rootProject.file("..")
|
||||
|
||||
class MagiskPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) = project.applyPlugin()
|
||||
|
||||
private fun Project.applyPlugin() {
|
||||
initRandom(rootProject.file("dict.txt"))
|
||||
props.clear()
|
||||
rootProject.file("gradle.properties").inputStream().use { props.load(it) }
|
||||
val configPath: String? by this
|
||||
val config = configPath?.let { File(it) } ?: File(baseDir, "config.prop")
|
||||
if (config.exists())
|
||||
config.inputStream().use { props.load(it) }
|
||||
|
||||
val repo = FileRepository(File(baseDir, ".git"))
|
||||
val refId = repo.refDatabase.exactRef("HEAD").objectId
|
||||
commitHash = repo.newObjectReader().abbreviate(refId, 8).name()
|
||||
}
|
||||
}
|
||||
497
app/buildSrc/src/main/java/Setup.kt
Normal file
497
app/buildSrc/src/main/java/Setup.kt
Normal file
@@ -0,0 +1,497 @@
|
||||
import com.android.build.api.artifact.ArtifactTransformationRequest
|
||||
import com.android.build.api.artifact.SingleArtifact
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.build.api.instrumentation.FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
|
||||
import com.android.build.api.instrumentation.InstrumentationScope
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.android.builder.internal.packaging.IncrementalPackager
|
||||
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.ZFileOptions
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.gradle.api.tasks.Delete
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.StopExecutionException
|
||||
import org.gradle.api.tasks.Sync
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.exclude
|
||||
import org.gradle.kotlin.dsl.filter
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getValue
|
||||
import org.gradle.kotlin.dsl.named
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.registering
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.HexFormat
|
||||
import java.util.jar.JarFile
|
||||
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
|
||||
|
||||
private fun Project.androidBase(configure: Action<BaseExtension>) =
|
||||
extensions.configure("android", configure)
|
||||
|
||||
private fun Project.android(configure: Action<BaseAppModuleExtension>) =
|
||||
extensions.configure("android", configure)
|
||||
|
||||
private val Project.androidApp: BaseAppModuleExtension
|
||||
get() = extensions["android"] as BaseAppModuleExtension
|
||||
|
||||
private val Project.androidLib: LibraryExtension
|
||||
get() = extensions["android"] as LibraryExtension
|
||||
|
||||
private val Project.androidComponents
|
||||
get() = extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
|
||||
|
||||
fun Project.setupCommon() {
|
||||
androidBase {
|
||||
compileSdkVersion(36)
|
||||
buildToolsVersion = "36.0.0"
|
||||
ndkPath = "$sdkDirectory/ndk/magisk"
|
||||
ndkVersion = "29.0.13113456"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += arrayOf(
|
||||
"/META-INF/*",
|
||||
"/META-INF/androidx/**",
|
||||
"/META-INF/versions/**",
|
||||
"/org/bouncycastle/**",
|
||||
"/org/apache/commons/**",
|
||||
"/kotlin/**",
|
||||
"/kotlinx/**",
|
||||
"/okhttp3/**",
|
||||
"/*.txt",
|
||||
"/*.bin",
|
||||
"/*.json",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Project.downloadFile(url: String, checksum: String): File {
|
||||
val file = layout.buildDirectory.file(checksum).get().asFile
|
||||
if (file.exists()) {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
file.inputStream().use { md.update(it.readAllBytes()) }
|
||||
val hash = HexFormat.of().formatHex(md.digest())
|
||||
if (hash != checksum) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.parentFile.mkdirs()
|
||||
URI(url).toURL().openStream().use { dl ->
|
||||
file.outputStream().use {
|
||||
dl.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
const val BUSYBOX_DOWNLOAD_URL =
|
||||
"https://github.com/topjohnwu/magisk-files/releases/download/files/busybox-1.36.1.1.zip"
|
||||
const val BUSYBOX_ZIP_CHECKSUM =
|
||||
"b4d0551feabaf314e53c79316c980e8f66432e9fb91a69dbbf10a93564b40951"
|
||||
|
||||
fun Project.setupCoreLib() {
|
||||
setupCommon()
|
||||
|
||||
val abiList = Config.abiList
|
||||
|
||||
val syncLibs by tasks.registering(Sync::class) {
|
||||
into("src/main/jniLibs")
|
||||
for (abi in abiList) {
|
||||
into(abi) {
|
||||
from(File(baseDir, "native/out/$abi")) {
|
||||
include("magiskboot", "magiskinit", "magiskpolicy", "magisk", "libinit-ld.so")
|
||||
rename { if (it.endsWith(".so")) it else "lib$it.so" }
|
||||
}
|
||||
}
|
||||
}
|
||||
onlyIf {
|
||||
if (inputs.sourceFiles.files.size != abiList.size * 5)
|
||||
throw StopExecutionException("Please build binaries first! (./build.py binary)")
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val downloadBusybox by tasks.registering(Copy::class) {
|
||||
dependsOn(syncLibs)
|
||||
from(zipTree(downloadFile(BUSYBOX_DOWNLOAD_URL, BUSYBOX_ZIP_CHECKSUM)))
|
||||
include(abiList.map { "$it/libbusybox.so" })
|
||||
into("src/main/jniLibs")
|
||||
}
|
||||
|
||||
val syncResources by tasks.registering(Sync::class) {
|
||||
into("src/main/resources/META-INF/com/google/android")
|
||||
from(File(baseDir, "scripts/update_binary.sh")) {
|
||||
rename { "update-binary" }
|
||||
}
|
||||
from(File(baseDir, "scripts/flash_script.sh")) {
|
||||
rename { "updater-script" }
|
||||
}
|
||||
}
|
||||
|
||||
androidLib.libraryVariants.all {
|
||||
val variantCapped = name.replaceFirstChar { it.uppercase() }
|
||||
|
||||
tasks.getByPath("merge${variantCapped}JniLibFolders").dependsOn(downloadBusybox)
|
||||
processJavaResourcesProvider.configure { dependsOn(syncResources) }
|
||||
|
||||
val stubTask = tasks.getByPath(":stub:comment$variantCapped")
|
||||
val stubApk = stubTask.outputs.files.asFileTree.filter {
|
||||
it.name.endsWith(".apk")
|
||||
}
|
||||
|
||||
val syncAssets = tasks.register("sync${variantCapped}Assets", Sync::class) {
|
||||
dependsOn(stubTask)
|
||||
inputs.property("version", Config.version)
|
||||
inputs.property("versionCode", Config.versionCode)
|
||||
into("src/${this@all.name}/assets")
|
||||
from(File(baseDir, "scripts")) {
|
||||
include("util_functions.sh", "boot_patch.sh", "addon.d.sh",
|
||||
"app_functions.sh", "uninstaller.sh", "module_installer.sh")
|
||||
}
|
||||
from(File(baseDir, "tools/bootctl"))
|
||||
into("chromeos") {
|
||||
from(File(baseDir, "tools/futility"))
|
||||
from(File(baseDir, "tools/keys")) {
|
||||
include("kernel_data_key.vbprivk", "kernel.keyblock")
|
||||
}
|
||||
}
|
||||
from(stubApk) {
|
||||
rename { "stub.apk" }
|
||||
}
|
||||
filesMatching("**/util_functions.sh") {
|
||||
filter {
|
||||
it.replace(
|
||||
"#MAGISK_VERSION_STUB",
|
||||
"MAGISK_VER='${Config.version}'\nMAGISK_VER_CODE=${Config.versionCode}"
|
||||
)
|
||||
}
|
||||
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
}
|
||||
}
|
||||
mergeAssetsProvider.configure { dependsOn(syncAssets) }
|
||||
}
|
||||
|
||||
tasks.named<Delete>("clean") {
|
||||
delete.addAll(listOf("src/main/jniLibs", "src/main/resources", "src/debug", "src/release"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ApkSigningConfig.getPrivateKey(): KeyStore.PrivateKeyEntry {
|
||||
val keyStore = KeyStore.getInstance(storeType ?: KeyStore.getDefaultType())
|
||||
storeFile!!.inputStream().use {
|
||||
keyStore.load(it, storePassword!!.toCharArray())
|
||||
}
|
||||
val keyPwdArray = keyPassword!!.toCharArray()
|
||||
val entry = keyStore.getEntry(keyAlias!!, KeyStore.PasswordProtection(keyPwdArray))
|
||||
return entry as KeyStore.PrivateKeyEntry
|
||||
}
|
||||
|
||||
abstract class AddCommentTask: DefaultTask() {
|
||||
@get:Input
|
||||
abstract val comment: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val signingConfig: Property<ApkSigningConfig>
|
||||
|
||||
@get:InputFiles
|
||||
abstract val apkFolder: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outFolder: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val transformationRequest: Property<ArtifactTransformationRequest<AddCommentTask>>
|
||||
|
||||
@TaskAction
|
||||
fun taskAction() = transformationRequest.get().submit(this) { artifact ->
|
||||
val inFile = File(artifact.outputFile)
|
||||
val outFile = outFolder.file(inFile.name).get().asFile
|
||||
|
||||
val privateKey = signingConfig.get().getPrivateKey()
|
||||
val signingOptions = SigningOptions.builder()
|
||||
.setMinSdkVersion(0)
|
||||
.setV1SigningEnabled(true)
|
||||
.setV2SigningEnabled(true)
|
||||
.setKey(privateKey.privateKey)
|
||||
.setCertificates(privateKey.certificate as X509Certificate)
|
||||
.setValidation(SigningOptions.Validation.ASSUME_INVALID)
|
||||
.build()
|
||||
val options = ZFileOptions().apply {
|
||||
noTimestamps = true
|
||||
autoSortFiles = true
|
||||
}
|
||||
outFile.parentFile?.mkdirs()
|
||||
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()
|
||||
}
|
||||
|
||||
outFile
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupAppCommon() {
|
||||
setupCommon()
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
create("config") {
|
||||
Config["keyStore"]?.also {
|
||||
storeFile = File(baseDir, it)
|
||||
storePassword = Config["keyStorePass"]
|
||||
keyAlias = Config["keyAlias"]
|
||||
keyPassword = Config["keyPass"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
targetSdk = 36
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt")
|
||||
)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
signingConfigs["config"].also {
|
||||
debug {
|
||||
signingConfig = if (it.storeFile?.exists() == true) it
|
||||
else signingConfigs["debug"]
|
||||
}
|
||||
release {
|
||||
signingConfig = if (it.storeFile?.exists() == true) it
|
||||
else signingConfigs["debug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "MissingTranslation"
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
val commentTask = tasks.register(
|
||||
"comment${variant.name.replaceFirstChar { it.uppercase() }}",
|
||||
AddCommentTask::class.java
|
||||
)
|
||||
val transformationRequest = variant.artifacts.use(commentTask)
|
||||
.wiredWithDirectories(AddCommentTask::apkFolder, AddCommentTask::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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupMainApk() {
|
||||
setupAppCommon()
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64", "riscv64")
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
variant.instrumentation.apply {
|
||||
setAsmFramesComputationMode(COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
|
||||
transformClassesWith(
|
||||
DesugarClassVisitorFactory::class.java, InstrumentationScope.ALL) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupStubApk() {
|
||||
setupAppCommon()
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
val variantName = variant.name
|
||||
val variantCapped = variantName.replaceFirstChar { it.uppercase() }
|
||||
val manifestUpdater =
|
||||
project.tasks.register("${variantName}ManifestProducer", ManifestUpdater::class.java) {
|
||||
dependsOn("generate${variantCapped}ObfuscatedClass")
|
||||
applicationId = variant.applicationId
|
||||
appClassDir.set(layout.buildDirectory.dir("generated/source/app/$variantName"))
|
||||
factoryClassDir.set(layout.buildDirectory.dir("generated/source/factory/$variantName"))
|
||||
}
|
||||
variant.artifacts.use(manifestUpdater)
|
||||
.wiredWithFiles(
|
||||
ManifestUpdater::mergedManifest,
|
||||
ManifestUpdater::outputManifest)
|
||||
.toTransform(SingleArtifact.MERGED_MANIFEST)
|
||||
}
|
||||
|
||||
androidApp.applicationVariants.all {
|
||||
val variantCapped = name.replaceFirstChar { it.uppercase() }
|
||||
val variantLowered = name.lowercase()
|
||||
val outFactoryClassDir = layout.buildDirectory.file("generated/source/factory/${variantLowered}").get().asFile
|
||||
val outAppClassDir = layout.buildDirectory.file("generated/source/app/${variantLowered}").get().asFile
|
||||
val outResDir = layout.buildDirectory.dir("generated/source/res/${variantLowered}").get().asFile
|
||||
val aapt = File(androidApp.sdkDirectory, "build-tools/${androidApp.buildToolsVersion}/aapt2")
|
||||
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
|
||||
"${variantLowered}/process${variantCapped}Resources/linked-resources-binary-format-${variantLowered}.ap_").get().asFile
|
||||
|
||||
val genManifestTask = tasks.register("generate${variantCapped}ObfuscatedClass") {
|
||||
inputs.property("seed", RAND_SEED)
|
||||
outputs.dirs(outFactoryClassDir, outAppClassDir)
|
||||
doLast {
|
||||
outFactoryClassDir.mkdirs()
|
||||
outAppClassDir.mkdirs()
|
||||
genStubClasses(outFactoryClassDir, outAppClassDir)
|
||||
}
|
||||
}
|
||||
registerJavaGeneratingTask(genManifestTask, outFactoryClassDir, outAppClassDir)
|
||||
|
||||
val processResourcesTask = tasks.named("process${variantCapped}Resources") {
|
||||
outputs.dir(outResDir)
|
||||
doLast {
|
||||
val apkTmp = File("${apk}.tmp")
|
||||
exec {
|
||||
commandLine(aapt, "optimize", "-o", apkTmp, "--collapse-resource-names", apk)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
DeflaterOutputStream(bos, Deflater(Deflater.BEST_COMPRESSION)).use {
|
||||
src.getInputStream(src.getEntry("resources.arsc")).transferTo(it)
|
||||
}
|
||||
}
|
||||
apkTmp.delete()
|
||||
genEncryptedResources(bos.toByteArray(), outResDir)
|
||||
}
|
||||
}
|
||||
|
||||
registerJavaGeneratingTask(processResourcesTask, outResDir)
|
||||
}
|
||||
// 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"))
|
||||
}
|
||||
}
|
||||
|
||||
const val LSPOSED_DOWNLOAD_URL =
|
||||
"https://github.com/LSPosed/LSPosed/releases/download/v1.9.2/LSPosed-v1.9.2-7024-zygisk-release.zip"
|
||||
const val LSPOSED_CHECKSUM =
|
||||
"0ebc6bcb465d1c4b44b7220ab5f0252e6b4eb7fe43da74650476d2798bb29622"
|
||||
|
||||
const val SHAMIKO_DOWNLOAD_URL =
|
||||
"https://github.com/LSPosed/LSPosed.github.io/releases/download/shamiko-383/Shamiko-v1.2.1-383-release.zip"
|
||||
const val SHAMIKO_CHECKSUM =
|
||||
"93754a038c2d8f0e985bad45c7303b96f70a93d8335060e50146f028d3a9b13f"
|
||||
|
||||
fun Project.setupTestApk() {
|
||||
setupAppCommon()
|
||||
|
||||
androidApp.applicationVariants.all {
|
||||
val variantCapped = name.replaceFirstChar { it.uppercase() }
|
||||
val dlTask by tasks.register("download${variantCapped}Lsposed", Sync::class) {
|
||||
from(downloadFile(LSPOSED_DOWNLOAD_URL, LSPOSED_CHECKSUM)) {
|
||||
rename { "lsposed.zip" }
|
||||
}
|
||||
from(downloadFile(SHAMIKO_DOWNLOAD_URL, SHAMIKO_CHECKSUM)) {
|
||||
rename { "shamiko.zip" }
|
||||
}
|
||||
into("src/${this@all.name}/assets")
|
||||
}
|
||||
mergeAssetsProvider.configure { dependsOn(dlTask) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user