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
25 changes: 25 additions & 0 deletions docs/modules/ROOT/pages/running.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ 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 Maven or Gradle build file as a dependency. JBang asks that build tool for its runtime classpath and adds the returned entries.

In source:

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

Or from the command line:

[source, bash]
----
jbang run --deps pom.xml app.java
jbang run --deps ^pom.xml src/main/java/app.java
----

The `^` form searches upward from the script location, so `^pom.xml` means "use the nearest parent `pom.xml`".

NOTE: Maven and Gradle build files can execute code. Only use this with build files you trust.

== Preview

The example above can alternatively also be written with `PREVIEW` instead of `--enable-preview`, like this:
Expand Down
203 changes: 203 additions & 0 deletions src/main/java/dev/jbang/dependencies/BuildSystemClassPaths.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package dev.jbang.dependencies;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import dev.jbang.Cache;
import dev.jbang.Settings;
import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.ExitException;
import dev.jbang.util.Util;

public class BuildSystemClassPaths {

private static final List<String> BUILD_FILES = Arrays.asList("pom.xml", "build.gradle", "build.gradle.kts");

public static List<String> dependencies(List<String> dependencies) {
if (dependencies == null || dependencies.isEmpty()) {
return Collections.emptyList();
}
return dependencies.stream()
.filter(dep -> !isBuildFileDependency(dep))
.collect(Collectors.toList());
}

public static List<String> classPaths(List<String> dependencies, Path sourceFile) {
if (dependencies == null || dependencies.isEmpty()) {
return Collections.emptyList();
}
Path sourceDir = sourceFile != null && sourceFile.getParent() != null ? sourceFile.getParent() : Util.getCwd();
List<String> resolved = new ArrayList<>();
for (String dependency : dependencies) {
Path buildFile = toBuildFile(dependency, sourceDir);
if (buildFile != null) {
resolved.addAll(resolve(buildFile));
} else if (isBuildFileDependency(dependency)) {
throw new ExitException(BaseCommand.EXIT_INVALID_INPUT, "Build file not found: " + dependency);
}
}
return resolved;
}

public static boolean isBuildFileDependency(String dependency) {
if (dependency.startsWith("^")) {
return BUILD_FILES.contains(dependency.substring(1));
}
Path fileName = Paths.get(dependency).getFileName();
return fileName != null && BUILD_FILES.contains(fileName.toString());
}

private static Path toBuildFile(String dependency, Path sourceDir) {
if (dependency.startsWith("^")) {
String fileName = dependency.substring(1);
if (BUILD_FILES.contains(fileName)) {
return findNearest(sourceDir, fileName);
}
return null;
}
Path path = Paths.get(dependency);
if (!path.isAbsolute()) {
path = Util.getCwd().resolve(path);
}
path = path.normalize();
if (Files.isRegularFile(path) && BUILD_FILES.contains(path.getFileName().toString())) {
return path;
}
return null;
}

private static Path findNearest(Path dir, String fileName) {
Path cur = dir.toAbsolutePath().normalize();
while (cur != null && cur.startsWith(Settings.getLocalRootDir())) {
Path candidate = cur.resolve(fileName);
if (Files.isRegularFile(candidate)) {
return candidate;
}
cur = cur.getParent();
}
return null;
}

private static List<String> resolve(Path buildFile) {
try {
Util.infoMsg("Resolving classpath from " + buildFile);
Path cache = cacheFile(buildFile);
if (!Util.isFresh() && Files.isRegularFile(cache)) {
Util.verboseMsg("Using cached classpath from " + cache);
List<String> cached = split(Util.readString(cache));
Util.verboseMsg("Resolved classpath: " + String.join(File.pathSeparator, cached));
return cached;
}
Files.createDirectories(cache.getParent());
List<String> result;
if (buildFile.getFileName().toString().equals("pom.xml")) {
runMaven(buildFile, cache);
result = split(Util.readString(cache));
} else {
String cp = runGradle(buildFile);
Util.writeString(cache, cp);
result = split(cp);
}
Util.verboseMsg("Resolved classpath: " + String.join(File.pathSeparator, result));
return result;
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR,
"Could not resolve classpath from " + buildFile, e);
}
}

private static Path cacheFile(Path buildFile) throws IOException {
String stat = buildFile.toAbsolutePath() + ":" + Files.size(buildFile) + ":"
+ Files.getLastModifiedTime(buildFile).toMillis();
return Settings.getCacheDir(Cache.CacheClass.deps)
.resolve("build-classpath-" + Util.getStableID(stat) + ".txt");
}

