Write Your First Script (Kotlin)
By the end of this page you will have written a bleep script that:
- compiles a Kotlin library,
- packages it into a publishable JAR (plus sources, javadoc, POM),
- runs ProGuard over the main JAR to shrink dead code, and
- publishes the optimized library to your local Maven repo.
That's a small, real release pipeline you can wrap your head around in 100 lines of Kotlin.
Why it's a good first example: the same task in Gradle is what
you'd reach for a plugin for. Here it's a regular Kotlin class. The
ProGuard step lives in its own file (Proguard.kt) that the script
calls. That file is shareable: copy it into another project, point a
second build's sources: at it, or publish it to Maven. Same shape
either way.
For what a script is and why bleep models scripts as a core concept, see Scripts and source generation.
Step 1: The build
Two projects: a Kotlin library (mylib) and the scripts project that
hosts our publish logic. Both use the same Kotlin version and JVM
target. The scripts project depends on bleepscript (the Java API,
fully usable from Kotlin) and on io.get-coursier:interface to fetch
ProGuard at runtime.
$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 1.0.0-M10
jvm:
name: graalvm-community:25.0.1
projects:
mylib:
kotlin:
jvmTarget: "17"
version: 2.3.0
platform:
name: jvm
publish:
groupId: com.example
scripts:
dependencies:
- build.bleep:bleepscript:${BLEEP_VERSION}
kotlin:
jvmTarget: "25"
version: 2.3.0
platform:
name: jvm
scripts:
proguard-publish:
main: scripts.ProguardPublish
project: scripts
Key points:
${BLEEP_VERSION}is interpolated by bleep to your installed version.mylibhas apublish:block. Any project with agroupIdunderpublish:is publishable.- The Kotlin class
scripts.ProguardPublishis referenced by its JVM class name — noKtsuffix because it's a class, not top-level functions.
Step 2: A tiny library to publish
Anything Kotlin will do. Put it under mylib/src/kotlin/....
package com.example
object MyLib {
fun greet(name: String): String = "Hello, $name!"
}
Step 3: A reusable ProGuard helper
Before the script, the actual ProGuard logic lives in a separate class. It takes JAR bytes, returns shrunk JAR bytes — nothing in it is specific to this build:
package scripts
import bleepscript.Cli
import bleepscript.Coursier
import bleepscript.Started
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.deleteExisting
/**
* A reusable ProGuard helper. Takes JAR bytes, returns shrunk JAR bytes. Reusable
* across projects: copy this file, depend on it via `sources:` in bleep.yaml, or
* publish it as a library.
*
* This is what a "plugin" looks like when you don't have a plugin API: a regular
* class with a regular method. No registration, no lifecycle, no autoplugin
* trigger — just code you can call.
*
* Instantiate via [create] — the factory fetches ProGuard once and caches the
* classpath. Reuse one instance across multiple shrink calls in the same script.
*/
class Proguard private constructor(
private val started: Started,
private val proguardVersion: String,
private val classpath: String,
) {
/**
* Run ProGuard over the given JAR bytes; return the shrunk JAR bytes. Writes
* input/output JARs to a temp directory and forks a JVM through bleep's cli
* wrapper.
*/
fun shrink(inputJarBytes: ByteArray): ByteArray {
val workdir: Path = Files.createTempDirectory("proguard")
try {
val inputJar = workdir.resolve("input.jar")
val outputJar = workdir.resolve("output.jar")
val configFile = workdir.resolve("config.pro")
Files.write(inputJar, inputJarBytes)
Files.writeString(configFile, config(inputJar, outputJar))
started.logger().info("running ProGuard $proguardVersion")
Cli.command("proguard")
.args(
started.jvmCommand().toString(),
"-cp", classpath,
"proguard.ProGuard",
"@${configFile.toAbsolutePath()}"
)
.cwd(workdir)
.run(started)
return Files.readAllBytes(outputJar)
} finally {
Files.walk(workdir).use { stream ->
stream.sorted(Comparator.reverseOrder()).forEach { it.deleteExisting() }
}
}
}
/**
* Minimal ProGuard config: shrink only (no obfuscation or optimization), keep
* all public members so the published artifact stays byte-compatible. Customize
* for real use.
*/
private fun config(inputJar: Path, outputJar: Path): String =
listOf(
"-injars ${inputJar.toAbsolutePath()}",
"-outjars ${outputJar.toAbsolutePath()}",
"-dontoptimize",
"-dontobfuscate",
"-dontwarn",
"-keep class ** { *; }"
).joinToString("\n")
companion object {
const val DEFAULT_VERSION = "7.7.0"
/** Resolve and cache the ProGuard classpath. */
fun create(started: Started, proguardVersion: String = DEFAULT_VERSION): Proguard {
val jars = Coursier.fetchClasspath(
started, "com.guardsquare:proguard-base:$proguardVersion"
)
val classpath = jars.joinToString(File.pathSeparator) { it.toString() }
return Proguard(started, proguardVersion, classpath)
}
}
}
This is what a "plugin" looks like when you don't have a plugin API
— a regular class with a regular method. You can copy this file into
any other bleep project, share it via a source dependency (point
another build's sources: field at the same directory), or publish
it to Maven and depend on it from the scripts project. No
registration, no requires graph, no lifecycle hooks — same as any
other Kotlin class.
Coursier.fetchClasspath and Cli.command come from the
bleepscript API: ProGuard is fetched through the build's
configured resolver, and the JVM fork goes through bleep's
logger-aware process wrapper. started.resolvedJvm().javaBin() is
the JVM bleep chose for this build, so the child process inherits
the right JDK.
Step 4: The script
Now the script is thin. Extend bleepscript.BleepScript, parse
args, package, hand off to Proguard, publish:
package scripts
import bleepscript.BleepScript
import bleepscript.BleepscriptServices
import bleepscript.Commands
import bleepscript.CrossProjectName
import bleepscript.PackagedLibrary
import bleepscript.Packaging
import bleepscript.PublishLayout
import bleepscript.Started
import java.util.Optional
/**
* Compose: compile, package, [Proguard.shrink] the main JAR, publish.
*
* The "plugin" lives next door in [Proguard]. This script is just glue: arg
* parsing + four bleepscript API calls.
*
* Usage: `bleep proguard-publish <projectName> <version> <groupId>`.
*/
class ProguardPublish : BleepScript("proguard-publish") {
override fun run(started: Started, commands: Commands, args: List<String>) {
require(args.size >= 3) {
"Usage: bleep proguard-publish <projectName> <version> <groupId>"
}
val projectName = args[0]
val version = args[1]
val groupId = args[2]
val project = CrossProjectName(projectName, Optional.empty())
val log = started.logger().withContext("project", projectName)
commands.compile(listOf(project))
log.info("packaging")
val library = Packaging.packageProject(
started,
project,
groupId,
version,
PublishLayout.Maven.INSTANCE,
BleepscriptServices.Holder.INSTANCE.defaultManifestCreator()
)
val originalJar = library.jarFile()
log.info("main JAR: ${library.jarFilePath().asString()} (${originalJar.size} bytes)")
val shrunk = Proguard.create(started).shrink(originalJar)
log.info("ProGuard output: ${shrunk.size} bytes (saved ${originalJar.size - shrunk.size})")
Packaging.publishToLocalMaven(library.withJarFile(shrunk))
log.info("Published $groupId:$projectName:$version")
}
}
It walks four bleepscript primitives:
commands.compile— make sure classes are fresh.Packaging.packageProject— get the publish artifacts (JAR, sources, javadoc, POM) as bytes.PackagedLibrary.withJarFile— swap the main JAR for the ProGuard'd version, leaving sources/javadoc/POM untouched.Packaging.publishToLocalMaven— publish the mutated library.
Notes:
- The Kotlin primary constructor passes the script name to the Java
super constructor:
class ProguardPublish : BleepScript("proguard-publish"). BleepScriptsuppliesmainby reflection.argsisList<String>(Kotlin's interface, compatible with the JavaList<String>signature on the base class).- The
Packaging,PackagedLibrary, andPublishLayouttypes come from the Java bleepscript API. Kotlin reads them like any other Java library — no special adapters.
Step 5: Run it
bleep proguard-publish mylib 1.0.0 com.example
Output:
[script proguard-publish]: ✅ compiled (274ms) [project => mylib]
[script proguard-publish]: Build Summary
[script proguard-publish]: Projects: compiled, 0 failed, 0 skipped
[script proguard-publish]: Time: 0,5s
[script proguard-publish]: packaging [project => mylib]
[script proguard-publish]: main JAR: com/example/mylib/1.0.0/mylib-1.0.0.jar (5288 bytes) [project => mylib]
[script proguard-publish]: running ProGuard 7.7.0
[script proguard-publish] / [subprocess: proguard]: ProGuard, version 7.7.0
[script proguard-publish]: ProGuard output: 4617 bytes (saved 671) [project => mylib]
[script proguard-publish]: Published com.example:mylib:1.0.0 [project => mylib]
Verify:
$ ls ~/.m2/repository/com/example/mylib/1.0.0/
mylib-1.0.0-javadoc.jar mylib-1.0.0-sources.jar mylib-1.0.0.jar mylib-1.0.0.pom
Where to go from here
- Bleep scripts (concept): the
full
BleepScriptAPI and how to call it from Kotlin via standard Java interop. - Source generation scripts: for
scripts whose output is consumed by
compile. - Java script tutorial: same API, different host language.