Skip to main content

Write Your First Script (Java)

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

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

Why it's a good first example: the same task in Maven or Gradle is what you'd reach for a plugin for. Here it's a regular Java class. The ProGuard step lives in its own file (Proguard.java) 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, why bleep models scripts as a core concept, and when to choose a script over a source generation script, see Scripts and source generation.

Other languages: Kotlin → · Scala →

Step 1: The build

Two projects: a library to publish (mylib) and the scripts project that hosts our publish logic. The scripts project depends on bleepscript for the build API 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:
java:
options: -proc:none --release 17
platform:
name: jvm
publish:
groupId: com.example
scripts:
dependencies:
- build.bleep:bleepscript:${BLEEP_VERSION}
java:
options: -proc:none --release 17
platform:
name: jvm
scripts:
proguard-publish:
main: scripts.ProguardPublish
project: scripts

Key points:

  • ${BLEEP_VERSION} is interpolated by bleep to your installed version, so the script project's dependency stays in sync with the bleep CLI you're running.
  • mylib has a publish: block. Any project with a groupId under publish: is publishable; the script will read that as the fallback when no explicit groupId is passed.
  • The top-level scripts: section maps a CLI name (proguard-publish) to a fully qualified class name (scripts.ProguardPublish) plus the project that contains it.

Step 2: A tiny library to publish

Any Java class will do. Put it under mylib/src/java/....

package com.example;

public final class MyLib {
private MyLib() {}

public static String greet(String name) {
return "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.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A reusable ProGuard helper. Takes JAR bytes, returns shrunk JAR bytes. Reusable
* across projects: copy this file, depend on it via {@code sources:} in
* bleep.yaml, or publish it as a library.
*
* <p>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.
*
* <p>Instantiate via {@link #create(Started)} — the factory fetches ProGuard
* once and caches the classpath. Reuse one instance across multiple shrink
* calls in the same script.
*/
public record Proguard(Started started, String proguardVersion, String classpath) {
public static final String DEFAULT_VERSION = "7.7.0";

/** Resolve and cache the ProGuard classpath. Default version. */
public static Proguard create(Started started) {
return create(started, DEFAULT_VERSION);
}

/** Resolve and cache the ProGuard classpath. Caller picks the version. */
public static Proguard create(Started started, String proguardVersion) {
List<Path> jars =
Coursier.fetchClasspath(
started, "com.guardsquare:proguard-base:" + proguardVersion);
String classpath =
jars.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator));
return new Proguard(started, proguardVersion, classpath);
}

/**
* 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 using the JVM bleep resolved for this build.
*/
public byte[] shrink(byte[] inputJarBytes) {
try {
Path workdir = Files.createTempDirectory("proguard");
try {
Path inputJar = workdir.resolve("input.jar");
Path outputJar = workdir.resolve("output.jar");
Path 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 {
deleteRecursively(workdir);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static void deleteRecursively(Path dir) throws IOException {
try (Stream<Path> paths = Files.walk(dir)) {
for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(p);
}
}
}

/**
* 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 static String config(Path inputJar, Path outputJar) {
return String.join(
"\n",
"-injars " + inputJar.toAbsolutePath(),
"-outjars " + outputJar.toAbsolutePath(),
"-dontoptimize",
"-dontobfuscate",
"-dontwarn",
"-keep class ** { *; }");
}
}

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 as com.example::my-proguard:1.0 and depend on it from the scripts project. No registration, no requires graph, no lifecycle hooks — same as any other Java class.

Coursier.fetchClasspath and Cli.command come from the bleepscript API: ProGuard is fetched through the build's configured resolver (same cache, same credentials as bleep compile uses), 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 can be 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.Logger;
import bleepscript.PackagedLibrary;
import bleepscript.Packaging;
import bleepscript.PublishLayout;
import bleepscript.Started;
import java.util.List;
import java.util.Optional;

/**
* Compose: compile, package, {@link Proguard#shrink} the main JAR, publish.
*
* <p>The "plugin" lives next door in {@link Proguard}. This script is just glue:
* arg parsing + four bleepscript API calls.
*
* <p>Usage: {@code bleep proguard-publish <projectName> <version> <groupId>}.
*/
public final class ProguardPublish extends BleepScript {
public ProguardPublish() {
super("proguard-publish");
}

@Override
public void run(Started started, Commands commands, List<String> args) {
if (args.size() < 3) {
throw new IllegalArgumentException(
"Usage: bleep proguard-publish <projectName> <version> <groupId>");
}
String projectName = args.get(0);
String version = args.get(1);
String groupId = args.get(2);

CrossProjectName project = new CrossProjectName(projectName, Optional.empty());
Logger log = started.logger().withContext("project", projectName);

commands.compile(List.of(project));

log.info("packaging");
PackagedLibrary library =
Packaging.packageProject(
started,
project,
groupId,
version,
PublishLayout.Maven.INSTANCE,
BleepscriptServices.Holder.INSTANCE.defaultManifestCreator());

byte[] originalJar = library.jarFile();
log.info("main JAR: " + library.jarFilePath().asString() + " (" + originalJar.length + " bytes)");

byte[] shrunk = Proguard.create(started).shrink(originalJar);
log.info(
"ProGuard output: " + shrunk.length + " bytes (saved " + (originalJar.length - shrunk.length) + ")");

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.

BleepScript provides the main method via reflection — no public static void main boilerplate. The Packaging helpers return an immutable PackagedLibrary; use withJarFile(...) to derive a new library from an existing one.

Step 5: Run it

bleep proguard-publish mylib 1.0.0 com.example

Output:

[script proguard-publish]: ✅ compiled (42ms) [project => mylib]
[script proguard-publish]: Build Summary
[script proguard-publish]: Projects: compiled, 0 failed, 0 skipped
[script proguard-publish]: Time: 0,2s
[script proguard-publish]: packaging [project => mylib]
[script proguard-publish]: main JAR: com/example/mylib/1.0.0/mylib-1.0.0.jar (833 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: 761 bytes (saved 72) [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