private static void runMaven(Path pom, Path outputFile) throws IOException {
Path dir = pom.getParent();
List<String> command = new ArrayList<>();
command.add(mavenCommand(dir));
command.add("-q");
command.add("dependency:build-classpath");
command.add("-Dmdep.outputFile=" + outputFile.toAbsolutePath());
command.add("-Dmdep.pathSeparator=" + File.pathSeparator);
run(command, dir, false);
}

private static String runGradle(Path buildFile) throws IOException {
Path dir = buildFile.getParent();
Path init = Settings.getCacheDir(Cache.CacheClass.deps).resolve("jbang-classpath.gradle");
Util.writeString(init,
"allprojects { plugins.withId('java') { tasks.register('jbangPrintClasspath') { doLast { println(sourceSets.main.runtimeClasspath.asPath) } } } }\n");
List<String> command = new ArrayList<>();
command.add(gradleCommand(dir));
command.add("-q");
command.add("-I");
command.add(init.toString());
command.add("jbangPrintClasspath");
return run(command, dir, true);
}

private static String mavenCommand(Path dir) {
Path wrapper = dir.resolve(Util.isWindows() ? "mvnw.cmd" : "mvnw");
return Files.isRegularFile(wrapper) ? wrapper.toString() : "mvn";
}

private static String gradleCommand(Path dir) {
Path wrapper = dir.resolve(Util.isWindows() ? "gradlew.bat" : "gradlew");
return Files.isRegularFile(wrapper) ? wrapper.toString() : "gradle";
}

private static String run(List<String> command, Path dir, boolean captureOutput) throws IOException {
Util.verboseMsg("Build classpath: " + String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command).directory(dir.toFile());
if (!captureOutput) {
pb.inheritIO();
} else {
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
}
Process process = pb.start();
String output = "";
if (captureOutput) {
try (InputStream is = process.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
}
}
try {
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, e);
}
if (process.exitValue() != 0) {
throw new ExitException(process.exitValue(),
"Could not resolve build classpath using: " + String.join(" ", command));
}
return output;
}

