Compare commits

..

1 commit

Author SHA1 Message Date
bdf39d741b
feat: builtin magisk module supports 2025-08-08 11:34:51 +08:00

View file

@ -1,16 +1,15 @@
package im.angry.openeuicc.build package im.angry.openeuicc.build
import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationDefaultConfig
import groovy.json.JsonOutput
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
import java.util.SortedMap
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.io.path.Path
import kotlin.io.path.name
class MagiskModule : Plugin<Project> { class MagiskModule : Plugin<Project> {
@ -19,131 +18,78 @@ class MagiskModule : Plugin<Project> {
group = "magisk" group = "magisk"
description = "Assembles the Magisk Module" description = "Assembles the Magisk Module"
val config = target.defaultConfig
val appFile = target.layout.buildDirectory
.file("outputs/apk/debug/app-debug.apk")
.get().asFile
val options = MagiskModuleOptions(
appId = config.applicationId!!,
appFile = appFile,
permissionFile = target.rootProject.file("privapp_whitelist_${config.applicationId}.xml"),
versionName = config.versionName!!,
versionCode = config.versionCode!!,
manifestUri = ""
)
val manifest = MagiskModuleManifest(
versionName = options.versionName,
versionCode = options.versionCode,
releaseUri = "",
changelogUri = ""
)
project.layout.buildDirectory project.layout.buildDirectory
.file("outputs/magisk-module.zip") .file("outputs/magisk-module.zip")
.get().asFile .get().asFile
.writeBytes(buildMagiskModule(options)) .writeBytes(buildMagiskModule(target))
project.layout.buildDirectory
.file("outputs/magisk-module-manifest.json")
.get().asFile
.writeText(buildMagiskModuleManifestFile(manifest))
} }
} }
} }
data class MagiskModuleOptions( private fun buildMagiskModule(target: Project) = buildZipFile {
val appId: String, val defaultConfig = target.defaultConfig
val appName: String = "OpenEUICC", val appId = defaultConfig.applicationId
val appFile: File, val apkPath = "system/system_ext/priv-app/OpenEUICC/OpenEUICC.apk"
val permissionFile: File,
val versionName: String,
val versionCode: Int,
val author: String = "Peter Cai",
val description: String = "OpenEUICC provides system-level eUICC integration",
val manifestUri: String? = null,
)
data class MagiskModuleManifest(
val versionName: String,
val versionCode: Int,
val releaseUri: String,
val changelogUri: String,
)
private fun buildMagiskModule(options: MagiskModuleOptions) = buildZipFile {
// https://topjohnwu.github.io/Magisk/guides.html
val systemExt = "system/system_ext"
val metaInfo = "META-INF/com/google/android"
val apkPath = "$systemExt/priv-app/${options.appName}/${options.appName}.apk"
put("module.prop") { put("module.prop") {
val module = buildList { val module = buildList {
add("id" to options.appId) add("id" to appId.toString())
add("name" to options.appName) add("name" to "OpenEUICC")
add("version" to options.versionName) add("version" to defaultConfig.versionName + defaultConfig.versionNameSuffix.orEmpty())
add("versionCode" to options.versionCode) add("versionCode" to defaultConfig.versionCode.toString())
add("author" to options.author) add("author" to "Peter Cai")
add("description" to options.description) add("description" to "OpenEUICC provides system-level eUICC integration")
if (options.manifestUri != null) add("updateJson" to options.manifestUri) add("updateJson" to "")
} }
module module
.joinToString("\n") { (key, value) -> "${key}=${value}" } .joinToString("\n") { (key, value) -> "${key}=${value}" }
.encodeToByteArray() .encodeToByteArray()
} }
put("customize.sh") { put("customize.sh") {
val copiedApkPath = "\$TMPDIR/${options.appName}.apk" val copiedApkPath = "\$TMPDIR/${Path(apkPath).name}"
val script = buildString { val script = buildString {
appendLine("chmod u+x \"\$MODPATH/uninstall.sh\"") appendLine("chmod u+x \"\$MODPATH/uninstall.sh\"")
appendLine()
appendLine("cp \"\$MODPATH/$apkPath\" \"$copiedApkPath\"") appendLine("cp \"\$MODPATH/$apkPath\" \"$copiedApkPath\"")
appendLine("pm install -r \"$copiedApkPath\"") appendLine("pm install -r \"$copiedApkPath\"")
appendLine("rm -f \"$copiedApkPath\"") appendLine("rm -f \"$copiedApkPath\"")
appendLine()
appendLine("pm grant \"${options.appId}\" android.permission.READ_PHONE_STATE") appendLine("pm grant \"${appId}\" android.permission.READ_PHONE_STATE")
} }
script.encodeToByteArray() script.encodeToByteArray()
} }
put("uninstall.sh") { put("uninstall.sh") {
"pm uninstall ${options.appId}\n".encodeToByteArray() "pm uninstall ${appId}\n".encodeToByteArray()
} }
put("$metaInfo/update-binary") { put("META-INF/com/google/android/update-binary") {
val installerUri = val installerUri =
URI("https://github.com/topjohnwu/Magisk/raw/bf4ed29/scripts/module_installer.sh") URI("https://github.com/topjohnwu/Magisk/raw/bf4ed29/scripts/module_installer.sh")
val connection = URL.of(installerUri, null).openConnection() val connection = URL.of(installerUri, null).openConnection()
connection.inputStream.readBytes() connection.inputStream.readBytes()
} }
put("$metaInfo/updater-script") { put("META-INF/com/google/android/updater-script") {
"#MAGISK\n".encodeToByteArray() "#MAGISK\n".encodeToByteArray()
} }
put(apkPath) { put(apkPath) {
options.appFile.readBytes() target.layout.buildDirectory
.file("outputs/apk/debug/app-debug.apk")
.get().asFile
.readBytes()
} }
put("$systemExt/etc/permissions/privapp_whitelist_${options.appId}.xml") { put("system/system_ext/etc/permissions/privapp_whitelist_${appId}.xml") {
options.permissionFile.readBytes() target.rootProject
.file("privapp_whitelist_${appId}.xml")
.readBytes()
} }
} }
private fun buildMagiskModuleManifestFile(manifest: MagiskModuleManifest): String { private fun buildZipFile(builderAction: MutableMap<String, (ZipEntry) -> ByteArray>.() -> Unit): ByteArray {
val entries = buildMap {
put("version", manifest.versionName)
put("versionCode", manifest.versionCode)
put("zipUrl", manifest.releaseUri)
put("changelog", manifest.changelogUri)
}
val jsonPayload = JsonOutput.toJson(entries)
return JsonOutput.prettyPrint(jsonPayload)
}
private fun buildZipFile(builderAction: SortedMap<String, () -> ByteArray>.() -> Unit): ByteArray {
val out = ByteArrayOutputStream() val out = ByteArrayOutputStream()
val zip = ZipOutputStream(out) val zip = ZipOutputStream(out)
val entries = buildMap { builderAction(this.toSortedMap()) } for ((name, invoke) in buildMap(builderAction)) {
for ((name, generate) in entries) {
val entry = ZipEntry(name) val entry = ZipEntry(name)
entry.time = 0 // reproducible builds entry.time = 0 // reproducible builds
zip.putNextEntry(entry) zip.putNextEntry(entry)
zip.write(generate()) zip.write(invoke(entry))
zip.closeEntry() zip.closeEntry()
} }
zip.close() zip.close()
@ -151,6 +97,7 @@ private fun buildZipFile(builderAction: SortedMap<String, () -> ByteArray>.() ->
return out.toByteArray() return out.toByteArray()
} }
private val Project.defaultConfig: ApplicationDefaultConfig private val Project.defaultConfig: ApplicationDefaultConfig
get() = when (val android = extensions.findByName("android")) { get() = when (val android = extensions.findByName("android")) {
is com.android.build.gradle.AppExtension -> android.defaultConfig is com.android.build.gradle.AppExtension -> android.defaultConfig