Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/modules/ROOT/pages/running.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,95 @@ Since Java 9 https://docs.oracle.com/javase/9/tools/java.htm#GUID-3B1CE181-CD30-

For Java 8 and if you want to set explicitly only for `jbang` you can also add flags by setting `JBANG_JAVA_OPTIONS`.

=== Classpath from build files (EXPERIMENTAL)

You can use a build file as a dependency. JBang runs the matching build tool and adds its compile dependencies to the script's classpath.

[source, java]
----
//DEPS pom.xml
//DEPS build.gradle
//DEPS build.sbt
//DEPS build.mill
----

Build file paths are resolved relative to the script, like Java source dependencies. Prefix a file name with `^` to search the script's directory and its parents, stopping after the user home directory:

[source, java]
----
//DEPS ^pom.xml
----

The same references can be passed on the command line, for example `jbang run --deps ^pom.xml src/main/java/app.java`.

==== Why use a build file as a dependency?

Using this is great for reusing the same classpath as an existing project's build tool without having to manually maintain a separate list, and without needing to build the full project just to get at its dependencies.

It makes it possible to seamlessly experiment with and extend your project with minimal ceremony.

Pointing `//DEPS` at the build file lets you have:

* *One place to change versions.* Bump `commons-lang3` in `pom.xml` and the script picks it up on the next run — no second edit, no forgotten stale copy.
* *Reuse an existing project's dependencies from a small script.* Drop a `run.java` next to a Maven/Gradle project and let it borrow the project's compile classpath instead of duplicating a curated subset.
* *Bridge scripts and applications.* A prototype often starts as a JBang script and later grows into a Maven/Gradle project; letting the script consume the future project's build file makes the transition incremental instead of a big rewrite.
* *Complex dependency graphs.* BOMs, exclusions, dependency-management stanzas, custom repositories, and platform constraints are already expressed cleanly in build files. Letting the native tool resolve them is more accurate than approximating the graph in `//DEPS` lines.

This feature does come at a cost: JBang has to actually invoke Maven, Gradle, sbt, or Mill, which is slower than JBang's built-in resolver and requires the tool to be installed. Results are cached (see below), so the cost is paid once per build file until it changes.

==== Supported build tools

[cols="1,3,3,2", options="header"]
|===
| Tool | Build files | How JBang resolves the classpath | Wrapper detected

| Maven
| `pom.xml`
| Runs `dependency:build-classpath` and reads the result from a temp file. `<includeScope>compile` so only compile dependencies land on the classpath.
| `mvnw` / `mvnw.cmd`, else `mvn` on `PATH`

| Gradle
| `build.gradle`, `build.gradle.kts`
| Applies a temporary init script that registers a `jbangPrintClasspath` task on any subproject with the `java` plugin and prints `sourceSets.main.compileClasspath`. Fails loudly if no subproject applies the `java` plugin.
| `gradlew` / `gradlew.bat`, else `gradle` on `PATH`

| sbt
| `build.sbt`
| Runs `sbt --error --batch "export Compile / dependencyClasspath"` and takes the last non-blank line (validated to look like a classpath).
| `sbt` / `sbt.bat`

| Mill
| `build.mill`, `build.mill.yaml`, legacy `build.sc`
| Runs `mill --disable-ticker show compileClasspath` and parses the JSON output. Requires a build that exposes a root module; multi-module Mill builds have no single unambiguous compile classpath.
| `mill` / `mill.bat`
|===

==== Caching

Resolved classpaths are cached per build file under `$JBANG_DIR/cache/projects/buildclasspaths/` and reused until the build file changes (modification time or size). The cache is also invalidated automatically if any jar it references has disappeared from disk (e.g. because your local Maven repo was cleared). Force a re-resolution with:

[source, shell]
----
jbang cache clear --projects
----

==== Limitations

* Only scripts that live on the local filesystem can use build files as dependencies. An embedded `//DEPS build.gradle` in a script fetched from a URL or catalog is rejected, because a single build file cannot be resolved in isolation from the rest of its project tree. Pass the build file via `--deps` on the command line instead.
* Each strategy requires the matching build tool to be available (via a wrapper in the project or on `PATH`). If the tool is missing, JBang fails with the tool's own error output rather than silently degrading.
* The first resolution pays the tool's normal cold-start cost (Maven downloads, Gradle daemon startup, sbt/Mill JVM warmup). Subsequent runs use the cache.
* Only the project's declared compile dependencies are added to the classpath — the project's own compiled classes are not. If you need to reference the project's source from your script, pull the file(s) in directly with `//SOURCES`:
+
[source, java]
----
//DEPS pom.xml
//SOURCES src/main/java/example/Greeter.java
----
+
A future release may add a scope option (e.g. `compile` vs `runtime`) so the project's own output can be included automatically. Feedback on which scope should be the default is welcome — open an issue at https://github.com/jbangdev/jbang.

