mirror of
https://github.com/ethauvin/bld.git
synced 2025-04-26 00:37:10 -07:00
543 lines
19 KiB
Java
543 lines
19 KiB
Java
/*
|
|
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
* Licensed under the Apache License, Version 2.0 (the "License")
|
|
*/
|
|
package rife.bld;
|
|
|
|
import rife.bld.dependencies.Repository;
|
|
import rife.bld.help.HelpHelp;
|
|
import rife.bld.operations.HelpOperation;
|
|
import rife.bld.operations.exceptions.ExitStatusException;
|
|
import rife.ioc.HierarchicalProperties;
|
|
import rife.tools.ExceptionUtils;
|
|
|
|
import java.io.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.logging.Logger;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Base class that executes build commands from a list of arguments.
|
|
*
|
|
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
* @see BuildCommand
|
|
* @see CommandDefinition
|
|
* @since 1.5
|
|
*/
|
|
public class BuildExecutor {
|
|
public static final File BLD_USER_DIR = new File(System.getProperty("user.home"), ".bld");
|
|
public static final File RIFE2_USER_DIR = new File(System.getProperty("user.home"), ".rife2");
|
|
public static final String BLD_PROPERTIES = "bld.properties";
|
|
public static final String LOCAL_PROPERTIES = "local.properties";
|
|
|
|
private static final String ARG_OFFLINE = "--offline";
|
|
private static final String ARG_HELP1 = "--help";
|
|
private static final String ARG_HELP2 = "-h";
|
|
private static final String ARG_HELP3 = "-?";
|
|
private static final String ARG_STACKTRACE1 = "--stacktrace";
|
|
private static final String ARG_STACKTRACE2 = "-s";
|
|
|
|
private final HierarchicalProperties properties_;
|
|
private List<String> arguments_ = Collections.emptyList();
|
|
private boolean offline_ = false;
|
|
private Map<String, CommandDefinition> buildCommands_ = null;
|
|
private Map<String, String> buildAliases_ = null;
|
|
private final AtomicReference<String> currentCommandName_ = new AtomicReference<>();
|
|
private final AtomicReference<CommandDefinition> currentCommandDefinition_ = new AtomicReference<>();
|
|
private int exitStatus_ = 0;
|
|
|
|
/**
|
|
* Show the full Java stacktrace when exceptions occur, as opposed
|
|
* to the chain of messages.
|
|
* <p>
|
|
* Defaults to {@code false}, can be set to {@code true} by setting
|
|
* through code or by adding {@code --stacktrace} as a CLI argument.
|
|
*
|
|
* @since 1.5.19
|
|
*/
|
|
protected boolean showStacktrace = false;
|
|
|
|
/**
|
|
* Creates a new build executor instance.
|
|
*
|
|
* @since 1.5
|
|
*/
|
|
public BuildExecutor() {
|
|
properties_ = setupProperties(workDirectory());
|
|
Repository.resolveMavenLocal(properties());
|
|
}
|
|
|
|
/**
|
|
* Creates a properties hierarchy for bld execution.
|
|
*
|
|
* @param workDirectory the directory where the project build files are location
|
|
* @return the properties hierarchy
|
|
* @since 1.5.12
|
|
*/
|
|
public static HierarchicalProperties setupProperties(File workDirectory) {
|
|
var system_properties = HierarchicalProperties.createSystemInstance();
|
|
|
|
var java_properties = system_properties;
|
|
system_properties = java_properties.getParent();
|
|
|
|
HierarchicalProperties bld_properties = null;
|
|
HierarchicalProperties local_properties = null;
|
|
|
|
var bld_properties_file = new File(BLD_USER_DIR, BLD_PROPERTIES);
|
|
if (!bld_properties_file.exists() || !bld_properties_file.isFile() || !bld_properties_file.canRead()) {
|
|
bld_properties_file = new File(RIFE2_USER_DIR, BLD_PROPERTIES);
|
|
}
|
|
if (bld_properties_file.exists() && bld_properties_file.isFile() && bld_properties_file.canRead()) {
|
|
try {
|
|
var bld = new Properties();
|
|
bld.load(new FileReader(bld_properties_file));
|
|
bld_properties = new HierarchicalProperties();
|
|
bld_properties.putAll(bld);
|
|
|
|
bld_properties.parent(system_properties);
|
|
} catch (IOException e) {
|
|
Logger.getLogger("rife.bld").warning("Unable to parse " + bld_properties_file + " as a properties file:\n" + e.getMessage());
|
|
}
|
|
}
|
|
|
|
var local_properties_file = new File(workDirectory, LOCAL_PROPERTIES);
|
|
if (local_properties_file.exists() && local_properties_file.isFile() && local_properties_file.canRead()) {
|
|
try {
|
|
var local = new Properties();
|
|
local.load(new FileReader(local_properties_file));
|
|
local_properties = new HierarchicalProperties();
|
|
local_properties.putAll(local);
|
|
|
|
local_properties.parent(Objects.requireNonNullElse(bld_properties, system_properties));
|
|
} catch (IOException e) {
|
|
Logger.getLogger("rife.bld").warning("Unable to parse " + local_properties_file + " as a properties file:\n" + e.getMessage());
|
|
}
|
|
}
|
|
|
|
java_properties.parent(
|
|
Objects.requireNonNullElse(local_properties,
|
|
Objects.requireNonNullElse(bld_properties, system_properties)));
|
|
|
|
final HierarchicalProperties properties = new HierarchicalProperties();
|
|
properties.parent(java_properties);
|
|
return properties;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the bld execution is intended to be offline.
|
|
*
|
|
* @return {@code true} if the execution is intended to be offline;
|
|
* or {@code false} otherwise
|
|
* @since 2.0
|
|
*/
|
|
public boolean offline() {
|
|
return offline_;
|
|
}
|
|
|
|
/**
|
|
* Returns the properties uses for bld execution.
|
|
*
|
|
* @return the instance of {@code HierarchicalProperties} that is used
|
|
* by this build executor
|
|
* @since 1.5
|
|
*/
|
|
public HierarchicalProperties properties() {
|
|
return properties_;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a property from the {@link #properties()}.
|
|
*
|
|
* @param name the name of the property
|
|
* @return the requested property; or {@code null} if it doesn't exist
|
|
* @since 1.5.15
|
|
*/
|
|
public String property(String name) {
|
|
return properties().getValueString(name);
|
|
}
|
|
|
|
/**
|
|
* Retrieve a property from the {@link #properties()} with a default value.
|
|
*
|
|
* @param name the name of the property
|
|
* @param defaultValue the value that should be used as a fallback
|
|
* @return the requested property; or the default value if it doesn't exist
|
|
* @since 1.5.15
|
|
*/
|
|
public String property(String name, String defaultValue) {
|
|
return properties().getValueString(name, defaultValue);
|
|
}
|
|
|
|
/**
|
|
* Checks for the existence of a property in {@link #properties()}.
|
|
*
|
|
* @param name the name of the property
|
|
* @return {@code true} if the property exists; or {@code false} otherwise
|
|
* @since 1.5.15
|
|
*/
|
|
public boolean hasProperty(String name) {
|
|
return properties().contains(name);
|
|
}
|
|
|
|
/**
|
|
* Returns the work directory of the project.
|
|
* Defaults to this process's user working directory, which when running
|
|
* through the bld wrapper corresponds to the top-level project directory.
|
|
*
|
|
* @since 1.5.12
|
|
*/
|
|
public File workDirectory() {
|
|
return new File(System.getProperty("user.dir"));
|
|
}
|
|
|
|
/**
|
|
* Set the exist status to use at the end of the execution.
|
|
*
|
|
* @param status sets the exit status
|
|
* @since 1.5.1
|
|
*/
|
|
public void exitStatus(int status) {
|
|
exitStatus_ = status;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the exit status.
|
|
*
|
|
* @return the exit status
|
|
* @since 1.5.1
|
|
*/
|
|
public int exitStatus() {
|
|
return exitStatus_;
|
|
}
|
|
|
|
/**
|
|
* Execute the build commands from the provided arguments.
|
|
* <p>
|
|
* While the build is executing, the arguments can be retrieved
|
|
* using {@link #arguments()}.
|
|
*
|
|
* @param arguments the arguments to execute the build with
|
|
* @return the exist status
|
|
* @since 1.5.1
|
|
*/
|
|
public int execute(String[] arguments) {
|
|
arguments_ = new ArrayList<>(Arrays.asList(arguments));
|
|
|
|
var show_help = false;
|
|
show_help |= arguments_.removeAll(List.of(ARG_HELP1, ARG_HELP2, ARG_HELP3));
|
|
showStacktrace = arguments_.removeAll(List.of(ARG_STACKTRACE1, ARG_STACKTRACE2));
|
|
|
|
if (show_help) {
|
|
new HelpOperation(this, Collections.emptyList()).execute();
|
|
return exitStatus_;
|
|
}
|
|
else if (arguments_.isEmpty()) {
|
|
showBldHelp();
|
|
return exitStatus_;
|
|
}
|
|
|
|
while (!arguments_.isEmpty()) {
|
|
var command = arguments_.remove(0);
|
|
|
|
try {
|
|
if (!executeCommand(command)) {
|
|
break;
|
|
}
|
|
} catch (Throwable e) {
|
|
exitStatus(ExitStatusException.EXIT_FAILURE);
|
|
outputCommandExecutionException(e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return exitStatus_;
|
|
}
|
|
|
|
private void outputCommandExecutionException(Throwable e) {
|
|
System.err.println();
|
|
|
|
if (showStacktrace) {
|
|
System.err.println(ExceptionUtils.getExceptionStackTrace(e));
|
|
} else {
|
|
boolean first_exception = true;
|
|
var e2 = e;
|
|
while (e2 != null) {
|
|
if (e2.getMessage() != null) {
|
|
if (!first_exception) {
|
|
System.err.print("> ");
|
|
}
|
|
System.err.println(e2.getMessage());
|
|
first_exception = false;
|
|
}
|
|
e2 = e2.getCause();
|
|
}
|
|
|
|
if (first_exception) {
|
|
System.err.println(ExceptionUtils.getExceptionStackTrace(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the execution of the build. This method will call
|
|
* System.exit() when done with the appropriate exit status.
|
|
*
|
|
* @param arguments the arguments to execute the build with
|
|
* @see #execute
|
|
* @since 1.5.1
|
|
*/
|
|
public void start(String[] arguments) {
|
|
if (arguments.length > 0 && arguments[0].equals(ARG_OFFLINE)) {
|
|
offline_ = true;
|
|
arguments = Arrays.copyOfRange(arguments, 1, arguments.length);
|
|
}
|
|
|
|
System.exit(execute(arguments));
|
|
}
|
|
|
|
/**
|
|
* Retrieves the list of arguments that are being processed.
|
|
*
|
|
* @return the list of arguments
|
|
* @since 1.5
|
|
*/
|
|
public List<String> arguments() {
|
|
return arguments_;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the commands that can be executed by this {@code BuildExecutor}.
|
|
*
|
|
* @return a map containing the name of the build command and the method that
|
|
* corresponds to execution
|
|
* @see BuildCommand
|
|
* @since 1.5
|
|
*/
|
|
public Map<String, CommandDefinition> buildCommands() {
|
|
if (buildCommands_ == null) {
|
|
var build_commands = new TreeMap<String, CommandDefinition>();
|
|
var build_aliases = new HashMap<String, String>();
|
|
|
|
Class<?> klass = getClass();
|
|
|
|
try {
|
|
while (klass != null) {
|
|
for (var method : klass.getDeclaredMethods()) {
|
|
if (method.getParameters().length == 0 && method.isAnnotationPresent(BuildCommand.class)) {
|
|
method.setAccessible(true);
|
|
|
|
var name = method.getName();
|
|
var annotation = method.getAnnotation(BuildCommand.class);
|
|
|
|
var annotation_name = annotation.value();
|
|
if (annotation_name != null && !annotation_name.isEmpty()) {
|
|
name = annotation_name;
|
|
}
|
|
|
|
var annotation_alias = annotation.alias();
|
|
if (annotation_alias != null && !annotation_alias.isEmpty()) {
|
|
build_aliases.put(annotation_alias, name);
|
|
}
|
|
|
|
if (!build_commands.containsKey(name)) {
|
|
var build_help = annotation.help();
|
|
CommandHelp command_help = null;
|
|
if (build_help != null && build_help != CommandHelp.class) {
|
|
command_help = build_help.getDeclaredConstructor().newInstance();
|
|
}
|
|
|
|
var summary = annotation.summary();
|
|
var description = annotation.description();
|
|
if ((summary != null && !summary.isBlank()) ||
|
|
(description != null && !description.isBlank())) {
|
|
if (summary == null) summary = "";
|
|
if (description == null) description = "";
|
|
if (command_help != null) {
|
|
if (summary.isBlank()) summary = command_help.getSummary();
|
|
if (description.isBlank()) description = command_help.getDescription(name);
|
|
}
|
|
command_help = new AnnotatedCommandHelp(summary, description);
|
|
}
|
|
|
|
build_commands.put(name, new CommandAnnotated(this, method, command_help));
|
|
}
|
|
}
|
|
}
|
|
|
|
klass = klass.getSuperclass();
|
|
}
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
buildCommands_ = build_commands;
|
|
buildAliases_ = build_aliases;
|
|
}
|
|
|
|
return buildCommands_;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the command aliases that can be executed by this {@code BuildExecutor}.
|
|
*
|
|
* @return a map containing the alias and the associated name of the build command
|
|
* @see BuildCommand
|
|
* @since 1.9
|
|
*/
|
|
public Map<String, String> buildAliases() {
|
|
if (buildAliases_ == null) {
|
|
buildCommands();
|
|
}
|
|
|
|
return buildAliases_;
|
|
}
|
|
|
|
private static class AnnotatedCommandHelp implements CommandHelp {
|
|
private final String summary_;
|
|
private final String description_;
|
|
|
|
AnnotatedCommandHelp(String summary, String description) {
|
|
summary_ = summary;
|
|
description_ = description;
|
|
}
|
|
|
|
@Override
|
|
public String getSummary() {
|
|
return summary_;
|
|
}
|
|
|
|
@Override
|
|
public String getDescription(String topic) {
|
|
return description_;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs the execution of a single command.
|
|
*
|
|
* @param command the name of the command to execute
|
|
* @return {@code true} when the command was found and executed; or
|
|
* {@code false} if the command couldn't be found
|
|
* @throws Throwable when an exception occurred during the command execution
|
|
* @see BuildCommand
|
|
* @since 1.5
|
|
*/
|
|
public boolean executeCommand(String command)
|
|
throws Throwable {
|
|
var matched_command = command;
|
|
var definition = buildCommands().get(command);
|
|
|
|
// try to find an alias
|
|
if (definition == null) {
|
|
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
|
|
var matched = false;
|
|
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);
|
|
matched = true;
|
|
definition = buildCommands().get(matched_command);
|
|
}
|
|
}
|
|
|
|
// execute the command if we found one
|
|
if (definition != null) {
|
|
currentCommandName_.set(matched_command);
|
|
currentCommandDefinition_.set(definition);
|
|
try {
|
|
if (matched) {
|
|
System.out.println("Executing matched command: " + matched_command);
|
|
}
|
|
else {
|
|
System.out.println("Executing command: " + currentCommandName_);
|
|
}
|
|
|
|
definition.execute();
|
|
} catch (ExitStatusException e) {
|
|
exitStatus(e.getExitStatus());
|
|
return e.getExitStatus() == ExitStatusException.EXIT_SUCCESS;
|
|
} finally {
|
|
currentCommandDefinition_.set(null);
|
|
currentCommandName_.set(null);
|
|
}
|
|
} else {
|
|
var message = "Unknown command '" + command + "'";
|
|
showBldHelp();
|
|
System.err.println("ERROR: " + message);
|
|
exitStatus(ExitStatusException.EXIT_FAILURE);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void showBldHelp() {
|
|
var help = new HelpOperation(this, arguments());
|
|
help.executePrintWelcome();
|
|
System.err.println("""
|
|
The bld CLI provides its features through a series of commands that
|
|
perform specific tasks.""");
|
|
help.executePrintCommands();
|
|
help.executePrintBldArguments();
|
|
}
|
|
|
|
/**
|
|
* Retrieves the name of the currently executing command.
|
|
*
|
|
* @return the name of the current command; or
|
|
* {@code null} if no command is currently executing
|
|
* @since 1.5.12
|
|
*/
|
|
public String getCurrentCommandName() {
|
|
return currentCommandName_.get();
|
|
}
|
|
|
|
/**
|
|
* Retrieves the definition of the currently executing command.
|
|
*
|
|
* @return the definition of the current command; or
|
|
* {@code null} if no command is currently executing
|
|
* @since 1.5.12
|
|
*/
|
|
public CommandDefinition getCurrentCommandDefinition() {
|
|
return currentCommandDefinition_.get();
|
|
}
|
|
|
|
/**
|
|
* The standard {@code help} command.
|
|
*
|
|
* @since 1.5
|
|
*/
|
|
@BuildCommand(help = HelpHelp.class)
|
|
public void help() {
|
|
new HelpOperation(this, arguments()).execute();
|
|
}
|
|
}
|