Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embed the Zipline app in Android app assets #1563

Merged
merged 1 commit into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build-support/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation libs.spotlessPlugin
implementation libs.androidGradlePlugin
implementation libs.jetbrains.compose.gradlePlugin
implementation libs.zipline

// Expose the generated version catalog API to the plugin.
implementation(files(libs.class.superclass.protectionDomain.codeSource.location))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ interface RedwoodBuildExtension {
*/
fun publishing()

/** Bundle a zip with dependencies and startup scripts. */
fun application(name: String, mainClass: String)
/** Bundle a zip with dependencies and startup scripts for a CLI. */
fun cliApplication(name: String, mainClass: String)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like how this plugin is structured


/** Bundle a zip of a Zipline application's compiled `.zipline` files. */
fun ziplineApplication()

/**
* Consume a Zipline application in an Android application and embed it within assets.
*
* @name Name of the Treehouse application. Will be used to prefix the Zipline manifest file.
*/
fun embedZiplineApplication(dependencyNotation: Any, name: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Attribute
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
Expand Down Expand Up @@ -344,7 +346,7 @@ private class RedwoodBuildExtensionImpl(private val project: Project) : RedwoodB
}
}

override fun application(name: String, mainClass: String) {
override fun cliApplication(name: String, mainClass: String) {
project.plugins.apply("application")

val application = project.extensions.getByName("application") as JavaApplication
Expand Down Expand Up @@ -374,4 +376,74 @@ private class RedwoodBuildExtensionImpl(private val project: Project) : RedwoodB
}
}
}

override fun ziplineApplication() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I aught to move this behavior to Zipline itself!

var hasZipline = false
project.afterEvaluate {
check(hasZipline) {
"Project ${project.path} must have Zipline plugin to create Zipline application"
}
}
project.plugins.withId("app.cash.zipline") {
hasZipline = true

val ziplineConfiguration = project.configurations.create("zipline") {
it.isVisible = false
it.isCanBeResolved = false
it.isCanBeConsumed = true
it.attributes {
it.attribute(ziplineAttribute, ziplineAttributeValue)
}
}

// Only a single file can be used as an artifact so zip up the compiled contents.
val zipTask = project.tasks.register("zipProductionZiplineFiles", Zip::class.java) {
// Note: This makes assumptions about our setup having a JS target with the default name.
it.from(project.tasks.named("compileProductionExecutableKotlinJsZipline"))
it.destinationDirectory.set(project.layout.buildDirectory.dir("libs"))
it.archiveClassifier.set("zipline")
}

project.artifacts.add(ziplineConfiguration.name, zipTask)
}
}

override fun embedZiplineApplication(dependencyNotation: Any, name: String) {
var hasApplication = false
project.afterEvaluate {
check(hasApplication) {
"Project ${project.path} must have Android Application plugin to embed Zipline application"
}
}
project.plugins.withId("com.android.application") {
hasApplication = true

// Note: This will crash if you call it twice. We don't need this today, so it's not supported.
val ziplineConfiguration = project.configurations.create("zipline") {
it.isVisible = false
it.isCanBeResolved = true
it.isCanBeConsumed = false
it.attributes {
it.attribute(ziplineAttribute, ziplineAttributeValue)
}
}
project.dependencies.add(ziplineConfiguration.name, dependencyNotation)

val task = project.tasks.register("copyEmbeddedZiplineApplication", ZiplineAppAssetCopyTask::class.java) {
it.appName.set(name)
it.files.setFrom(ziplineConfiguration)
}

val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants {
val assets = checkNotNull(it.sources.assets) {
"Project ${project.path} assets must be enabled to embed Zipline application"
}
assets.addGeneratedSourceDirectory(task, ZiplineAppAssetCopyTask::outputDirectory)
}
}
}
}

private val ziplineAttribute = Attribute.of("zipline", String::class.java)
private const val ziplineAttributeValue = "yep"
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.buildsupport

import app.cash.zipline.ZiplineManifest
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction

private const val ZIPLINE_MANIFEST_JSON = "manifest.zipline.json"

internal abstract class ZiplineAppAssetCopyTask
@Inject constructor(
private val archiveOperations: ArchiveOperations,
) : DefaultTask() {
@get:InputFiles
abstract val files: ConfigurableFileCollection

@get:Input
abstract val appName: Property<String>

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty

@TaskAction
fun execute() {
val outputDirectory = outputDirectory.get()
outputDirectory.asFile.apply {
deleteRecursively()
mkdirs()
}

val fileMap = archiveOperations.zipTree(files.singleFile)
.files
.associateBy { it.name }
.toMutableMap()

val inputManifestFile = checkNotNull(fileMap.remove(ZIPLINE_MANIFEST_JSON)) {
"No zipline.manifest.json file found in input files ${fileMap.keys}"
}
val inputManifest = ZiplineManifest.decodeJson(inputManifestFile.readText())

// Add a timestamp to the manifest which is required for Zipline to load as an embedded app.
val outputManifest = inputManifest.copy(
unsigned = inputManifest.unsigned.copy(
freshAtEpochMs = System.currentTimeMillis(),
),
)
val outputManifestFile = outputDirectory.file("${appName.get()}.$ZIPLINE_MANIFEST_JSON").asFile
outputManifestFile.writeText(outputManifest.encodeJson())

// Rewrite all .zipline files to their SHA-256 hashes which is how Zipline loads when embedded.
for (module in outputManifest.modules.values) {
val inputFile = checkNotNull(fileMap.remove(module.url)) {
"No ${module.url} file found in input files as specified by the manifest"
}
val outputFile = outputDirectory.file(module.sha256.hex()).asFile
inputFile.copyTo(outputFile)
}
Comment on lines +63 to +79
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future change: going to move all this to happen on the production-side rather than the consumer-side so as to support iOS more easily.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And move this to the Zipline Gradle plugin!

}
}
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jbCompose = "1.5.2"
lint = "31.1.1"
zipline = "1.3.0"
coil = "2.4.0"
okio = "3.6.0"

