Skip to content

Repository files navigation

embed-resource

A Gradle plugin that embeds files into Kotlin sources, so an application ships its resources with the code instead of reading them from a file system, a classpath or a platform bundle.

Resources are zlib compressed at build time and decoded on the device by a small dependency-free Kotlin Multiplatform runtime. The generated code lives in commonMain, so the same call works on every target of a project: JVM, Android, Kotlin/Native (Apple, Linux, Windows), Kotlin/JS and Kotlin/Wasm.

plugins {
    kotlin("multiplatform")
    id("cn.enaium.embedresource") version "1.0.0"
}

embedres {
    packageName.set("com.example.resources")
}

The plugin adds the matching cn.enaium:embed-resource-runtime dependency (the version of the plugin itself) to commonMain of a multiplatform project or to implementation of a Kotlin/JVM project, and wires the generated sources, so no build script has to declare either.

// Anything below src/commonMain/resources is embedded, with `/` separated paths relative to that directory.
val config = Resources.readText("config/app.properties")
val icon = Resources.readBytes("images/logo.png")

if (Resources.exists("i18n/de.txt")) {
    val german = Resources.readText("i18n/de.txt")
}

Resources.paths.forEach(::println)

Status

Published as 1.0.0:

  • Gradle Plugin Portal: id("cn.enaium.embedresource") version "1.0.0"
  • Maven Central: cn.enaium:embed-resource-gradle-plugin, cn.enaium:embed-resource-runtime and cn.enaium:embed-resource-compressor

The samples in this repository build the plugin and the runtime from source through Gradle composite builds, so they always exercise the code under review rather than the released artifacts.

How it works

resources/                    build time                          run time
├── config/app.properties     scan visible files                  lookup path
├── i18n/en.txt          ───► zlib compress, keep the smaller ───► Base64 decode
└── images/logo.png           Base64 into Kotlin string constants  inflate (if compressed)
                              generate facade + data objects       size + checksum verified
  • Compression is per file and optional. A payload is stored as it is whenever zlib does not make it smaller, so PNG, JPEG or ZIP files never pay for a wrapper they cannot win back. compression { enabled.set(false) } turns compression off entirely.
  • Payloads become Base64 string constants, not byteArrayOf(...). A byteArrayOf call with tens of thousands of arguments is orders of magnitude slower to parse and compile, and a class file cannot hold a string constant larger than 64 KiB. Payloads are therefore split into chunks (16 KiB by default) and chunks are grouped into source files (32 per file by default).
  • Decoding happens on read. readBytes and readText decode and decompress on every call and hand out a fresh array, so nothing is cached and the caller owns the result. Corrupt or truncated data fails with IllegalStateException instead of returning wrong bytes: the decoded size and the zlib Adler-32 checksum are verified.
  • The plugin declares the runtime. The runtime artifact is versioned together with the plugin, so the plugin adds it to the project itself; declaring a newer version explicitly is enough to override it.
  • The runtime needs no platform library. JVM and Android use java.util.zip.Inflater; Kotlin/Native, Kotlin/JS and Kotlin/Wasm use a pure Kotlin inflater (puff-style, RFC 1950/1951), which keeps every non-JVM target identical and free of extra native dependencies.

DSL

Everything is configured through the embedres extension:

Property Default Meaning
sourceDirectory src/commonMain/resources Directory that is scanned. The build fails when it does not exist.
packageName required Package of the generated facade.
objectName Resources Name of the generated facade object.
compression.enabled true Whether compressible payloads are zlib compressed.
compression.level 6 zlib level, 0 (store) to 9 (best).
chunkSize 16384 Payload bytes per generated string constant, 1 to 32768.
chunksPerSourceFile 32 Chunks per generated data source file.

Scanning rules: every regular file below sourceDirectory is embedded; files or directories whose name starts with . are skipped; resource paths always use / as separator and are sorted in natural order.

Generated code

build/generated/embed-resource/kotlin/com/example/resources/
├── Resources.kt        the facade
└── ResourcesData0.kt   internal Base64 payload constants, one object per group of chunks

