Write Your First Script (Scala)
By the end of this page you will have written a bleep script that:
- compiles a Scala 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 Ivy cache.
That's a small, real release pipeline you can wrap your head around in 100 lines of Scala.
Why it's a good first example: the same task in sbt usually means
an AutoPlugin or settings and tasks spread across a plugin module
and build.sbt. Here it's a regular Scala class. The ProGuard step
lives in its own file (Proguard.scala) 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.
The bleep API for scripts is bleepscript.*, a pure-Java library
that Scala consumes through standard Java interop. (Bleep's internal
bleep.* Scala API is not part of the user surface.)
Step 1: The build
Two projects: a Scala library (mylib) and the scripts project that
hosts our publish logic. Both use Scala 3. The scripts project
depends on bleepscript (single : since it's a Java artifact, not
Scala-cross-versioned) 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:
platform:
name: jvm
publish:
groupId: com.example
scala:
version: 3.8.3
scripts:
dependencies:
- build.bleep:bleepscript:${BLEEP_VERSION}
platform:
name: jvm
scala:
version: 3.8.3
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 Scala class compiles to
scripts.ProguardPublish(no$suffix because it's aclass, not anobject).
Step 2: A tiny library to publish
Anything Scala will do. Put it under mylib/src/scala/....
package com.example
object MyLib:
def greet(name: String): String = s"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, Coursier, Started}
import java.io.File
import java.nio.file.{Files, Path}
import java.util.Comparator
import scala.jdk.CollectionConverters.*
import scala.util.Using
/** 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 [[Proguard.apply]] — the factory fetches ProGuard once and
* caches the classpath. Reuse one instance across multiple shrink calls in
* the same script.
*/
class Proguard private (started: Started, proguardVersion: String, 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.
*/
def shrink(inputJarBytes: Array[Byte]): Array[Byte] =
val workdir = 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, Proguard.config(inputJar, outputJar))
started.logger.info(s"running ProGuard $proguardVersion")
Cli.command("proguard")
.args(
started.jvmCommand.toString,
"-cp",
classpath,
"proguard.ProGuard",
s"@${configFile.toAbsolutePath}"
)
.cwd(workdir)
.run(started)
Files.readAllBytes(outputJar)
finally
Using.resource(Files.walk(workdir)): stream =>
stream.sorted(Comparator.reverseOrder).forEach(Files.delete)
object Proguard:
val DefaultVersion = "7.7.0"
/** Resolve and cache the ProGuard classpath. */
def apply(started: Started, proguardVersion: String = DefaultVersion): Proguard =
val jars = Coursier.fetchClasspath(started, s"com.guardsquare:proguard-base:$proguardVersion")
val classpath = jars.asScala.map(_.toString).mkString(File.pathSeparator)
new Proguard(started, proguardVersion, classpath)
/** 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 def config(inputJar: Path, outputJar: Path): String =
List(
s"-injars ${inputJar.toAbsolutePath}",
s"-outjars ${outputJar.toAbsolutePath}",
"-dontoptimize",
"-dontobfuscate",
"-dontwarn",
"-keep class ** { *; }"
).mkString("\n")
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 Scala 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,
BleepscriptServices,
Commands,
CrossProjectName,
Packaging,
PublishLayout,
Started
}
import java.util.Optional
import scala.jdk.CollectionConverters.*
/** 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 extends BleepScript("proguard-publish"):
override def run(started: Started, commands: Commands, args: java.util.List[String]): Unit =
val argList = args.asScala.toList
require(argList.length >= 3, "Usage: bleep proguard-publish <projectName> <version> <groupId>")
val List(projectName, version, groupId, _*) = argList
val project = new CrossProjectName(projectName, Optional.empty())
val log = started.logger.withContext("project", projectName)
commands.compile(java.util.List.of(project))
log.info("packaging")
val library = Packaging.packageProject(
started,
project,
groupId,
version,
PublishLayout.Ivy.INSTANCE,
BleepscriptServices.Holder.INSTANCE.defaultManifestCreator()
)
val originalJar = library.jarFile
log.info(s"main JAR: ${library.jarFilePath.asString} (${originalJar.length} bytes)")
val shrunk = Proguard(started).shrink(originalJar)
log.info(s"ProGuard output: ${shrunk.length} bytes (saved ${originalJar.length - shrunk.length})")
Packaging.publishToLocalIvy(library.withJarFile(shrunk))
log.info(s"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.publishToLocalIvy— publish the mutated library.
Notes:
- We use
class ProguardPublishrather thanobject ProguardPublishso the class inheritsBleepScript'smainmethod via standard JVM inheritance. Scala objects don't expose inherited statics on the forwarder class. - The constructor takes the script name:
extends BleepScript("proguard-publish"). argsisjava.util.List[String]from the Java API. Convert withasScalafromscala.jdk.CollectionConverters.*.- The
Packaging,PackagedLibrary, andPublishLayouttypes are the Java bleepscript API. Scala reads them as plain Java interop — no special adapter required.
Step 5: Run it
bleep proguard-publish mylib 1.0.0 com.example
Output:
[script proguard-publish]: ✅ compiled (221ms) [project => mylib]
[script proguard-publish]: Build Summary
[script proguard-publish]: Projects: compiled, 0 failed, 0 skipped
[script proguard-publish]: Time: 0,4s
[script proguard-publish]: packaging [project => mylib]
[script proguard-publish]: main JAR: com.example/mylib_3/1.0.0/jars/mylib_3.jar (2360 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: 2205 bytes (saved 155) [project => mylib]
[script proguard-publish]: Published com.example:mylib:1.0.0 [project => mylib]
Verify:
$ ls ~/.ivy2/local/com.example/mylib_3/1.0.0/
docs ivys jars poms srcs
Where to go from here
- Bleep scripts (concept): the
full
BleepScriptAPI. From Scala you can drop()on no-arg Java getters; everything else reads as plain interop. - Source generation scripts: for
scripts whose output is consumed by
compile. - Java script tutorial: same API, different host language.