#!/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.