
Gradle plugin embedding project resources into generated source as Base64 chunked constants, optionally zlib-compressed at build time, with a tiny runtime for decoding and checksum verification.
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)Published as 1.0.0:
id("cn.enaium.embedresource") version "1.0.0"
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.
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 { enabled.set(false) } turns
compression off entirely.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).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.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.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.
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. |
| 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.
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.
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 checkembedResources 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-CACHEEach 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 publishPluginsThe 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.
Resources.images.logo.zlib fast path for Kotlin/Native (the pure Kotlin inflater is used instead).MIT, see LICENSE.
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)Published as 1.0.0:
id("cn.enaium.embedresource") version "1.0.0"
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.
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 { enabled.set(false) } turns
compression off entirely.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).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.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.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.
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. |
| 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.
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.
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 checkembedResources 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-CACHEEach 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 publishPluginsThe 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.
Resources.images.logo.zlib fast path for Kotlin/Native (the pure Kotlin inflater is used instead).MIT, see LICENSE.