#!/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 <a href='http://reluk.ca/project/Makeshift/bin/build.brec.xht'>
  *       The `build` command</a>
  */
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 <project> <target>..." );
        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<String> 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.’
//        <https://openjdk.org/jeps/330>
//        <https://docs.oracle.com/javase/specs/jls/se11/html/jls-7.html#jls-7.6>
//
//        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”.  <https://openjdk.org/jeps/458>
//
//   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.