#!/usr/bin/env --split-string=${JDK_HOME}/bin/java @Makeshift/java_arguments @Makeshift/java_javac_arguments \c [SS]
// Changes to this file immediately affect the next build. Treat it as a build script.
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static java.io.File.separatorChar;
import static java.lang.ProcessBuilder.Redirect.INHERIT;
import static java.nio.file.Files.createDirectory;
import static java.nio.file.Files.getLastModifiedTime;
/** A shell command to compile the software of a project and prepare it for use.
*
* @see
* The `build` command
*/
final class BuildCommand { // [AFN]
private BuildCommand( final Path projectPath, final String[] arguments ) {
this.projectPath = projectPath;
this.arguments = arguments; }
/** Takes a `build` command from the shell and executes it.
*/
public static void main( final String[] arguments ) {
if( arguments.length < 2 ) abortWithUsage();
final String a = arguments[0];
if( a.startsWith( "-" )) abortWithUsage(); // Allowing e.g. for a deliberate `-?`.
final Path projectPath = Path.of( a ); /* Filters out input variance en passent.
The resulting `projectPath` is the same whether or not `a` ends with slash. */
if( projectPath.isAbsolute() ) {
System.err.println( "build: Not a relative path: " + a );
System.exit( 1 ); }
new BuildCommand(projectPath,arguments).run(); }
//// P r i v a t e ////////////////////////////////////////////////////////////////////////////////////
private static void abortWithUsage() {
System.err.println( "Usage: build ..." );
System.exit( 1 ); }
private final String[] arguments;
private Class> load( final String className ) throws ClassNotFoundException {
return loader == null? Class.forName(className) : loader.loadClass(className); }
private ClassLoader loader; // Null unless a special one is required.
/** The proper path of the project to build.
*/
private final Path projectPath;
private void run() { // A bootstrapped process comprising three build stages:
final Path projectOutputDirectory = Path.of( System.getProperty("java.io.tmpdir"),
"Makeshift" );
final boolean wasClean;
if( Files.isDirectory( projectOutputDirectory )) wasClean = false;
else {
try { createDirectory( projectOutputDirectory ); } /* So avoid a warning on the first call
to `javac`: `“[path] bad path element… no such file or directory”. */
catch( IOException x ) { throw new Unhandled( x ); }
wasClean = true; }
// 1. Build the builder builder
// ────────────────────────────
final List compilerArguments = new ArrayList<>(); { // Empty if no code needs compiling.
// Already the working directory is the command directory, as stipulated in `./build.brec`.
final Path p = toProperPath( "Makeshift" ); // Proper path of the present project.
for( String t: new String[]{ // ↓ Changing these? Sync → `BuilderBuilder` API description.
"Bootstrap", "Builder", "BuilderBuilder", "BuilderBuilderDefault" }) {
final Path sourceFile = p.resolve( t + ".java" );
final boolean toCompile;
if( wasClean ) toCompile = true;
else {
final Path classFile = projectOutputDirectory.resolve( p.resolve( t + ".class" ));
if( Files.exists( classFile )) {
try { toCompile = getLastModifiedTime(sourceFile)
.compareTo(getLastModifiedTime(classFile)) >= 0; }
catch( IOException x ) { throw new Unhandled( x ); }}
else toCompile = true; }
if( toCompile ) compilerArguments.add( sourceFile.toString() ); }}
final int sourceCount = compilerArguments.size();
if( sourceCount > 0 ) {
// compile the code
// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
// Changing? Sync → `Bootstrap.compile`.
int a = 0;
compilerArguments.add( a++, System.getProperty("java.home") + "/bin/javac" );
// The Java installation at `java.home` is known to include `javac` because also
// it is a JDK installation, as assured by the `JDK_HOME` at top.
compilerArguments.add( a++, "@Makeshift/java_javac_arguments" );
compilerArguments.add( a, "@Makeshift/javac_arguments" );
final ProcessBuilder pB = new ProcessBuilder( compilerArguments );
pB.redirectOutput( INHERIT );
pB.redirectError( INHERIT );
try {
final int exitValue = pB.start().waitFor();
if( exitValue == 1 ) {
System.err.println( "build: Stopped on `javac` error" );
System.exit( 1 ); } // Already `javac` has told the details.
else if( exitValue != 0 ) throw new Unhandled( "Exit value of " + exitValue
+ " from process: " + pB.command() ); }
catch( final InterruptedException x ) {
Thread.currentThread().interrupt(); // Avoid hiding the fact of interruption.
throw new Unhandled( x ); } /* The only known interrupt source is the user,
e.g. via `Ctrl-C` and `SIGINT`, and already the runtime handles it.
https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html */
catch( IOException x ) { throw new Unhandled( x ); }}
if( wasClean ) {
// prepare to load the code
// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
// The directory given in `../java_javac_arguments` for the class path (the output directory)
// did not exist before the present runtime. It exists now, but still the class loader will
// not look there for class files (JDK 14.0.2). Therefore make a new class loader.
try {
loader = new java.net.URLClassLoader( new java.net.URL[] {
new URI( /*scheme*/"file", /*authority*/null, /*path*/projectOutputDirectory + "/",
/*query*/null, /*fragment*/null ).toURL() }); }
catch( MalformedURLException|URISyntaxException x ) { throw new Unhandled( x ); }}
try {
Class> c;
if( sourceCount > 0 ) {
// inform the user
// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
c = load( "Makeshift.Bootstrap" );
c.getMethod( "printProgressLeader", String.class, String.class )
.invoke( null/*static*/, null/*builder builder*/, "javac" );
System.out.println( sourceCount ); }
// get a builder builder for the project to be built
// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
c = load( "Makeshift.BuilderBuilder" );
final Object builderBuilder = c.getMethod( "forPath", Path.class )
.invoke( null/*static*/, projectPath );
c = builderBuilder.getClass();
// 2. Build the builder
// ────────────────────
c.getMethod("build").invoke( builderBuilder );
final Object builder = c.getMethod("newBuilder").invoke( builderBuilder );
c = builder.getClass();
// 3. Build the requested targets
// ──────────────────────────────
final int tN = arguments.length;
int t = 1;
do c.getMethod("build",String.class).invoke( builder, arguments[t] );
while( ++t < tN ); }
catch( final InvocationTargetException xIT ) {
final Throwable x = xIT.getCause();
if( x != null && "Makeshift.Bootstrap$UserError".equals( x.getClass().getName() )) {
System.err.println( "build: " + x.getMessage() );
System.exit( 1 ); }
else throw new Unhandled( xIT ); }
catch( ReflectiveOperationException x ) { throw new Unhandled( x ); }}
/** Returns the proper path of the named Java package.
*
* @param name The name of a Java package.
* @return The relative path proper to the named package.
*/
public static Path toProperPath( final String name ) { return Path.of( toProperPathString( name )); }
// Changing? Sync → `Bootstrap.toProperPath`.
/** Returns the proper path of the named Java package.
*
* @param name The name of a Java package.
* @return The relative path proper to the named package.
*/
public static String toProperPathString( final String name ) {
return name.replace( '.', separatorChar ); }
// Changing? Sync → `Bootstrap.toProperPathString`.
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
/** Thrown for an event that might better be handled, given a reason to do so.
*/
private static final class Unhandled extends RuntimeException {
// Bootstrap equivalent of library exception `Java.Unhandled`.
// http://reluk.ca/project/Java/Unhandled.java
/** @see #getCause()
*/
private Unhandled( Exception cause ) { super( cause ); }
/** @see #getMessage()
*/
private Unhandled( String message ) { super( message ); }}}
// NOTES
// ─────
// AFN Atypical file naming is allowed here. ‘The compiler does not enforce the optional restriction
// defined at the end of JLS §7.6, that a type in a named package should exist in a file whose
// name is composed from the type name followed by the .java extension.’
//
//
//
// No longer, however, does this allowance extend to the package name. While in JDK releases
// prior to 22 “the launcher's source-file mode was permissive about which package, if any,
// was declared”, current releases enforce a correspondence between the declared package name
// and the file path. Failing this, the launcher aborts with “end of path to source file
// does not match its package name”.
//
// SS · Here the long form `--split-string` (as opposed to `-S`) enables Emacs to recognize this file
// as Java source code. See the note apropos of ‘source-launch files encoded with a shebang’ at
// `http://reluk.ca/project/Java/Emacs/jmt-mode.el`.
// Copyright © 2020-2021, 2023-2024 Michael Allan. Licence MIT.