The facade is added to commonMain of a Kotlin Multiplatform project and to main of a Kotlin/JVM project, so no source set wiring is needed:

Member Behaviour
paths: List<String> Every embedded resource, sorted.
exists(path: String): Boolean Never throws, never decodes.
readBytes(path: String): ByteArray? null when nothing is embedded at path.
readText(path: String): String? Same, decoded as UTF-8.

Platform support

Target Decoder Tested here
JVM, Android java.util.zip.Inflater jvmTest
macOS, iOS, tvOS, watchOS (arm64, x64) pure Kotlin inflater macosArm64Test
Linux x64 / arm64, Windows x64 pure Kotlin inflater compiled here, tested on their own hosts
Kotlin/JS, Kotlin/Wasm (JS and WASI) pure Kotlin inflater jsNodeTest, wasmJsNodeTest, wasmWasiNodeTest

Android has no target of its own: an androidJvm compilation consumes the JVM artifact, which is what integration-tests/android-consumer asserts. Test tasks of native targets other than the host's are disabled in this repository; they need their own machine, a booted simulator or a device.

Repository layout

embed-resource-compressor/      zlib compression, used at build time only
embed-resource-gradle-plugin/   the plugin: DSL, scanning, code generation, source set and dependency wiring
embed-resource-runtime/         the Kotlin Multiplatform runtime that decodes embedded payloads
example/                        Kotlin Multiplatform sample with usage examples
integration-tests/
├── simple-multiplatform/       multiplatform consumer, hash verification on every target
├── kotlin-jvm/                 plain Kotlin/JVM consumer
└── android-consumer/           asserts that an androidJvm compilation resolves the JVM artifact

The three modules are builds of their own, included by the root build: they publish separately, build standalone, and example — the only subproject — applies the plugin by id exactly like a published plugin would.

Building

Requires JDK 17 or newer; the Gradle wrapper is the only other prerequisite.

./gradlew build                       # compressor, plugin, runtime and example
./gradlew :example:jvmRun             # runs the example on the JVM
./gradlew :example:check              # runs the example's tests on jvm, macosArm64, jsNode, wasmJsNode

cd embed-resource-gradle-plugin && ../gradlew build   # every module is also a standalone build
cd integration-tests/simple-multiplatform && ../../gradlew check
cd integration-tests/kotlin-jvm && ../../gradlew check
cd integration-tests/android-consumer && ../../gradlew check

embedResources is a cacheable task with fully declared inputs and outputs, so up-to-date checks, the configuration cache and the build cache all work:

./gradlew --configuration-cache jvmTest   # reuses the cached configuration on the second run
./gradlew --build-cache clean jvmTest     # :embedResources and :compileKotlinJvm come FROM-CACHE

Releasing

Each module publishes with com.vanniktech.maven.publish to the Central Portal, and the plugin additionally publishes to the Gradle Plugin Portal:

cd embed-resource-compressor && ../gradlew publishToMavenCentral
cd embed-resource-runtime && ../gradlew publishToMavenCentral
cd embed-resource-gradle-plugin && ../gradlew publishToMavenCentral publishPlugins

The order matters: the plugin's POM depends on the compressor, and the Plugin Portal resolves the plugin's dependencies from Maven Central.

Credentials and signing keys live in ~/.gradle/gradle.properties: mavenCentralUsername/mavenCentralPassword for the Central Portal, signing.keyId/signing.password/signing.secretKeyRingFile for the signatures, and gradle.publish.key/gradle.publish.secret for the Gradle Plugin Portal. The plugin declares its configurationCache compatibility through org.gradle.plugin.compatibility.

Not implemented yet

  • Deduplication of identical resources: two files with the same content are embedded twice.
  • Caching of decoded resources: every read decodes again.
  • Type-safe accessors such as Resources.images.logo.
  • A platform zlib fast path for Kotlin/Native (the pure Kotlin inflater is used instead).

License

MIT, see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages