Write Your First Script (Java)
By the end of this page you will have written a bleep script that:
- compiles a Java 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 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.
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.mylibhas apublish:block. Any project with agroupIdunderpublish: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:
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.
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
- Bleep scripts (concept): the
full
BleepScriptAPI, working with the build model, the script protocol, and IDE debugging. - Source generation scripts: for
scripts whose output is consumed by
compile. - Publish to Maven Central: the same flow targeting Sonatype with PGP signing.