NOTE: Build files can execute code. Only use this feature with build files you trust.

== Preview

The example above can alternatively also be written with `PREVIEW` instead of `--enable-preview`, like this:
Expand Down
41 changes: 41 additions & 0 deletions itests/builddeps/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
= Build-file dependency integration tests

Minimal but realistic project fixtures exercising `//DEPS <build-file>` for
each supported build system:

[cols="1,2,2", options="header"]
|===
| Tool | Fixture root | Layout

| Maven | `maven/` | `pom.xml`, `src/main/java/example/Greeter.java`
| Gradle | `gradle/` | `build.gradle`, `src/main/java/example/Greeter.java`
| sbt | `sbt/` | `build.sbt`, `src/main/java/example/Greeter.java`
| Mill | `mill/` | `build.mill`, `.mill-version`, `./mill` bootstrap, `src/example/Greeter.java`
|===

Each fixture is a real, self-contained project that compiles with its own
build tool. `Greeter.java` uses `commons-lang3` — the same dependency
declared in the build file — so it exercises the project's real dependency
graph, not just a token declaration.

`App.java` at the fixture root is the JBang script under test. It uses
`//DEPS <build-file>` to borrow the project's declared compile
dependencies and prints `<tool>-classpath:olleh`. `App.java` does **not**
reference `Greeter` because the current implementation only reuses
dependencies — the project's own compiled classes are not included on the
JBang classpath.

== Running

Each folder declares the tool it needs in a `mise.toml`. If you have
https://mise.jdx.dev/[mise] installed:

[source, shell]
----
cd itests/builddeps/maven && mise install # installs java + maven
mise exec -- jbang App.java # verifies the fixture
----

The `BuildSystemClassPathsIT` integration test runs `mise install`
automatically for each fixture and skips the whole test class if mise
itself is missing.
10 changes: 10 additions & 0 deletions itests/builddeps/gradle/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS build.gradle

import org.apache.commons.lang3.StringUtils;

public class App {
public static void main(String... args) {
System.out.println("gradle-classpath:" + StringUtils.reverse("hello"));
}
}
11 changes: 11 additions & 0 deletions itests/builddeps/gradle/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
id 'java'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.apache.commons:commons-lang3:3.14.0'
}
3 changes: 3 additions & 0 deletions itests/builddeps/gradle/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tools]
java = "temurin-21"
gradle = "8.10"
15 changes: 15 additions & 0 deletions itests/builddeps/gradle/src/main/java/example/Greeter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package example;

import org.apache.commons.lang3.StringUtils;

/**
* Illustrative source belonging to the real Gradle project. Compiled by
* {@code gradle build}; not on the classpath of the accompanying JBang
* script — JBang only borrows the project's declared compile dependencies
* via {@code //DEPS build.gradle}.
*/
public class Greeter {
public static String greet(String name) {
return "Hello, " + StringUtils.capitalize(name) + "!";
}
}
10 changes: 10 additions & 0 deletions itests/builddeps/maven/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS pom.xml

import org.apache.commons.lang3.StringUtils;

public class App {
public static void main(String... args) {
System.out.println("maven-classpath:" + StringUtils.reverse("hello"));
}
}
3 changes: 3 additions & 0 deletions itests/builddeps/maven/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tools]
java = "temurin-21"
maven = "3.9"
22 changes: 22 additions & 0 deletions itests/builddeps/maven/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.jbang.itests</groupId>
<artifactId>maven-example</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</project>
15 changes: 15 additions & 0 deletions itests/builddeps/maven/src/main/java/example/Greeter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package example;

import org.apache.commons.lang3.StringUtils;

/**
* Illustrative source belonging to the real Maven project. This class is
* compiled by {@code mvn compile}; it is not on the classpath of the
* accompanying JBang script — JBang only borrows the project's declared
* compile dependencies via {@code //DEPS pom.xml}.
*/
public class Greeter {
public static String greet(String name) {
return "Hello, " + StringUtils.capitalize(name) + "!";
}
}
1 change: 1 addition & 0 deletions itests/builddeps/mill/.mill-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.12.5
10 changes: 10 additions & 0 deletions itests/builddeps/mill/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS build.mill

import org.apache.commons.lang3.StringUtils;

public class App {
public static void main(String... args) {
System.out.println("mill-classpath:" + StringUtils.reverse("hello"));
}
}
9 changes: 9 additions & 0 deletions itests/builddeps/mill/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package build

import mill._, javalib._

object `package` extends RootModule with JavaModule {
def ivyDeps = Agg(
ivy"org.apache.commons:commons-lang3:3.14.0"
)
}
Loading
Loading