nobuild 0.2: markdown javadoc, use newer Java methods

This commit is contained in:
2025-11-13 22:15:04 +03:00
parent 8880de04d0
commit 6529568baa

View File

@@ -1,8 +1,4 @@
/*
* NoBuild system
*
* @version 0.1
* @since 1.8
/* NoBuild system
*
* MIT License
*
@@ -32,90 +28,110 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
/**
* NoBuild system
*
* <h2>Quick Guide</h2>
*
* A simple build script:
*
* <pre>
* // Build.java
* import static nobuild.Nobuild.*;
*
* public class Build {
* public static void main(String[] args) {
* rebuildSelf(Build.class, args);
* compileJava("Hello.java");
* runJava("Hello");
* }
* }
* </pre>
*
* Which can be run as follows:
*
* <pre>
* $ javac -d build/ Build.java
* $ java -cp build/ Build
* </pre>
*
* Note that by using {@link NoBuild#rebuildSelf}, the build script {@code Build} will automatically
* rebuild itself if {@code Build.java} is modified.
*
* @author Naofal Helal
* @version 0.1
* @since 1.8
*/
/// # NoBuild system
///
/// ## Quick Guide
///
/// A simple build script:
///
/// ```java
/// // Build.java
/// import static nobuild.Nobuild.*;
///
/// public class Build {
/// public static void main(String[] args) {
/// rebuildSelf(Build.class, args);
/// compileJava("Hello.java");
/// runJava("Hello");
/// }
/// }
/// ```
///
/// Which can be run as follows:
///
/// ```console
/// $ javac -d build/ Build.java
/// $ java -cp build/ Build
/// ```
///
/// or:
///
/// ```console
/// $ java -cp build/ Build.java
/// ```
///
/// Note that by using [rebuildSelf][#rebuildSelf], the build script `Build` will automatically
/// rebuild itself if `Build.java` is modified.
///
/// @author Naofal Helal
/// @version 0.2
/// @since 23
////
public class NoBuild {
public static Logger logger = Logger.getLogger("logger");
public static Handler loggingHandler = new NoBuildLogHandler();
public static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
public static Path javaHome = Paths.get(System.getProperty("java.home"));
public static Path javaHome = Path.of(System.getProperty("java.home"));
public static String javaBin = javaHome.resolve("bin", "java").toString();
public static String javacBin = javaHome.resolve("bin", "javac").toString();
/** The class path used by the java executable */
/// The class path used by the java executable.
public static String javaClassPath = System.getProperty("java.class.path");
/**
* Path to the build script class file, or {@code null} if {@link #rebuildSelf} hasn't been
* called.
*
* <p>Will be used as the default output directory for compilation.
*/
/// Path to the build script class file, or `null` if [rebuildSelf][#rebuildSelf] hasn't been
/// called.
///
/// Will be used as the default output directory for compilation.
public static String buildClassPath = null;
/** Returns the class path of the build script */
/// Returns the class path of the build script.
public static String defaultBuildClassPath(Class<?> buildClass) {
URL buildClassUrl = buildClass.getResource(buildClass.getSimpleName() + ".class");
return Paths.get(buildClassUrl.getPath()).getParent().toString();
return Optional.ofNullable(Path.of(buildClassUrl.getPath()).getParent())
.orElseGet(
() -> {
Path path = Path.of(".", "build");
boolean isPathInClasspath =
Arrays.stream(javaClassPath.split(File.pathSeparator))
.anyMatch(
it -> {
try {
return Files.isSameFile(Path.of(it), path);
} catch (IOException e) {
return false;
}
});
if (!isPathInClasspath) {
logger.warning(
"Using %s as default classpath. Make sure to include with -cp %1$s"
.formatted(path));
}
return path;
})
.toString();
}
static {
@@ -165,28 +181,24 @@ public class NoBuild {
}
}
if (record.getThrown() != null) {
System.err.println(indent(record.getThrown().toString(), 4));
System.err.println(record.getThrown().toString().indent(4));
}
}
}
/**
* Rebuilds the build script if necessary
*
* @param buildClass The main class of the build script
* @param args arguments from the main method
*/
/// Rebuilds the build script if necessary.
///
/// @param buildClass The main class of the build script
/// @param args arguments from the main method
public static void rebuildSelf(Class<?> buildClass, String[] args) {
rebuildSelf(buildClass, args, new String[0]);
}
/**
* Rebuilds the build script if necessary
*
* @param buildClass The main class of the build script
* @param args Arguments from the main method
* @param additionalSources Additional sources to watch and compile
*/
/// Rebuilds the build script if necessary.
///
/// @param buildClass The main class of the build script
/// @param args Arguments from the main method
/// @param additionalSources Additional sources to watch and compile
public static void rebuildSelf(Class<?> buildClass, String[] args, String... additionalSources) {
String buildSource = buildClass.getName().replaceAll("\\.", File.separator) + ".java";
buildClassPath = defaultBuildClassPath(buildClass);
@@ -197,7 +209,7 @@ public class NoBuild {
if (!classNeedsRebuild(buildClass.getName(), sourcePaths)) return;
logger.info(String.format("Recompiling %s...", buildSource));
logger.info("Recompiling %s...".formatted(buildSource));
if (!compileJava(buildClassPath, sourcePaths)) {
logger.severe("Compilation failed");
@@ -208,35 +220,29 @@ public class NoBuild {
System.exit(status);
}
/**
* Runs a Java class
*
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
* @return Exit status code
*/
/// Runs a Java class.
///
/// @param mainClass The fully qualified class name, e.g. `com.example.MyClass$MySubClass`
/// @return Exit status code
public static int runJava(String mainClass) {
return runJava(mainClass, new String[0]);
}
/**
* Runs a Java class
*
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
* @param args Arguments to pass to the main method
* @return Exit status code
*/
/// Runs a Java class.
///
/// @param mainClass The fully qualified class name, e.g. `com.example.MyClass$MySubClass`
/// @param args Arguments to pass to the main method
/// @return Exit status code
public static int runJava(String mainClass, String... args) {
return runJava(new String[0], mainClass, args);
}
/**
* Runs a Java class
*
* @param additionalClassPaths Additional class paths to pass to the compiler
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
* @param args Arguments to pass to the main method
* @return Exit status code
*/
/// Runs a Java class.
///
/// @param additionalClassPaths Additional class paths to pass to the compiler
/// @param mainClass The fully qualified class name, e.g. `com.example.MyClass$MySubClass`
/// @param args Arguments to pass to the main method
/// @return Exit status code
public static int runJava(String[] additionalClassPaths, String mainClass, String... args) {
String pathSeparator = System.getProperty("path.separator");
@@ -253,15 +259,13 @@ public class NoBuild {
return command(commandLineStream.toArray(String[]::new));
}
/**
* Runs a Java class
*
* @param additionalClassPaths Additional class paths to pass to the compiler
* @param javaArguments Arguments to pass to the java binary
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
* @param args Arguments to pass to the main method
* @return Exit status code
*/
/// Runs a Java class.
///
/// @param additionalClassPaths Additional class paths to pass to the compiler
/// @param javaArguments Arguments to pass to the java binary
/// @param mainClass The fully qualified class name, e.g. `com.example.MyClass$MySubClass`
/// @param args Arguments to pass to the main method
/// @return Exit status code
public static int runJava(
String[] additionalClassPaths, String[] javaArguments, String mainClass, String... args) {
String pathSeparator = System.getProperty("path.separator");
@@ -283,38 +287,30 @@ public class NoBuild {
return command(commandLineStream.toArray(String[]::new));
}
/**
* Compiles java sources
*
* @return {@code true} if all the sources compiled successfully
*/
/// Compiles java sources.
///
/// @return `true` if all the sources compiled successfully
public static boolean compileJava(String... sourcePaths) {
return compileJava(new String[0], buildClassPath, sourcePaths);
}
/**
* Compiles java sources
*
* @return {@code true} if all the sources compiled successfully
*/
/// Compiles java sources.
///
/// @return `true` if all the sources compiled successfully
public static boolean compileJava(String[] additionalClassPaths, String... sourcePaths) {
return compileJava(additionalClassPaths, buildClassPath, sourcePaths);
}
/**
* Compiles java sources
*
* @return {@code true} if all the sources compiled successfully
*/
/// Compiles java sources.
///
/// @return `true` if all the sources compiled successfully
public static boolean compileJava(String classOutputPath, String... sourcePaths) {
return compileJava(new String[0], classOutputPath, sourcePaths);
}
/**
* Compiles java sources
*
* @return {@code true} if all the sources compiled successfully
*/
/// Compiles java sources.
///
/// @return `true` if all the sources compiled successfully
public static boolean compileJava(
String[] additionalClassPaths, String classOutputPath, String... sourcePaths) {
try (StandardJavaFileManager fileManager =
@@ -343,11 +339,9 @@ public class NoBuild {
}
}
/**
* Runs a shell command
*
* @return Exit status code
*/
/// Runs a shell command.
///
/// @return Exit status code
public static int command(String... command) {
try {
return commandThrows(command);
@@ -357,12 +351,10 @@ public class NoBuild {
}
}
/**
* Runs a shell command. May throw an {@code IOException}
*
* @return Exit status code
* @throws Exception
*/
/// Runs a shell command. May throw an `IOException`.
///
/// @return Exit status code
/// @throws Exception
public static int commandThrows(String... command) throws IOException {
ProcessBuilder pb = new ProcessBuilder(command).inheritIO();
Process process;
@@ -377,13 +369,11 @@ public class NoBuild {
}
}
/**
* Returns file paths that match a {@code globPattern}
*
* @see java.nio.file.FileSystem#getPathMatcher(String) getPathMatcher
*/
/// Returns file paths that match a `globPattern`.
///
/// @see java.nio.file.FileSystem#getPathMatcher(String) getPathMatcher
public static String[] glob(String globPattern) {
Path cwd = Paths.get(".");
Path cwd = Path.of(".");
PathMatcher pathMatcher =
FileSystems.getDefault()
.getPathMatcher(String.join("", "glob:", cwd.toString(), File.separator, globPattern));
@@ -396,7 +386,7 @@ public class NoBuild {
}
}
/** Checks if the {@code targetPath} needs to be rebuilt from {@code sourcePaths} */
/// Checks if the `targetPath` needs to be rebuilt from `sourcePaths`.
public static boolean needsRebuild(String targetPath, String... sourcePaths) {
long targetLastModified = new File(targetPath).lastModified();
return targetLastModified == 0
@@ -404,7 +394,7 @@ public class NoBuild {
.anyMatch(sourcePath -> new File(sourcePath).lastModified() > targetLastModified);
}
/** Checks if the {@code targetPath} needs to be rebuilt from {@code sourcePaths} */
/// Checks if the `targetPath` needs to be rebuilt from `sourcePaths`.
public static boolean needsRebuild(Path targetPath, Path... sourcePaths) {
long targetLastModified = targetPath.toFile().lastModified();
return targetLastModified == 0
@@ -412,21 +402,17 @@ public class NoBuild {
.anyMatch(sourcePath -> sourcePath.toFile().lastModified() > targetLastModified);
}
/**
* Checks if the class {@code className} needs to be rebuilt from {@code sourcePaths}
*
* @param className The fully qualified class name
*/
/// Checks if the class `className` needs to be rebuilt from `sourcePaths`.
///
/// @param className The fully qualified class name
public static boolean classNeedsRebuild(String className, String... sourcePaths) {
return classNeedsRebuild(buildClassPath, className, sourcePaths);
}
/**
* Checks if the class {@code className} needs to be rebuilt from {@code sourcePaths}
*
* @param classPath Path to look for the class in
* @param className The fully qualified class name
*/
/// Checks if the class `className` needs to be rebuilt from `sourcePaths`.
///
/// @param classPath Path to look for the class in
/// @param className The fully qualified class name
public static boolean classNeedsRebuild(
String classPath, String className, String... sourcePaths) {
String targetClass =
@@ -435,50 +421,13 @@ public class NoBuild {
return needsRebuild(targetClass, sourcePaths);
}
/** Indents lines in {@code str} with {@code n} spaces */
public static String indent(String str, int n) {
if (str.isEmpty()) {
return "";
} else {
StringBuilder sb = new StringBuilder(n);
for (int i = 0; i < n; i++) {
sb.append(" ");
}
String spaces = sb.toString();
return Arrays.stream(str.split("\n"))
.map(line -> spaces + line)
.collect(Collectors.joining("\n", "", "\n"));
}
}
/** Transfers bytes from IO streams {@code in} to {@code out} */
public static long transferTo(InputStream in, OutputStream out) throws IOException {
Objects.requireNonNull(out, "out");
long transferred = 0L;
byte[] buffer = new byte[16384];
int read;
while ((read = in.read(buffer, 0, 16384)) >= 0) {
out.write(buffer, 0, read);
if (transferred < Long.MAX_VALUE) {
try {
transferred = Math.addExact(transferred, (long) read);
} catch (ArithmeticException ex) {
transferred = Long.MAX_VALUE;
}
}
}
return transferred;
}
/** Downloads file from {@code url} to {@code destination} */
/// Downloads file from `url` to `destination`.
public static boolean downloadArtefact(String url, Path destination) {
try {
InputStream urlStream = URI.create(url).toURL().openStream();
Files.createDirectories(destination.getParent());
transferTo(urlStream, new FileOutputStream(destination.toFile()));
urlStream.transferTo(new FileOutputStream(destination.toFile()));
return true;
} catch (IOException ex) {
@@ -487,13 +436,13 @@ public class NoBuild {
}
}
/** Downloads file from {@code url} to {@code destination} */
/// Downloads file from `url` to `destination`.
public static boolean downloadArtefact(URL url, Path destination) {
try {
InputStream urlStream = url.openStream();
Files.createDirectories(destination.getParent());
transferTo(urlStream, new FileOutputStream(destination.toFile()));
urlStream.transferTo(new FileOutputStream(destination.toFile()));
return true;
} catch (IOException ex) {
@@ -502,26 +451,22 @@ public class NoBuild {
}
}
/** Describes a Java dependency */
/// Describes a Java dependency.
public static record Dependency(String group, String name, String version) {}
/** URL template for maven central dependencies */
/// URL template for maven central dependencies.
public static String mavenCentral = "https://repo1.maven.org/maven2/%1$s/%2$s/%3$s/%2$s-%3$s.jar";
/**
* Downloads {@code dependencies} from {@code repository}
*
* @param repository URL template for a repository, template arguments are provided:
* <ul>
* <li>{@code %1$s}: group id, delimeted with slashes
* <li>{@code %2$s}: artefact name
* <li>{@code %3$s}: artefact version
* </ul>
* See {@link NoBuild#mavenCentral}
* @param destination Path to download artefacts to
* @param dependencies Dependencies to download
* @return {@code true} if all dependencies were downloaded successfully
*/
/// Downloads `dependencies` from `repository`.
///
/// @param repository URL template for a repository, template arguments are provided:
/// - `%1$s`: group id, delimeted with slashes
/// - `%2$s`: artefact name
/// - `%3$s`: artefact version
/// See [mavenCentral][NoBuild#mavenCentral]
/// @param destination Path to download artefacts to
/// @param dependencies Dependencies to download
/// @return `true` if all dependencies were downloaded successfully
public static boolean downloadDependencies(
String repository, Path destination, Dependency... dependencies) {
boolean success = true;