Compare commits

...

25 commits

Author SHA1 Message Date
Geert Bevin f2a568a654
Merge branch 'main' into main 2026-07-17 22:48:41 -04:00
Geert Bevin 09418fb979 Clarify that the MCP durationMs covers the whole tool call process
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-17 22:46:26 -04:00
Geert Bevin fcb348615e Add the mcp install argument that registers the server with MCP clients 2026-07-17 22:43:51 -04:00
Geert Bevin 124c7695e9 Add structured content with execution metadata to MCP tool results 2026-07-17 21:28:18 -04:00
Erik C. Thauvin 736372673e
Add tests coverage for local/localModule, Path/File overloads and addLocalJars filtering 2026-07-17 18:21:29 -07:00
Geert Bevin 025e68e9e6 Stream MCP tool call output as log message notifications 2026-07-17 20:27:20 -04:00
Geert Bevin 5083ecef78 Omit unconfigured values from the MCP project resource 2026-07-17 19:42:23 -04:00
Geert Bevin cdd4efa460 Fix javadoc typo and updated RIFE2/core 2026-07-17 17:47:09 -04:00
Geert Bevin 62c0d4569d Add experimental WIP MCP support that serves the build commands as tools 2026-07-17 17:28:29 -04:00
Geert Bevin ec185788a1 Warn when declared versions differ from applicable BOMs and only report in download 2026-07-17 17:18:33 -04:00
Geert Bevin 7d77f48aec Order inherited BOM precedence by the standard scope order
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-17 08:01:07 -04:00
Geert Bevin d7d176eef6 Give scope-local BOMs precedence and warn about conflicting BOM versions
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-16 14:34:27 -04:00
Geert Bevin c9298d3de6 Apply BOMs across scopes like classpaths and harden overrides and publishing
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-16 00:49:46 -04:00
Geert Bevin 49e21cfeb8 Updated RIFE2/core 2026-07-15 23:39:29 -04:00
Geert Bevin f6aeaa090d Emit dependency management in published POMs and freeze uncovered versions
Some checks failed
bld-ci / build (./bld, 17, macos-latest, false) (push) Has been cancelled
bld-ci / build (./bld, 21, macos-latest, false) (push) Has been cancelled
bld-ci / build (./bld, 25, macos-latest, false) (push) Has been cancelled
bld-ci / build (./bld, 26, macos-latest, false) (push) Has been cancelled
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Has been cancelled
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Has been cancelled
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Has been cancelled
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Has been cancelled
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Has been cancelled
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Has been cancelled
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Has been cancelled
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Has been cancelled
javadocs-pages / deploy (push) Has been cancelled
2026-07-14 21:52:48 -04:00
Geert Bevin 734000e6dd Fix BOM loss in dependency set copies and warn about uncovered dependencies 2026-07-14 21:52:44 -04:00
Geert Bevin 2bfcc1bb78 Add bill of materials support for dependency version management
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-13 16:13:05 -04:00
Geert Bevin d307e06a5d Updated wrapper to bld 2.4.0-SNAPSHOT 2026-07-13 16:12:47 -04:00
Geert Bevin 734ac585ab Announce dependency analysis before resolution starts
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-13 01:26:02 -04:00
Geert Bevin 27c45033fb Bump @since tags to 2.4.0 for the next release 2026-07-12 23:05:15 -04:00
Geert Bevin b2987b6b23 Updated version to 2.4.0-SNAPSHOT 2026-07-12 23:02:44 -04:00
Geert Bevin 1e76ee31c1 Batch artifact transfers across scopes in the download operation 2026-07-12 23:00:54 -04:00
Geert Bevin 4447610d02 Parallelize update checks and wrapper extension resolution through new ParallelDependencyResolver 2026-07-12 22:14:37 -04:00
Geert Bevin f3f4799190 Parallelize transitive dependency resolution with speculative POM prefetching 2026-07-12 21:33:49 -04:00
Geert Bevin 5859a07a4c Parallelize artifact transfers with live console progress 2026-07-12 12:18:35 -04:00
47 changed files with 7596 additions and 176 deletions

2
core

@ -1 +1 @@
Subproject commit 5f977636531d1b978b762a615d720097baa37830 Subproject commit fb2e5022e5c1d125e45227b49076c2b200763986

View file

@ -9,4 +9,4 @@ bld.javaOptions=
bld.javacOptions= bld.javacOptions=
bld.repositories=MAVEN_CENTRAL,RIFE2_RELEASES,RIFE2_SNAPSHOTS bld.repositories=MAVEN_CENTRAL,RIFE2_RELEASES,RIFE2_SNAPSHOTS
bld.sourceDirectories=core/src/bld/java bld.sourceDirectories=core/src/bld/java
bld.version=2.3.1-SNAPSHOT bld.version=2.4.0-SNAPSHOT

View file