private static List<String> split(String classPath) {
if (Util.isBlankString(classPath)) {
return Collections.emptyList();
}
return Arrays.stream(classPath.trim().split(java.util.regex.Pattern.quote(File.pathSeparator)))
.filter(p -> !p.isEmpty())
.collect(Collectors.toList());
}
}
25 changes: 19 additions & 6 deletions src/main/java/dev/jbang/source/ProjectBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import dev.jbang.catalog.Catalog;
import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.ExitException;
import dev.jbang.dependencies.BuildSystemClassPaths;
import dev.jbang.dependencies.DependencyResolver;
import dev.jbang.dependencies.DependencyUtil;
import dev.jbang.dependencies.Detector;
Expand Down Expand Up @@ -363,8 +364,13 @@ private Project createJbangProject(ResourceRef resourceRef) {
ResourceResolver resolver = getAliasResourceResolver(null);
ResourceResolver sibRes2 = getSiblingResolver(resourceRef, resolver);
for (String srcDep : directives.sourceDependencies()) {
ResourceRef subRef = resolver.resolve(srcDep, true);
prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
if (BuildSystemClassPaths.isBuildFileDependency(srcDep)) {
ss.addClassPaths(BuildSystemClassPaths.classPaths(
Collections.singletonList(srcDep), resourceRef.getFile()));
} else {
ResourceRef subRef = resolver.resolve(srcDep, true);
prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
}
}

boolean first = true;
Expand Down Expand Up @@ -487,7 +493,9 @@ private static void copyManifestAttribute(Attributes attrs, Project prj, String
private Project updateProject(Project prj) {
SourceSet ss = prj.getMainSourceSet();
prj.addRepositories(allToMavenRepo(replaceAllProps(additionalRepos)));
ss.addDependencies(replaceAllProps(additionalDeps));
List<String> deps = replaceAllProps(additionalDeps);
ss.addDependencies(BuildSystemClassPaths.dependencies(deps));
ss.addClassPaths(BuildSystemClassPaths.classPaths(deps, prj.getResourceRef().getFile()));
ss.addClassPaths(replaceAllProps(additionalClasspaths));
updateAllSources(prj, replaceAllProps(additionalSources));
ss.addResources(
Expand Down Expand Up @@ -682,8 +690,13 @@ private Project updateProject(Source src, Project prj, ResourceResolver resolver
}
}
for (String srcDep : src.collectSourceDependencies()) {
ResourceRef subRef = sibRes1.resolve(srcDep, true);
prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
if (BuildSystemClassPaths.isBuildFileDependency(srcDep)) {
ss.addClassPaths(BuildSystemClassPaths.classPaths(
Collections.singletonList(srcDep), srcRef.getFile()));
} else {
ResourceRef subRef = sibRes1.resolve(srcDep, true);
prj.addSubProject(new ProjectBuilder(buildRefs).build(subRef));
}
}
ResourceResolver sibRes2 = getSiblingResolver(srcRef, resolver);
List<Source> includedSources = allToSource(src.getDirectives().sources(), srcRef, sibRes2);
Expand Down Expand Up @@ -726,7 +739,7 @@ private ModularClassPath resolveDependency(String dep) {
.addDependency(dep)
.addRepositories(allToMavenRepo(
replaceAllProps(additionalRepos)))
.addDependencies(replaceAllProps(additionalDeps))
.addDependencies(BuildSystemClassPaths.dependencies(replaceAllProps(additionalDeps)))
.addClassPaths(
replaceAllProps(additionalClasspaths));
mcp = resolver.resolve();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dev.jbang.dependencies;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;

import org.junit.jupiter.api.Test;

import dev.jbang.BaseTest;
import dev.jbang.Settings;
import dev.jbang.source.Project;
import dev.jbang.util.Util;

public class TestBuildSystemClassPaths extends BaseTest {

@Test
void explicitPomClasspathUsesMavenWrapper() throws Exception {
assumeFalse(Util.isWindows());
Path dep = cwdDir.resolve("dep.jar");
Path pom = cwdDir.resolve("pom.xml");
Files.write(dep, new byte[0]);
Files.write(pom, Collections.singletonList("<project/>"));
writeMvnw(cwdDir, dep.toString());
Path src = cwdDir.resolve("main.java");
Files.write(src, Collections.singletonList("class main {}"));

Project prj = Project.builder()
.additionalDependencies(Collections.singletonList(pom.toString()))
.build(src);

assertThat(prj.getMainSourceSet().getClassPaths(), contains(dep.toString()));
}

@Test
void caretPomFindsNearestUpwardsFromSource() throws Exception {
assumeFalse(Util.isWindows());
Path project = cwdDir.resolve("project");
Path nested = project.resolve("src/main/java");
Files.createDirectories(nested);
Path dep = project.resolve("dep.jar");
Path pom = project.resolve("pom.xml");
Files.write(dep, new byte[0]);
Files.write(pom, Collections.singletonList("<project/>"));
writeMvnw(project, dep.toString());
Path src = nested.resolve("main.java");
Files.write(src, Collections.singletonList("class main {}"));

Project prj = Project.builder()
.additionalDependencies(Collections.singletonList("^pom.xml"))
.build(src);

assertThat(prj.getMainSourceSet().getClassPaths(), contains(dep.toString()));
}

@Test
void depsDirectiveWithPomXml() throws Exception {
assumeFalse(Util.isWindows());
Path dep = cwdDir.resolve("dep.jar");
Path pom = cwdDir.resolve("pom.xml");
Files.write(dep, new byte[0]);
Files.write(pom, Collections.singletonList("<project/>"));
writeMvnw(cwdDir, dep.toString());
Path src = cwdDir.resolve("main.java");
Files.write(src, Collections.singletonList("//DEPS pom.xml\nclass main {}"));

Project prj = Project.builder().build(src);

assertThat(prj.getMainSourceSet().getClassPaths(), contains(dep.toString()));
}

private static void writeMvnw(Path dir, String classpath) throws Exception {
Path mvnw = dir.resolve("mvnw");
Files.write(mvnw,
Collections
.singletonList("#!/bin/sh\nfor arg in \"$@\"; do case \"$arg\" in -Dmdep.outputFile=*) echo '"
+ classpath + "' > \"${arg#-Dmdep.outputFile=}\" ;; esac; done\n"));
mvnw.toFile().setExecutable(true);
Files.createDirectories(Settings.getCacheDir(dev.jbang.Cache.CacheClass.deps));
}
}
Loading