fix: local builds bundle the addon packs they pin (#1908)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-31 23:32:58 -04:00
1 parent 8a7022fc14
commit 2ff3ab447d
3 files changed
+57 -1

No files matched your search

+10
View File
@@ -300,6 +300,16 @@ def stageGogNotices = tasks.register('stageGogNotices', Copy) {
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices'))
tasks.named('preBuild').configure { dependsOn(stageGogNotices) }
// CI publishes its packs; a local build pins packs nobody hosts (see addonPacks), so the APK carries them.
if (!ciBuild) {
def stageLocalPacks = tasks.register('stageLocalPacks', Sync) {
from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' }
into(layout.buildDirectory.dir('localPackAssets/packs'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets'))
tasks.named('preBuild').configure { dependsOn(stageLocalPacks) }
}
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
workingDir rootProject.projectDir
@@ -22,6 +22,7 @@ import java.io.FileInputStream
internal class AddonPack(
val name: String, private val sha256: String, val size: Long, private val required: List<String>,
private val reuseDeviceCopy: Boolean = sha256.isEmpty() && !BuildConfig.CI_BUILD,
private val bundle: (Context) -> java.io.InputStream = { it.assets.open("packs/$name.tar.zst") },
) {
sealed interface State {
data object Missing : State
@@ -104,7 +105,7 @@ internal class AddonPack(
}
val root = root(context).apply { mkdirs() }
val archive = File(root, "$sha256.tar.zst")
val downloaded = urls.any { url ->
val downloaded = bundled(context, archive) || urls.any { url ->
Downloader.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
.also { if (!it) archive.delete() }
}
@@ -126,6 +127,17 @@ internal class AddonPack(
return true
}
// A local build pins the packs it compiled, which no release hosts, so it ships them as assets.
internal fun bundled(context: Context, archive: File): Boolean {
val copied = runCatching {
bundle(context).use { input -> archive.outputStream().use { input.copyTo(it) } }
}.isSuccess
if (copied && Hashes.sha256(archive).equals(sha256, ignoreCase = true)) return true
if (copied) Log.w(TAG, "$name: the bundled pack does not match the pinned digest")
archive.delete()
return false
}
companion object {
private const val TAG = "AddonPack"
private const val REPO = "Droid-Deck/DroidDeck-Components"
@@ -2,6 +2,9 @@ package com.droiddeck.launcher.core
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.github.luben.zstd.ZstdOutputStream
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import java.io.File
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -54,4 +57,35 @@ class AddonPackTest {
installed("b".repeat(64))
assertFalse(AddonPack("sample", "", 0L, listOf("tool"), reuseDeviceCopy = false).installed(context))
}
@Test fun localBuildInstallsTheBundledPackWithoutDownloading() {
val archive = tarZst("tool" to "x")
val pack = AddonPack("sample", Hashes.sha256(archive), archive.length(), listOf("tool"), bundle = { archive.inputStream() })
assertTrue(pack.await(context))
assertEquals("x", pack.file(context, "tool").readText())
}
@Test fun bundledPackWithTheWrongDigestIsRejected() {
val archive = tarZst("tool" to "x")
val pack = AddonPack("sample", "a".repeat(64), archive.length(), listOf("tool"),
bundle = { archive.inputStream() })
val copy = File(context.cacheDir, "copy.tar.zst")
assertFalse(pack.bundled(context, copy))
assertFalse(copy.exists())
}
private fun tarZst(vararg files: Pair<String, String>): File =
File(context.cacheDir, "sample-${System.nanoTime()}.tar.zst").apply {
TarArchiveOutputStream(ZstdOutputStream(outputStream())).use { tar ->
for ((path, text) in files) {
val bytes = text.toByteArray()
tar.putArchiveEntry(TarArchiveEntry(path).apply { size = bytes.size.toLong() })
tar.write(bytes)
tar.closeArchiveEntry()
}
}
}
}