mirror of
https://github.com/rife2/bld
synced 2026-08-04 15:07:46 +02:00
Compare commits
25 commits
385b0cb2a8
...
f2a568a654
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a568a654 | ||
|
|
09418fb979 | ||
|
|
fcb348615e | ||
|
|
124c7695e9 | ||
|
|
736372673e | ||
|
|
025e68e9e6 | ||
|
|
5083ecef78 | ||
|
|
cdd4efa460 | ||
|
|
62c0d4569d | ||
|
|
ec185788a1 | ||
|
|
7d77f48aec | ||
|
|
d7d176eef6 | ||
|
|
c9298d3de6 | ||
|
|
49e21cfeb8 | ||
|
|
f6aeaa090d | ||
|
|
734000e6dd | ||
|
|
2bfcc1bb78 | ||
|
|
d307e06a5d | ||
|
|
734ac585ab | ||
|
|
27c45033fb | ||
|
|
b2987b6b23 | ||
|
|
1e76ee31c1 | ||
|
|
4447610d02 | ||
|
|
f3f4799190 | ||
|
|
5859a07a4c |
2
core
2
core
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5f977636531d1b978b762a615d720097baa37830
|
Subproject commit fb2e5022e5c1d125e45227b49076c2b200763986
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
66
src/main/java/rife/bld/dependencies/Bom.java
Normal file
66
src/main/java/rife/bld/dependencies/Bom.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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_;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
129
src/main/java/rife/bld/dependencies/DependencyTransferBatch.java
Normal file
129
src/main/java/rife/bld/dependencies/DependencyTransferBatch.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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_;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/main/java/rife/bld/dependencies/ParallelExecution.java
Normal file
64
src/main/java/rife/bld/dependencies/ParallelExecution.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/main/java/rife/bld/dependencies/PomPrefetcher.java
Normal file
72
src/main/java/rife/bld/dependencies/PomPrefetcher.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/main/java/rife/bld/dependencies/TransferOutput.java
Normal file
185
src/main/java/rife/bld/dependencies/TransferOutput.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,10 +342,16 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -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_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
|
|
|
||||||
47
src/main/java/rife/bld/help/McpHelp.java
Normal file
47
src/main/java/rife/bld/help/McpHelp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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_;
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
121
src/main/java/rife/bld/operations/McpControl.java
Normal file
121
src/main/java/rife/bld/operations/McpControl.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
1496
src/main/java/rife/bld/operations/McpOperation.java
Normal file
1496
src/main/java/rife/bld/operations/McpOperation.java
Normal file
File diff suppressed because it is too large
Load diff
180
src/main/java/rife/bld/operations/McpToolRunner.java
Normal file
180
src/main/java/rife/bld/operations/McpToolRunner.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
if (latest.compareTo(dependency.version()) > 0) {
|
dependencies.add(bom);
|
||||||
var latest_dependency = new Dependency(dependency.groupId(), dependency.artifactId(), latest,
|
}
|
||||||
dependency.classifier(), dependency.type());
|
|
||||||
result.scope(scope).include(latest_dependency);
|
// 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 (dependency instanceof Bom) {
|
||||||
|
result.scope(scopes.get(i)).include(new Bom(dependency.groupId(), dependency.artifactId(), latest));
|
||||||
|
} 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.3.1-SNAPSHOT
|
2.4.0-SNAPSHOT
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
319
src/test/java/rife/bld/BaseProjectTest.java
Normal file
319
src/test/java/rife/bld/BaseProjectTest.java
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1278
src/test/java/rife/bld/dependencies/TestBom.java
Normal file
1278
src/test/java/rife/bld/dependencies/TestBom.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/test/java/rife/bld/dependencies/TransferTestHelper.java
Normal file
71
src/test/java/rife/bld/dependencies/TransferTestHelper.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1248
src/test/java/rife/bld/operations/TestMcpOperation.java
Normal file
1248
src/test/java/rife/bld/operations/TestMcpOperation.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue