mirror of
https://github.com/rife2/bld
synced 2026-08-05 07:07:48 +02:00
Compare commits
1 commit
4a7d39ec5a
...
e6c933eb6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6c933eb6a |
2
core
2
core
|
|
@ -1 +1 @@
|
||||||
Subproject commit fb2e5022e5c1d125e45227b49076c2b200763986
|
Subproject commit 9d73b7bd044df1df94907554c6807c6305ba113e
|
||||||
|
|
@ -395,7 +395,6 @@ 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();
|
||||||
|
|
@ -443,16 +442,6 @@ 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.
|
||||||
*
|
*
|
||||||
|
|
@ -557,18 +546,6 @@ 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.
|
||||||
*
|
*
|
||||||
|
|
@ -933,7 +910,8 @@ 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.
|
||||||
|
|
@ -942,9 +920,8 @@ 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.4.0
|
* @since 2.3.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public LocalDependency local(Path path) {
|
public LocalDependency local(Path path) {
|
||||||
return new LocalDependency(path.toString());
|
return new LocalDependency(path.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -954,7 +931,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.4.0
|
* @since 2.3.1
|
||||||
*/
|
*/
|
||||||
public LocalDependency local(File path) {
|
public LocalDependency local(File path) {
|
||||||
return new LocalDependency(path.getAbsolutePath());
|
return new LocalDependency(path.getAbsolutePath());
|
||||||
|
|
@ -1118,7 +1095,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.4.0
|
* @since 2.3.1
|
||||||
*/
|
*/
|
||||||
public LocalModule localModule(Path path) {
|
public LocalModule localModule(Path path) {
|
||||||
return new LocalModule(path.toString());
|
return new LocalModule(path.toString());
|
||||||
|
|
@ -1130,7 +1107,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.4.0
|
* @since 2.3.1
|
||||||
*/
|
*/
|
||||||
public LocalModule localModule(File path) {
|
public LocalModule localModule(File path) {
|
||||||
return new LocalModule(path.getAbsolutePath());
|
return new LocalModule(path.getAbsolutePath());
|
||||||
|
|
@ -2141,22 +2118,6 @@ 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);
|
||||||
|
|
@ -2180,12 +2141,6 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ 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 = "-?";
|
||||||
|
|
@ -43,7 +41,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_ = new ArrayList<>();
|
private List<String> arguments_ = Collections.emptyList();
|
||||||
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;
|
||||||
|
|
@ -140,17 +138,6 @@ 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.
|
||||||
|
|
@ -163,42 +150,6 @@ public class BuildExecutor {
|
||||||
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.
|
||||||
*
|
*
|
||||||
|
|
@ -292,19 +243,12 @@ 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_;
|
||||||
}
|
}
|
||||||
|
|
@ -497,14 +441,47 @@ public class BuildExecutor {
|
||||||
*/
|
*/
|
||||||
public boolean executeCommand(String command)
|
public boolean executeCommand(String command)
|
||||||
throws Throwable {
|
throws Throwable {
|
||||||
var matched_command = resolveCommand(command);
|
var matched_command = command;
|
||||||
CommandDefinition definition = null;
|
var definition = buildCommands().get(command);
|
||||||
if (matched_command != null) {
|
|
||||||
if (!matched_command.equals(command) &&
|
// try to find an alias
|
||||||
!matched_command.equals(buildAliases().get(command))) {
|
if (definition == null) {
|
||||||
System.out.println("Executing matched command: " + matched_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);
|
||||||
|
definition = buildCommands().get(matched_command);
|
||||||
}
|
}
|
||||||
definition = buildCommands().get(matched_command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the command if we found one
|
// execute the command if we found one
|
||||||
|
|
@ -530,54 +507,6 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -178,40 +178,6 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
|
||||||
return result;
|
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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -233,78 +233,6 @@ public class VersionResolution {
|
||||||
return conflicts;
|
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
|
// builds the identity that dependency management entries are matched
|
||||||
// on, mirroring Maven this includes the type and the classifier, the
|
// on, mirroring Maven this includes the type and the classifier, the
|
||||||
// modular and forced-classpath JAR types match the plain jar entries
|
// modular and forced-classpath JAR types match the plain jar entries
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -89,9 +89,6 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
|
||||||
for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) {
|
for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) {
|
||||||
System.out.println(formatBomVersionConflict(conflict));
|
System.out.println(formatBomVersionConflict(conflict));
|
||||||
}
|
}
|
||||||
for (var conflict : dependencies().declaredVersionConflicts(properties(), artifactRetriever(), repositories())) {
|
|
||||||
System.out.println(formatDeclaredVersionConflict(conflict));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,20 +110,6 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
|
||||||
return message.toString();
|
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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
|
||||||
*/
|
|
||||||
package rife.bld.operations;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The details that a single MCP tool call hands to the build process it
|
|
||||||
* launches.
|
|
||||||
* <p>
|
|
||||||
* The command, its arguments, the flags and the excluded commands are
|
|
||||||
* written to a control file instead of the command line, so they can never
|
|
||||||
* be mistaken for the command's own arguments. The values are escaped so
|
|
||||||
* that any character, including newlines, comes through intact. Only the
|
|
||||||
* MCP code uses this file, the build itself knows nothing about it.
|
|
||||||
*
|
|
||||||
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
||||||
* @since 2.4.0
|
|
||||||
*/
|
|
||||||
final class McpControl {
|
|
||||||
final String command;
|
|
||||||
final List<String> arguments;
|
|
||||||
final boolean offline;
|
|
||||||
final boolean verbose;
|
|
||||||
final boolean stacktrace;
|
|
||||||
final Set<String> exclusions;
|
|
||||||
|
|
||||||
private McpControl(String command, List<String> arguments, boolean offline, boolean verbose, boolean stacktrace, Set<String> exclusions) {
|
|
||||||
this.command = command;
|
|
||||||
this.arguments = arguments;
|
|
||||||
this.offline = offline;
|
|
||||||
this.verbose = verbose;
|
|
||||||
this.stacktrace = stacktrace;
|
|
||||||
this.exclusions = exclusions;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void write(File file, String command, List<String> arguments,
|
|
||||||
boolean offline, boolean verbose, boolean stacktrace,
|
|
||||||
Collection<String> exclusions)
|
|
||||||
throws IOException {
|
|
||||||
var lines = new ArrayList<String>();
|
|
||||||
lines.add("command " + escape(command));
|
|
||||||
for (var argument : arguments) {
|
|
||||||
lines.add("arg " + escape(argument));
|
|
||||||
}
|
|
||||||
if (offline) {
|
|
||||||
lines.add("offline");
|
|
||||||
}
|
|
||||||
if (verbose) {
|
|
||||||
lines.add("verbose");
|
|
||||||
}
|
|
||||||
if (stacktrace) {
|
|
||||||
lines.add("stacktrace");
|
|
||||||
}
|
|
||||||
for (var exclusion : exclusions) {
|
|
||||||
lines.add("exclude " + escape(exclusion));
|
|
||||||
}
|
|
||||||
Files.write(file.toPath(), lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
static McpControl read(File file)
|
|
||||||
throws IOException {
|
|
||||||
String command = null;
|
|
||||||
var arguments = new ArrayList<String>();
|
|
||||||
var offline = false;
|
|
||||||
var verbose = false;
|
|
||||||
var stacktrace = false;
|
|
||||||
var exclusions = new LinkedHashSet<String>();
|
|
||||||
for (var line : Files.readAllLines(file.toPath())) {
|
|
||||||
if (line.equals("offline")) {
|
|
||||||
offline = true;
|
|
||||||
} else if (line.equals("verbose")) {
|
|
||||||
verbose = true;
|
|
||||||
} else if (line.equals("stacktrace")) {
|
|
||||||
stacktrace = true;
|
|
||||||
} else if (line.startsWith("command ")) {
|
|
||||||
command = unescape(line.substring("command ".length()));
|
|
||||||
} else if (line.startsWith("arg ")) {
|
|
||||||
arguments.add(unescape(line.substring("arg ".length())));
|
|
||||||
} else if (line.startsWith("exclude ")) {
|
|
||||||
exclusions.add(unescape(line.substring("exclude ".length())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (command == null) {
|
|
||||||
throw new IOException("the control file doesn't specify a command");
|
|
||||||
}
|
|
||||||
return new McpControl(command, arguments, offline, verbose, stacktrace, exclusions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escape(String value) {
|
|
||||||
return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String unescape(String value) {
|
|
||||||
var result = new StringBuilder();
|
|
||||||
for (var i = 0; i < value.length(); ++i) {
|
|
||||||
var c = value.charAt(i);
|
|
||||||
if (c == '\\' && i + 1 < value.length()) {
|
|
||||||
var next = value.charAt(++i);
|
|
||||||
switch (next) {
|
|
||||||
case 'n' -> result.append('\n');
|
|
||||||
case 'r' -> result.append('\r');
|
|
||||||
case '\\' -> result.append('\\');
|
|
||||||
default -> result.append(next);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,180 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -54,6 +54,7 @@ public class PurgeOperation extends AbstractOperation<PurgeOperation> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeReportUncoveredDependencies();
|
||||||
executePurgeCompileDependencies();
|
executePurgeCompileDependencies();
|
||||||
executePurgeProvidedDependencies();
|
executePurgeProvidedDependencies();
|
||||||
executePurgeRuntimeDependencies();
|
executePurgeRuntimeDependencies();
|
||||||
|
|
@ -64,6 +65,24 @@ public class PurgeOperation extends AbstractOperation<PurgeOperation> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(DownloadOperation.formatBomVersionConflict(conflict));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Part of the {@link #execute} operation, purge the {@code compile} scope artifacts.
|
* Part of the {@link #execute} operation, purge the {@code compile} scope artifacts.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ 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";
|
||||||
|
|
@ -98,11 +97,6 @@ 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))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
/*
|
|
||||||
* 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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -506,48 +506,6 @@ public class TestBom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDeclaredVersionConflictsAreDetected() throws Exception {
|
|
||||||
var server = createArtifactServer(Map.of(
|
|
||||||
"bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0") + managed("b", "3.0.0"))),
|
|
||||||
Map.of());
|
|
||||||
server.start();
|
|
||||||
try {
|
|
||||||
var retriever = ArtifactRetriever.cachingInstance();
|
|
||||||
var repositories = serverRepositories(server);
|
|
||||||
var scopes = new DependencyScopes();
|
|
||||||
scopes.scope(compile)
|
|
||||||
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
|
|
||||||
// declared at a different version than the BOM manages
|
|
||||||
.include(new Dependency("com.example", "a", new VersionNumber(2, 2, 0)))
|
|
||||||
// declared at the same version as the BOM manages
|
|
||||||
.include(new Dependency("com.example", "b", new VersionNumber(3, 0, 0)));
|
|
||||||
scopes.scope(Scope.test)
|
|
||||||
// the same difference through scope composition is
|
|
||||||
// reported once
|
|
||||||
.include(new Dependency("com.example", "a", new VersionNumber(2, 2, 0)))
|
|
||||||
// a version-less dependency takes the BOM version and
|
|
||||||
// isn't a difference
|
|
||||||
.include(new Dependency("com.example", "b"));
|
|
||||||
|
|
||||||
var conflicts = scopes.declaredVersionConflicts(new HierarchicalProperties(), retriever, repositories);
|
|
||||||
assertEquals(1, conflicts.size());
|
|
||||||
var conflict = conflicts.get(0);
|
|
||||||
assertEquals("com.example:a", conflict.dependency());
|
|
||||||
assertEquals(Version.parse("2.2.0"), conflict.declaredVersion());
|
|
||||||
assertEquals("com.example:bom1", conflict.bom());
|
|
||||||
assertEquals(Version.parse("1.4.0"), conflict.bomVersion());
|
|
||||||
|
|
||||||
// a bld.override that supplies the version prevents the report
|
|
||||||
var override_properties = new HierarchicalProperties();
|
|
||||||
override_properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:a:2.2.0");
|
|
||||||
assertEquals(List.of(),
|
|
||||||
scopes.declaredVersionConflicts(override_properties, retriever, repositories));
|
|
||||||
} finally {
|
|
||||||
server.stop(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEffectiveBomsComposition() {
|
void testEffectiveBomsComposition() {
|
||||||
var compile_bom = new Bom("com.example", "compile-bom", new VersionNumber(1, 0, 0));
|
var compile_bom = new Bom("com.example", "compile-bom", new VersionNumber(1, 0, 0));
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue