Skip to main content

Write Your First Script (Scala)

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

  1. compiles a Scala 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 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.)

Other languages: Java → · Kotlin →

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.
  • mylib has a publish: block. Any project with a groupId under publish: is publishable.
  • The Scala class compiles to scripts.ProguardPublish (no $ suffix because it's a class, not an object).

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:

  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.publishToLocalIvy — publish the mutated library.

Notes:

  • We use class ProguardPublish rather than object ProguardPublish so the class inherits BleepScript's main method 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").
  • args is java.util.List[String] from the Java API. Convert with asScala from scala.jdk.CollectionConverters.*.
  • The Packaging, PackagedLibrary, and PublishLayout types 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