fix: remove product flavors and use APK splits

This commit is contained in:
Akash Yadav
2024-01-04 00:49:32 +05:30
parent 3611d833a6
commit e55fabdc32
16 changed files with 200 additions and 513 deletions
+18 -10
View File
@@ -46,7 +46,6 @@ jobs:
strategy:
matrix:
build_type: ['debug', 'release']
flavor: ['arm64-v8a', 'armeabi-v7a', 'x86_64']
name: Build APK
runs-on: ubuntu-latest
steps:
@@ -74,20 +73,29 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Assemble ${{ matrix.flavor }} ${{ matrix.build_type }} APK
- name: Assemble ${{ matrix.build_type }} APK
run: |
build_type=${{ matrix.build_type }}
flavor=${{ matrix.flavor }}
./gradlew :app:assemble${flavor^}${build_type^}
- name: Copy ${{ matrix.flavor }} ${{ matrix.build_type }} APK
./gradlew :app:assemble${build_type^}
- name: Copy ${{ matrix.build_type }} APK
run: |
cp app/build/outputs/apk/${{ matrix.flavor }}/${{ matrix.build_type }}/app-${{ matrix.flavor }}-${{ matrix.flavor }}-${{ matrix.build_type }}.apk ./
- name: Upload ${{ matrix.flavor }} ${{ matrix.build_type }} APK
cp app/build/outputs/apk/${{ matrix.build_type }}/app-*.apk ./
- name: Upload arm64-v8a ${{ matrix.build_type }} APK
uses: actions/upload-artifact@v3
with:
name: apk-${{ matrix.flavor }}-${{ matrix.build_type }}
path: app-${{ matrix.flavor }}-${{ matrix.flavor }}-${{ matrix.build_type }}.apk
- name: Upload ${{ matrix.flavor }} ${{ matrix.build_type }} mappings
name: apk-arm64-v8a-${{ matrix.build_type }}
path: app-arm64-v8a-${{ matrix.build_type }}.apk
- name: Upload armeabi-v7a ${{ matrix.build_type }} APK
uses: actions/upload-artifact@v3
with:
name: apk-armeabi-v7a-${{ matrix.build_type }}
path: app-armeabi-v7a-${{ matrix.build_type }}.apk
- name: Upload x86_64 ${{ matrix.build_type }} APK
uses: actions/upload-artifact@v3
with:
name: apk-x86_64-${{ matrix.build_type }}
path: app-x86_64-${{ matrix.build_type }}.apk
- name: Upload ${{ matrix.build_type }} mappings
if: ${{ matrix.build_type == 'release' }}
uses: actions/upload-artifact@v3
with:
+3 -3
View File
@@ -17,17 +17,17 @@ releaseAssets:
apk_arm64:
fileName: "androidide-{{version}}-arm64-v8a.apk"
description: "AndroidIDE {{version}} arm64-v8a release APK"
path: app-arm64-v8a-arm64-v8a-release.apk
path: app-arm64-v8a-release.apk
type: application/octet-stream
apk_x86_64:
fileName: "androidide-{{version}}-x86_64.apk"
description: "AndroidIDE {{version}} x86_64 release APK"
path: app-x86_64-x86_64-release.apk
path: app-x86_64-release.apk
type: application/octet-stream
apk_arm:
fileName: "androidide-{{version}}-armeabi-v7a.apk"
description: "AndroidIDE {{version}} armeabi-v7a release APK"
path: app-armeabi-v7a-armeabi-v7a-release.apk
path: app-armeabi-v7a-release.apk
type: application/octet-stream
releaseTypes:
-20
View File
@@ -1,20 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.itsaky.androidide",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 230,
"versionName": "v2.3.0-beta.internal.1+branch.main.commit.9ef92d5.timestamp.20230315180430",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}
@@ -204,7 +204,7 @@ class AboutActivity : LimitlessIDEActivity() {
builder.append("v")
builder.append(BuildInfo.VERSION_NAME_SIMPLE)
builder.append("-")
builder.append(BuildConfig.FLAVOR)
builder.append(BuildConfig.ABI_CURRENT)
builder.append(" ")
val colorPositive = ContextCompat.getColor(this, R.color.color_success)
@@ -177,7 +177,7 @@ class OnboardingActivity : AppIntro2() {
getString(string.title_unsupported_device),
getString(
string.msg_unsupported_device,
configProvider.flavorArch.abi,
configProvider.cpuArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
@@ -186,14 +186,14 @@ class OnboardingActivity : AppIntro2() {
return false
}
if (configProvider.flavorArch != configProvider.deviceArch) {
if (configProvider.cpuArch != configProvider.deviceArch) {
// IDE's build flavor is NOT the primary arch of the device
// warn the user
if (!archConfigExperimentalWarningIsShown()) {
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_experiment_flavor),
getString(string.msg_experimental_flavor,
configProvider.flavorArch.abi,
configProvider.cpuArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
@@ -20,6 +20,7 @@ package com.itsaky.androidide.app.configuration
import android.os.Build
import com.google.auto.service.AutoService
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.app.IDEApplication
/**
* @author Akash Yadav
@@ -28,35 +29,23 @@ import com.itsaky.androidide.BuildConfig
@Suppress("UNUSED")
open class IDEBuildConfigProviderImpl : IDEBuildConfigProvider {
override val buildFlavor: String
get() = BuildConfig.FLAVOR
override val cpuAbiName: String by lazy {
val applicationInfo = IDEApplication.instance.applicationInfo!!
override val flavorArch: CpuArch
get() = CpuArch.forAbi(buildFlavor)!!
// transform to valid ABI names
return@lazy when (val abi = applicationInfo.nativeLibraryDir!!.substringAfterLast('/')) {
"arm64" -> BuildConfig.ABI_ARM64_V8A
"arm" -> BuildConfig.ABI_ARMEABI_V7A
else -> abi
}
}
override val cpuArch: CpuArch
get() = CpuArch.forAbi(cpuAbiName)!!
override val deviceArch: CpuArch
get() = CpuArch.forAbi(supportedAbis[0])!!
override val supportedAbis: Array<String>
get() = Build.SUPPORTED_ABIS
override val supportedBuildFlavors: Array<String> by lazy {
val flavors = mutableListOf<String>()
if (supportedAbis.contains(BuildConfig.FLAVOR_ARM64_V8A)) {
flavors.add(BuildConfig.FLAVOR_ARM64_V8A)
}
if (supportedAbis.contains(BuildConfig.FLAVOR_X86_64)) {
flavors.add(BuildConfig.FLAVOR_X86_64)
}
if (supportedAbis.contains(BuildConfig.FLAVOR_ARMEABI_V7A)) {
flavors.add(BuildConfig.FLAVOR_ARMEABI_V7A)
}
return@lazy flavors.toTypedArray()
}
companion object {
private fun Array<String>?.checkIsPrimaryArch(arch: String): Boolean {
return this?.isNotEmpty() == true && this[0] == arch
}
}
}
@@ -39,7 +39,7 @@ object BuildInfoUtils {
"CI Build" to BuildInfo.CI_BUILD,
"Branch" to BuildInfo.CI_GIT_BRANCH,
"Commit" to BuildInfo.CI_GIT_COMMIT_HASH,
"Variant" to "${BuildConfig.FLAVOR} (${BuildConfig.BUILD_TYPE})",
"Variant" to "${BuildConfig.ABI_CURRENT} (${BuildConfig.BUILD_TYPE})",
"Build type" to getBuildType(),
"SDK Version" to Build.VERSION.SDK_INT,
"Supported ABIs" to "[${Build.SUPPORTED_ABIS.joinToString(separator = ", ")}]",
@@ -18,10 +18,9 @@
package com.itsaky.androidide.app
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.BuildConfig.FLAVOR_ARM64_V8A
import com.itsaky.androidide.BuildConfig.FLAVOR_ARMEABI_V7A
import com.itsaky.androidide.BuildConfig.FLAVOR_X86_64
import com.itsaky.androidide.BuildConfig.ABI_ARM64_V8A
import com.itsaky.androidide.BuildConfig.ABI_ARMEABI_V7A
import com.itsaky.androidide.BuildConfig.ABI_X86_64
import com.itsaky.androidide.app.configuration.CpuArch
import com.itsaky.androidide.app.configuration.IDEBuildConfigProviderImpl
import org.junit.Test
@@ -31,18 +30,18 @@ import org.junit.Test
*/
class IDEBuildConfigProviderImplTest {
class TestBuildConfigProvider(override val buildFlavor: String,
class TestBuildConfigProvider(override val cpuAbiName: String,
override val supportedAbis: Array<String>) : IDEBuildConfigProviderImpl()
@Test
fun `test aarch64 build on aarch64-only device`() {
TestBuildConfigProvider(FLAVOR_ARM64_V8A, arrayOf(FLAVOR_ARM64_V8A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARM64_V8A)
assertThat(flavorArch).isEqualTo(CpuArch.AARCH64)
TestBuildConfigProvider(ABI_ARM64_V8A, arrayOf(ABI_ARM64_V8A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARM64_V8A)
assertThat(cpuArch).isEqualTo(CpuArch.AARCH64)
assertThat(deviceArch).isEqualTo(CpuArch.AARCH64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_ARM64_V8A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_ARM64_V8A)
assertThat(supportedAbis.toList()).containsExactly(ABI_ARM64_V8A)
assertThat(isArm64v8aBuild()).isTrue()
assertThat(isX86_64Build()).isFalse()
@@ -53,19 +52,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isEqualTo(deviceArch)
assertThat(cpuArch).isEqualTo(deviceArch)
}
}
@Test
fun `test arm build on arm-only device`() {
TestBuildConfigProvider(FLAVOR_ARMEABI_V7A, arrayOf(FLAVOR_ARMEABI_V7A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARMEABI_V7A)
assertThat(flavorArch).isEqualTo(CpuArch.ARM)
TestBuildConfigProvider(ABI_ARMEABI_V7A, arrayOf(ABI_ARMEABI_V7A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARMEABI_V7A)
assertThat(cpuArch).isEqualTo(CpuArch.ARM)
assertThat(deviceArch).isEqualTo(CpuArch.ARM)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_ARMEABI_V7A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_ARMEABI_V7A)
assertThat(supportedAbis.toList()).containsExactly(ABI_ARMEABI_V7A)
assertThat(isArm64v8aBuild()).isFalse()
assertThat(isX86_64Build()).isFalse()
@@ -76,19 +75,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isTrue()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isEqualTo(deviceArch)
assertThat(cpuArch).isEqualTo(deviceArch)
}
}
@Test
fun `test x86_64 build on x86_64-only device`() {
TestBuildConfigProvider(FLAVOR_X86_64, arrayOf(FLAVOR_X86_64)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_X86_64)
assertThat(flavorArch).isEqualTo(CpuArch.X86_64)
TestBuildConfigProvider(ABI_X86_64, arrayOf(ABI_X86_64)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_X86_64)
assertThat(cpuArch).isEqualTo(CpuArch.X86_64)
assertThat(deviceArch).isEqualTo(CpuArch.X86_64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_X86_64)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_X86_64)
assertThat(supportedAbis.toList()).containsExactly(ABI_X86_64)
assertThat(isArm64v8aBuild()).isFalse()
assertThat(isX86_64Build()).isTrue()
@@ -99,19 +98,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isEqualTo(deviceArch)
assertThat(cpuArch).isEqualTo(deviceArch)
}
}
@Test
fun `test arm build on (aarch64,arm) device`() {
TestBuildConfigProvider(FLAVOR_ARMEABI_V7A, arrayOf(FLAVOR_ARM64_V8A, FLAVOR_ARMEABI_V7A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARMEABI_V7A)
assertThat(flavorArch).isEqualTo(CpuArch.ARM)
TestBuildConfigProvider(ABI_ARMEABI_V7A, arrayOf(ABI_ARM64_V8A, ABI_ARMEABI_V7A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARMEABI_V7A)
assertThat(cpuArch).isEqualTo(CpuArch.ARM)
assertThat(deviceArch).isEqualTo(CpuArch.AARCH64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_ARM64_V8A, FLAVOR_ARMEABI_V7A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_ARM64_V8A, FLAVOR_ARMEABI_V7A)
assertThat(supportedAbis.toList()).containsExactly(ABI_ARM64_V8A, ABI_ARMEABI_V7A)
assertThat(isArm64v8aBuild()).isFalse()
assertThat(isX86_64Build()).isFalse()
@@ -122,19 +121,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isNotEqualTo(deviceArch)
assertThat(cpuArch).isNotEqualTo(deviceArch)
}
}
@Test
fun `test arm build on (x86_64,arm) device`() {
TestBuildConfigProvider(FLAVOR_ARMEABI_V7A, arrayOf(FLAVOR_X86_64, FLAVOR_ARMEABI_V7A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARMEABI_V7A)
assertThat(flavorArch).isEqualTo(CpuArch.ARM)
TestBuildConfigProvider(ABI_ARMEABI_V7A, arrayOf(ABI_X86_64, ABI_ARMEABI_V7A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARMEABI_V7A)
assertThat(cpuArch).isEqualTo(CpuArch.ARM)
assertThat(deviceArch).isEqualTo(CpuArch.X86_64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_X86_64, FLAVOR_ARMEABI_V7A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_X86_64, FLAVOR_ARMEABI_V7A)
assertThat(supportedAbis.toList()).containsExactly(ABI_X86_64, ABI_ARMEABI_V7A)
assertThat(isArm64v8aBuild()).isFalse()
assertThat(isX86_64Build()).isFalse()
@@ -145,19 +144,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isNotEqualTo(deviceArch)
assertThat(cpuArch).isNotEqualTo(deviceArch)
}
}
@Test
fun `test aarch64 build on arm-only device`() {
TestBuildConfigProvider(FLAVOR_ARM64_V8A, arrayOf(FLAVOR_ARMEABI_V7A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARM64_V8A)
assertThat(flavorArch).isEqualTo(CpuArch.AARCH64)
TestBuildConfigProvider(ABI_ARM64_V8A, arrayOf(ABI_ARMEABI_V7A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARM64_V8A)
assertThat(cpuArch).isEqualTo(CpuArch.AARCH64)
assertThat(deviceArch).isEqualTo(CpuArch.ARM)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_ARMEABI_V7A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_ARMEABI_V7A)
assertThat(supportedAbis.toList()).containsExactly(ABI_ARMEABI_V7A)
assertThat(isArm64v8aBuild()).isTrue()
assertThat(isX86_64Build()).isFalse()
@@ -168,19 +167,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isTrue()
assertThat(supportsBuildFlavor()).isFalse()
assertThat(flavorArch).isNotEqualTo(deviceArch)
assertThat(cpuArch).isNotEqualTo(deviceArch)
}
}
@Test
fun `test aarch64 build on x86_64-only device`() {
TestBuildConfigProvider(FLAVOR_ARM64_V8A, arrayOf(FLAVOR_X86_64)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARM64_V8A)
assertThat(flavorArch).isEqualTo(CpuArch.AARCH64)
TestBuildConfigProvider(ABI_ARM64_V8A, arrayOf(ABI_X86_64)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARM64_V8A)
assertThat(cpuArch).isEqualTo(CpuArch.AARCH64)
assertThat(deviceArch).isEqualTo(CpuArch.X86_64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_X86_64)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_X86_64)
assertThat(supportedAbis.toList()).containsExactly(ABI_X86_64)
assertThat(isArm64v8aBuild()).isTrue()
assertThat(isX86_64Build()).isFalse()
@@ -191,19 +190,19 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isFalse()
assertThat(flavorArch).isNotEqualTo(deviceArch)
assertThat(cpuArch).isNotEqualTo(deviceArch)
}
}
@Test
fun `test aarch64 build on (x86_64,aarch64) device`() {
TestBuildConfigProvider(FLAVOR_ARM64_V8A, arrayOf(FLAVOR_X86_64, FLAVOR_ARM64_V8A)).apply {
assertThat(buildFlavor).isEqualTo(FLAVOR_ARM64_V8A)
assertThat(flavorArch).isEqualTo(CpuArch.AARCH64)
TestBuildConfigProvider(ABI_ARM64_V8A, arrayOf(ABI_X86_64, ABI_ARM64_V8A)).apply {
assertThat(cpuAbiName).isEqualTo(ABI_ARM64_V8A)
assertThat(cpuArch).isEqualTo(CpuArch.AARCH64)
assertThat(deviceArch).isEqualTo(CpuArch.X86_64)
assertThat(supportedAbis.toList()).containsExactly(FLAVOR_X86_64, FLAVOR_ARM64_V8A)
assertThat(supportedBuildFlavors.toList()).containsExactly(FLAVOR_X86_64, FLAVOR_ARM64_V8A)
assertThat(supportedAbis.toList()).containsExactly(ABI_X86_64, ABI_ARM64_V8A)
assertThat(isArm64v8aBuild()).isTrue()
assertThat(isX86_64Build()).isFalse()
@@ -214,7 +213,7 @@ class IDEBuildConfigProviderImplTest {
assertThat(isArmeabiv7aDevice()).isFalse()
assertThat(supportsBuildFlavor()).isTrue()
assertThat(flavorArch).isNotEqualTo(deviceArch)
assertThat(cpuArch).isNotEqualTo(deviceArch)
}
}
}
+12 -4
View File
@@ -39,14 +39,16 @@ object FDroidConfig {
var fDroidVersionCode: Int? = null
private set
var fDroidAapt2File: String? = null
var aapt2Files: Map<String, String?> = emptyMap()
private set
const val PROP_FDROID_BUILD = "ide.build.fdroid"
const val PROP_FDROID_BUILD_ARCH = "ide.build.fdroid.arch"
const val PROP_FDROID_BUILD_VERSION = "ide.build.fdroid.version"
const val PROP_FDROID_BUILD_VERCODE = "ide.build.fdroid.vercode"
const val PROP_FDROID_AAPT2FILE = "ide.build.fdroid.aapt2File"
const val PROP_FDROID_AAPT2FILE_ARM64 = "ide.build.fdroid.aapt2File.arm64"
const val PROP_FDROID_AAPT2FILE_ARM = "ide.build.fdroid.aapt2File.arm"
const val PROP_FDROID_AAPT2FILE_X86_64 = "ide.build.fdroid.aapt2File.x86_64"
fun load(project: Project) {
val propsFile = File(project.rootDir, "fdroid.properties")
@@ -67,7 +69,13 @@ object FDroidConfig {
fDroidBuildArch = properties.getProperty(PROP_FDROID_BUILD_ARCH, null)
fDroidVersionName = properties.getProperty(PROP_FDROID_BUILD_VERSION, null)
fDroidVersionCode = properties.getProperty(PROP_FDROID_BUILD_VERCODE, null)?.toInt()
fDroidAapt2File = properties.getProperty(PROP_FDROID_AAPT2FILE, null)
fDroidVersionCode = properties.getProperty(PROP_FDROID_BUILD_VERCODE, null)?.toInt()
aapt2Files = mutableMapOf<String, String?>().also { files ->
files[PROP_FDROID_AAPT2FILE_ARM64] = properties.getProperty(PROP_FDROID_AAPT2FILE_ARM64, null)
files[PROP_FDROID_AAPT2FILE_ARM] = properties.getProperty(PROP_FDROID_AAPT2FILE_ARM, null)
files[PROP_FDROID_AAPT2FILE_X86_64] = properties.getProperty(PROP_FDROID_AAPT2FILE_X86_64,
null)
}
}
}
@@ -19,8 +19,7 @@ package com.itsaky.androidide.plugins
import BuildConfig
import VersionUtils
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.itsaky.androidide.plugins.tasks.AddAndroidJarToAssetsTask
import com.itsaky.androidide.plugins.tasks.AddFileToAssetsTask
import com.itsaky.androidide.plugins.tasks.GenerateInitScriptTask
@@ -30,7 +29,6 @@ import com.itsaky.androidide.plugins.util.SdkUtils.getAndroidJar
import downloadVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
import org.gradle.configurationcache.extensions.capitalized
/**
@@ -40,25 +38,15 @@ import org.gradle.configurationcache.extensions.capitalized
*/
class AndroidIDEAssetsPlugin : Plugin<Project> {
companion object {
private val AAPT2_CHECKSUMS = mapOf(
"arm64-v8a" to "be2cea61814678f7a9e61bf818a6666e6097a7d67d6c19498a4d7aa690bc4151",
"armeabi-v7a" to "ba3413c680933dffd3c3d35da8d450c474ff5ccab95c4b9db28841c53b7a3cdf",
"x86_64" to "4861171c1efcffe41f4466937e6a392b243ffb014813b4e60f0b77bb46ab254d"
)
}
override fun apply(target: Project) {
target.run {
val wrapperGeneratorTaskProvider = tasks.register("generateGradleWrapper",
GradleWrapperGeneratorTask::class.java
)
GradleWrapperGeneratorTask::class.java)
val androidComponentsExtension = extensions.getByType(AndroidComponentsExtension::class.java)
val baseExtension = extensions.getByType(BaseExtension::class.java)
val androidComponentsExtension = extensions.getByType(
ApplicationAndroidComponentsExtension::class.java)
val aapt2Tasks = getAapt2SetupTasks(baseExtension)
val setupAapt2TaskTaskProvider = tasks.register("setupAapt2", SetupAapt2Task::class.java)
val addAndroidJarTaskProvider = tasks.register("addAndroidJarToAssets",
AddAndroidJarToAssetsTask::class.java) {
@@ -67,15 +55,11 @@ class AndroidIDEAssetsPlugin : Plugin<Project> {
androidComponentsExtension.onVariants { variant ->
val aapt2SetupTaskProvider = aapt2Tasks[variant.flavorName]
?: throw IllegalStateException(
"'aapt2' task not registered for flavor '${variant.flavorName}'")
variant.sources.jniLibs?.addGeneratedSourceDirectory(aapt2SetupTaskProvider,
SetupAapt2Task::outputDirectory)
val variantNameCapitalized = variant.name.capitalized()
variant.sources.jniLibs?.addGeneratedSourceDirectory(setupAapt2TaskTaskProvider,
SetupAapt2Task::outputDirectory)
variant.sources.assets?.addGeneratedSourceDirectory(wrapperGeneratorTaskProvider,
GradleWrapperGeneratorTask::outputDirectory)
@@ -83,64 +67,32 @@ class AndroidIDEAssetsPlugin : Plugin<Project> {
AddAndroidJarToAssetsTask::outputDirectory)
// Init script generator
val generateInitScript = tasks.register(
"generate${variantNameCapitalized}InitScript",
GenerateInitScriptTask::class.java
) {
val generateInitScript = tasks.register("generate${variantNameCapitalized}InitScript",
GenerateInitScriptTask::class.java) {
mavenGroupId.set(BuildConfig.packageName)
snapshotsRepository.set(VersionUtils.SNAPSHOTS_REPO)
downloadVersion.set(this@run.downloadVersion)
}
variant.sources.assets?.addGeneratedSourceDirectory(
generateInitScript,
GenerateInitScriptTask::outputDir
)
variant.sources.assets?.addGeneratedSourceDirectory(generateInitScript,
GenerateInitScriptTask::outputDir)
// Tooling API JAR copier
val copyToolingApiJar = tasks.register(
"copy${variantNameCapitalized}ToolingApiJar",
AddFileToAssetsTask::class.java
) {
val copyToolingApiJar = tasks.register("copy${variantNameCapitalized}ToolingApiJar",
AddFileToAssetsTask::class.java) {
val toolingApi = rootProject.findProject(":subprojects:tooling-api-impl")!!
dependsOn(toolingApi.tasks.getByName("copyJar"))
val toolingApiJar = toolingApi.layout.buildDirectory.file(
"libs/tooling-api-all.jar")
val toolingApiJar = toolingApi.layout.buildDirectory.file("libs/tooling-api-all.jar")
inputFile.set(toolingApiJar)
baseAssetsPath.set("data/common")
}
variant.sources.assets?.addGeneratedSourceDirectory(
copyToolingApiJar,
AddFileToAssetsTask::outputDirectory
)
variant.sources.assets?.addGeneratedSourceDirectory(copyToolingApiJar,
AddFileToAssetsTask::outputDirectory)
}
}
}
private fun Project.getAapt2SetupTasks(
baseExtension: BaseExtension
): Map<String, TaskProvider<SetupAapt2Task>> {
val aapt2Tasks = mutableMapOf<String, TaskProvider<SetupAapt2Task>>()
baseExtension.productFlavors.forEach { flavor ->
val task = tasks.register(
"setupAapt2${flavor.name.capitalized()}",
SetupAapt2Task::class.java
) {
arch.set(flavor.name)
checksum.set(AAPT2_CHECKSUMS[flavor.name] ?: throw IllegalStateException(
"Checksum for aapt2-${flavor.name} not found!"))
}
aapt2Tasks[flavor.name] = task
}
return aapt2Tasks
}
}
@@ -19,9 +19,11 @@ package com.itsaky.androidide.plugins.conf
import BuildConfig
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.FilterConfiguration
import com.android.build.api.variant.impl.getFilter
import com.android.build.gradle.BaseExtension
import com.itsaky.androidide.plugins.util.SdkUtils.getAndroidJar
import isFDroidBuild
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import projectVersionCode
@@ -36,6 +38,7 @@ import projectVersionCode
* flavor, the version code will be `100 * 270 + 1` i.e. `2701`
*/
internal val flavorsAbis = mapOf("arm64-v8a" to 1, "armeabi-v7a" to 2, "x86_64" to 3)
private const val BUILD_CONFIG_ABI_FIELD_BASE_NAME = "ABI_"
private val disableCoreLibDesugaringForModules = arrayOf(":logsender", ":logger")
fun Project.configureAndroidModule(
@@ -78,21 +81,28 @@ fun Project.configureAndroidModule(
configureCoreLibDesugaring(this, coreLibDesugDep)
if (":app" == project.path) {
flavorDimensions("default")
flavorsAbis.forEach { (abi, _) ->
// the common defaultConfig, not the flavor-specific
defaultConfig.buildConfigField("String",
"FLAVOR_${abi.replace('-', '_').uppercase()}",
"${BUILD_CONFIG_ABI_FIELD_BASE_NAME}${abi.replace('-', '_').uppercase()}",
"\"${abi}\"")
}
productFlavors {
val fdroidSuffix = if (isFDroidBuild) "-fdroid" else ""
flavorsAbis.forEach { (abi, verCodeIncrement) ->
val flavor = create(abi)
flavor.versionNameSuffix = "-${abi}${fdroidSuffix}"
flavor.versionCode = 100 * projectVersionCode + verCodeIncrement
splits {
abi {
reset()
isEnable = true
isUniversalApk = true
include(*flavorsAbis.keys.toTypedArray())
}
}
extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
onVariants { variant ->
variant.outputs.forEach { output ->
val verCodeIncr = flavorsAbis[output.getFilter(FilterConfiguration.FilterType.ABI)?.identifier] ?: 0
output.versionCode.set(100 * projectVersionCode + verCodeIncr)
}
}
}
} else {
@@ -104,25 +114,6 @@ fun Project.configureAndroidModule(
}
}
// configure split APKs for ':app' module only
if (this@configureAndroidModule == rootProject.findProject(":app")) {
splits {
abi {
reset()
isEnable = true
isUniversalApk = false
// TODO: Find a way to enable split APKs in product flavors. If this is possible, we can configure
// each flavor to include only a single ABI. For example, for the 'arm64-v8a' flavor,
// we can configure it to generate APK only for 'arm64-v8a'.
//
// See the contribution guidelines for more information.
include(*flavorsAbis.keys.toTypedArray())
}
}
}
buildTypes.getByName("debug") { isMinifyEnabled = false }
buildTypes.getByName("release") {
isMinifyEnabled = true
@@ -17,11 +17,10 @@
package com.itsaky.androidide.plugins.tasks
import FDroidConfig
import com.itsaky.androidide.plugins.util.DownloadUtils
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File
@@ -31,73 +30,49 @@ import java.io.File
*/
abstract class SetupAapt2Task : DefaultTask() {
/**
* The CPU architecture for which the `aapt2` should be downloaded.
*/
@get:Input
abstract val arch: Property<String>
/**
* The version of the file, basically the tag name of the release.
*/
@get:Input
abstract val version: Property<String>
/**
* The SHA-256 checksum of the file to download. This will be used to verify the file's
* integrity.
*/
@get:Input
abstract val checksum: Property<String>
/**
* The path of the static `aapt2` file which should be used instead of downloading it from GitHub.
*/
@get:Input
abstract val staticAapt2: Property<String>
/**
* The output directory.
*/
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
init {
run {
version.convention(DEFAULT_VERSION)
staticAapt2.convention("")
}
}
companion object {
private val AAPT2_CHECKSUMS = mapOf(
"arm64-v8a" to "be2cea61814678f7a9e61bf818a6666e6097a7d67d6c19498a4d7aa690bc4151",
"armeabi-v7a" to "ba3413c680933dffd3c3d35da8d450c474ff5ccab95c4b9db28841c53b7a3cdf",
"x86_64" to "4861171c1efcffe41f4466937e6a392b243ffb014813b4e60f0b77bb46ab254d"
)
private const val DEFAULT_VERSION = "34.0.4"
private const val AAPT2_DOWNLOAD_URL = "https://github.com/AndroidIDEOfficial/platform-tools/releases/download/v%1\$s/aapt2-%2\$s"
}
@TaskAction
fun setupAapt2() {
val file = outputDirectory.file("${arch.get()}/libaapt2.so").get().asFile
file.parentFile.deleteRecursively()
file.parentFile.mkdirs()
if (staticAapt2.getOrElse("").isNotBlank()) {
val aapt2 = File(staticAapt2.get())
AAPT2_CHECKSUMS.forEach { (arch, checksum) ->
val file = outputDirectory.file("${arch}/libaapt2.so").get().asFile
file.parentFile.deleteRecursively()
file.parentFile.mkdirs()
require(aapt2.exists() && aapt2.isFile) {
"F-Droid AAPT2 file does not exist or is not a file: $aapt2"
if (FDroidConfig.isFDroidBuild) {
val aapt2File = requireNotNull(FDroidConfig.aapt2Files[arch]) {
"F-Droid build is enabled but path to AAPT2 file for $arch is not set."
}
val aapt2 = File(aapt2File)
require(aapt2.exists() && aapt2.isFile) {
"F-Droid AAPT2 file does not exist or is not a file: $aapt2"
}
aapt2.copyTo(file, overwrite = true)
return
}
aapt2.copyTo(file, overwrite = true)
return
val remoteUrl = AAPT2_DOWNLOAD_URL.format(DEFAULT_VERSION, arch)
DownloadUtils.doDownload(file, remoteUrl, checksum, logger)
}
val arch = this.arch.get()
val version = this.version.get()
val expectedChecksum = this.checksum.get()
val remoteUrl = AAPT2_DOWNLOAD_URL.format(version, arch)
DownloadUtils.doDownload(file, remoteUrl, expectedChecksum, logger)
}
}
@@ -29,17 +29,12 @@ interface IDEBuildConfigProvider {
/**
* The current product flavor of the IDE.
*/
val buildFlavor: String
val cpuAbiName: String
/**
* Get the [CpuArch] for the build flavor.
*/
val flavorArch: CpuArch
/**
* The build flavors which can be installed on the current device.
*/
val supportedBuildFlavors: Array<String>
val cpuArch: CpuArch
/**
* Get the primary CPU architecture of the device.
@@ -54,7 +49,7 @@ interface IDEBuildConfigProvider {
/**
* @return Whether this build is the `arm64-v8a` variant of the IDE.
*/
fun isArm64v8aBuild(): Boolean = flavorArch == CpuArch.AARCH64
fun isArm64v8aBuild(): Boolean = cpuArch == CpuArch.AARCH64
/**
* @return Whether the device supports `arm64-v8a` instructions.
@@ -64,7 +59,7 @@ interface IDEBuildConfigProvider {
/**
* @return Whether this build is the 32-bit variant of the IDE.
*/
fun isArmeabiv7aBuild(): Boolean = flavorArch == CpuArch.ARM
fun isArmeabiv7aBuild(): Boolean = cpuArch == CpuArch.ARM
/**
* @return Whether the device supports `armeabi-v7a` instructions.
@@ -74,7 +69,7 @@ interface IDEBuildConfigProvider {
/**
* @return Whether this build is the `x86_64` variant of the IDE.
*/
fun isX86_64Build(): Boolean = flavorArch == CpuArch.X86_64
fun isX86_64Build(): Boolean = cpuArch == CpuArch.X86_64
/**
* @return Whether the device supports `x86_64` instructions.
@@ -84,7 +79,7 @@ interface IDEBuildConfigProvider {
/**
* @return Whether the IDE can be run on the device's CPU arch.
*/
fun supportsBuildFlavor(): Boolean = supportedBuildFlavors.contains(buildFlavor)
fun supportsBuildFlavor(): Boolean = supportedAbis.contains(cpuAbiName)
companion object {
@@ -23,7 +23,6 @@ import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ResourceUtils;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.TaskExecutorKt;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
@@ -43,8 +42,6 @@ public class ToolsManager {
private static final ILogger LOG = ILogger.newInstance("ToolsManager");
public static String ARCH_SPECIFIC_ASSET_DATA_DIR =
"data/" + IDEBuildConfigProvider.getInstance().getBuildFlavor();
public static String COMMON_ASSET_DATA_DIR = "data/common";
public static void init(@NonNull BaseApplication app, Runnable onFinish) {
@@ -54,27 +51,24 @@ public class ToolsManager {
return;
}
CompletableFuture.runAsync(
() -> {
writeNoMediaFile();
extractAapt2();
extractToolingApi();
extractAndroidJar();
extractColorScheme(app);
writeInitScript();
CompletableFuture.runAsync(() -> {
writeNoMediaFile();
extractAapt2();
extractToolingApi();
extractAndroidJar();
extractColorScheme(app);
writeInitScript();
deleteIdeenv();
})
.whenComplete(
(__, error) -> {
if (error != null) {
LOG.error("Error extracting tools", error);
}
deleteIdeenv();
}).whenComplete((__, error) -> {
if (error != null) {
LOG.error("Error extracting tools", error);
}
if (onFinish != null) {
onFinish.run();
}
});
if (onFinish != null) {
onFinish.run();
}
});
}
private static void extractColorScheme(final BaseApplication app) {
@@ -84,8 +78,8 @@ public class ToolsManager {
for (final String asset : app.getAssets().list(defPath)) {
final var prop = new File(dir, asset + "/" + "scheme.prop");
if (prop.exists()
&& !shouldExtractScheme(app, new File(dir, asset), defPath + "/" + asset)) {
if (prop.exists() && !shouldExtractScheme(app, new File(dir, asset),
defPath + "/" + asset)) {
continue;
}
@@ -101,8 +95,8 @@ public class ToolsManager {
}
}
private static boolean shouldExtractScheme(
final BaseApplication app, final File dir, final String path) throws IOException {
private static boolean shouldExtractScheme(final BaseApplication app, final File dir,
final String path) throws IOException {
final var schemePropFile = new File(dir, "scheme.prop");
if (!schemePropFile.exists()) {
@@ -159,8 +153,8 @@ public class ToolsManager {
private static void extractAndroidJar() {
if (!Environment.ANDROID_JAR.exists()) {
ResourceUtils.copyFileFromAssets(
getCommonAsset("android.jar"), Environment.ANDROID_JAR.getAbsolutePath());
ResourceUtils.copyFileFromAssets(getCommonAsset("android.jar"),
Environment.ANDROID_JAR.getAbsolutePath());
}
}
@@ -171,12 +165,6 @@ public class ToolsManager {
}
}
@NonNull
@Contract(pure = true)
public static String getArchSpecificAsset(String name) {
return ARCH_SPECIFIC_ASSET_DATA_DIR + "/" + name;
}
@NonNull
@Contract(pure = true)
public static String getCommonAsset(String name) {
@@ -190,7 +178,9 @@ public class ToolsManager {
final var sourceAapt2 = new File(nativeLibraryDir, "libaapt2.so");
if (!(sourceAapt2.exists() && sourceAapt2.isFile())) {
// halt the application
TaskExecutorKt.runOnUiThread(() -> {throw new IllegalStateException(sourceAapt2 + " does not exist!");});
TaskExecutorKt.runOnUiThread(() -> {
throw new IllegalStateException(sourceAapt2 + " does not exist!");
});
}
FilesKt.copyTo(sourceAapt2, Environment.AAPT2, true, ConstantsKt.DEFAULT_BUFFER_SIZE);
}
@@ -205,8 +195,8 @@ public class ToolsManager {
FileUtils.delete(Environment.TOOLING_API_JAR);
}
ResourceUtils.copyFileFromAssets(
getCommonAsset("tooling-api-all.jar"), Environment.TOOLING_API_JAR.getAbsolutePath());
ResourceUtils.copyFileFromAssets(getCommonAsset("tooling-api-all.jar"),
Environment.TOOLING_API_JAR.getAbsolutePath());
}
private static void writeInitScript() {
@@ -221,10 +211,4 @@ public class ToolsManager {
return ResourceUtils.readAssets2String(getCommonAsset("androidide.init.gradle"));
}
public static void extractLibHooks() {
if (!Environment.LIB_HOOK.exists()) {
ResourceUtils.copyFileFromAssets(
getArchSpecificAsset("libhook.so"), Environment.LIB_HOOK.getAbsolutePath());
}
}
}
@@ -1,194 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils;
import static com.itsaky.androidide.utils.Environment.PREFIX;
import android.content.Context;
import android.system.ErrnoException;
import android.system.Os;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import com.blankj.utilcode.util.FileUtils;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.managers.ToolsManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Installs the bootstraps zip into {@code $SYSROOT}.
*
* <p>This class in very much based on Termux's TermuxInstaller.java.
*
* @author Akash Yadav
*/
public class BootstrapInstaller {
private static final ILogger LOG = ILogger.newInstance("BootstrapInstaller");
@NonNull
public static CompletableFuture<Void> doInstall(
final Context context, @Nullable final UpdateListener listener) {
return CompletableFuture.runAsync(
() -> {
var msg = "Opening bootstrap zip input stream...";
LOG.debug(msg);
notify(listener, msg);
try (final var assetIn =
context.getAssets().open(ToolsManager.getArchSpecificAsset("bootstrap.zip"));
final var zip = new ZipInputStream(assetIn)) {
final var buffer = new byte[8096];
final var symlinks = new ArrayList<Pair<String, String>>(50);
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
msg = "Reading zip entry " + entry.getName();
notify(listener, msg);
LOG.verbose(msg);
if (entry.getName().equals("SYMLINKS.txt")) {
msg = "Reading SYMLINKS.txt...";
notify(listener, msg);
LOG.debug(msg);
final var symlinksReader = new BufferedReader(new InputStreamReader(zip));
String line;
while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split("←");
if (parts.length != 2) {
final var err = "Malformed symlink line: " + line;
LOG.error(err);
throw new CompletionException(new InstallationException(err));
}
String oldPath = parts[0];
String newPath = PREFIX.getAbsolutePath() + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
final var parentFile = new File(newPath).getParentFile();
if (!FileUtils.createOrExistsDir(parentFile)) {
LOG.error("Cannot create symlink parent directory.");
throw new CompletionException(
new InstallationException("Unable to create directory: " + parentFile));
}
}
} else {
String zipEntryName = entry.getName();
File targetFile = new File(PREFIX, zipEntryName);
boolean isDirectory = entry.isDirectory();
final var dir = isDirectory ? targetFile : targetFile.getParentFile();
if (dir != null && !dir.exists() && !dir.mkdirs()) {
LOG.error("Cannot create target file parent directory");
throw new CompletionException(
new InstallationException("Unable to create directory: " + dir));
}
// If the file exists and it is not a directory
// Delete that file
final var targetFilePath = targetFile.toPath();
if (Files.exists(targetFilePath) && !Files.isDirectory(targetFilePath)) {
try {
Files.delete(targetFilePath);
} catch (Throwable th) {
throw new CompletionException(th);
}
}
if (!isDirectory) {
try (final var outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zip.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/")
|| zipEntryName.startsWith("libexec")
|| zipEntryName.startsWith("lib/apt/apt-helper")
|| zipEntryName.startsWith("lib/apt/methods")) {
notify(listener, context.getString(R.string.msg_chmod_exec));
LOG.verbose("chmod 0700", zipEntryName);
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
if (symlinks.isEmpty()) {
LOG.error("No SYMLINKS.txt file encountered...");
throw new CompletionException(
new InstallationException("No SYMLINKS.txt encountered"));
}
for (Pair<String, String> symlink : symlinks) {
notify(
listener, context.getString(R.string.msg_linking, symlink.second, symlink.first));
final var target = Paths.get(symlink.second);
if (Files.exists(target) && !Files.isDirectory(target)) {
try {
Files.delete(target);
} catch (Throwable throwable) {
throw new CompletionException(throwable);
}
}
Os.symlink(symlink.first, symlink.second);
}
notify(listener, context.getString(R.string.msg_extracting_hooks));
ToolsManager.extractLibHooks();
LOG.info("Bootstrap packages installed successfully.");
} catch (IOException | ErrnoException e) {
LOG.error("Failed to extract bootstrap", e);
throw new RuntimeException(e);
}
});
}
private static void notify(@Nullable UpdateListener listener, String message) {
if (listener != null) {
listener.onUpdate(message);
}
}
public static class InstallationException extends RuntimeException {
public InstallationException(String message) {
super(message);
}
}
public interface UpdateListener {
void onUpdate(String message);
}
}
@@ -59,7 +59,7 @@ object AndroidIDEStats {
}
val cpuArch by lazy {
IDEBuildConfigProvider.getInstance().buildFlavor
IDEBuildConfigProvider.getInstance().cpuAbiName
}
val statData by lazy {