[libraries]
kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin" }
Expand Down Expand Up @@ -62,7 +63,8 @@ junit = { module = "junit:junit", version = "4.13.2" }
testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version = "1.12" }
assertk = "com.willowtreeapps.assertk:assertk:0.27.0"
robolectric = { module = "org.robolectric:robolectric", version = "4.10.3" }
okio = { module = "com.squareup.okio:okio", version = "3.6.0" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
okio-assetfilesystem = { module = "com.squareup.okio:okio-assetfilesystem", version.ref = "okio" }
okHttp = { module = "com.squareup.okhttp3:okhttp", version = "4.11.0" }
paging-compose-common = { module = "app.cash.paging:paging-compose-common", version = "3.2.0-alpha05-0.2.3" }
zipline = { module = "app.cash.zipline:zipline", version.ref = "zipline" }
Expand Down
2 changes: 1 addition & 1 deletion redwood-tooling-codegen/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apply plugin: 'org.jetbrains.kotlin.jvm'

redwoodBuild {
publishing()
application('redwood-codegen', 'app.cash.redwood.tooling.codegen.Main')
cliApplication('redwood-codegen', 'app.cash.redwood.tooling.codegen.Main')
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion redwood-tooling-lint/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ apply plugin: 'org.jetbrains.kotlin.plugin.serialization'

redwoodBuild {
publishing()
application('redwood-lint', 'app.cash.redwood.tooling.lint.Main')
cliApplication('redwood-lint', 'app.cash.redwood.tooling.lint.Main')
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion redwood-tooling-schema/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ apply plugin: 'org.jetbrains.kotlin.plugin.serialization'

redwoodBuild {
publishing()
application('redwood-schema', 'app.cash.redwood.tooling.schema.Main')
cliApplication('redwood-schema', 'app.cash.redwood.tooling.schema.Main')
}

dependencies {
Expand Down
5 changes: 5 additions & 0 deletions samples/emoji-search/android-composeui/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'

redwoodBuild {
embedZiplineApplication(projects.samples.emojiSearch.presenterTreehouse, 'emoji-search')
}

android {
namespace 'com.example.redwood.emojisearch.android.composeui'

Expand Down Expand Up @@ -28,4 +32,5 @@ dependencies {
implementation projects.redwoodTreehouseHost
implementation projects.redwoodTreehouseHostComposeui
implementation projects.redwoodWidgetCompose
implementation libs.okio.assetfilesystem
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okio.Path.Companion.toPath
import okio.assetfilesystem.asFileSystem

@NoLiveLiterals
class EmojiSearchActivity : ComponentActivity() {
Expand Down Expand Up @@ -128,6 +130,8 @@ class EmojiSearchActivity : ComponentActivity() {
httpClient = httpClient,
manifestVerifier = ManifestVerifier.Companion.NO_SIGNATURE_CHECKS,
eventListener = appEventListener,
embeddedDir = "/".toPath(),
embeddedFileSystem = applicationContext.assets.asFileSystem(),
)

val manifestUrlFlow = flowOf("http://10.0.2.2:8080/manifest.zipline.json")
Expand Down
5 changes: 5 additions & 0 deletions samples/emoji-search/android-views/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'

redwoodBuild {
embedZiplineApplication(projects.samples.emojiSearch.presenterTreehouse, 'emoji-search')
}

android {
namespace "com.example.redwood.emojisearch.android.views"
}
Expand All @@ -18,4 +22,5 @@ dependencies {
implementation projects.redwoodLazylayoutView
implementation projects.redwoodTreehouse
implementation projects.redwoodTreehouseHost
implementation libs.okio.assetfilesystem
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Path.Companion.toPath
import okio.assetfilesystem.asFileSystem

class EmojiSearchActivity : ComponentActivity() {
private val scope: CoroutineScope = CoroutineScope(Main)
Expand Down Expand Up @@ -119,6 +121,8 @@ class EmojiSearchActivity : ComponentActivity() {
httpClient = httpClient,
manifestVerifier = ManifestVerifier.NO_SIGNATURE_CHECKS,
eventListener = appEventListener,
embeddedDir = "/".toPath(),
embeddedFileSystem = applicationContext.assets.asFileSystem(),
stateStore = FileStateStore(
json = Json,
fileSystem = FileSystem.SYSTEM,
Expand Down
4 changes: 4 additions & 0 deletions samples/emoji-search/presenter-treehouse/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'app.cash.zipline'

redwoodBuild {
ziplineApplication()
}

kotlin {
iosArm64()
iosX64()
Expand Down