@ -31,7 +31,7 @@ public class BaseProject extends BuildExecutor {
/** /**
* The CLI option to trigger automatic dependency download and purge. * The CLI option to trigger automatic dependency download and purge.
* *
* @since 2.3.1 * @since 2.4.0
*/ */
public static final String AUTO_DOWNLOAD_PURGE_OPTION = "--auto-download-purge"; public static final String AUTO_DOWNLOAD_PURGE_OPTION = "--auto-download-purge";
@ -395,6 +395,7 @@ public class BaseProject extends BuildExecutor {
private final CompileOperation compileOperation_ = new CompileOperation(); private final CompileOperation compileOperation_ = new CompileOperation();
private final DependencyTreeOperation dependencyTreeOperation_ = new DependencyTreeOperation(); private final DependencyTreeOperation dependencyTreeOperation_ = new DependencyTreeOperation();
private final DownloadOperation downloadOperation_ = new DownloadOperation(); private final DownloadOperation downloadOperation_ = new DownloadOperation();
private final McpOperation mcpOperation_ = new McpOperation();
private final PurgeOperation purgeOperation_ = new PurgeOperation(); private final PurgeOperation purgeOperation_ = new PurgeOperation();
private final PublishOperation publishOperation_ = new PublishOperation(); private final PublishOperation publishOperation_ = new PublishOperation();
private final RunOperation runOperation_ = new RunOperation(); private final RunOperation runOperation_ = new RunOperation();
@ -442,6 +443,16 @@ public class BaseProject extends BuildExecutor {
return downloadOperation_; return downloadOperation_;
} }
/**
* Retrieves the project's default MCP operation.
*
* @return the default MCP operation instance
* @since 2.4.0
*/
public McpOperation mcpOperation() {
return mcpOperation_;
}
/** /**
* Retrieves the project's default publish operation. * Retrieves the project's default publish operation.
* *
@ -546,6 +557,18 @@ public class BaseProject extends BuildExecutor {
downloadOperation().executeOnce(() -> downloadOperation().fromProject(this)); downloadOperation().executeOnce(() -> downloadOperation().fromProject(this));
} }
/**
* Standard build command, starts an MCP server that exposes the build
* commands as tools.
*
* @since 2.4.0
*/
@BuildCommand(help = McpHelp.class)
public void mcp()
throws Exception {
mcpOperation().executeOnce(() -> mcpOperation().fromProject(this));
}
/** /**
* Standard build command, purges all unused artifacts from the project. * Standard build command, purges all unused artifacts from the project.
* *
@ -910,8 +933,7 @@ public class BaseProject extends BuildExecutor {
*/ */
public LocalDependency local(String path) { public LocalDependency local(String path) {
return new LocalDependency(path); return new LocalDependency(path);
} /** }
/** /**
* Creates a local dependency instance. * Creates a local dependency instance.
@ -920,8 +942,9 @@ public class BaseProject extends BuildExecutor {
* *
* @param path the file system path (absolute or relative to the {@link #workDirectory}) * @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local dependency * of the local dependency
* @since 2.3.1 * @since 2.4.0
*/ */
public LocalDependency local(Path path) { public LocalDependency local(Path path) {
return new LocalDependency(path.toString()); return new LocalDependency(path.toString());
} }
@ -931,7 +954,7 @@ public class BaseProject extends BuildExecutor {
* If the local dependency points to a directory, it will be scanned for jar files. * If the local dependency points to a directory, it will be scanned for jar files.
* *
* @param path the file system path of the local dependency * @param path the file system path of the local dependency
* @since 2.3.1 * @since 2.4.0
*/ */
public LocalDependency local(File path) { public LocalDependency local(File path) {
return new LocalDependency(path.getAbsolutePath()); return new LocalDependency(path.getAbsolutePath());
@ -1019,6 +1042,62 @@ public class BaseProject extends BuildExecutor {
return Module.parse(description); return Module.parse(description);
} }
/**
* Creates a new bill of materials instance.
*
* @param groupId the BOM group identifier
* @param artifactId the BOM artifact identifier
* @return a newly created {@code Bom} instance
* @since 2.4.0
*/
public Bom bom(String groupId, String artifactId) {
return new Bom(groupId, artifactId);
}
/**
* Creates a new bill of materials instance.
*
* @param groupId the BOM group identifier
* @param artifactId the BOM artifact identifier
* @param version the BOM version
* @return a newly created {@code Bom} instance
* @since 2.4.0
*/
public Bom bom(String groupId, String artifactId, String version) {
return new Bom(groupId, artifactId, version(version));
}
/**
* Creates a new bill of materials instance.
*
* @param groupId the BOM group identifier
* @param artifactId the BOM artifact identifier
* @param version the BOM version
* @return a newly created {@code Bom} instance
* @since 2.4.0
*/
public Bom bom(String groupId, String artifactId, Version version) {
return new Bom(groupId, artifactId, version);
}
/**
* Creates a new bill of materials instance from a string representation.
* The format is {@code groupId:artifactId:version}.
* The {@code version} is optional, and an optional {@code @bom} or
* {@code @pom} type suffix is accepted. BOMs can't have classifiers,
* strings that contain one are rejected.
* <p>
* If the string can't be successfully parsed, {@code null} will be returned.
*
* @param description the BOM string to parse
* @return a parsed instance of {@code Bom}; or
* {@code null} when the string couldn't be parsed
* @since 2.4.0
*/
public Bom bom(String description) {
return Bom.parse(description);
}
/** /**
* Creates a local module instance. * Creates a local module instance.
* <p> * <p>
@ -1039,7 +1118,7 @@ public class BaseProject extends BuildExecutor {
* *
* @param path the file system path (absolute or relative to the {@link #workDirectory}) * @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local module * of the local module
* @since 2.3.1 * @since 2.4.0
*/ */
public LocalModule localModule(Path path) { public LocalModule localModule(Path path) {
return new LocalModule(path.toString()); return new LocalModule(path.toString());
@ -1051,7 +1130,7 @@ public class BaseProject extends BuildExecutor {
* If the local module points to a directory, it will be scanned for jar files. * If the local module points to a directory, it will be scanned for jar files.
* *
* @param path the file system path of the local module * @param path the file system path of the local module
* @since 2.3.1 * @since 2.4.0
*/ */
public LocalModule localModule(File path) { public LocalModule localModule(File path) {
return new LocalModule(path.getAbsolutePath()); return new LocalModule(path.getAbsolutePath());
@ -2062,6 +2141,22 @@ public class BaseProject extends BuildExecutor {
purge(); purge();
} }
/**
* Performs the automatic download and purge of the project
* dependencies when it's enabled through {@link #autoDownloadPurge()}
* and the project isn't offline.
* <p>
* The dependencies are only refreshed when the dependency cache is
* stale, so that repeated invocations are cheap.
*
* @since 2.4.0
*/
public void performAutoDownloadPurgeIfEnabled() {
if (!offline() && autoDownloadPurge()) {
performAutoDownloadPurge();
}
}
private void performAutoDownloadPurge() { private void performAutoDownloadPurge() {
var resolution = new VersionResolution(properties()); var resolution = new VersionResolution(properties());
var cache = new BldCache(libBldDirectory(), resolution); var cache = new BldCache(libBldDirectory(), resolution);
@ -2085,6 +2180,12 @@ public class BaseProject extends BuildExecutor {
var remainingArguments = new ArrayList<>(List.of(arguments)); var remainingArguments = new ArrayList<>(List.of(arguments));
var auto = remainingArguments.remove(AUTO_DOWNLOAD_PURGE_OPTION); var auto = remainingArguments.remove(AUTO_DOWNLOAD_PURGE_OPTION);
if (remainingArguments.contains(BuildExecutor.ARG_USE_STDERR)) {
// the build output is sent to standard error so that standard
// output stays free for the MCP protocol, done before the
// automatic download and purge produces any output
System.setOut(System.err);
}
if (!offline() && if (!offline() &&
(autoDownloadPurge() || auto)) { (autoDownloadPurge() || auto)) {
performAutoDownloadPurge(); performAutoDownloadPurge();

View file

@ -271,6 +271,10 @@ public class BldCache {
finger_print.append(entry.getKey()); finger_print.append(entry.getKey());
finger_print.append('\n'); finger_print.append('\n');
if (entry.getValue() != null) { if (entry.getValue() != null) {
for (var bom : entry.getValue().boms()) {
finger_print.append(bom.toString());
finger_print.append('\n');
}
for (var dependency : entry.getValue()) { for (var dependency : entry.getValue()) {
finger_print.append(dependency.toString()); finger_print.append(dependency.toString());
finger_print.append('\n'); finger_print.append('\n');

View file

@ -32,6 +32,8 @@ public class BuildExecutor {
public static final String LOCAL_PROPERTIES = "local.properties"; public static final String LOCAL_PROPERTIES = "local.properties";
private static final String ARG_OFFLINE = "--offline"; private static final String ARG_OFFLINE = "--offline";
static final String ARG_USE_STDERR = "--use-stderr";
private static final String ARG_HELP1 = "--help"; private static final String ARG_HELP1 = "--help";
private static final String ARG_HELP2 = "-h"; private static final String ARG_HELP2 = "-h";
private static final String ARG_HELP3 = "-?"; private static final String ARG_HELP3 = "-?";
@ -41,7 +43,7 @@ public class BuildExecutor {
private static final String ARG_VERBOSE2 = "-v"; private static final String ARG_VERBOSE2 = "-v";
private final HierarchicalProperties properties_; private final HierarchicalProperties properties_;
private List<String> arguments_ = Collections.emptyList(); private List<String> arguments_ = new ArrayList<>();
private boolean offline_ = false; private boolean offline_ = false;
private boolean verbose_ = false; private boolean verbose_ = false;
private Map<String, CommandDefinition> buildCommands_ = null; private Map<String, CommandDefinition> buildCommands_ = null;
@ -138,18 +140,65 @@ public class BuildExecutor {
return offline_; return offline_;
} }
/**
* Changes whether the bld execution is intended to be offline.
*
* @param offline {@code true} if the execution is intended to be offline;
* or {@code false} otherwise
* @since 2.4.0
*/
public void offline(boolean offline) {
offline_ = offline;
}
/** /**
* Returns whether the bld execution should output detailed information * Returns whether the bld execution should output detailed information
* about the operations it performs. * about the operations it performs.
* *
* @return {@code true} if the execution is verbose; * @return {@code true} if the execution is verbose;
* or {@code false} otherwise * or {@code false} otherwise
* @since 2.3.1 * @since 2.4.0
*/ */
public boolean verbose() { public boolean verbose() {
return verbose_; return verbose_;
} }
/**
* Changes whether the bld execution should output detailed information
* about the operations it performs.
*
* @param verbose {@code true} if the execution is verbose;
* or {@code false} otherwise
* @since 2.4.0
*/
public void verbose(boolean verbose) {
verbose_ = verbose;
}
/**
* Returns whether the bld execution prints out the stacktrace for
* exceptions.
*
* @return {@code true} if the stacktrace is printed;
* or {@code false} otherwise
* @since 2.4.0
*/
public boolean showStacktrace() {
return showStacktrace;
}
/**
* Changes whether the bld execution prints out the stacktrace for
* exceptions.
*
* @param showStacktrace {@code true} if the stacktrace is printed;
* or {@code false} otherwise
* @since 2.4.0
*/
public void showStacktrace(boolean showStacktrace) {
this.showStacktrace = showStacktrace;
}
/** /**
* Returns the properties uses for bld execution. * Returns the properties uses for bld execution.
* *
@ -243,12 +292,19 @@ public class BuildExecutor {
show_help |= arguments_.removeAll(List.of(ARG_HELP1, ARG_HELP2, ARG_HELP3)); show_help |= arguments_.removeAll(List.of(ARG_HELP1, ARG_HELP2, ARG_HELP3));
showStacktrace = arguments_.removeAll(List.of(ARG_STACKTRACE1, ARG_STACKTRACE2)); showStacktrace = arguments_.removeAll(List.of(ARG_STACKTRACE1, ARG_STACKTRACE2));
verbose_ = arguments_.removeAll(List.of(ARG_VERBOSE1, ARG_VERBOSE2)); verbose_ = arguments_.removeAll(List.of(ARG_VERBOSE1, ARG_VERBOSE2));
if (arguments_.removeAll(List.of(ARG_USE_STDERR))) {
// the build output is sent to standard error so that standard
// output stays free for the MCP protocol, which the MCP server
// relies on to keep its protocol stream clean
System.setOut(System.err);
}
if (show_help) { if (show_help) {
new HelpOperation(this, Collections.emptyList()).execute(); new HelpOperation(this, Collections.emptyList()).execute();
return exitStatus_; return exitStatus_;
} }
else if (arguments_.isEmpty()) {
if (arguments_.isEmpty()) {
showBldHelp(); showBldHelp();
return exitStatus_; return exitStatus_;
} }
@ -441,47 +497,14 @@ public class BuildExecutor {
*/ */
public boolean executeCommand(String command) public boolean executeCommand(String command)
throws Throwable { throws Throwable {
var matched_command = command; var matched_command = resolveCommand(command);
var definition = buildCommands().get(command); CommandDefinition definition = null;
if (matched_command != null) {
// try to find an alias if (!matched_command.equals(command) &&
if (definition == null) { !matched_command.equals(buildAliases().get(command))) {
var aliased_command = buildAliases().get(command);
if (aliased_command != null) {
matched_command = aliased_command;
definition = buildCommands().get(aliased_command);
}
}
// try to find a match for the provided command amongst
// the ones that are known
if (definition == null) {
// try to find starting matching options
var matches = new ArrayList<>(buildCommands().keySet().stream()
.filter(c -> c.toLowerCase().startsWith(command.toLowerCase()))
.toList());
if (matches.isEmpty()) {
// try to find fuzzy matching options
var fuzzy_regexp = new StringBuilder("^.*");
for (var ch : command.toCharArray()) {
fuzzy_regexp.append("\\Q");
fuzzy_regexp.append(ch);
fuzzy_regexp.append("\\E.*");
}
fuzzy_regexp.append('$');
var fuzzy_pattern = Pattern.compile(fuzzy_regexp.toString());
matches.addAll(buildCommands().keySet().stream()
.filter(c -> fuzzy_pattern.matcher(c.toLowerCase()).matches())
.toList());
}
// only proceed if exactly one match was found
if (matches.size() == 1) {
matched_command = matches.get(0);
System.out.println("Executing matched command: " + matched_command); System.out.println("Executing matched command: " + matched_command);
definition = buildCommands().get(matched_command);
} }
definition = buildCommands().get(matched_command);
} }
// execute the command if we found one // execute the command if we found one
@ -507,6 +530,54 @@ public class BuildExecutor {
return true; return true;
} }
/**
* Resolves a command name to the name of the build command that would
* be executed for it, taking the command aliases, unique name prefixes
* and unique fuzzy matches into account.
*
* @param command the command name to resolve
* @return the name of the build command that would be executed; or
* {@code null} when the name couldn't be resolved
* @since 2.4.0
*/
public String resolveCommand(String command) {
if (buildCommands().containsKey(command)) {
return command;
}
// try to find an alias
var aliased_command = buildAliases().get(command);
if (aliased_command != null) {
return aliased_command;
}
// try to find starting matching options
var matches = new ArrayList<>(buildCommands().keySet().stream()
.filter(c -> c.toLowerCase().startsWith(command.toLowerCase()))
.toList());
if (matches.isEmpty()) {
// try to find fuzzy matching options
var fuzzy_regexp = new StringBuilder("^.*");
for (var ch : command.toCharArray()) {
fuzzy_regexp.append("\\Q");
fuzzy_regexp.append(ch);
fuzzy_regexp.append("\\E.*");
}
fuzzy_regexp.append('$');
var fuzzy_pattern = Pattern.compile(fuzzy_regexp.toString());
matches.addAll(buildCommands().keySet().stream()
.filter(c -> fuzzy_pattern.matcher(c.toLowerCase()).matches())
.toList());
}
// only resolve if exactly one match was found
if (matches.size() == 1) {
return matches.get(0);
}
return null;
}
private void showBldHelp() { private void showBldHelp() {
var help = new HelpOperation(this, arguments()); var help = new HelpOperation(this, arguments());
help.executePrintWelcome(); help.executePrintWelcome();

View file

@ -19,6 +19,7 @@ import java.net.URLConnection;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static rife.tools.HttpUtils.HEADER_AUTHORIZATION; import static rife.tools.HttpUtils.HEADER_AUTHORIZATION;
import static rife.tools.HttpUtils.basicAuthorizationHeader; import static rife.tools.HttpUtils.basicAuthorizationHeader;
@ -35,6 +36,8 @@ import static rife.tools.StringUtils.encodeHexLower;
* @since 1.5.18 * @since 1.5.18
*/ */
public abstract class ArtifactRetriever { public abstract class ArtifactRetriever {
private static final int TRANSFER_CHUNK_SIZE = 128 * 1024;
private final static ArtifactRetriever UNCACHED = new ArtifactRetriever() { private final static ArtifactRetriever UNCACHED = new ArtifactRetriever() {
String getCached(RepositoryArtifact artifact) { String getCached(RepositoryArtifact artifact) {
return null; return null;
@ -42,6 +45,10 @@ public abstract class ArtifactRetriever {
void cache(RepositoryArtifact artifact, String content) { void cache(RepositoryArtifact artifact, String content) {
} }
boolean isCaching() {
return false;
}
}; };
/** /**
@ -63,7 +70,7 @@ public abstract class ArtifactRetriever {
*/ */
public static ArtifactRetriever cachingInstance() { public static ArtifactRetriever cachingInstance() {
return new ArtifactRetriever() { return new ArtifactRetriever() {
private final Map<RepositoryArtifact, String> artifactCache = new HashMap<>(); private final Map<RepositoryArtifact, String> artifactCache = new ConcurrentHashMap<>();
String getCached(RepositoryArtifact artifact) { String getCached(RepositoryArtifact artifact) {
return artifactCache.get(artifact); return artifactCache.get(artifact);
@ -73,6 +80,9 @@ public abstract class ArtifactRetriever {
artifactCache.put(artifact, content); artifactCache.put(artifact, content);
} }
boolean isCaching() {
return true;
}
}; };
} }
@ -83,6 +93,8 @@ public abstract class ArtifactRetriever {
abstract void cache(RepositoryArtifact artifact, String content); abstract void cache(RepositoryArtifact artifact, String content);
abstract boolean isCaching();
/** /**
* Reads the contents of an artifact as a string. * Reads the contents of an artifact as a string.
* *
@ -141,17 +153,17 @@ public abstract class ArtifactRetriever {
var download_filename = artifact.location().substring(artifact.location().lastIndexOf("/") + 1); var download_filename = artifact.location().substring(artifact.location().lastIndexOf("/") + 1);
var download_file = new File(directory, download_filename); var download_file = new File(directory, download_filename);
System.out.print("Downloading: " + artifact.location() + " ... "); var transfer = TransferOutput.instance().start(artifact.location());
System.out.flush(); var status = "";
try { try {
if (artifact.repository().isLocal()) { if (artifact.repository().isLocal()) {
var source = new File(artifact.location()); var source = new File(artifact.location());
if (source.exists()) { if (source.exists()) {
FileUtils.copy(source, download_file); FileUtils.copy(source, download_file);
System.out.print("done"); status = "done";
return true; return true;
} else { } else {
System.out.print("not found"); status = "not found";
return false; return false;
} }
} else { } else {
@ -159,7 +171,7 @@ public abstract class ArtifactRetriever {
if (download_file.exists() && download_file.canRead()) { if (download_file.exists() && download_file.canRead()) {
if (checkHash(artifact, download_file, ".sha256", "SHA-256") || if (checkHash(artifact, download_file, ".sha256", "SHA-256") ||
checkHash(artifact, download_file, ".md5", "MD5")) { checkHash(artifact, download_file, ".md5", "MD5")) {
System.out.print("exists"); status = "exists";
return true; return true;
} }
} }
@ -170,23 +182,29 @@ public abstract class ArtifactRetriever {
HEADER_AUTHORIZATION, HEADER_AUTHORIZATION,
basicAuthorizationHeader(artifact.repository().username(), artifact.repository().password())); basicAuthorizationHeader(artifact.repository().username(), artifact.repository().password()));
} }
var content_length = connection.getContentLengthLong();
try (var input_stream = connection.getInputStream()) { try (var input_stream = connection.getInputStream()) {
var readableByteChannel = Channels.newChannel(input_stream); var readableByteChannel = Channels.newChannel(input_stream);
try (var fileOutputStream = new FileOutputStream(download_file)) { try (var fileOutputStream = new FileOutputStream(download_file)) {
var fileChannel = fileOutputStream.getChannel(); var fileChannel = fileOutputStream.getChannel();
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE); var position = 0L;
long transferred;
while ((transferred = fileChannel.transferFrom(readableByteChannel, position, TRANSFER_CHUNK_SIZE)) > 0) {
position += transferred;
transfer.progress(position, content_length);
}
System.out.print("done"); status = "done";
return true; return true;
} }
} }
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
System.out.print("not found"); status = "not found";
return false; return false;
} }
} }
} finally { } finally {
System.out.println(); transfer.finish(status);
} }
} }

View file

@ -0,0 +1,66 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import java.util.regex.Pattern;
/**
* Contains the information required to describe a bill of materials (BOM)
* in the build system.
* <p>
* A BOM is a POM whose dependency management section supplies versions
* during dependency resolution: dependencies that are declared without a
* version take the BOM's version, and transitive dependencies that match a
* BOM entry are pinned to the BOM's version. Versions that are explicitly
* declared in the build file, and versions provided through the
* {@code bld.override} property, always take precedence over a BOM.
* <p>
* BOMs don't transfer any artifacts, they only participate in resolution.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
public class Bom extends Dependency {
public Bom(String groupId, String artifactId) {
this(groupId, artifactId, null);
}
public Bom(String groupId, String artifactId, Version version) {
super(groupId, artifactId, version, null, TYPE_BOM);
}
private static final Pattern BOM_PATTERN = Pattern.compile("^(?<groupId>[^:@]+):(?<artifactId>[^:@]+)(?::(?<version>[^:@]+))?(?:@(?:bom|pom))?$");
/**
* Parses a BOM from a string representation.
* The format is {@code groupId:artifactId:version}.
* The {@code version} is optional, and an optional {@code @bom} or
* {@code @pom} type suffix is accepted. BOMs can't have classifiers,
* strings that contain one are rejected.
* <p>
* If the string can't be successfully parsed, {@code null} will be returned.
*
* @param bom the BOM string to parse
* @return a parsed instance of {@code Bom}; or
* {@code null} when the string couldn't be parsed
* @since 2.4.0
*/
public static Bom parse(String bom) {
if (bom == null || bom.isEmpty()) {
return null;
}
var matcher = BOM_PATTERN.matcher(bom);
if (!matcher.matches()) {
return null;
}
var groupId = matcher.group("groupId");
var artifactId = matcher.group("artifactId");
var version = Version.parse(matcher.group("version"));
return new Bom(groupId, artifactId, version);
}
}

View file

@ -40,6 +40,14 @@ public class Dependency {
// see https://github.com/apache/maven/blob/maven-4.0.0-beta-3/api/maven-api-core/src/main/java/org/apache/maven/api/Type.java // see https://github.com/apache/maven/blob/maven-4.0.0-beta-3/api/maven-api-core/src/main/java/org/apache/maven/api/Type.java
public static final String TYPE_MODULAR_JAR = "modular-jar"; public static final String TYPE_MODULAR_JAR = "modular-jar";
/**
* The dependency type name for a bill of materials POM whose
* dependency management supplies versions during resolution.
*
* @since 2.4.0
*/
public static final String TYPE_BOM = "bom";
private final String groupId_; private final String groupId_;
private final String artifactId_; private final String artifactId_;
private final Version version_; private final Version version_;

View file

@ -102,7 +102,7 @@ public class DependencyResolver {
var pom_dependencies = getMavenPom(dependency_).getDependencies(scopes); var pom_dependencies = getMavenPom(dependency_).getDependencies(scopes);
var result = new DependencySet(); var result = new DependencySet();
for (var dependency : pom_dependencies) { for (var dependency : pom_dependencies) {
result.add(resolution_.overrideDependency(dependency.convertToDependency())); result.add(resolution_.overrideTransitiveDependency(dependency.convertToDependency()));
} }
return result; return result;
} }
@ -121,8 +121,17 @@ public class DependencyResolver {
* @since 1.5 * @since 1.5
*/ */
public DependencySet getAllDependencies(Scope... scopes) { public DependencySet getAllDependencies(Scope... scopes) {
var prefetcher = new PomPrefetcher(resolution_, retriever_, repositories_);
try {
return getAllDependencies(prefetcher, scopes);
} finally {
prefetcher.shutdown();
}
}
DependencySet getAllDependencies(PomPrefetcher prefetcher, Scope... scopes) {
var result = new DependencySet(); var result = new DependencySet();
var overridden = resolution_.overrideDependency(dependency_); var overridden = resolution_.overrideDeclaredDependency(dependency_);
result.add(overridden); result.add(overridden);
var dependency_queue = new ArrayList<PomDependency>(); var dependency_queue = new ArrayList<PomDependency>();
@ -138,8 +147,11 @@ public class DependencyResolver {
next_dependencies.removeIf(it -> matchesExclusions(exclusion_context, it)); next_dependencies.removeIf(it -> matchesExclusions(exclusion_context, it));
// add all next dependencies to the queue // add all next dependencies to the queue
dependency_queue.addAll(next_dependencies); dependency_queue.addAll(next_dependencies);
// speculatively retrieve the POMs of the queued dependencies in
// parallel so that they are cached when they're processed in order
prefetcher.prefetch(next_dependencies);
// unless we find a next set of dependencies to add, stop resolving // unless we find the next set of dependencies to add, stop resolving
parent = null; parent = null;
next_dependencies = null; next_dependencies = null;
@ -147,7 +159,7 @@ public class DependencyResolver {
// part of the results yet // part of the results yet
while (!dependency_queue.isEmpty()) { while (!dependency_queue.isEmpty()) {
var candidate = dependency_queue.remove(0); var candidate = dependency_queue.remove(0);
var dependency = resolution_.overrideDependency(candidate.convertToDependency()); var dependency = resolution_.overrideTransitiveDependency(candidate.convertToDependency());
if (!result.contains(dependency)) { if (!result.contains(dependency)) {
result.add(dependency); result.add(dependency);

View file

@ -6,6 +6,8 @@ package rife.bld.dependencies;
import rife.ioc.HierarchicalProperties; import rife.ioc.HierarchicalProperties;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -37,7 +39,8 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
} }
/** /**
* Includes all the dependencies from another dependency scope map. * Includes all the dependencies, local dependencies, local modules and
* BOMs from another dependency scope map.
* *
* @param other the other map to include dependencies from * @param other the other map to include dependencies from
* @since 1.5 * @since 1.5
@ -49,7 +52,7 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
dependencies = new DependencySet(); dependencies = new DependencySet();
put(entry.getKey(), dependencies); put(entry.getKey(), dependencies);
} }
dependencies.addAll(entry.getValue()); dependencies.include(entry.getValue());
} }
} }
@ -65,6 +68,150 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
return computeIfAbsent(scope, k -> new DependencySet()); return computeIfAbsent(scope, k -> new DependencySet());
} }
/**
* Returns the BOMs that apply to the version resolution of a particular
* scope.
* <p>
* BOMs follow the same scope composition as the classpaths: the
* {@code compile} scope BOMs also apply to the {@code provided},
* {@code runtime} and {@code test} scopes, and the {@code runtime} and
* {@code provided} scope BOMs also apply to the {@code test} scope.
* The BOMs are returned with the scope's own BOMs first, followed by
* the inherited ones in the standard scope order, for the {@code test}
* scope that is {@code test}, {@code compile}, {@code provided},
* {@code runtime}. This order determines their precedence when several
* manage the same dependency: a BOM declared in the scope where a
* dependency is used takes precedence over a BOM that is inherited from
* another scope, and among the inherited BOMs the more fundamental
* scope wins.
* <p>
* The {@code standalone} scope only uses its own BOMs, and its BOMs
* deliberately never apply to any other scope.
*
* @param scope the scope to retrieve the applicable BOMs for
* @return the BOMs that apply to the scope's version resolution
* @since 2.4.0
*/
public List<Bom> effectiveBoms(Scope scope) {
var boms = new ArrayList<Bom>();
for (var bom_scope : bomScopes(scope)) {
var scoped_dependencies = get(bom_scope);
if (scoped_dependencies != null) {
boms.addAll(scoped_dependencies.boms());
}
}
return boms;
}
private static Scope[] bomScopes(Scope scope) {
return switch (scope) {
case provided -> new Scope[]{Scope.provided, Scope.compile};
case runtime -> new Scope[]{Scope.runtime, Scope.compile};
case test -> new Scope[]{Scope.test, Scope.compile, Scope.provided, Scope.runtime};
default -> new Scope[]{scope};
};
}
/**
* Returns the version-less dependencies that are not covered by the
* BOMs that apply to their scope.
* <p>
* Scopes that no BOMs apply to are not considered, their
* version-less dependencies simply resolve to the latest version.
* Dependencies whose version is supplied by a {@code bld.override}
* property are not reported either.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the BOM resolution
* @return the version-less dependencies that no applicable BOM covers
* @since 2.4.0
*/
public List<Dependency> versionlessDependenciesWithoutBom(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
var result = new ArrayList<Dependency>();
for (var entry : entrySet()) {
var effective_boms = effectiveBoms(entry.getKey());
if (effective_boms.isEmpty()) {
continue;
}
var resolution = new VersionResolution(properties, retriever, repositories, effective_boms);
for (var dependency : entry.getValue()) {
if (dependency.version().equals(VersionNumber.UNKNOWN) &&
!resolution.coversDependency(dependency) &&
!resolution.versionOverrides().containsKey(dependency.toArtifactString()) &&
!result.contains(dependency)) {
result.add(dependency);
}
}
}
return result;
}
/**
* Returns the version conflicts between the BOMs that apply to the
* scopes, where more than one applicable BOM manages the same
* dependency at a different version.
* <p>
* Each conflict is reported once, the versions are keyed by their BOM
* in precedence order so that the first is the version that is used.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the BOM resolution
* @return the version conflicts between the applicable BOMs
* @since 2.4.0
*/
public List<VersionResolution.BomVersionConflict> bomVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
var result = new ArrayList<VersionResolution.BomVersionConflict>();
var seen = new HashSet<String>();
for (var scope : keySet()) {
var effective_boms = effectiveBoms(scope);
if (effective_boms.size() < 2) {
continue;
}
for (var conflict : VersionResolution.resolveBomVersionConflicts(properties, retriever, repositories, effective_boms)) {
if (seen.add(conflict.dependency() + conflict.bomVersions())) {
result.add(conflict);
}
}
}
return result;
}
/**
* Returns the declared dependencies whose explicit version differs
* from the version that a BOM applying to their scope manages them at.
* <p>
* The declared version is used for the dependency itself, its
* transitive dependencies still resolve to the versions that the BOMs
* manage. Dependencies whose version is supplied by a
* {@code bld.override} property are not reported. Each difference is
* reported once.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the BOM resolution
* @return the version differences between the declared dependencies
* and the applicable BOMs
* @since 2.4.0
*/
public List<VersionResolution.DeclaredVersionConflict> declaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
var result = new ArrayList<VersionResolution.DeclaredVersionConflict>();
var seen = new HashSet<String>();
for (var entry : entrySet()) {
var effective_boms = effectiveBoms(entry.getKey());
if (effective_boms.isEmpty()) {
continue;
}
for (var conflict : VersionResolution.resolveDeclaredVersionConflicts(properties, retriever, repositories, effective_boms, entry.getValue())) {
if (seen.add(conflict.dependency() + conflict.declaredVersion() + conflict.bom() + conflict.bomVersion())) {
result.add(conflict);
}
}
}
return result;
}
/** /**
* Returns the transitive set of dependencies that would be used for the compile scope in a project. * Returns the transitive set of dependencies that would be used for the compile scope in a project.
* *
@ -76,6 +223,7 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
*/ */
public DependencySet resolveCompileDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) { public DependencySet resolveCompileDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
return resolveScopedDependencies(properties, retriever, repositories, return resolveScopedDependencies(properties, retriever, repositories,
Scope.compile,
new Scope[]{Scope.compile}, new Scope[]{Scope.compile},
new Scope[]{Scope.compile}, new Scope[]{Scope.compile},
null); null);
@ -92,6 +240,7 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
*/ */
public DependencySet resolveProvidedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) { public DependencySet resolveProvidedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
return resolveScopedDependencies(properties, retriever, repositories, return resolveScopedDependencies(properties, retriever, repositories,
Scope.provided,
new Scope[]{Scope.provided}, new Scope[]{Scope.provided},
new Scope[]{Scope.compile, Scope.runtime}, new Scope[]{Scope.compile, Scope.runtime},
null); null);
@ -108,6 +257,7 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
*/ */
public DependencySet resolveRuntimeDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) { public DependencySet resolveRuntimeDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
return resolveScopedDependencies(properties, retriever, repositories, return resolveScopedDependencies(properties, retriever, repositories,
Scope.runtime,
new Scope[]{Scope.compile, Scope.runtime}, new Scope[]{Scope.compile, Scope.runtime},
new Scope[]{Scope.compile, Scope.runtime}, new Scope[]{Scope.compile, Scope.runtime},
resolveCompileDependencies(properties, retriever, repositories)); resolveCompileDependencies(properties, retriever, repositories));
@ -124,6 +274,7 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
*/ */
public DependencySet resolveStandaloneDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) { public DependencySet resolveStandaloneDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
return resolveScopedDependencies(properties, retriever, repositories, return resolveScopedDependencies(properties, retriever, repositories,
Scope.standalone,
new Scope[]{Scope.standalone}, new Scope[]{Scope.standalone},
new Scope[]{Scope.compile, Scope.runtime}, new Scope[]{Scope.compile, Scope.runtime},
null); null);
@ -140,22 +291,22 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
*/ */
public DependencySet resolveTestDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) { public DependencySet resolveTestDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
return resolveScopedDependencies(properties, retriever, repositories, return resolveScopedDependencies(properties, retriever, repositories,
Scope.test,
new Scope[]{Scope.test}, new Scope[]{Scope.test},
new Scope[]{Scope.compile, Scope.runtime}, new Scope[]{Scope.compile, Scope.runtime},
null); null);
} }
private DependencySet resolveScopedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Scope[] resolvedScopes, Scope[] transitiveScopes, DependencySet excluded) { private DependencySet resolveScopedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Scope scope, Scope[] resolvedScopes, Scope[] transitiveScopes, DependencySet excluded) {
var resolution = new VersionResolution(properties); var roots = new ArrayList<Dependency>();
var dependencies = new DependencySet(); for (var resolved_scope : resolvedScopes) {
for (var scope : resolvedScopes) { var scoped_dependencies = get(resolved_scope);
var scoped_dependencies = get(scope);
if (scoped_dependencies != null) { if (scoped_dependencies != null) {
for (var dependency : scoped_dependencies) { roots.addAll(scoped_dependencies);
dependencies.addAll(new DependencyResolver(resolution, retriever, repositories, dependency).getAllDependencies(transitiveScopes));
}
} }
} }
var resolution = new VersionResolution(properties, retriever, repositories, effectiveBoms(scope));
var dependencies = new ParallelDependencyResolver(resolution, retriever, repositories).resolveAllDependencies(roots, transitiveScopes);
if (excluded != null) { if (excluded != null) {
dependencies.removeAll(excluded); dependencies.removeAll(excluded);
} }

View file

@ -24,6 +24,7 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
private final Map<Dependency, Dependency> dependencies_ = new LinkedHashMap<>(); private final Map<Dependency, Dependency> dependencies_ = new LinkedHashMap<>();
private final Set<LocalDependency> localDependencies_ = new LinkedHashSet<>(); private final Set<LocalDependency> localDependencies_ = new LinkedHashSet<>();
private final Set<LocalModule> localModules_ = new LinkedHashSet<>(); private final Set<LocalModule> localModules_ = new LinkedHashSet<>();
private final Set<Bom> boms_ = new LinkedHashSet<>();
/** /**
* Creates an empty dependency set. * Creates an empty dependency set.
@ -40,7 +41,23 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
* @since 1.5 * @since 1.5
*/ */
public DependencySet(DependencySet other) { public DependencySet(DependencySet other) {
include(other);
}
/**
* Includes all the dependencies, local dependencies, local modules and
* BOMs from another dependency set.
*
* @param other the other set to include
* @return this dependency set instance
* @since 2.4.0
*/
public DependencySet include(DependencySet other) {
addAll(other); addAll(other);
localDependencies_.addAll(other.localDependencies_);
localModules_.addAll(other.localModules_);
boms_.addAll(other.boms_);
return this;
} }
/** /**
@ -81,7 +98,32 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
} }
/** /**
* Includes a local module into the dependency set. * Includes a bill of materials in the dependency set.
* <p>
* BOMs aren't transferred, their dependency management sections supply
* versions during the resolution of this dependency set.
*
* @param bom the BOM to include
* @return this dependency set instance
* @since 2.4.0
*/
public DependencySet include(Bom bom) {
boms_.add(bom);
return this;
}
/**
* Retrieves the bills of materials.
*
* @return the set of BOMs
* @since 2.4.0
*/
public Set<Bom> boms() {
return boms_;
}
/**
* Includes a local module in the dependency set.
* <p> * <p>
* Local modules aren't resolved and point to a location on * Local modules aren't resolved and point to a location on
* the file system. * the file system.
@ -128,6 +170,11 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
* including other classifiers. * including other classifiers.
* <p> * <p>
* The destination directory must exist and be writable. * The destination directory must exist and be writable.
* <p>
* The artifacts of different dependencies are transferred in parallel,
* the {@value VersionResolution#PROPERTY_TRANSFER_PARALLELISM} property
* can be used to change the number of simultaneous transfers, setting it
* to {@code 1} makes the transfers sequential.
* *
* @param resolution the version resolution state that can be cached * @param resolution the version resolution state that can be cached
* @param retriever the retriever to use to get artifacts * @param retriever the retriever to use to get artifacts
@ -140,48 +187,15 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
* @since 2.1 * @since 2.1
*/ */
public List<RepositoryArtifact> transferIntoDirectory(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories, File directory, File modulesDirectory, String... classifiers) { public List<RepositoryArtifact> transferIntoDirectory(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories, File directory, File modulesDirectory, String... classifiers) {
var result = new ArrayList<RepositoryArtifact>(); return new DependencyTransferBatch()
for (var dependency : this) { .add(this, directory, modulesDirectory, classifiers)
var transfer_directory = directory; .transfer(resolution, retriever, repositories);
if (dependency.isModularJar()) {
if (modulesDirectory == null) {
throw new DependencyTransferException(dependency, "modules directory is not provided");
}
transfer_directory = modulesDirectory;
}
else if (directory == null) {
throw new DependencyTransferException(dependency, "artifacts directory is not provided");
}
if (!transfer_directory.exists()) {
if (!transfer_directory.mkdirs()) {
throw new DependencyTransferException(dependency, transfer_directory, "couldn't create directory");
}
}
var artifact = new DependencyResolver(resolution, retriever, repositories, dependency).transferIntoDirectory(transfer_directory);
if (artifact != null) {
result.add(artifact);
}
if (classifiers != null) {
for (var classifier : classifiers) {
if (classifier != null && !dependency.excludedClassifiers().contains(classifier)) {
var classifier_artifact = new DependencyResolver(resolution, retriever, repositories, dependency.withClassifier(classifier)).transferIntoDirectory(transfer_directory);
if (classifier_artifact != null) {
result.add(classifier_artifact);
}
}
}
}
}
return result;
} }
/** /**
* Returns the dependency that was stored in the set. * Returns the dependency that was stored in the set.
* <p> * <p>
* The version can be different from the dependency passed in and this * The version can be different from the dependency passed in, and this
* method can be used to look up the actual version of the dependency in the set. * method can be used to look up the actual version of the dependency in the set.
* *
* @param dependency the dependency to look for * @param dependency the dependency to look for
@ -206,11 +220,8 @@ public class DependencySet extends AbstractSet<Dependency> implements Set<Depend
* @since 2.0 * @since 2.0
*/ */
public String generateTransitiveDependencyTree(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories, Scope... scopes) { public String generateTransitiveDependencyTree(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories, Scope... scopes) {
var compile_dependencies = new DependencySet(); resolution = resolution.withBoms(retriever, repositories, boms_);
for (var dependency : this) { return new ParallelDependencyResolver(resolution, retriever, repositories).resolveAllDependencies(this, scopes).generateDependencyTree();
compile_dependencies.addAll(new DependencyResolver(resolution, retriever, repositories, dependency).getAllDependencies(scopes));
}
return compile_dependencies.generateDependencyTree();
} }
/** /**

View file

@ -0,0 +1,129 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import rife.bld.dependencies.exceptions.DependencyTransferException;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
/**
* Collects the artifact transfers of one or more dependency sets so that
* they can be transferred together in a single parallel batch, instead of
* separate consecutive batches per dependency set.
* <p>
* The batch itself is a passive collector, the resolution context is only
* provided when the transfers are {@linkplain #transfer performed}. The
* parallelism is determined by {@link VersionResolution#transferParallelism()},
* setting it to {@code 1} makes the transfers sequential. Identical
* transfers into the same directory are only performed once.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
public class DependencyTransferBatch {
private record TransferRequest(Dependency dependency, File directory, String[] classifiers) {
}
private final List<TransferRequest> requests_ = new ArrayList<>();
private final Set<String> transferTargets_ = new HashSet<>();
/**
* Adds the artifact transfers for a dependency set to this batch.
* <p>
* The destination directory must exist and be writable.
*
* @param dependencies the dependencies whose artifacts to transfer
* @param directory the directory to transfer the artifacts into
* @param modulesDirectory the directory to download the modules into
* @param classifiers the additional classifiers to transfer
* @return this batch instance
* @throws DependencyTransferException when the transfer couldn't be prepared
* @since 2.4.0
*/
public DependencyTransferBatch add(DependencySet dependencies, File directory, File modulesDirectory, String... classifiers) {
for (var dependency : dependencies) {
var transfer_directory = directory;
if (dependency.isModularJar()) {
if (modulesDirectory == null) {
throw new DependencyTransferException(dependency, "modules directory is not provided");
}
transfer_directory = modulesDirectory;
}
else if (directory == null) {
throw new DependencyTransferException(dependency, "artifacts directory is not provided");
}
if (!transfer_directory.exists()) {
if (!transfer_directory.mkdirs()) {
throw new DependencyTransferException(dependency, transfer_directory, "couldn't create directory");
}
}
// skip transfers that are already batched for the same directory
if (!transferTargets_.add(dependency + " -> " + transfer_directory.getAbsolutePath())) {
continue;
}
requests_.add(new TransferRequest(dependency, transfer_directory, classifiers));
}
return this;
}
/**
* Performs all the collected artifact transfers in a single parallel
* batch, in the order they were added.
* <p>
* This empties the batch, transfers can be collected and transferred
* again with the same instance.
*
* @param resolution the version resolution state that can be cached
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the transfer
* @return the list of artifacts that were transferred successfully
* @throws DependencyTransferException when an error occurred during the transfer
* @since 2.4.0
*/
public List<RepositoryArtifact> transfer(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories) {
final var repos = (repositories == null ? List.<Repository>of() : repositories);
try {
var transfers = new ArrayList<Supplier<List<RepositoryArtifact>>>(requests_.size());
for (var request : requests_) {
transfers.add(() -> {
var artifacts = new ArrayList<RepositoryArtifact>();
var artifact = new DependencyResolver(resolution, retriever, repos, request.dependency()).transferIntoDirectory(request.directory());
if (artifact != null) {
artifacts.add(artifact);
}
if (request.classifiers() != null) {
for (var classifier : request.classifiers()) {
if (classifier != null && !request.dependency().excludedClassifiers().contains(classifier)) {
var classifier_artifact = new DependencyResolver(resolution, retriever, repos, request.dependency().withClassifier(classifier)).transferIntoDirectory(request.directory());
if (classifier_artifact != null) {
artifacts.add(classifier_artifact);
}
}
}
}
return artifacts;
});
}
var result = new ArrayList<RepositoryArtifact>();
for (var artifacts : ParallelExecution.execute(transfers, resolution.transferParallelism())) {
result.addAll(artifacts);
}
return result;
} finally {
requests_.clear();
transferTargets_.clear();
}
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
/**
* Resolves multiple dependencies in parallel within a list of
* Maven-compatible repositories.
* <p>
* The parallelism is determined by {@link VersionResolution#resolutionParallelism()},
* setting it to {@code 1} makes the resolution sequential. The results are
* always identical to resolving each dependency sequentially with a
* {@link DependencyResolver}, in the same order.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
public class ParallelDependencyResolver {
private final VersionResolution resolution_;
private final ArtifactRetriever retriever_;
private final List<Repository> repositories_;
/**
* Creates a new parallel resolver.
* <p>
* The repositories will be checked in the order they're listed.
*
* @param resolution the version resolution state that can be cached
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the resolution
* @since 2.4.0
*/
public ParallelDependencyResolver(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories) {
resolution_ = resolution;
retriever_ = retriever;
if (repositories == null) {
repositories = List.of();
}
repositories_ = repositories;
}
/**
* Resolves the transitive dependencies of multiple root dependencies,
* merging the results in the order of the provided roots.
* <p>
* The roots are resolved in parallel while a shared prefetcher
* speculatively warms the retriever cache across all of them.
*
* @param roots the root dependencies to resolve
* @param scopes the scopes to return the transitive dependencies for
* @return the merged transitive dependencies of all the roots
* @since 2.4.0
*/
public DependencySet resolveAllDependencies(Collection<Dependency> roots, Scope... scopes) {
var result = new DependencySet();
if (roots.isEmpty()) {
return result;
}
var prefetcher = new PomPrefetcher(resolution_, retriever_, repositories_);
try {
var resolutions = new ArrayList<Supplier<DependencySet>>(roots.size());
for (var root : roots) {
resolutions.add(() -> new DependencyResolver(resolution_, retriever_, repositories_, root).getAllDependencies(prefetcher, scopes));
}
for (var dependencies : ParallelExecution.execute(resolutions, resolution_.resolutionParallelism())) {
result.addAll(dependencies);
}
} finally {
prefetcher.shutdown();
}
return result;
}
/**
* Resolves the latest versions of multiple dependencies, returning them
* in the same order as the provided dependencies.
*
* @param dependencies the dependencies to resolve the latest versions of
* @return the latest versions in the order of the provided dependencies
* @since 2.4.0
*/
public List<Version> resolveLatestVersions(List<Dependency> dependencies) {
var resolutions = new ArrayList<Supplier<Version>>(dependencies.size());
for (var dependency : dependencies) {
resolutions.add(() -> new DependencyResolver(resolution_, retriever_, repositories_, dependency).latestVersion());
}
return ParallelExecution.execute(resolutions, resolution_.resolutionParallelism());
}
/**
* Returns the version resolution state that can be cached.
*
* @return the version resolution state
* @since 2.4.0
*/
public VersionResolution resolution() {
return resolution_;
}
/**
* Retrieve the repositories that are used by this resolver.
*
* @return the resolver's repositories
* @since 2.4.0
*/
public List<Repository> repositories() {
return repositories_;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
/**
* Executes tasks in parallel while preserving the order of their results
* and the sequential semantics of failures: the first task that fails in
* order will have its exception rethrown.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
final class ParallelExecution {
private ParallelExecution() {
}
static <T> List<T> execute(List<Supplier<T>> tasks, int parallelism) {
var result = new ArrayList<T>(tasks.size());
parallelism = Math.min(tasks.size(), parallelism);
if (parallelism <= 1) {
for (var task : tasks) {
result.add(task.get());
}
return result;
}
var executor = Executors.newFixedThreadPool(parallelism);
try {
var futures = new ArrayList<Future<T>>(tasks.size());
for (var task : tasks) {
futures.add(executor.submit(task::get));
}
for (var future : futures) {
try {
result.add(future.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Parallel execution was interrupted", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof RuntimeException runtime) {
throw runtime;
}
if (e.getCause() instanceof Error error) {
throw error;
}
throw new IllegalStateException(e.getCause());
}
}
} finally {
executor.shutdownNow();
}
return result;
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Speculatively retrieves POMs in parallel so that they are already cached
* by the artifact retriever when the sequential dependency resolution
* processes them, without influencing the resolution semantics.
* <p>
* A single instance can be shared by multiple concurrent resolutions, the
* POM of each unique dependency will only be prefetched once.
* <p>
* Prefetching only has benefits when the retrieved POMs are cached for the
* sequential resolution that follows, the prefetcher is inert when the
* retriever doesn't cache or when the resolution parallelism disables it.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
class PomPrefetcher {
private final VersionResolution resolution_;
private final ArtifactRetriever retriever_;
private final List<Repository> repositories_;
private final ExecutorService executor_;
private final Set<Dependency> submitted_ = ConcurrentHashMap.newKeySet();
PomPrefetcher(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories) {
resolution_ = resolution;
retriever_ = retriever;
repositories_ = repositories;
if (retriever.isCaching() && resolution.resolutionParallelism() > 1) {
executor_ = Executors.newFixedThreadPool(resolution.resolutionParallelism());
} else {
executor_ = null;
}
}
void prefetch(Collection<PomDependency> candidates) {
if (executor_ == null) {
return;
}
for (var candidate : candidates) {
var dependency = resolution_.overrideTransitiveDependency(candidate.convertToDependency());
if (submitted_.add(dependency)) {
executor_.submit(() -> {
try {
new DependencyResolver(resolution_, retriever_, repositories_, dependency).getMavenPom(dependency);
} catch (Throwable e) {
// failures are ignored since they will resurface
// with the proper context when the dependency is
// resolved in order
}
});
}
}
}
void shutdown() {
if (executor_ != null) {
executor_.shutdownNow();
}
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import java.util.ArrayList;
import java.util.List;
/**
* Outputs the status of artifact transfers to the console.
* <p>
* When the console is an interactive terminal, the transfers that are in
* progress are displayed in a live updating block at the bottom of the
* output, with each transfer reporting its progress as it happens.
* Otherwise, a single line is printed when each transfer finishes, keeping
* the output stable for CI logs, pipes and IDE consoles, while still never
* interleaving the lines of parallel transfers.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
abstract class TransferOutput {
private static final TransferOutput INSTANCE = create();
static TransferOutput instance() {
return INSTANCE;
}
private static TransferOutput create() {
if (System.console() != null && !"dumb".equals(System.getenv("TERM"))) {
return new AnsiTransferOutput();
}
return new PlainTransferOutput();
}
/**
* Starts tracking a single artifact transfer.
*
* @param location the location of the artifact that is being transferred
* @return the transfer to report progress and completion on
* @since 2.4.0
*/
abstract Transfer start(String location);
/**
* A single artifact transfer whose progress is being tracked.
*
* @since 2.4.0
*/
abstract static class Transfer {
protected final String location_;
protected Transfer(String location) {
location_ = location;
}
/**
* Reports transfer progress.
*
* @param transferred the number of bytes transferred so far
* @param total the total number of bytes; or {@code -1} when unknown
* @since 2.4.0
*/
abstract void progress(long transferred, long total);
/**
* Reports that the transfer finished.
*
* @param status the final status of the transfer, like
* {@code done}, {@code exists} or {@code not found}
* @since 2.4.0
*/
abstract void finish(String status);
}
static String describe(String location, String status) {
return "Downloading: " + location + " ... " + status;
}
private static class PlainTransferOutput extends TransferOutput {
Transfer start(String location) {
return new Transfer(location) {
void progress(long transferred, long total) {
// no-op, only the finished state is printed
}
void finish(String status) {
System.out.println(describe(location_, status));
}
};
}
}
private static class AnsiTransferOutput extends TransferOutput {
private static final long REPAINT_INTERVAL_MS = 100;
private static final int MAX_LINE_WIDTH = 79;
private final List<AnsiTransfer> active_ = new ArrayList<>();
private int paintedLines_ = 0;
private long lastRepaint_ = 0;
synchronized Transfer start(String location) {
var transfer = new AnsiTransfer(location);
active_.add(transfer);
repaint(null);
return transfer;
}
private synchronized void reportProgress() {
if (System.currentTimeMillis() - lastRepaint_ >= REPAINT_INTERVAL_MS) {
repaint(null);
}
}
private synchronized void reportFinished(AnsiTransfer transfer, String status) {
active_.remove(transfer);
repaint(describe(transfer.location_, status));
}
private void repaint(String finishedLine) {
var output = new StringBuilder();
if (paintedLines_ > 0) {
// move the cursor to the beginning of the live block
// and clear everything below it
output.append("\u001B[").append(paintedLines_).append("F\u001B[0J");
}
if (finishedLine != null) {
output.append(finishedLine).append('\n');
}
for (var transfer : active_) {
output.append(transfer.statusLine()).append('\n');
}
paintedLines_ = active_.size();
lastRepaint_ = System.currentTimeMillis();
System.out.print(output);
System.out.flush();
}
private class AnsiTransfer extends Transfer {
private volatile long transferred_ = 0;
private volatile long total_ = -1;
AnsiTransfer(String location) {
super(location);
}
void progress(long transferred, long total) {
transferred_ = transferred;
total_ = total;
reportProgress();
}
void finish(String status) {
reportFinished(this, status);
}
String statusLine() {
var filename = location_.substring(location_.lastIndexOf('/') + 1);
var line = new StringBuilder("Downloading: ").append(filename).append(" ... ");
if (total_ > 0) {
line.append(transferred_ * 100 / total_).append('%');
} else if (transferred_ > 0) {
line.append(humanBytes(transferred_));
}
// prevent lines from wrapping since that would break
// the cursor repositioning of the repaints
if (line.length() > MAX_LINE_WIDTH) {
line.setLength(MAX_LINE_WIDTH);
}
return line.toString();
}
private String humanBytes(long bytes) {
if (bytes >= 1024 * 1024) {
return (bytes / (1024 * 1024)) + " MB";
}
if (bytes >= 1024) {
return (bytes / 1024) + " KB";
}
return bytes + " B";
}
}
}
}

View file

@ -6,13 +6,19 @@ package rife.bld.dependencies;
import rife.ioc.HierarchicalProperties; import rife.ioc.HierarchicalProperties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger;
/** /**
* This class is responsible for managing version overrides for dependencies. * This class is responsible for managing the versions that are applied
* during dependency resolution.
* <p> * <p>
* It allows users to specify a property keys with the prefix "{@code bld.override}" where the values will be parsed as * It allows users to specify a property key with the prefix "{@code bld.override}" where the values will be parsed as
* a comma-separated list of dependencies with the versions that should override any other versions that are encountered. * a comma-separated list of dependencies with the versions that should override any other versions that are encountered.
* <p> * <p>
* For instance: * For instance:
@ -27,6 +33,20 @@ import java.util.Map;
* bld.override-tests=com.uwyn.rife2:bld-tests-badge:1.4.7 * bld.override-tests=com.uwyn.rife2:bld-tests-badge:1.4.7
* bld.override-h2=com.h2database:h2:2.2.222 * bld.override-h2=com.h2database:h2:2.2.222
* </pre> * </pre>
* <p>
* It can also import versions from the dependency management sections of
* bills of materials (BOMs). When resolving versions, the
* following precedence applies: a version from a "{@code bld.override}"
* property always wins, followed by a version explicitly declared
* in the build file, followed by a version from a BOM. Dependencies that
* are declared without a version and that are not covered by a BOM,
* resolve to their latest version.
* <p>
* It also captures other dependency resolution preferences, like the number
* of parallel artifact transfers through the "{@code bld.transferParallelism}"
* property and the number of parallel POM retrievals during transitive
* dependency resolution through the "{@code bld.resolutionParallelism}"
* property.
* @since 2.0 * @since 2.0
*/ */
public class VersionResolution { public class VersionResolution {
@ -36,7 +56,27 @@ public class VersionResolution {
*/ */
public static final String PROPERTY_OVERRIDE_PREFIX = "bld.override"; public static final String PROPERTY_OVERRIDE_PREFIX = "bld.override";
/**
* The property key that determines how many artifact transfers are
* performed in parallel, {@code 1} makes them sequential.
* @since 2.4.0
*/
public static final String PROPERTY_TRANSFER_PARALLELISM = "bld.transferParallelism";
private static final int DEFAULT_TRANSFER_PARALLELISM = 6;
/**
* The property key that determines how many POMs are speculatively
* retrieved in parallel during transitive dependency resolution,
* {@code 1} disables the parallel retrieval.
* @since 2.4.0
*/
public static final String PROPERTY_RESOLUTION_PARALLELISM = "bld.resolutionParallelism";
private static final int DEFAULT_RESOLUTION_PARALLELISM = 6;
private final Map<String, Version> versionOverrides_ = new HashMap<>(); private final Map<String, Version> versionOverrides_ = new HashMap<>();
private final Map<String, Version> bomVersions_;
private final int transferParallelism_;
private final int resolutionParallelism_;
/** /**
* Returns a dummy {@code VersionResolution} instance that doesn't override anything. * Returns a dummy {@code VersionResolution} instance that doesn't override anything.
@ -74,6 +114,223 @@ public class VersionResolution {
} }
} }
} }
transferParallelism_ = parseParallelism(properties, PROPERTY_TRANSFER_PARALLELISM, DEFAULT_TRANSFER_PARALLELISM);
resolutionParallelism_ = parseParallelism(properties, PROPERTY_RESOLUTION_PARALLELISM, DEFAULT_RESOLUTION_PARALLELISM);
bomVersions_ = Map.of();
}
private VersionResolution(VersionResolution base, Map<String, Version> bomVersions) {
versionOverrides_.putAll(base.versionOverrides_);
transferParallelism_ = base.transferParallelism_;
resolutionParallelism_ = base.resolutionParallelism_;
bomVersions_ = Map.copyOf(bomVersions);
}
/**
* Creates a new instance of the {@code VersionResolution} class from
* hierarchical properties and bills of materials whose dependency
* management sections supply versions during resolution.
* <p>
* The BOMs are imported in the order they're provided, the first BOM
* that manages a particular dependency determines its version. The
* version overrides of the properties take precedence over any BOM
* version, they also apply when resolving the BOMs themselves.
*
* @param properties the hierarchical properties that will be used to determine the version overrides
* @param retriever the retriever to use to get the BOMs
* @param repositories the repositories to resolve the BOMs in
* @param boms the BOMs to import
* @since 2.4.0
*/
public VersionResolution(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
this(new VersionResolution(properties), retriever, repositories, boms);
}
private VersionResolution(VersionResolution base, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
this(base, resolveBomVersions(base, retriever, repositories, boms));
}
// returns a resolution that additionally imports the provided BOMs,
// versions that are already imported take precedence over the new ones
VersionResolution withBoms(ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
if (boms == null || boms.isEmpty()) {
return this;
}
var merged = new HashMap<>(resolveBomVersions(this, retriever, repositories, boms));
merged.putAll(bomVersions_);
return new VersionResolution(this, merged);
}
private static Map<String, Version> resolveBomVersions(VersionResolution resolution, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
var bom_versions = new HashMap<String, Version>();
if (boms != null) {
for (var bom : boms) {
var pom = new DependencyResolver(resolution, retriever, repositories, bom).getMavenPom(bom);
for (var managed : pom.getManagedDependencies()) {
if (managed.version() != null && !managed.version().isBlank()) {
var dependency = managed.convertToDependency();
bom_versions.putIfAbsent(managedKey(dependency), dependency.version());
}
}
}
}
return bom_versions;
}
/**
* Describes a version conflict between bills of materials, where more
* than one applicable BOM manages the same dependency at a different
* version.
*
* @param dependency the group and artifact identifiers of the
* dependency that is managed at conflicting versions
* @param bomVersions the versions that the BOMs manage the dependency
* at, keyed by the BOM, in precedence order so that
* the first entry is the version that is used
* @since 2.4.0
*/
public record BomVersionConflict(String dependency, Map<String, Version> bomVersions) {
}
/**
* Resolves the version conflicts between the provided bills of
* materials, where more than one of them manages the same dependency
* at a different version.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get the BOMs
* @param repositories the repositories to resolve the BOMs in
* @param boms the BOMs to check, in precedence order
* @return the version conflicts between the BOMs
* @since 2.4.0
*/
public static List<BomVersionConflict> resolveBomVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
var base = new VersionResolution(properties);
var versions_by_key = new LinkedHashMap<String, LinkedHashMap<String, Version>>();
var dependency_by_key = new LinkedHashMap<String, String>();
if (boms != null) {
for (var bom : boms) {
var pom = new DependencyResolver(base, retriever, repositories, bom).getMavenPom(bom);
for (var managed : pom.getManagedDependencies()) {
if (managed.version() != null && !managed.version().isBlank()) {
var dependency = managed.convertToDependency();
var key = managedKey(dependency);
versions_by_key.computeIfAbsent(key, k -> new LinkedHashMap<>())
.putIfAbsent(bom.toArtifactString(), dependency.version());
dependency_by_key.putIfAbsent(key, dependency.toArtifactString());
}
}
}
}
var conflicts = new ArrayList<BomVersionConflict>();
for (var entry : versions_by_key.entrySet()) {
var bom_versions = entry.getValue();
if (bom_versions.values().stream().distinct().count() > 1) {
conflicts.add(new BomVersionConflict(dependency_by_key.get(entry.getKey()), new LinkedHashMap<>(bom_versions)));
}
}
return conflicts;
}
/**
* Describes a declared dependency whose explicit version differs from
* the version that an applicable bill of materials manages it at.
* <p>
* The declared version is used for the dependency itself, its
* transitive dependencies still resolve to the versions that the BOM
* manages.
*
* @param dependency the group and artifact identifiers of the
* declared dependency
* @param declaredVersion the version the dependency is declared with
* @param bom the BOM that manages the dependency, the one
* with the highest precedence when several do
* @param bomVersion the version the BOM manages the dependency at
* @since 2.4.0
*/
public record DeclaredVersionConflict(String dependency, Version declaredVersion, String bom, Version bomVersion) {
}
/**
* Finds the declared dependencies whose explicit version differs from
* the version that the provided bills of materials manage them at.
* <p>
* Dependencies that are declared without a version or whose version is
* supplied by a {@code bld.override} property are not reported.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get the BOMs
* @param repositories the repositories to resolve the BOMs in
* @param boms the BOMs to check, in precedence order
* @param declared the declared dependencies to check
* @return the version differences between the declared dependencies and the BOMs
* @since 2.4.0
*/
public static List<DeclaredVersionConflict> resolveDeclaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms, Collection<Dependency> declared) {
var base = new VersionResolution(properties);
var managed_versions = new LinkedHashMap<String, Version>();
var managed_boms = new LinkedHashMap<String, String>();
if (boms != null) {
for (var bom : boms) {
var pom = new DependencyResolver(base, retriever, repositories, bom).getMavenPom(bom);
for (var managed : pom.getManagedDependencies()) {
if (managed.version() != null && !managed.version().isBlank()) {
var dependency = managed.convertToDependency();
var key = managedKey(dependency);
// the first BOM that manages a dependency determines
// its version, mirroring the resolution precedence
if (managed_versions.putIfAbsent(key, dependency.version()) == null) {
managed_boms.put(key, bom.toArtifactString());
}
}
}
}
}
var conflicts = new ArrayList<DeclaredVersionConflict>();
if (declared != null) {
for (var dependency : declared) {
if (VersionNumber.UNKNOWN.equals(dependency.version()) ||
base.versionOverrides_.containsKey(dependency.toArtifactString())) {
continue;
}
var key = managedKey(dependency);
var managed_version = managed_versions.get(key);
if (managed_version != null && !managed_version.equals(dependency.version())) {
conflicts.add(new DeclaredVersionConflict(dependency.toArtifactString(), dependency.version(), managed_boms.get(key), managed_version));
}
}
}
return conflicts;
}
// builds the identity that dependency management entries are matched
// on, mirroring Maven this includes the type and the classifier, the
// modular and forced-classpath JAR types match the plain jar entries
// that BOMs manage
private static String managedKey(Dependency dependency) {
var type = dependency.type();
if (type == null || type.isBlank() ||
Dependency.TYPE_MODULAR_JAR.equals(type) ||
Dependency.TYPE_CLASSPATH_JAR.equals(type)) {
type = Dependency.TYPE_JAR;
}
return dependency.toArtifactString() + ":" + type + ":" + dependency.classifier();
}
private static int parseParallelism(HierarchicalProperties properties, String property, int defaultValue) {
if (properties != null) {
var parallelism = properties.getValueString(property);
if (parallelism != null && !parallelism.isBlank()) {
try {
return Math.max(1, Integer.parseInt(parallelism.trim()));
} catch (NumberFormatException e) {
Logger.getLogger("rife.bld").warning("Unable to parse the " + property + " property as an integer: '" + parallelism + "', using " + defaultValue + " instead");
}
}
}
return defaultValue;
} }
/** /**
@ -85,11 +342,17 @@ public class VersionResolution {
*/ */
public Version overrideVersion(Dependency original) { public Version overrideVersion(Dependency original) {
var overridden = versionOverrides_.get(original.toArtifactString()); var overridden = versionOverrides_.get(original.toArtifactString());
if (overridden == null) { if (overridden != null) {
return original.version();
}
return overridden; return overridden;
} }
if (VersionNumber.UNKNOWN.equals(original.version())) {
var bom_version = bomVersions_.get(managedKey(original));
if (bom_version != null) {
return bom_version;
}
}
return original.version();
}
/** /**
* Overrides the version of a given dependency with the corresponding overridden version and * Overrides the version of a given dependency with the corresponding overridden version and
@ -104,9 +367,63 @@ public class VersionResolution {
if (overridden == null) { if (overridden == null) {
return original; return original;
} }
return withVersion(original, overridden);
}
/**
* Applies version overrides to a dependency that was explicitly declared
* in the build file.
* <p>
* A version from the {@code bld.override} property always takes
* precedence. A bill of materials version is only applied when the
* dependency was declared without a version, an explicitly declared
* version is never rewritten by a BOM.
*
* @param declared the declared dependency to apply overrides to
* @return the dependency with the overridden version if one applies; or
* the original dependency otherwise
* @since 2.4.0
*/
public Dependency overrideDeclaredDependency(Dependency declared) {
var overridden = versionOverrides_.get(declared.toArtifactString());
if (overridden == null && VersionNumber.UNKNOWN.equals(declared.version())) {
overridden = bomVersions_.get(managedKey(declared));
}
if (overridden == null) {
return declared;
}
return withVersion(declared, overridden);
}
/**
* Applies version overrides to a transitive dependency that was
* encountered during resolution.
* <p>
* A version from the {@code bld.override} property always takes
* precedence, followed by a matching bill of materials version that
* pins the transitive dependency regardless of the version its parent
* POM declared.
*
* @param transitive the transitive dependency to apply overrides to
* @return the dependency with the overridden version if one applies; or
* the original dependency otherwise
* @since 2.4.0
*/
public Dependency overrideTransitiveDependency(Dependency transitive) {
var overridden = versionOverrides_.get(transitive.toArtifactString());
if (overridden == null) {
overridden = bomVersions_.get(managedKey(transitive));
}
if (overridden == null) {
return transitive;
}
return withVersion(transitive, overridden);
}
private static Dependency withVersion(Dependency original, Version version) {
return new Dependency(original.groupId(), return new Dependency(original.groupId(),
original.artifactId(), original.artifactId(),
overridden, version,
original.classifier(), original.classifier(),
original.type(), original.type(),
original.exclusions(), original.exclusions(),
@ -122,4 +439,56 @@ public class VersionResolution {
public Map<String, Version> versionOverrides() { public Map<String, Version> versionOverrides() {
return versionOverrides_; return versionOverrides_;
} }
/**
* Indicates whether a bill of materials supplies the version of a
* particular dependency.
* <p>
* The dependency identity takes the group, artifact, type and
* classifier into account, mirroring Maven's dependency management.
*
* @param dependency the dependency to check
* @return {@code true} when a BOM manages the dependency's version;
* {@code false} otherwise
* @since 2.4.0
*/
public boolean coversDependency(Dependency dependency) {
return bomVersions_.containsKey(managedKey(dependency));
}
/**
* Returns the map of versions that were imported from bills of
* materials, where the key is the dependency identity composed of the
* group, artifact, type and classifier, and the value is the managed
* version.
*
* @return the map of BOM versions
* @since 2.4.0
*/
public Map<String, Version> bomVersions() {
return bomVersions_;
}
/**
* Returns the number of artifact transfers that are performed in parallel,
* {@code 1} means transfers are sequential.
*
* @return the number of parallel artifact transfers
* @since 2.4.0
*/
public int transferParallelism() {
return transferParallelism_;
}
/**
* Returns the number of POMs that are speculatively retrieved in parallel
* during transitive dependency resolution, {@code 1} means the parallel
* retrieval is disabled.
*
* @return the number of parallel POM retrievals
* @since 2.4.0
*/
public int resolutionParallelism() {
return resolutionParallelism_;
}
} }

View file

@ -129,6 +129,17 @@ class Xml2MavenPom extends Xml2Data {
return result; return result;
} }
Set<PomDependency> getManagedDependencies() {
var result = new LinkedHashSet<PomDependency>();
// iterate the values since putting an entry with an equal key
// preserves the original key instance while the value reflects
// the actual managed dependency
for (var managed : dependencyManagement_.values()) {
result.add(resolveDependency(managed));
}
return result;
}
PomDependency resolveDependency(PomDependency dependency) { PomDependency resolveDependency(PomDependency dependency) {
return new PomDependency( return new PomDependency(
resolveMavenProperties(dependency.groupId()), resolveMavenProperties(dependency.groupId()),

View file

@ -0,0 +1,47 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.help;
import rife.bld.CommandHelp;
import rife.tools.StringUtils;
/**
* Provides help for the MCP command.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
public class McpHelp implements CommandHelp {
public String getSummary() {
return "Starts an MCP server that exposes the build commands";
}
public String getDescription(String topic) {
return StringUtils.replace("""
Starts a Model Context Protocol (MCP) server that exposes the
build commands as tools, so that AI agents can drive the build.
This is an experimental feature and may still change.
The server communicates over standard input and output with the
MCP stdio transport and runs until its input stream ends. Launch
bld with the --use-stderr option to keep standard output free
for the protocol while the build starts up.
The install argument doesn't start the server, it registers the
project with an MCP client by writing the standard configuration
file inside the project directory:
install writes .mcp.json (Claude Code and compatible)
install cursor writes .cursor/mcp.json
install vscode writes .vscode/mcp.json
install --print prints the configuration instead of writing it
Existing configuration files are merged with, other servers are
preserved. The registered command launches the wrapper directly
through java, so that the same file works on every platform.
Usage : ${topic} [install [claude | cursor | vscode] [--print]]""", "${topic}", topic);
}
}

View file

@ -179,7 +179,7 @@ public abstract class AbstractCreateOperation<T extends AbstractCreateOperation<
* parents, outputting the location when the operation is {@link #verbose()}. * parents, outputting the location when the operation is {@link #verbose()}.
* *
* @param directory the directory to create * @param directory the directory to create
* @since 2.3.1 * @since 2.4.0
*/ */
protected void executeCreateDirectory(File directory) { protected void executeCreateDirectory(File directory) {
if (verbose()) { if (verbose()) {
@ -194,7 +194,7 @@ public abstract class AbstractCreateOperation<T extends AbstractCreateOperation<
* *
* @param content the content to write * @param content the content to write
* @param file the file to write the content into * @param file the file to write the content into
* @since 2.3.1 * @since 2.4.0
*/ */
protected void executeWriteProjectFile(String content, File file) protected void executeWriteProjectFile(String content, File file)
throws FileUtilsErrorException { throws FileUtilsErrorException {

View file

@ -55,7 +55,7 @@ public abstract class AbstractOperation<T extends AbstractOperation<T>> {
* @param verbose {@code true} if the operation should be verbose; * @param verbose {@code true} if the operation should be verbose;
* {@code false} otherwise * {@code false} otherwise
* @return this operation instance * @return this operation instance
* @since 2.3.1 * @since 2.4.0
*/ */
public T verbose(boolean verbose) { public T verbose(boolean verbose) {
verbose_ = verbose; verbose_ = verbose;
@ -68,7 +68,7 @@ public abstract class AbstractOperation<T extends AbstractOperation<T>> {
* *
* @return {@code true} if the operation should be verbose; * @return {@code true} if the operation should be verbose;
* {@code false} otherwise * {@code false} otherwise
* @since 2.3.1 * @since 2.4.0
*/ */
public boolean verbose() { public boolean verbose() {
return verbose_; return verbose_;

View file

@ -241,7 +241,7 @@ public abstract class AbstractProcessOperation<T extends AbstractProcessOperatio
* *
* @param classpath classpath entries for the operation * @param classpath classpath entries for the operation
* @return this operation instance * @return this operation instance
* @since 2.3.1 * @since 2.4.0
* *
*/ */
public T classpath(File... classpath) { public T classpath(File... classpath) {
@ -269,7 +269,7 @@ public abstract class AbstractProcessOperation<T extends AbstractProcessOperatio
* *
* @param classpath a list of classpath entries for the operation * @param classpath a list of classpath entries for the operation
* @return this operation instance * @return this operation instance
* @since 2.3.1 * @since 2.4.0
*/ */
public T classpath(Collection<File> classpath) { public T classpath(Collection<File> classpath) {
classpath_.addAll(classpath.stream().map(File::getAbsolutePath).toList()); classpath_.addAll(classpath.stream().map(File::getAbsolutePath).toList());
@ -293,7 +293,7 @@ public abstract class AbstractProcessOperation<T extends AbstractProcessOperatio
* *
* @param modulePath module path entries for the operation * @param modulePath module path entries for the operation
* @return this operation instance * @return this operation instance
* @since 2.3.1 * @since 2.4.0
*/ */
public T modulePath(File... modulePath) { public T modulePath(File... modulePath) {
return modulePath(List.of(modulePath)); return modulePath(List.of(modulePath));
@ -320,7 +320,7 @@ public abstract class AbstractProcessOperation<T extends AbstractProcessOperatio
* *
* @param modulePath a list of module path entries for the operation * @param modulePath a list of module path entries for the operation
* @return this operation instance * @return this operation instance
* @since 2.3.1 * @since 2.4.0
*/ */
public T modulePath(Collection<File> modulePath) { public T modulePath(Collection<File> modulePath) {
modulePath_.addAll(modulePath.stream().map(File::getAbsolutePath).toList()); modulePath_.addAll(modulePath.stream().map(File::getAbsolutePath).toList());

View file

@ -178,7 +178,7 @@ public class DependencyTreeOperation extends AbstractOperation<DependencyTreeOpe
* @since 1.5.21 * @since 1.5.21
*/ */
protected String executeGenerateCompileDependencies() { protected String executeGenerateCompileDependencies() {
var compile_tree = dependencies().scope(compile).generateTransitiveDependencyTree(new VersionResolution(properties()), artifactRetriever(), repositories(), compile); var compile_tree = dependencies().scope(compile).generateTransitiveDependencyTree(new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies().effectiveBoms(compile)), artifactRetriever(), repositories(), compile);
if (compile_tree.isEmpty()) { if (compile_tree.isEmpty()) {
compile_tree = "no dependencies" + System.lineSeparator(); compile_tree = "no dependencies" + System.lineSeparator();
} }
@ -191,7 +191,7 @@ public class DependencyTreeOperation extends AbstractOperation<DependencyTreeOpe
* @since 1.7.3 * @since 1.7.3
*/ */
protected String executeGenerateProvidedDependencies() { protected String executeGenerateProvidedDependencies() {
var provided_tree = dependencies().scope(provided).generateTransitiveDependencyTree(new VersionResolution(properties()), artifactRetriever(), repositories(), compile, runtime); var provided_tree = dependencies().scope(provided).generateTransitiveDependencyTree(new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies().effectiveBoms(provided)), artifactRetriever(), repositories(), compile, runtime);
if (provided_tree.isEmpty()) { if (provided_tree.isEmpty()) {
provided_tree = "no dependencies" + System.lineSeparator(); provided_tree = "no dependencies" + System.lineSeparator();
} }
@ -204,7 +204,7 @@ public class DependencyTreeOperation extends AbstractOperation<DependencyTreeOpe
* @since 1.5.21 * @since 1.5.21
*/ */
protected String executeGenerateRuntimeDependencies() { protected String executeGenerateRuntimeDependencies() {
var runtime_tree = dependencies().scope(runtime).generateTransitiveDependencyTree(new VersionResolution(properties()), artifactRetriever(), repositories(), compile, runtime); var runtime_tree = dependencies().scope(runtime).generateTransitiveDependencyTree(new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies().effectiveBoms(runtime)), artifactRetriever(), repositories(), compile, runtime);
if (runtime_tree.isEmpty()) { if (runtime_tree.isEmpty()) {
runtime_tree = "no dependencies" + System.lineSeparator(); runtime_tree = "no dependencies" + System.lineSeparator();
} }
@ -217,7 +217,7 @@ public class DependencyTreeOperation extends AbstractOperation<DependencyTreeOpe
* @since 1.7.3 * @since 1.7.3
*/ */
protected String executeGenerateTestDependencies() { protected String executeGenerateTestDependencies() {
var test_tree = dependencies().scope(test).generateTransitiveDependencyTree(new VersionResolution(properties()), artifactRetriever(), repositories(), compile, runtime); var test_tree = dependencies().scope(test).generateTransitiveDependencyTree(new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies().effectiveBoms(test)), artifactRetriever(), repositories(), compile, runtime);
if (test_tree.isEmpty()) { if (test_tree.isEmpty()) {
test_tree = "no dependencies" + System.lineSeparator(); test_tree = "no dependencies" + System.lineSeparator();
} }

View file

@ -42,9 +42,13 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
private File libTestModulesDirectory_; private File libTestModulesDirectory_;
private boolean downloadSources_ = false; private boolean downloadSources_ = false;
private boolean downloadJavadoc_ = false; private boolean downloadJavadoc_ = false;
private final DependencyTransferBatch transfers_ = new DependencyTransferBatch();
/** /**
* Performs the download operation. * Performs the download operation.
* <p>
* The artifact transfers of all the scopes are collected first and then
* performed together in a single parallel batch.
* *
* @since 1.5 * @since 1.5
*/ */
@ -54,16 +58,75 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
return; return;
} }
if (!silent()) {
System.out.println("Analyzing dependencies...");
}
executeReportUncoveredDependencies();
executeDownloadCompileDependencies(); executeDownloadCompileDependencies();
executeDownloadProvidedDependencies(); executeDownloadProvidedDependencies();
executeDownloadRuntimeDependencies(); executeDownloadRuntimeDependencies();
executeDownloadStandaloneDependencies(); executeDownloadStandaloneDependencies();
executeDownloadTestDependencies(); executeDownloadTestDependencies();
executeTransferDependencies();
if (!silent()) { if (!silent()) {
System.out.println("Downloading finished successfully."); System.out.println("Downloading finished successfully.");
} }
} }
/**
* Part of the {@link #execute} operation, warns about version-less
* dependencies that are not covered by a BOM in their scope.
*
* @since 2.4.0
*/
protected void executeReportUncoveredDependencies() {
if (silent()) {
return;
}
for (var dependency : dependencies().versionlessDependenciesWithoutBom(properties(), artifactRetriever(), repositories())) {
System.out.println("Warning: '" + dependency.toArtifactString() + "' isn't covered by a BOM, its latest version will be used");
}
for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) {
System.out.println(formatBomVersionConflict(conflict));
}
for (var conflict : dependencies().declaredVersionConflicts(properties(), artifactRetriever(), repositories())) {
System.out.println(formatDeclaredVersionConflict(conflict));
}
}
/**
* Formats a warning message for a BOM version conflict.
*
* @param conflict the BOM version conflict to format
* @return the formatted warning message
* @since 2.4.0
*/
protected static String formatBomVersionConflict(rife.bld.dependencies.VersionResolution.BomVersionConflict conflict) {
var entries = conflict.bomVersions().entrySet().iterator();
var used = entries.next();
var message = new StringBuilder("Warning: '" + conflict.dependency() + "' is managed by multiple BOMs, using " +
used.getValue() + " from '" + used.getKey() + "'");
while (entries.hasNext()) {
var other = entries.next();
message.append(", not ").append(other.getValue()).append(" from '").append(other.getKey()).append('\'');
}
return message.toString();
}
/**
* Formats a warning message for a declared dependency whose version
* differs from the version that a BOM manages it at.
*
* @param conflict the declared version conflict to format
* @return the formatted warning message
* @since 2.4.0
*/
protected static String formatDeclaredVersionConflict(rife.bld.dependencies.VersionResolution.DeclaredVersionConflict conflict) {
return "Warning: '" + conflict.dependency() + "' is declared with version " + conflict.declaredVersion() +
" while BOM '" + conflict.bom() + "' manages it at " + conflict.bomVersion() +
", the declared version is used but transitive dependencies still follow the BOM";
}
/** /**
* Part of the {@link #execute} operation, download the {@code compile} scope artifacts. * Part of the {@link #execute} operation, download the {@code compile} scope artifacts.
* *
@ -110,7 +173,11 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
} }
/** /**
* Part of the {@link #execute} operation, download the artifacts for a particular dependency scope. * Part of the {@link #execute} operation, collect the artifact transfers
* for a particular dependency scope into the {@linkplain #transfers()
* transfer batch}.
* <p>
* The transfers are performed by {@link #executeTransferDependencies}.
* *
* @param destinationDirectory the directory in which the artifacts should be downloaded * @param destinationDirectory the directory in which the artifacts should be downloaded
* @param modulesDirectory the directory in which the modules should be downloaded * @param modulesDirectory the directory in which the modules should be downloaded
@ -128,7 +195,29 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
additional_classifiers = classifiers.toArray(new String[0]); additional_classifiers = classifiers.toArray(new String[0]);
} }
dependencies.transferIntoDirectory(new VersionResolution(properties()), artifactRetriever(), repositories(), destinationDirectory, modulesDirectory, additional_classifiers); transfers().add(dependencies, destinationDirectory, modulesDirectory, additional_classifiers);
}
/**
* Part of the {@link #execute} operation, perform all the collected
* artifact transfers of the {@linkplain #transfers() transfer batch}
* together in parallel.
*
* @since 2.4.0
*/
protected void executeTransferDependencies() {
transfers().transfer(new VersionResolution(properties()), artifactRetriever(), repositories());
}
/**
* Returns the batch that collects the artifact transfers of this
* operation.
*
* @return the artifact transfer batch of this operation
* @since 2.4.0
*/
protected DependencyTransferBatch transfers() {
return transfers_;
} }
/** /**

View file

@ -190,6 +190,7 @@ public class HelpOperation {
The following bld arguments are supported: The following bld arguments are supported:
--offline Works without Internet (only as first argument) --offline Works without Internet (only as first argument)
--use-stderr Sends the build output to standard error
-?, -h, --help Shows the help -?, -h, --help Shows the help
-D<name>=<value> Sets a JVM system property -D<name>=<value> Sets a JVM system property
-s, --stacktrace Prints out the stacktrace for exceptions -s, --stacktrace Prints out the stacktrace for exceptions

View file

@ -136,7 +136,7 @@ public class JavacOptions extends ArrayList<String> {
* of other-module is ALL-UNNAMED * of other-module is ALL-UNNAMED
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions addExports(String... modules) { public JavacOptions addExports(String... modules) {
if (isNotEmpty(modules)) { if (isNotEmpty(modules)) {
@ -151,7 +151,7 @@ public class JavacOptions extends ArrayList<String> {
* of other-module is ALL-UNNAMED * of other-module is ALL-UNNAMED
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions addExports(Collection<String> modules) { public JavacOptions addExports(Collection<String> modules) {
return addCommaSeparatedOption("--add-exports", modules); return addCommaSeparatedOption("--add-exports", modules);
@ -161,7 +161,7 @@ public class JavacOptions extends ArrayList<String> {
* Specifies additional modules to be considered as required by a given module * Specifies additional modules to be considered as required by a given module
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions addReads(String... modules) { public JavacOptions addReads(String... modules) {
if (isNotEmpty(modules)) { if (isNotEmpty(modules)) {
@ -174,7 +174,7 @@ public class JavacOptions extends ArrayList<String> {
* Specifies additional modules to be considered as required by a given module * Specifies additional modules to be considered as required by a given module
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions addReads(Collection<String> modules) { public JavacOptions addReads(Collection<String> modules) {
return addCommaSeparatedOption("--add-reads", modules); return addCommaSeparatedOption("--add-reads", modules);
@ -222,7 +222,7 @@ public class JavacOptions extends ArrayList<String> {
* if none specified or inferred * if none specified or inferred
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions defaultModuleForCreatedFiles(String module) { public JavacOptions defaultModuleForCreatedFiles(String module) {
add("--default-module-for-created-files"); add("--default-module-for-created-files");
@ -417,7 +417,7 @@ public class JavacOptions extends ArrayList<String> {
* Overrides or augments a module with classes and resources in JAR files or directories * Overrides or augments a module with classes and resources in JAR files or directories
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions patchModule(String module) { public JavacOptions patchModule(String module) {
add("--patch-module"); add("--patch-module");
@ -441,7 +441,7 @@ public class JavacOptions extends ArrayList<String> {
* Provide source compatibility with the specified Java SE release * Provide source compatibility with the specified Java SE release
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions source(int version) { public JavacOptions source(int version) {
add("--source"); add("--source");
@ -453,7 +453,7 @@ public class JavacOptions extends ArrayList<String> {
* Generate class files suitable for the specified Java SE release * Generate class files suitable for the specified Java SE release
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions target(int version) { public JavacOptions target(int version) {
add("--target"); add("--target");
@ -645,7 +645,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find input source files for multiple modules * Specify where to find input source files for multiple modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions moduleSourcePathStrings(Collection<String> paths) { public JavacOptions moduleSourcePathStrings(Collection<String> paths) {
return addPathOption("--module-source-path", paths); return addPathOption("--module-source-path", paths);
@ -655,7 +655,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find input source files for multiple modules * Specify where to find input source files for multiple modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions moduleSourcePathPaths(Collection<Path> paths) { public JavacOptions moduleSourcePathPaths(Collection<Path> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -668,7 +668,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find input source files for multiple modules * Specify where to find input source files for multiple modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions moduleSourcePath(Collection<File> paths) { public JavacOptions moduleSourcePath(Collection<File> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -827,7 +827,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify a module path where to find annotation processors * Specify a module path where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorModulePathPaths(Collection<Path> paths) { public JavacOptions processorModulePathPaths(Collection<Path> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -840,7 +840,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify a module path where to find annotation processors * Specify a module path where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorModulePathStrings(Collection<String> paths) { public JavacOptions processorModulePathStrings(Collection<String> paths) {
return addPathOption("--processor-module-path", paths); return addPathOption("--processor-module-path", paths);
@ -850,7 +850,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify a module path where to find annotation processors * Specify a module path where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorModulePath(Collection<File> paths) { public JavacOptions processorModulePath(Collection<File> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -902,7 +902,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find annotation processors * Specify where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorPathStrings(Collection<String> paths) { public JavacOptions processorPathStrings(Collection<String> paths) {
return addPathOption("--processor-path", paths); return addPathOption("--processor-path", paths);
@ -912,7 +912,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find annotation processors * Specify where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorPath(Collection<File> paths) { public JavacOptions processorPath(Collection<File> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -925,7 +925,7 @@ public class JavacOptions extends ArrayList<String> {
* Specify where to find annotation processors * Specify where to find annotation processors
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions processorPathPaths(Collection<Path> paths) { public JavacOptions processorPathPaths(Collection<Path> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -1036,7 +1036,7 @@ public class JavacOptions extends ArrayList<String> {
* Override location of upgradeable modules * Override location of upgradeable modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions upgradeModulePathStrings(Collection<String> paths) { public JavacOptions upgradeModulePathStrings(Collection<String> paths) {
return addPathOption("--upgrade-module-path", paths); return addPathOption("--upgrade-module-path", paths);
@ -1046,7 +1046,7 @@ public class JavacOptions extends ArrayList<String> {
* Override location of upgradeable modules * Override location of upgradeable modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions upgradeModulePath(Collection<File> paths) { public JavacOptions upgradeModulePath(Collection<File> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -1059,7 +1059,7 @@ public class JavacOptions extends ArrayList<String> {
* Override location of upgradeable modules * Override location of upgradeable modules
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions upgradeModulePathPaths(Collection<Path> paths) { public JavacOptions upgradeModulePathPaths(Collection<Path> paths) {
if (isNotEmpty(paths)) { if (isNotEmpty(paths)) {
@ -1083,7 +1083,7 @@ public class JavacOptions extends ArrayList<String> {
* Enable recommended warning categories * Enable recommended warning categories
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions xLint() { public JavacOptions xLint() {
add("-Xlint"); add("-Xlint");
@ -1094,7 +1094,7 @@ public class JavacOptions extends ArrayList<String> {
* Warning categories to enable * Warning categories to enable
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions xLint(XLintKey... keys) { public JavacOptions xLint(XLintKey... keys) {
if (isNotEmpty(keys)) { if (isNotEmpty(keys)) {
@ -1107,7 +1107,7 @@ public class JavacOptions extends ArrayList<String> {
* Warning categories to enable * Warning categories to enable
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions xLint(Collection<XLintKey> keys) { public JavacOptions xLint(Collection<XLintKey> keys) {
if (isNotEmpty(keys)) { if (isNotEmpty(keys)) {
@ -1120,7 +1120,7 @@ public class JavacOptions extends ArrayList<String> {
* Warning categories to disable * Warning categories to disable
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions xLintDisable(XLintKey... keys) { public JavacOptions xLintDisable(XLintKey... keys) {
if (isNotEmpty(keys)) { if (isNotEmpty(keys)) {
@ -1133,7 +1133,7 @@ public class JavacOptions extends ArrayList<String> {
* Warning categories to disable * Warning categories to disable
* *
* @return this list of options * @return this list of options
* @since 2.3.1 * @since 2.4.0
*/ */
public JavacOptions xLintDisable(Collection<XLintKey> keys) { public JavacOptions xLintDisable(Collection<XLintKey> keys) {
if (isNotEmpty(keys)) { if (isNotEmpty(keys)) {

View file

@ -561,7 +561,7 @@ public class JpackageOptions extends LinkedHashMap<String, String> {
/** /**
* Creates a shortcut for the application. * Creates a shortcut for the application.
* *
* @param shortcut {@code true| to create a shortcut, {@code false} otherwise * @param shortcut {@code true} to create a shortcut, {@code false} otherwise
* @return this map of options * @return this map of options
*/ */
public JpackageOptions linuxShortcut(boolean shortcut) { public JpackageOptions linuxShortcut(boolean shortcut) {

View file

@ -0,0 +1,121 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.operations;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* The details that a single MCP tool call hands to the build process it
* launches.
* <p>
* The command, its arguments, the flags and the excluded commands are
* written to a control file instead of the command line, so they can never
* be mistaken for the command's own arguments. The values are escaped so
* that any character, including newlines, comes through intact. Only the
* MCP code uses this file, the build itself knows nothing about it.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
final class McpControl {
final String command;
final List<String> arguments;
final boolean offline;
final boolean verbose;
final boolean stacktrace;
final Set<String> exclusions;
private McpControl(String command, List<String> arguments, boolean offline, boolean verbose, boolean stacktrace, Set<String> exclusions) {
this.command = command;
this.arguments = arguments;
this.offline = offline;
this.verbose = verbose;
this.stacktrace = stacktrace;
this.exclusions = exclusions;
}
static void write(File file, String command, List<String> arguments,
boolean offline, boolean verbose, boolean stacktrace,
Collection<String> exclusions)
throws IOException {
var lines = new ArrayList<String>();
lines.add("command " + escape(command));
for (var argument : arguments) {
lines.add("arg " + escape(argument));
}
if (offline) {
lines.add("offline");
}
if (verbose) {
lines.add("verbose");
}
if (stacktrace) {
lines.add("stacktrace");
}
for (var exclusion : exclusions) {
lines.add("exclude " + escape(exclusion));
}
Files.write(file.toPath(), lines);
}
static McpControl read(File file)
throws IOException {
String command = null;
var arguments = new ArrayList<String>();
var offline = false;
var verbose = false;
var stacktrace = false;
var exclusions = new LinkedHashSet<String>();
for (var line : Files.readAllLines(file.toPath())) {
if (line.equals("offline")) {
offline = true;
} else if (line.equals("verbose")) {
verbose = true;
} else if (line.equals("stacktrace")) {
stacktrace = true;
} else if (line.startsWith("command ")) {
command = unescape(line.substring("command ".length()));
} else if (line.startsWith("arg ")) {
arguments.add(unescape(line.substring("arg ".length())));
} else if (line.startsWith("exclude ")) {
exclusions.add(unescape(line.substring("exclude ".length())));
}
}
if (command == null) {
throw new IOException("the control file doesn't specify a command");
}
return new McpControl(command, arguments, offline, verbose, stacktrace, exclusions);
}
private static String escape(String value) {
return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r");
}
private static String unescape(String value) {
var result = new StringBuilder();
for (var i = 0; i < value.length(); ++i) {
var c = value.charAt(i);
if (c == '\\' && i + 1 < value.length()) {
var next = value.charAt(++i);
switch (next) {
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case '\\' -> result.append('\\');
default -> result.append(next);
}
} else {
result.append(c);
}
}
return result.toString();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.operations;
import rife.bld.BaseProject;
import rife.bld.BuildExecutor;
import rife.bld.operations.exceptions.ExitStatusException;
import rife.tools.ExceptionUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* The entry point that runs exactly one build command for an MCP tool
* call, in a separate build process.
* <p>
* It reads the command, its arguments, the flags and the excluded commands
* from the control file, creates the build executor, and runs the single
* command through its public API. All of this single command handling lives
* here in the MCP feature, the build itself stays unaware of it. The runner
* is launched with the build executor's class name as its last argument,
* any earlier arguments, like the wrapper's {@code --offline} option, are
* ignored.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
*/
public final class McpToolRunner {
/**
* The environment variable with the path of the control file that
* requests the execution of exactly one command.
* @since 2.4.0
*/
public static final String ENV_CONTROL_FILE = "BLD_MCP_CONTROL_FILE";
/**
* The environment variable with the path of the file that the runner
* writes its result to.
* @since 2.4.0
*/
public static final String ENV_STATUS_FILE = "BLD_MCP_STATUS_FILE";
/**
* The outcome that is written when the command couldn't be resolved.
* @since 2.4.0
*/
public static final String STATUS_UNKNOWN_COMMAND = "unknown-command";
/**
* The outcome that is written when the command is excluded.
* @since 2.4.0
*/
public static final String STATUS_EXCLUDED_COMMAND = "excluded-command";
/**
* The outcome that is written when the runner itself couldn't be set
* up, for instance when the build executor class couldn't be
* instantiated, this is a server failure rather than a tool failure.
* @since 2.4.0
*/
public static final String STATUS_RUNNER_ERROR = "runner-error";
private McpToolRunner() {
}
/**
* Runs a single build command for an MCP tool call.
*
* @param arguments the build executor class name as the last argument
* @since 2.4.0
*/
public static void main(String[] arguments) {
System.exit(run(arguments));
}
private static int run(String[] arguments) {
var control_path = System.getenv(ENV_CONTROL_FILE);
if (control_path == null) {
writeStatus(STATUS_RUNNER_ERROR);
System.err.println("ERROR: no MCP control file provided");
return ExitStatusException.EXIT_FAILURE;
}
var control_file = new File(control_path);
McpControl control;
try {
control = McpControl.read(control_file);
} catch (IOException e) {
writeStatus(STATUS_RUNNER_ERROR);
System.err.println("ERROR: the MCP control file couldn't be read");
return ExitStatusException.EXIT_FAILURE;
} finally {
// the control file is consumed immediately, so that nested
// processes that inherit the environment don't re-enter the
// single command execution and don't overwrite its status
control_file.delete();
}
if (arguments.length == 0) {
writeStatus(STATUS_RUNNER_ERROR);
System.err.println("ERROR: no build executor class provided");
return ExitStatusException.EXIT_FAILURE;
}
// the build executor class is instantiated directly through its
// no-arg constructor and driven through its public API, its main
// method is not invoked, so that a tool call runs exactly the
// requested command and nothing else
BuildExecutor executor;
try {
var executor_class = Class.forName(arguments[arguments.length - 1]);
executor = (BuildExecutor) executor_class.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException | ClassCastException e) {
writeStatus(STATUS_RUNNER_ERROR);
System.err.println("ERROR: the build executor couldn't be instantiated: " + e);
return ExitStatusException.EXIT_FAILURE;
}
executor.offline(control.offline);
executor.verbose(control.verbose);
executor.showStacktrace(control.stacktrace);
var resolved = executor.resolveCommand(control.command);
if (resolved == null) {
// the result goes into the status file so that it can never be
// confused with a regular exit status of a build command
writeStatus(STATUS_UNKNOWN_COMMAND);
System.err.println("ERROR: Unknown command '" + control.command + "'");
return ExitStatusException.EXIT_FAILURE;
}
// the exclusions are enforced here, in the freshly compiled build,
// so that aliases or commands added to the build sources can't
// reach an excluded command either
if (control.exclusions.contains(control.command) || control.exclusions.contains(resolved)) {
writeStatus(STATUS_EXCLUDED_COMMAND);
System.err.println("ERROR: Command '" + control.command + "' is not available");
return ExitStatusException.EXIT_FAILURE;
}
// an automatic dependency download and purge that the project is
// configured for runs before the command, exactly like it would on
// the command line
if (executor instanceof BaseProject project) {
project.performAutoDownloadPurgeIfEnabled();
}
// the command's arguments come from the control file, they are
// never interpreted as additional commands or as global flags
executor.arguments().clear();
executor.arguments().addAll(control.arguments);
try {
executor.executeCommand(resolved);
} catch (Throwable e) {
executor.exitStatus(ExitStatusException.EXIT_FAILURE);
System.err.println();
if (control.stacktrace) {
System.err.println(ExceptionUtils.getExceptionStackTrace(e));
} else if (e.getMessage() != null) {
System.err.println(e.getMessage());
} else {
System.err.println(e.getClass().getName());
}
}
return executor.exitStatus();
}
private static void writeStatus(String status) {
var status_file = System.getenv(ENV_STATUS_FILE);
if (status_file != null) {
try {
Files.writeString(Path.of(status_file), status);
} catch (IOException e) {
// the caller falls back to the exit status
}
}
}
}

View file

@ -27,6 +27,7 @@ import java.security.*;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -52,6 +53,7 @@ public class PublishOperation extends AbstractOperation<PublishOperation> {
private ZonedDateTime moment_ = null; private ZonedDateTime moment_ = null;
private final List<Repository> repositories_ = new ArrayList<>(); private final List<Repository> repositories_ = new ArrayList<>();
private final List<Repository> dependencyRepositories_ = new ArrayList<>();
private final DependencyScopes dependencies_ = new DependencyScopes(); private final DependencyScopes dependencies_ = new DependencyScopes();
private PublishInfo info_ = new PublishInfo(); private PublishInfo info_ = new PublishInfo();
private PublishProperties publishProperties_ = new PublishProperties(); private PublishProperties publishProperties_ = new PublishProperties();
@ -78,6 +80,7 @@ public class PublishOperation extends AbstractOperation<PublishOperation> {
} }
executeValidateArtifacts(); executeValidateArtifacts();
executeResolveVersionlessDependencies();
var actual_version = info().version(); var actual_version = info().version();
@ -103,6 +106,101 @@ public class PublishOperation extends AbstractOperation<PublishOperation> {
} }
} }
/**
* Part of the {@link #execute} operation, freezes the versions of
* dependencies that were declared without one, so that the published
* POM is always complete.
* <p>
* Dependencies that are covered by a BOM that applies to their scope
* stay version-less, the dependency management imports of the POM
* supply their versions. They are frozen anyway when the flattened
* dependency management of the POM would supply a different version
* than the BOMs that apply to their scope, or when a
* {@code bld.override} property supplies their version, since the POM
* can't reflect either. Uncovered dependencies are frozen to their
* latest version.
* <p>
* BOMs that were declared without a version are frozen too, to their
* {@code bld.override} version or to their latest version.
*
* @since 2.4.0
*/
protected void executeResolveVersionlessDependencies() {
// freeze the versions of the declared BOMs, a version from a
// bld.override property is used by the build itself and takes
// precedence, version-less BOMs resolve to their latest version
var base_resolution = new VersionResolution(properties());
for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
var scoped_dependencies = dependencies().get(scope);
if (scoped_dependencies == null || scoped_dependencies.boms().isEmpty()) {
continue;
}
var declared_boms = List.copyOf(scoped_dependencies.boms());
scoped_dependencies.boms().clear();
for (var bom : declared_boms) {
var version = base_resolution.overrideVersion(bom);
if (version.equals(VersionNumber.UNKNOWN)) {
version = new DependencyResolver(base_resolution, artifactRetriever(), dependencyRepositories(), bom).latestVersion();
}
if (!version.equals(bom.version())) {
bom = new Bom(bom.groupId(), bom.artifactId(), version);
}
scoped_dependencies.boms().add(bom);
}
}
// the published POM flattens all the BOM imports into a single
// dependency management list, compute the versions that this list
// supplies so that scoped differences can be frozen explicitly
var flattened_boms = new LinkedHashMap<String, Bom>();
for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
var scoped_dependencies = dependencies().get(scope);
if (scoped_dependencies != null) {
for (var bom : scoped_dependencies.boms()) {
flattened_boms.putIfAbsent(bom.toArtifactString(), bom);
}
}
}
var flattened_resolution = new VersionResolution(properties(), artifactRetriever(), dependencyRepositories(), flattened_boms.values());
// freeze the versions of version-less dependencies that no
// applicable BOM covers
for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
var scoped_dependencies = dependencies().get(scope);
if (scoped_dependencies == null) {
continue;
}
var resolution = new VersionResolution(properties(), artifactRetriever(), dependencyRepositories(), dependencies().effectiveBoms(scope));
for (var dependency : List.copyOf(scoped_dependencies)) {
if (!dependency.version().equals(VersionNumber.UNKNOWN)) {
continue;
}
// a version from a bld.override property is used by the
// build itself, it's frozen into the POM even when a BOM
// covers the dependency since the import can't reflect it
var version = resolution.versionOverrides().get(dependency.toArtifactString());
if (version == null) {
if (resolution.coversDependency(dependency)) {
// the dependency can only stay version-less when the
// flattened dependency management of the POM supplies
// the same version as the BOMs that apply to its scope
var scoped_version = resolution.overrideVersion(dependency);
if (scoped_version.equals(flattened_resolution.overrideVersion(dependency))) {
continue;
}
version = scoped_version;
} else {
version = new DependencyResolver(resolution, artifactRetriever(), dependencyRepositories(), dependency).latestVersion();
}
}
scoped_dependencies.add(new Dependency(dependency.groupId(), dependency.artifactId(), version,
dependency.classifier(), dependency.type(), dependency.exclusions(), dependency.parent()));
}
}
}
/** /**
* Part of the {@link #execute} operation, validates the publishing artifacts. * Part of the {@link #execute} operation, validates the publishing artifacts.
* *
@ -625,6 +723,7 @@ public class PublishOperation extends AbstractOperation<PublishOperation> {
offline(project.offline()); offline(project.offline());
properties(project.properties()); properties(project.properties());
artifactRetriever(project.artifactRetriever()); artifactRetriever(project.artifactRetriever());
dependencyRepositories(project.repositories());
dependencies().include(project.dependencies()); dependencies().include(project.dependencies());
artifacts(List.of( artifacts(List.of(
new PublishArtifact(new File(project.buildDistDirectory(), project.jarFileName()), "", TYPE_JAR), new PublishArtifact(new File(project.buildDistDirectory(), project.jarFileName()), "", TYPE_JAR),
@ -733,6 +832,33 @@ public class PublishOperation extends AbstractOperation<PublishOperation> {
return this; return this;
} }
/**
* Provides a list of repositories to resolve the dependencies against.
* <p>
* These are used to resolve the versions of dependencies that were
* declared without one, they are not publication targets.
*
* @param repositories a list of repositories against which dependencies will be resolved
* @return this operation instance
* @since 2.4.0
*/
public PublishOperation dependencyRepositories(List<Repository> repositories) {
dependencyRepositories_.addAll(repositories);
return this;
}
/**
* Retrieves the repositories that dependencies will be resolved against.
* <p>
* This is a modifiable list that can be retrieved and changed.
*
* @return the repositories used for dependency resolution
* @since 2.4.0
*/
public List<Repository> dependencyRepositories() {
return dependencyRepositories_;
}
/** /**
* Provides scoped dependencies to reference in the publication. * Provides scoped dependencies to reference in the publication.
* *

View file

@ -31,15 +31,47 @@ public class UpdatesOperation extends AbstractOperation<UpdatesOperation> {
*/ */
public void execute() { public void execute() {
var resolution = new VersionResolution(properties()); var resolution = new VersionResolution(properties());
var result = new DependencyScopes();
var scopes = new ArrayList<Scope>();
var dependencies = new ArrayList<Dependency>();
for (var entry : dependencies_.entrySet()) { for (var entry : dependencies_.entrySet()) {
var scope = entry.getKey(); var scoped_dependencies = entry.getValue();
for (var dependency : entry.getValue()) { for (var bom : scoped_dependencies.boms()) {
var latest = new DependencyResolver(resolution, artifactRetriever(), repositories(), dependency).latestVersion(); scopes.add(entry.getKey());
dependencies.add(bom);
}
// dependencies that are declared without a version and that are
// covered by a BOM that applies to their scope have their version
// pinned by that BOM, the BOM update itself carries their update
// signal, unless a bld.override supplies the version and takes
// that control away from the BOM
var scope_resolution = new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies_.effectiveBoms(entry.getKey()));
for (var dependency : scoped_dependencies) {
if (VersionNumber.UNKNOWN.equals(dependency.version()) &&
scope_resolution.coversDependency(dependency) &&
!scope_resolution.versionOverrides().containsKey(dependency.toArtifactString())) {
continue;
}
scopes.add(entry.getKey());
// an overridden version is the one the build uses, it's the
// baseline that update checks compare against
dependencies.add(scope_resolution.overrideDependency(dependency));
}
}
var latest_versions = new ParallelDependencyResolver(resolution, artifactRetriever(), repositories()).resolveLatestVersions(dependencies);
var result = new DependencyScopes();
for (var i = 0; i < dependencies.size(); ++i) {
var dependency = dependencies.get(i);
var latest = latest_versions.get(i);
if (latest.compareTo(dependency.version()) > 0) { if (latest.compareTo(dependency.version()) > 0) {
var latest_dependency = new Dependency(dependency.groupId(), dependency.artifactId(), latest, if (dependency instanceof Bom) {
dependency.classifier(), dependency.type()); result.scope(scopes.get(i)).include(new Bom(dependency.groupId(), dependency.artifactId(), latest));
result.scope(scope).include(latest_dependency); } else {
result.scope(scopes.get(i)).include(new Dependency(dependency.groupId(), dependency.artifactId(), latest,
dependency.classifier(), dependency.type()));
} }
} }
} }
@ -53,6 +85,9 @@ public class UpdatesOperation extends AbstractOperation<UpdatesOperation> {
for (var entry : result.entrySet()) { for (var entry : result.entrySet()) {
var scope = entry.getKey(); var scope = entry.getKey();
System.out.println(scope + ":"); System.out.println(scope + ":");
for (var bom : entry.getValue().boms()) {
System.out.println(" " + bom);
}
for (var dependency : entry.getValue()) { for (var dependency : entry.getValue()) {
System.out.println(" " + dependency); System.out.println(" " + dependency);
} }

View file

@ -12,6 +12,8 @@ import rife.tools.StringUtils;
import rife.tools.exceptions.FileUtilsErrorException; import rife.tools.exceptions.FileUtilsErrorException;
import java.io.File; import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import static rife.bld.dependencies.Dependency.TYPE_JAR; import static rife.bld.dependencies.Dependency.TYPE_JAR;
@ -152,6 +154,7 @@ public class PomBuilder {
} }
if (dependencies() != null && !dependencies().isEmpty()) { if (dependencies() != null && !dependencies().isEmpty()) {
addDependencyManagement(t);
addDependencies(t, Scope.compile); addDependencies(t, Scope.compile);
addDependencies(t, Scope.runtime); addDependencies(t, Scope.runtime);
addDependencies(t, Scope.provided); addDependencies(t, Scope.provided);
@ -172,6 +175,32 @@ public class PomBuilder {
FileUtils.writeString(pomBuilder.build(), file); FileUtils.writeString(pomBuilder.build(), file);
} }
private void addDependencyManagement(Template t) {
var boms = new LinkedHashMap<String, Bom>();
for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
var scoped_dependencies = dependencies().get(scope);
if (scoped_dependencies == null) {
continue;
}
for (var bom : scoped_dependencies.boms()) {
if (!bom.version().equals(VersionNumber.UNKNOWN)) {
boms.putIfAbsent(bom.toArtifactString(), bom);
}
}
}
if (boms.isEmpty()) {
return;
}
for (var bom : boms.values()) {
t.setValueEncoded("dependency-management-groupId", bom.groupId());
t.setValueEncoded("dependency-management-artifactId", bom.artifactId());
t.setValueEncoded("dependency-management-version", bom.version());
t.appendBlock("dependency-management", "dependency-management-entry");
}
t.setBlock("dependency-management-tag");
}
private void addDependencies(Template t, Scope scope) { private void addDependencies(Template t, Scope scope) {
var scoped_dependencies = dependencies().scope(scope); var scoped_dependencies = dependencies().scope(scope);
if (!scoped_dependencies.isEmpty()) { if (!scoped_dependencies.isEmpty()) {

View file

@ -40,6 +40,7 @@ public class Wrapper {
public static final String BUILD_ARGUMENT = "--build"; public static final String BUILD_ARGUMENT = "--build";
public static final String OFFLINE_ARGUMENT = "--offline"; public static final String OFFLINE_ARGUMENT = "--offline";
public static final String USE_STDERR_ARGUMENT = "--use-stderr";
public static final String WRAPPER_PREFIX = "bld-wrapper"; public static final String WRAPPER_PREFIX = "bld-wrapper";
public static final String WRAPPER_PROPERTIES = WRAPPER_PREFIX + ".properties"; public static final String WRAPPER_PROPERTIES = WRAPPER_PREFIX + ".properties";
@ -97,6 +98,11 @@ public class Wrapper {
* @since 1.5 * @since 1.5
*/ */
public static void main(String[] arguments) { public static void main(String[] arguments) {
if (Arrays.asList(arguments).contains(USE_STDERR_ARGUMENT)) {
// the wrapper diagnostics are sent to standard error so that
// standard output stays free for the MCP protocol
System.setOut(System.err);
}
System.exit(new Wrapper().installAndLaunch(new ArrayList<>(Arrays.asList(arguments)))); System.exit(new Wrapper().installAndLaunch(new ArrayList<>(Arrays.asList(arguments))));
} }

View file

@ -89,12 +89,16 @@ public class WrapperExtensionResolver {
private Set<String> transferExtensionDependencies() { private Set<String> transferExtensionDependencies() {
var filenames = new HashSet<String>(); var filenames = new HashSet<String>();
var dependencies = new DependencySet(); var roots = new ArrayList<Dependency>();
for (var d : dependencies_) { for (var d : dependencies_) {
if (d != null) { if (d != null) {
dependencies.addAll(new DependencyResolver(resolution_, retriever_, repositories_, d).getAllDependencies(Scope.compile, Scope.runtime)); roots.add(d);
} }
} }
if (!roots.isEmpty()) {
System.out.println("Analyzing bld extension dependencies...");
}
var dependencies = new ParallelDependencyResolver(resolution_, retriever_, repositories_).resolveAllDependencies(roots, Scope.compile, Scope.runtime);
if (!dependencies.isEmpty()) { if (!dependencies.isEmpty()) {
ensurePrintedHeader(); ensurePrintedHeader();

View file

@ -1 +1 @@
2.3.1-SNAPSHOT 2.4.0-SNAPSHOT

View file

@ -32,6 +32,24 @@
</properties> </properties>
<!--/b--> <!--/b-->
<!--v dependency-management-tag--><!--/v-->
<!--b dependency-management-tag-->
<dependencyManagement>
<dependencies>
<!--v dependency-management--><!--/v-->
<!--b dependency-management-entry-->
<dependency>
<groupId><!--v dependency-management-groupId--><!--/v--></groupId>
<artifactId><!--v dependency-management-artifactId--><!--/v--></artifactId>
<version><!--v dependency-management-version--><!--/v--></version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--/b-->
</dependencies>
</dependencyManagement>
<!--/b-->
<!--v dependencies-tag--><!--/v--> <!--v dependencies-tag--><!--/v-->
<!--b dependencies-tag--> <!--b dependencies-tag-->
<dependencies> <dependencies>

View file

@ -0,0 +1,319 @@
/*
* Copyright 2026 Erik C. Thauvin (https://erik.thauvin.net/)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import rife.bld.dependencies.Scope;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class BaseProjectTest {
private static Method ADD_LOCAL_JARS;
@TempDir
Path tempDir;
private BaseProject project;
@BeforeAll
static void initReflection() throws Exception {
ADD_LOCAL_JARS = BaseProject.class.getDeclaredMethod("addLocalJars", List.class, String.class);
ADD_LOCAL_JARS.setAccessible(true);
}
@BeforeEach
void beforeEach() {
project = new BaseProject();
project.workDirectory = tempDir.toFile();
project.createProjectStructure();
}
private void invokeAddLocalJars(List<File> jars, String path) throws Exception {
ADD_LOCAL_JARS.invoke(project, jars, path);
}
@Nested
@DisplayName("local(Path) and local(File)")
class LocalDependencyTest {
@Test
@DisplayName("local(Path) relative should resolve via workDirectory")
void localPathRelative() throws Exception {
var jar = Files.createFile(tempDir.resolve("a.jar"));
var dep = project.local(Path.of("a.jar"));
assertAll(
() -> assertNotNull(dep, "local(Path) should not return null"),
() -> assertEquals("a.jar", dep.path(), "relative Path should be kept as-is")
);
project.dependencies().scope(Scope.compile).include(dep);
assertTrue(project.compileClasspathJars().stream()
.anyMatch(f -> f.getAbsolutePath().equals(jar.toFile().getAbsolutePath())),
"compileClasspathJars should resolve relative path against workDirectory");
}
@Test
@DisplayName("local(Path) absolute should be kept absolute")
void localPathAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("abs.jar"));
var dep = project.local(jar);
assertAll(
() -> assertNotNull(dep, "local(Path) absolute should not return null"),
() -> assertEquals(jar.toString(), dep.path(), "absolute Path should be kept absolute")
);
project.dependencies().scope(Scope.compile).include(dep);
assertTrue(project.compileClasspathJars().stream()
.anyMatch(f -> f.getName().equals("abs.jar")),
"compileClasspathJars should include absolute Path dependency");
}
@Test
@DisplayName("local(File) relative file")
void localFileRelative() {
var dep = project.local(new File("b.jar"));
assertAll(
() -> assertNotNull(dep, "local(File) should not return null"),
() -> assertTrue(dep.path().endsWith("b.jar"), "should end with file name"),
() -> assertTrue(Path.of(dep.path()).isAbsolute(), "File overload uses getAbsolutePath()")
);
}
@Test
@DisplayName("local(File) absolute file")
void localFileAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("c.jar"));
var dep = project.local(jar.toFile());
assertAll(
() -> assertNotNull(dep, "local(File) absolute should not return null"),
() -> assertEquals(jar.toFile().getAbsolutePath(), dep.path(),
"should use File.getAbsolutePath()")
);
}
@Test
@DisplayName("local(File) directory should scan jars excluding sources/javadoc")
void localFileDirectory() throws Exception {
var dir = Files.createDirectory(tempDir.resolve("libs-dep-filter"));
Files.createFile(dir.resolve("one.jar"));
Files.createFile(dir.resolve("one-sources.jar"));
Files.createFile(dir.resolve("one-JAVADOC.jar"));
var dep = project.local(dir.toFile());
project.dependencies().scope(Scope.compile).include(dep);
var cp = project.compileClasspathJars();
assertAll(
() -> assertTrue(cp.stream().anyMatch(
f -> f.getName().equals("one.jar")), "regular jar should be included"),
() -> assertFalse(cp.stream().anyMatch(f -> f.getName().equals("one-sources.jar")),
"sources jar should be excluded (case-insensitive)"),
() -> assertFalse(cp.stream().anyMatch(
f -> f.getName().toLowerCase().contains("javadoc")),
"javadoc jar should be excluded")
);
}
@Test
@DisplayName("local(Path) and local(String) should behave same")
void parityWithStringOverload() throws Exception {
Files.createFile(tempDir.resolve("parity.jar"));
var fromString = project.local("parity.jar");
var fromPath = project.local(Path.of("parity.jar"));
var fromFile = project.local(new File("parity.jar"));
assertAll(
() -> assertEquals(fromString.path(), fromPath.path(),
"String and Path overloads should have same path()"),
() -> assertTrue(fromFile.path().endsWith("parity.jar"),
"File overload is absolute but ends with same name"),
() -> assertTrue(Path.of(fromFile.path()).isAbsolute(),
"File overload should be absolute")
);
}
}
@Nested
@DisplayName("addLocalJars - private implementation")
class LocalJarsTest {
@Test
@DisplayName("should add relative file path")
void relativeFile() throws Exception {
Files.createFile(tempDir.resolve("my.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "my.jar");
assertEquals(1, jars.size(), "relative file that exists should be added");
}
@Test
@DisplayName("should add absolute file path")
void absoluteFile() throws Exception {
var jar = Files.createFile(tempDir.resolve("abs2.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, jar.toAbsolutePath().toString());
assertEquals(1, jars.size(), "absolute file that exists should be added");
}
@Test
@DisplayName("should resolve relative to workDirectory")
void relativeToWorkDirectory() throws Exception {
var sub = Files.createDirectory(tempDir.resolve("sub"));
var jar = Files.createFile(sub.resolve("x.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "sub/x.jar");
assertEquals(jar.toFile().getAbsoluteFile(), jars.get(0).getAbsoluteFile(),
"should resolve against workDirectory");
}
@Test
@DisplayName("should scan directory for jars only")
void directoryScansJars() throws Exception {
var libDir = Files.createDirectory(tempDir.resolve("libs-jars-scan"));
Files.createFile(libDir.resolve("a.jar"));
Files.createFile(libDir.resolve("b.jar"));
Files.createFile(libDir.resolve("not-a-jar.txt"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "libs-jars-scan");
assertEquals(2, jars.size(), "should only include .jar files");
}
@Test
@DisplayName("should exclude -sources and -javadoc jars in directory")
void directoryExcludesSourcesAndJavadoc() throws Exception {
var libDir = Files.createDirectory(tempDir.resolve("libs2-filter"));
Files.createFile(libDir.resolve("foo.jar"));
Files.createFile(libDir.resolve("foo-sources.jar"));
Files.createFile(libDir.resolve("foo-javadoc.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "libs2-filter");
assertAll(
() -> assertEquals(1, jars.size(), "should filter out -sources and -javadoc"),
() -> assertEquals("foo.jar", jars.get(0).getName(), "only foo.jar should remain")
);
}
@Nested
@DisplayName("integration via public classpath methods")
class Integration {
@Test
@DisplayName("compileClasspathJars should include local file dependency")
void viaCompileClasspath() throws Exception {
var myJar = Files.createFile(tempDir.resolve("custom.jar"));
project.dependencies().scope(Scope.compile).include(project.local("custom.jar"));
var classpath = project.compileClasspathJars();
assertTrue(classpath.stream().anyMatch(
f -> f.getAbsolutePath().equals(myJar.toFile().getAbsolutePath())),
"compileClasspathJars should include local file dependency");
}
}
@Nested
@DisplayName("verbose branch")
class VerboseBranch {
@BeforeEach
void enableVerbose() {
project = new BaseProject() {
@Override
public boolean verbose() {
return true;
}
};
project.workDirectory = tempDir.toFile();
project.createProjectStructure();
}
@Test
@DisplayName("should skip non-existent path without throwing, even verbose")
void nonExistentPathSkipped() {
var jars = new ArrayList<File>();
assertDoesNotThrow(() -> invokeAddLocalJars(jars, "does-not-exist-verbose"),
"should not throw for missing path");
assertTrue(jars.isEmpty(), "non-existent path should be skipped");
}
}
}
@Nested
@DisplayName("localModule(Path) and localModule(File)")
class LocalModuleTest {
@Test
@DisplayName("localModule(Path) relative")
void localModulePathRelative() {
var mod = project.localModule(Path.of("mod.jar"));
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals("mod.jar", mod.path(), "relative path kept as-is")
);
}
@Test
@DisplayName("localModule(Path) absolute")
void localModulePathAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("modAbs.jar"));
var mod = project.localModule(jar);
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals(jar.toString(), mod.path(), "absolute path kept absolute")
);
}
@Test
@DisplayName("localModule(File) absolute")
void localModuleFileAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("mod2.jar"));
var mod = project.localModule(jar.toFile());
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals(jar.toFile().getAbsolutePath(), mod.path(),
"should use getAbsolutePath()")
);
}
@Test
@DisplayName("localModule(File) directory scan")
void localModuleFileDirectory() throws Exception {
var dir = Files.createDirectory(tempDir.resolve("mods-filter"));
Files.createFile(dir.resolve("m1.jar"));
Files.createFile(dir.resolve("m1-javadoc.jar"));
var mod = project.localModule(dir.toFile());
project.dependencies().scope(Scope.compile).include(mod);
var mp = project.compileModulePathJars();
assertAll(
() -> assertTrue(mp.stream().anyMatch(f -> f.getName().equals("m1.jar")),
"regular jar should be in module path"),
() -> assertFalse(mp.stream().anyMatch(f -> f.getName().contains("javadoc")),
"javadoc jar should be excluded")
);
}
@Test
@DisplayName("all three overloads are consistent")
void overloadsConsistent() throws Exception {
Files.createFile(tempDir.resolve("cons.jar"));
var s = project.localModule("cons.jar");
var p = project.localModule(Path.of("cons.jar"));
var f = project.localModule(new File("cons.jar"));
assertAll(
() -> assertEquals(s.path(), p.path(), "String and Path overloads equal"),
() -> assertTrue(f.path().endsWith("cons.jar"), "File overload ends with same name")
);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,15 +4,20 @@
*/ */
package rife.bld.dependencies; package rife.bld.dependencies;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import rife.ioc.HierarchicalProperties; import rife.ioc.HierarchicalProperties;
import rife.tools.FileUtils; import rife.tools.FileUtils;
import rife.tools.StringUtils; import rife.tools.StringUtils;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static rife.bld.dependencies.Dependency.CLASSIFIER_JAVADOC; import static rife.bld.dependencies.Dependency.CLASSIFIER_JAVADOC;
@ -2341,4 +2346,98 @@ public class TestDependencyResolver {
FileUtils.deleteDirectory(tmp2); FileUtils.deleteDirectory(tmp2);
} }
} }
@Test
void testGetAllDependenciesParallelPomPrefetching() throws Exception {
var max_concurrent_retrievals = new AtomicInteger();
var server = createPomServer(max_concurrent_retrievals);
server.start();
try {
var repositories = List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/"));
var root = new Dependency("com.example", "root", new VersionNumber(1, 0, 0));
// the caching retriever enables parallel POM prefetching
var resolver = new DependencyResolver(VersionResolution.dummy(), ArtifactRetriever.cachingInstance(), repositories, root);
assertEquals(StringUtils.convertLineSeparator("""
com.example:root:1.0.0
com.example:child1:1.0.0
com.example:child2:1.0.0
com.example:child3:1.0.0
com.example:child4:1.0.0
com.example:child5:1.0.0
com.example:child6:1.0.0
com.example:shared:1.0.0"""), StringUtils.join(resolver.getAllDependencies(compile), System.lineSeparator()));
assertTrue(max_concurrent_retrievals.get() > 1, "expected concurrent POM retrievals, max was " + max_concurrent_retrievals.get());
} finally {
server.stop(0);
}
}
@Test
void testGetAllDependenciesSequentialWithoutCachingRetriever() throws Exception {
var max_concurrent_retrievals = new AtomicInteger();
var server = createPomServer(max_concurrent_retrievals);
server.start();
try {
var repositories = List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/"));
var root = new Dependency("com.example", "root", new VersionNumber(1, 0, 0));
// the uncached retriever disables prefetching, retrievals stay sequential
var resolver = new DependencyResolver(VersionResolution.dummy(), ArtifactRetriever.instance(), repositories, root);
assertEquals(8, resolver.getAllDependencies(compile).size());
assertEquals(1, max_concurrent_retrievals.get(), "expected sequential POM retrievals, max was " + max_concurrent_retrievals.get());
} finally {
server.stop(0);
}
}
private static HttpServer createPomServer(AtomicInteger maxConcurrentRetrievals)
throws IOException {
var graph = new HashMap<String, List<String>>();
graph.put("root", List.of("child1", "child2", "child3", "child4", "child5", "child6"));
graph.put("child1", List.of("shared"));
var active_retrievals = new AtomicInteger();
var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var active = active_retrievals.incrementAndGet();
maxConcurrentRetrievals.accumulateAndGet(active, Math::max);
try {
// delay the response so that parallel retrievals overlap
Thread.sleep(100);
// serve the POM of the artifact in the request path
var segments = exchange.getRequestURI().getPath().split("/");
var artifact = segments[segments.length - 3];
var body = buildPom(artifact, graph.getOrDefault(artifact, List.of())).getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
active_retrievals.decrementAndGet();
}
});
server.setExecutor(Executors.newCachedThreadPool());
return server;
}
private static String buildPom(String artifact, List<String> children) {
var dependencies = new StringBuilder();
for (var child : children) {
dependencies.append("<dependency><groupId>com.example</groupId><artifactId>")
.append(child)
.append("</artifactId><version>1.0.0</version></dependency>");
}
return """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>%s</artifactId>
<version>1.0.0</version>
<dependencies>%s</dependencies>
</project>""".formatted(artifact, dependencies);
}
} }

View file

@ -5,11 +5,16 @@
package rife.bld.dependencies; package rife.bld.dependencies;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import rife.ioc.HierarchicalProperties;
import rife.tools.FileUtils;
import rife.tools.StringUtils; import rife.tools.StringUtils;
import java.nio.file.Files;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static rife.bld.dependencies.TransferTestHelper.*;
import static rife.bld.dependencies.RepositoryTestHelper.getNextRepository; import static rife.bld.dependencies.RepositoryTestHelper.getNextRepository;
import static rife.bld.dependencies.Scope.compile; import static rife.bld.dependencies.Scope.compile;
import static rife.bld.dependencies.Scope.runtime; import static rife.bld.dependencies.Scope.runtime;
@ -373,4 +378,83 @@ public class TestDependencySet {
org.json:json:20250107 org.json:json:20250107
"""), dependencies.generateTransitiveDependencyTree(VersionResolution.dummy(), ArtifactRetriever.instance(), RepositoryTestHelper.getNextRepositories(), compile, runtime)); """), dependencies.generateTransitiveDependencyTree(VersionResolution.dummy(), ArtifactRetriever.instance(), RepositoryTestHelper.getNextRepositories(), compile, runtime));
} }
@Test
void testCopyAndIncludePreserveAllContents() {
var set = new DependencySet()
.include(new Dependency("com.uwyn.rife2", "rife2", new VersionNumber(1, 9, 1)))
.include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12)))
.include(new LocalDependency("lib/local.jar"))
.include(new LocalModule("lib/module.jar"));
var copy = new DependencySet(set);
assertEquals(set, copy);
assertEquals(set.boms(), copy.boms());
assertEquals(set.localDependencies(), copy.localDependencies());
assertEquals(set.localModules(), copy.localModules());
var included = new DependencySet().include(set);
assertEquals(set, included);
assertEquals(set.boms(), included.boms());
assertEquals(set.localDependencies(), included.localDependencies());
assertEquals(set.localModules(), included.localModules());
var scopes = new DependencyScopes();
scopes.scope(Scope.compile).include(set);
var scopes_copy = new DependencyScopes();
scopes_copy.include(scopes);
assertEquals(set.boms(), scopes_copy.scope(Scope.compile).boms());
assertEquals(set.localDependencies(), scopes_copy.scope(Scope.compile).localDependencies());
var scopes_ctor_copy = new DependencyScopes(scopes);
assertEquals(set.boms(), scopes_ctor_copy.scope(Scope.compile).boms());
assertEquals(set.localDependencies(), scopes_ctor_copy.scope(Scope.compile).localDependencies());
assertEquals(set.localModules(), scopes_ctor_copy.scope(Scope.compile).localModules());
}
@Test
void testTransferIntoDirectoryParallel() throws Exception {
var max_concurrent_transfers = new AtomicInteger();
var server = createTransferServer(max_concurrent_transfers);
server.start();
var tmp = Files.createTempDirectory("transfers").toFile();
try {
var dependencies = createTransferDependencies(1, 6);
var repositories = List.of(transferRepository(server));
var artifacts = dependencies.transferIntoDirectory(new VersionResolution(null), ArtifactRetriever.instance(), repositories, tmp, tmp);
assertTransferredArtifacts(dependencies, artifacts, tmp);
assertTrue(max_concurrent_transfers.get() > 1, "expected concurrent transfers, max was " + max_concurrent_transfers.get());
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testTransferIntoDirectorySequential() throws Exception {
var max_concurrent_transfers = new AtomicInteger();
var server = createTransferServer(max_concurrent_transfers);
server.start();
var tmp = Files.createTempDirectory("transfers").toFile();
try {
var properties = new HierarchicalProperties();
properties.put(VersionResolution.PROPERTY_TRANSFER_PARALLELISM, "1");
var resolution = new VersionResolution(properties);
assertEquals(1, resolution.transferParallelism());
var dependencies = createTransferDependencies(1, 6);
var repositories = List.of(transferRepository(server));
var artifacts = dependencies.transferIntoDirectory(resolution, ArtifactRetriever.instance(), repositories, tmp, tmp);
assertTransferredArtifacts(dependencies, artifacts, tmp);
assertEquals(1, max_concurrent_transfers.get(), "expected sequential transfers, max was " + max_concurrent_transfers.get());
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
} }

View file

@ -0,0 +1,143 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import org.junit.jupiter.api.Test;
import rife.bld.dependencies.exceptions.DependencyTransferException;
import rife.tools.FileUtils;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
import static rife.bld.dependencies.TransferTestHelper.*;
public class TestDependencyTransferBatch {
@Test
void testTransferAcrossSets() throws Exception {
var max_concurrent_transfers = new AtomicInteger();
var server = createTransferServer(max_concurrent_transfers);
server.start();
var tmp1 = Files.createTempDirectory("transfers1").toFile();
var tmp2 = Files.createTempDirectory("transfers2").toFile();
try {
var set1 = createTransferDependencies(1, 3);
var set2 = createTransferDependencies(4, 6);
var repositories = List.of(transferRepository(server));
var batch = new DependencyTransferBatch();
batch.add(set1, tmp1, tmp1)
.add(set2, tmp2, tmp2)
// identical transfers into the same directory are only performed once
.add(set1, tmp1, tmp1);
var artifacts = batch.transfer(new VersionResolution(null), ArtifactRetriever.instance(), repositories);
assertEquals(6, artifacts.size());
assertTransferredArtifacts(set1, artifacts.subList(0, 3), tmp1);
assertTransferredArtifacts(set2, artifacts.subList(3, 6), tmp2);
assertTrue(max_concurrent_transfers.get() > 1, "expected concurrent transfers, max was " + max_concurrent_transfers.get());
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp1);
FileUtils.deleteDirectory(tmp2);
}
}
@Test
void testTransferEmptiesTheBatch() throws Exception {
var server = createTransferServer(new AtomicInteger());
server.start();
var tmp = Files.createTempDirectory("transfers").toFile();
try {
var repositories = List.of(transferRepository(server));
var resolution = new VersionResolution(null);
var batch = new DependencyTransferBatch();
var set1 = createTransferDependencies(1, 3);
batch.add(set1, tmp, tmp);
assertTransferredArtifacts(set1, batch.transfer(resolution, ArtifactRetriever.instance(), repositories), tmp);
// the batch was emptied, nothing is transferred again
assertTrue(batch.transfer(resolution, ArtifactRetriever.instance(), repositories).isEmpty());
// the same instance can collect and transfer again,
// including targets that were transferred before
var set2 = createTransferDependencies(3, 5);
batch.add(set2, tmp, tmp);
assertTransferredArtifacts(set2, batch.transfer(resolution, ArtifactRetriever.instance(), repositories), tmp);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testTransferModularJarsIntoModulesDirectory() throws Exception {
var server = createTransferServer(new AtomicInteger());
server.start();
var artifacts_dir = Files.createTempDirectory("artifacts").toFile();
var modules_dir = Files.createTempDirectory("modules").toFile();
try {
var dependencies = new DependencySet()
.include(new Dependency("com.example", "artifact1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "module1", new VersionNumber(1, 0, 0), "", Dependency.TYPE_MODULAR_JAR));
var artifacts = new DependencyTransferBatch()
.add(dependencies, artifacts_dir, modules_dir)
.transfer(new VersionResolution(null), ArtifactRetriever.instance(), List.of(transferRepository(server)));
assertEquals(2, artifacts.size());
assertTrue(new File(artifacts_dir, "artifact1-1.0.0.jar").exists());
assertTrue(new File(modules_dir, "module1-1.0.0.jar").exists());
assertFalse(new File(artifacts_dir, "module1-1.0.0.jar").exists());
} finally {
server.stop(0);
FileUtils.deleteDirectory(artifacts_dir);
FileUtils.deleteDirectory(modules_dir);
}
}
@Test
void testTransferClassifiers() throws Exception {
var server = createTransferServer(new AtomicInteger());
server.start();
var tmp = Files.createTempDirectory("transfers").toFile();
try {
var dependencies = createTransferDependencies(1, 2);
var artifacts = new DependencyTransferBatch()
.add(dependencies, tmp, tmp, "sources")
.transfer(new VersionResolution(null), ArtifactRetriever.instance(), List.of(transferRepository(server)));
assertEquals(4, artifacts.size());
for (var i = 1; i <= 2; i++) {
assertTrue(new File(tmp, "artifact" + i + "-1.0.0.jar").exists());
assertTrue(new File(tmp, "artifact" + i + "-1.0.0-sources.jar").exists());
}
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testMissingDirectories() {
var regular = new DependencySet()
.include(new Dependency("com.example", "artifact1", new VersionNumber(1, 0, 0)));
var modular = new DependencySet()
.include(new Dependency("com.example", "module1", new VersionNumber(1, 0, 0), "", Dependency.TYPE_MODULAR_JAR));
assertThrows(DependencyTransferException.class, () -> new DependencyTransferBatch().add(regular, null, null));
assertThrows(DependencyTransferException.class, () -> new DependencyTransferBatch().add(modular, Files.createTempDirectory("artifacts").toFile(), null));
}
@Test
void testEmptyBatch() {
var artifacts = new DependencyTransferBatch()
.transfer(new VersionResolution(null), ArtifactRetriever.instance(), List.of());
assertTrue(artifacts.isEmpty());
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld.dependencies;
import com.sun.net.httpserver.HttpServer;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Provides a local artifact server and dependency fixtures for testing
* transfers without relying on remote repositories.
*/
abstract class TransferTestHelper {
static HttpServer createTransferServer(AtomicInteger maxConcurrentTransfers)
throws IOException {
var active_transfers = new AtomicInteger();
var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var active = active_transfers.incrementAndGet();
maxConcurrentTransfers.accumulateAndGet(active, Math::max);
try {
// delay the response so that parallel transfers overlap
Thread.sleep(200);
var body = exchange.getRequestURI().getPath().getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
active_transfers.decrementAndGet();
}
});
server.setExecutor(Executors.newCachedThreadPool());
return server;
}
static Repository transferRepository(HttpServer server) {
return new Repository("http://localhost:" + server.getAddress().getPort() + "/");
}
static DependencySet createTransferDependencies(int from, int to) {
var dependencies = new DependencySet();
for (var i = from; i <= to; i++) {
dependencies.include(new Dependency("com.example", "artifact" + i, new VersionNumber(1, 0, 0)));
}
return dependencies;
}
static void assertTransferredArtifacts(DependencySet dependencies, List<RepositoryArtifact> artifacts, File directory) {
assertEquals(dependencies.size(), artifacts.size());
var index = 0;
for (var dependency : dependencies) {
var filename = dependency.artifactId() + "-" + dependency.version() + ".jar";
assertTrue(artifacts.get(index).location().endsWith(filename), "expected artifact " + filename + " at index " + index);
assertTrue(new File(directory, filename).exists(), "expected file " + filename + " to be transferred");
++index;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.condition.OS;
import rife.bld.Project; import rife.bld.Project;
import rife.bld.WebProject;
import rife.bld.blueprints.AppProjectBlueprint; import rife.bld.blueprints.AppProjectBlueprint;
import rife.bld.dependencies.*; import rife.bld.dependencies.*;
import rife.bld.publish.PublishArtifact; import rife.bld.publish.PublishArtifact;
import rife.bld.publish.PublishInfo;
import rife.tools.FileUtils; import rife.tools.FileUtils;
import rife.tools.exceptions.FileUtilsErrorException; import rife.tools.exceptions.FileUtilsErrorException;
@ -704,4 +706,428 @@ public class TestPublishOperation {
FileUtils.deleteDirectory(tmp1); FileUtils.deleteDirectory(tmp1);
} }
} }
@Test
void testPublishLocalWithBom()
throws Exception {
var poms = java.util.Map.of(
"bom1:1.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bom1</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.4.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""");
var metadata = java.util.Map.of(
"b", """
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.example</groupId>
<artifactId>b</artifactId>
<versioning>
<latest>2.2.0</latest>
<release>2.2.0</release>
<versions><version>2.2.0</version></versions>
</versioning>
</metadata>""");
var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
String content = null;
if (filename.endsWith(".pom") && segments.length >= 3) {
content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
} else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
content = metadata.get(segments[segments.length - 2]);
}
if (content == null) {
exchange.sendResponseHeaders(404, -1);
} else {
var body = content.getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
}
exchange.close();
});
server.start();
var tmp_local = Files.createTempDirectory("bomlocal").toFile();
var artifact_file = File.createTempFile("myapp", ".jar");
try {
var operation = new PublishOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
.repositories(new Repository(tmp_local.getAbsolutePath()))
.dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
.info(new PublishInfo()
.groupId("test.pkg")
.artifactId("myapp")
.version(new VersionNumber(3, 3, 3)))
.artifacts(List.of(new PublishArtifact(artifact_file, "", "jar")));
operation.dependencies().scope(Scope.compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"))
.include(new Dependency("com.example", "b"));
operation.execute();
var pom = FileUtils.readString(Path.of(tmp_local.getAbsolutePath(), "test", "pkg", "myapp", "3.3.3", "myapp-3.3.3.pom").toFile());
// the BOM is imported through dependency management
assertTrue(pom.contains("<dependencyManagement>"), pom);
assertTrue(pom.contains("<artifactId>bom1</artifactId>"), pom);
assertTrue(pom.contains("<version>1.0.0</version>"), pom);
assertTrue(pom.contains("<type>pom</type>"), pom);
assertTrue(pom.contains("<scope>import</scope>"), pom);
// the covered dependency stays version-less,
// the BOM import provides its version downstream
assertTrue(pom.contains("<artifactId>a</artifactId>"), pom);
assertFalse(java.util.regex.Pattern.compile("<artifactId>a</artifactId>\\s*<version>").matcher(pom).find(), pom);
// the uncovered dependency is frozen to its resolved version
assertTrue(java.util.regex.Pattern.compile("<artifactId>b</artifactId>\\s*<version>2\\.2\\.0</version>").matcher(pom).find(), pom);
} finally {
server.stop(0);
artifact_file.delete();
FileUtils.deleteDirectory(tmp_local);
}
}
@Test
void testResolveVersionlessDependenciesWithOverrides()
throws Exception {
var poms = java.util.Map.of(
"bom1:1.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bom1</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.4.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""",
"bom2:2.5.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bom2</artifactId>
<version>2.5.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>d</artifactId><version>4.0.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""");
var metadata = java.util.Map.of(
"b", """
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.example</groupId>
<artifactId>b</artifactId>
<versioning>
<latest>2.2.0</latest>
<release>2.2.0</release>
<versions><version>2.2.0</version></versions>
</versioning>
</metadata>""");
var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
String content = null;
if (filename.endsWith(".pom") && segments.length >= 3) {
content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
} else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
content = metadata.get(segments[segments.length - 2]);
}
if (content == null) {
exchange.sendResponseHeaders(404, -1);
} else {
var body = content.getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
}
exchange.close();
});
server.start();
try {
var properties = new rife.ioc.HierarchicalProperties();
properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX,
"com.example:a:1.9.9,com.example:b:1.1.1,com.example:bom2:2.5.0");
var scopes = new DependencyScopes();
scopes.scope(Scope.compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"))
.include(new Dependency("com.example", "b"));
scopes.scope(Scope.runtime)
.include(new Bom("com.example", "bom2"));
var operation = new PublishOperation()
.properties(properties)
.artifactRetriever(ArtifactRetriever.cachingInstance())
.dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
.dependencies(scopes);
operation.executeResolveVersionlessDependencies();
var compile_scope = operation.dependencies().scope(Scope.compile);
// the override is what the build resolves, it's frozen into the
// POM even though the BOM covers the dependency
assertEquals(new VersionNumber(1, 9, 9), compile_scope.get(new Dependency("com.example", "a")).version());
// an uncovered dependency freezes to its override instead of
// the latest version
assertEquals(new VersionNumber(1, 1, 1), compile_scope.get(new Dependency("com.example", "b")).version());
// a version-less BOM freezes to its override
var runtime_boms = operation.dependencies().scope(Scope.runtime).boms();
assertEquals(new Bom("com.example", "bom2", new VersionNumber(2, 5, 0)), runtime_boms.iterator().next());
} finally {
server.stop(0);
}
}
@Test
void testResolveVersionlessDependenciesFreezesFlattenedConflicts()
throws Exception {
var poms = java.util.Map.of(
"bomr:1.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bomr</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.0.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""",
"bomp:1.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bomp</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>2.0.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""");
var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
String content = null;
if (filename.endsWith(".pom") && segments.length >= 3) {
content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
}
if (content == null) {
exchange.sendResponseHeaders(404, -1);
} else {
var body = content.getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
}
exchange.close();
});
server.start();
try {
var scopes = new DependencyScopes();
scopes.scope(Scope.runtime)
.include(new Bom("com.example", "bomr", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
scopes.scope(Scope.provided)
.include(new Bom("com.example", "bomp", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
var operation = new PublishOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
.dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
.dependencies(scopes);
operation.executeResolveVersionlessDependencies();
// the flattened dependency management of the POM imports the
// runtime BOM first and would supply 1.0.0, the runtime scope
// agrees with that so its dependency stays version-less
assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.runtime).get(new Dependency("com.example", "a")).version());
// the provided scope resolves 2.0.0 through its own BOM, that
// conflicts with the flattened list so the version is frozen
assertEquals(new VersionNumber(2, 0, 0), operation.dependencies().scope(Scope.provided).get(new Dependency("com.example", "a")).version());
} finally {
server.stop(0);
}
}
@Test
void testResolveVersionlessDependenciesUntouchedScopes() {
// no network access is needed when nothing has to be resolved :
// explicitly versioned dependencies are untouched and the test
// scope isn't part of the publication
var scopes = new DependencyScopes();
scopes.scope(Scope.compile)
.include(new Dependency("com.example", "c", new VersionNumber(5, 0, 0)));
scopes.scope(Scope.test)
.include(new Dependency("com.example", "t"));
var operation = new PublishOperation()
.dependencies(scopes);
operation.executeResolveVersionlessDependencies();
assertEquals(new VersionNumber(5, 0, 0), operation.dependencies().scope(Scope.compile).get(new Dependency("com.example", "c")).version());
assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.test).get(new Dependency("com.example", "t")).version());
}
static class BomPublishProject extends WebProject {
BomPublishProject(File tmp) {
workDirectory = tmp;
pkg = "test.pkg";
name = "myapp";
version = new VersionNumber(0, 0, 1);
repositories = List.of(new Repository("https://example.com/resolve"));
}
}
@Test
void testFromProjectDependencyRepositories()
throws Exception {
var tmp = Files.createTempDirectory("test").toFile();
try {
var project = new BomPublishProject(tmp);
var operation = new PublishOperation()
.fromProject(project);
assertEquals(project.repositories(), operation.dependencyRepositories());
} finally {
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testResolveVersionlessDependencies()
throws Exception {
var poms = java.util.Map.of(
"bom1:1.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bom1</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.4.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""",
"bom2:3.0.0", """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>bom2</artifactId>
<version>3.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency><groupId>com.example</groupId><artifactId>d</artifactId><version>4.0.0</version></dependency>
</dependencies>
</dependencyManagement>
</project>""");
var metadata = java.util.Map.of(
"b", """
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.example</groupId>
<artifactId>b</artifactId>
<versioning>
<latest>2.2.0</latest>
<release>2.2.0</release>
<versions><version>2.2.0</version></versions>
</versioning>
</metadata>""",
"bom2", """
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.example</groupId>
<artifactId>bom2</artifactId>
<versioning>
<latest>3.0.0</latest>
<release>3.0.0</release>
<versions><version>3.0.0</version></versions>
</versioning>
</metadata>""");
var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
String content = null;
if (filename.endsWith(".pom") && segments.length >= 3) {
content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
} else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
content = metadata.get(segments[segments.length - 2]);
}
if (content == null) {
exchange.sendResponseHeaders(404, -1);
} else {
var body = content.getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
}
exchange.close();
});
server.start();
try {
var scopes = new DependencyScopes();
scopes.scope(Scope.compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"))
.include(new Dependency("com.example", "b"))
.include(new Dependency("com.example", "c", new VersionNumber(5, 0, 0)));
scopes.scope(Scope.runtime)
.include(new Bom("com.example", "bom2"));
scopes.scope(Scope.provided)
// covered by the compile scope BOM through scope inheritance
.include(new Dependency("com.example", "a"));
var operation = new PublishOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
.dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
.dependencies(scopes);
operation.executeResolveVersionlessDependencies();
var compile_scope = operation.dependencies().scope(Scope.compile);
// covered by the BOM : stays version-less for the POM,
// the dependency management import provides the version
assertEquals(VersionNumber.UNKNOWN, compile_scope.get(new Dependency("com.example", "a")).version());
// not covered : resolved to its latest version
assertEquals(new VersionNumber(2, 2, 0), compile_scope.get(new Dependency("com.example", "b")).version());
// explicitly versioned : untouched
assertEquals(new VersionNumber(5, 0, 0), compile_scope.get(new Dependency("com.example", "c")).version());
// a version-less BOM is resolved to its latest version
var runtime_boms = operation.dependencies().scope(Scope.runtime).boms();
assertEquals(new Bom("com.example", "bom2", new VersionNumber(3, 0, 0)), runtime_boms.iterator().next());
// the compile scope BOM also covers the provided scope,
// its version-less dependency stays version-less
assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.provided).get(new Dependency("com.example", "a")).version());
} finally {
server.stop(0);
}
}
} }

View file

@ -4,14 +4,18 @@
*/ */
package rife.bld.operations; package rife.bld.operations;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import rife.bld.WebProject; import rife.bld.WebProject;
import rife.bld.dependencies.*; import rife.bld.dependencies.*;
import rife.tools.FileUtils; import rife.tools.FileUtils;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -168,4 +172,123 @@ public class TestUpdatesOperation {
FileUtils.deleteDirectory(tmp); FileUtils.deleteDirectory(tmp);
} }
} }
@Test
void testBomUpdateReported()
throws Exception {
var server = createArtifactServer(
Map.of("bom1:1.0.0", bomPom("bom1", "1.0.0", "<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.4.0</version></dependency>")),
Map.of("bom1", metadata("bom1", "2.0.0", "1.0.0", "2.0.0"),
"a", metadata("a", "9.9.9", "1.4.0", "9.9.9")));
server.start();
try {
var operation = new UpdatesOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
.repositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")));
operation.dependencies().scope(Scope.compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
operation.execute();
// the newer BOM version is reported
var boms = operation.updates().scope(Scope.compile).boms();
assertEquals(1, boms.size());
assertEquals(new Bom("com.example", "bom1", new VersionNumber(2, 0, 0)), boms.iterator().next());
// the version-less dependency covered by the BOM is not reported,
// even though newer versions exist in the metadata
assertTrue(operation.updates().scope(Scope.compile).isEmpty());
} finally {
server.stop(0);
}
}
@Test
void testExplicitVersionCheckedDespiteBom()
throws Exception {
var server = createArtifactServer(
Map.of("bom1:1.0.0", bomPom("bom1", "1.0.0", "<dependency><groupId>com.example</groupId><artifactId>a</artifactId><version>1.4.0</version></dependency>")),
Map.of("bom1", metadata("bom1", "1.0.0", "1.0.0"),
"a", metadata("a", "2.0.0", "1.0.0", "2.0.0")));
server.start();
try {
var operation = new UpdatesOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
.repositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")));
operation.dependencies().scope(Scope.compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
operation.execute();
// the BOM is up to date and not reported
assertTrue(operation.updates().scope(Scope.compile).boms().isEmpty());
// the explicitly versioned dependency is still checked for
// updates, even though the BOM covers it
var dependencies = operation.updates().scope(Scope.compile);
assertEquals(1, dependencies.size());
assertEquals(new VersionNumber(2, 0, 0), dependencies.iterator().next().version());
} finally {
server.stop(0);
}
}
private static HttpServer createArtifactServer(Map<String, String> poms, Map<String, String> metadata)
throws IOException {
var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
String content = null;
if (filename.endsWith(".pom") && segments.length >= 3) {
content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
} else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
content = metadata.get(segments[segments.length - 2]);
}
if (content == null) {
exchange.sendResponseHeaders(404, -1);
} else {
var body = content.getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
}
exchange.close();
});
return server;
}
private static String bomPom(String artifact, String version, String managedDependencies) {
return """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>%s</artifactId>
<version>%s</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>%s</dependencies>
</dependencyManagement>
</project>""".formatted(artifact, version, managedDependencies);
}
private static String metadata(String artifact, String latest, String... versions) {
var versions_xml = new StringBuilder();
for (var version : versions) {
versions_xml.append("<version>").append(version).append("</version>");
}
return """
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.example</groupId>
<artifactId>%s</artifactId>
<versioning>
<latest>%s</latest>
<release>%s</release>
<versions>%s</versions>
</versioning>
</metadata>""".formatted(artifact, latest, latest, versions_xml);
}
} }

View file

@ -652,4 +652,93 @@ public class TestPomBuilder {
</project> </project>
""", builder.build()); """, builder.build());
} }
@Test
void testDependencyManagementScopeBoundaries() {
var builder = new PomBuilder();
builder.dependencies().scope(Scope.provided)
.include(new Bom("org.eclipse.jetty", "jetty-bom", new VersionNumber(12, 0, 16)));
builder.dependencies().scope(Scope.test)
// the test scope isn't published, its BOMs are not emitted
.include(new Bom("org.junit", "junit-bom", new VersionNumber(6, 1, 1)));
assertEquals("""
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<name></name>
<description></description>
<url></url>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-bom</artifactId>
<version>12.0.16</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
</dependencies>
</project>
""", builder.build());
}
@Test
void testDependencyManagement() {
var builder = new PomBuilder();
builder.dependencies().scope(Scope.compile)
.include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12)))
.include(new Dependency("io.vertx", "vertx-core"));
builder.dependencies().scope(Scope.runtime)
// duplicate BOMs are only emitted once
.include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12)))
.include(new Bom("org.junit", "junit-bom", new VersionNumber(6, 1, 1)));
builder.dependencies().scope(Scope.provided)
// BOMs without a version are not emitted
.include(new Bom("com.example", "versionless-bom"));
assertEquals("""
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<name></name>
<description></description>
<url></url>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>4.5.12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
""", builder.build());
}
} }