Skip to main content

Write Your First Script (Kotlin)

By the end of this page you will have written a bleep script that:

  1. compiles a Kotlin library,
  2. packages it into a publishable JAR (plus sources, javadoc, POM),
  3. runs ProGuard over the main JAR to shrink dead code, and
  4. 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.

Other languages: Java → · Scala →

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.
  • mylib has a publish: block. Any project with a groupId under publish: is publishable.
  • The Kotlin class scripts.ProguardPublish is referenced by its JVM class name — no Kt suffix 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:

  1. commands.compile — make sure classes are fresh.
  2. Packaging.packageProject — get the publish artifacts (JAR, sources, javadoc, POM) as bytes.
  3. PackagedLibrary.withJarFile — swap the main JAR for the ProGuard'd version, leaving sources/javadoc/POM untouched.
  4. 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").
  • BleepScript supplies main by reflection.
  • args is List<String> (Kotlin's interface, compatible with the Java List<String> signature on the base class).
  • The Packaging, PackagedLibrary, and PublishLayout types 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