Exit Full View

Feather2 / src / main / kotlin / uk / co / nickthecoder / feather / Feather2.kt

package uk.co.nickthecoder.feather

import uk.co.nickthecoder.feather.core.*
import java.io.File
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import kotlin.system.exitProcess

private const val USAGE = """
Usage

    feather2 [OPTIONS] SOURCE_FILE... [ -- PROGRAM_ARGUMENTS... ]
"""
private const val STANDARD_OPTIONS = """
Options :
    
    --library JAR_FILE
        Specifies a 3rd party jar file dependency.
        Repeat this option as many times as needed.
        Alternatively, use a `library` directive in a feather source file.

    --main QUALIFIED_CLASS_NAME
        The fully qualified class name of the class with a main function matching this signature :
            func main( args : String ... )

    --sources FILE
        An alternative to specifying each SOURCE_FILE on the command line.
        
        FILE is a plain-text file, with paths to feather scripts (one per line)
        Blank lines, and lines starting with // are ignored.
        This unix command creates such a file :
            find . | grep '.*\.feather$' > sources.txt
"""

private const val HELP_JAR = """
    --help-jar
        Print help about creating jar files.
"""

private const val JAR_USAGE = """
Usage

    feather2 --jar JAR_FILE [OPTIONS] SOURCE_FILE...
"""

private const val JAR_OPTIONS = """
    --jar JAR_FILE
        If set, then a jar file is produced, and the script is NOT run.      

    --fat
        Creates a stand-alone jar file, without any external dependencies (other than Java itself).
        Includes :
            feather runtime
            jars specified by "library" directives in .feather scripts
            jars specified by each --library command line option

        Only applicable with --jar

    --runtime-jar FEATHER_RUNTIME_JAR_FILE
        If --main is specified and --fat is NOT specified, then the new jar's manifest
        needs to include a class-path which includes the feather-runtime.
        This is optional. If it is omitted, then feather2jar uses a absolute path to
        the feather-runtime jar file. Using an absolute path is problematic, because the new jar file
        will have a dependency which will not exist on another computer.

        Only applicable with --jar
    
    --verbose | -v
        Progress/debug information to stdout.
"""

/**
 * A command line tool for compiling feather scripts.
 * This has two modes :
 * 1. Compiling and running feather scripts.
 * 2. Compiling feather scripts into a jar file.
 */
object Feather2 {

    @JvmStatic
    fun main(vararg args: String) {
        try {
            val arguments = doubleDashArguments(
                listOf('h', 'v'),
                listOf("help", "help-jar", "verbose", "fat"),
                listOf("jar", "library", "main", "sources", "runtime-jar"),
                args
            )
            if (arguments.flag("help-jar")) {
                println(JAR_USAGE)
                println(STANDARD_OPTIONS)
                println(JAR_OPTIONS)
                exitProcess(0)
            }
            if (arguments.flag('h', "help")) {
                println(USAGE)
                println(STANDARD_OPTIONS)
                println(HELP_JAR)
                exitProcess(0)
            }

            // Get the values from arguments
            val verbose = arguments.flag('v', "verbose")
            val mainClassName = arguments["main"]
            val fat = arguments.flag("fat")
            val libraryJarFiles = arguments.values("library").map { File(it) }
            val sources = arguments["sources"]
            val remainingArgs = arguments.remainder()
            val jarPath = arguments["jar"]

            // Because of --sources SOURCES_FILE, working out which of the "remaining" arguments are
            // feather source code, and which are program arguments is slightly tricky
            val sourcePaths = mutableListOf<String>()
            val programArguments = mutableListOf<String>()

            if (sources == null) {

                val endIndex = remainingArgs.indexOf("--")

                if (endIndex < 0) {
                    sourcePaths.addAll(remainingArgs)
                } else {
                    sourcePaths.addAll(remainingArgs.slice(0 until endIndex))
                    programArguments.addAll(remainingArgs.slice(endIndex + 1 until remainingArgs.size))
                }

            } else {
                val sourcesFile = File(sources)
                if (! sourcesFile.exists()) throw IllegalArgumentException("--sources file not found : $sourcesFile")
                if (! sourcesFile.isFile) throw IllegalArgumentException("Expected a file for --sources : $sourcesFile")
                sourcePaths.addAll(
                    sourcesFile.readLines().filter { it.isNotEmpty() && ! it.startsWith("//") }
                )

                // If --sources FILE was specified, then any "remaining" arguments
                // are program arguments (not feather files).
                programArguments.addAll(remainingArgs)
            }

            // We now have all the information from the command line options.
            // Compile

            val sourceFiles = sourcePaths.map { File(it) }

            if (verbose) println("Compiling")

            val results = compile(sourceFiles, libraryJarFiles)

            if (verbose) {
                println("")
                println("Created : ${arguments["jar"]}")
            }

            // Are we creating a jar file, or running the script?

            if (jarPath == null) {
                // Running the script.

                try {
                    run(results, mainClassName, programArguments)
                } catch (e: Exception) {
                    // We don't want this exception being handled specially, as it isn't an exception
                    // cause by command line arguments etc.
                    e.printStackTrace()
                }

            } else {
                // Creating a jar file

                val jarFile = File(jarPath)
                val runtimeJar: File? = arguments["runtime-jar"]?.let { File(it) }

                results.createJar(jarFile, mainClassName, fat, runtimeJar, verbose)

                if (verbose && mainClassName != null) {
                    println("")
                    println("To run the program : ")
                    println("    java -jar $jarPath")
                    println("")
                    println("On Linux, you can run the program directly (without using java -jar) by making the jar executable :")
                    println("    chmod +x $jarFile")
                    if (jarFile.isAbsolute || jarPath.startsWith(".")) {
                        println("    $jarPath")
                    } else {
                        println("    ./$jarPath")
                    }
                }
            }

        } catch (e: IllegalArgumentException) {
            error(e.message ?: e.toString())
            exitProcess(1)
        } catch (e: FeatherException) {
            error("${e.position} ${e.plainMessage}")
            exitProcess(1)
        } catch (e: Exception) {
            error("Error")
            e.printStackTrace()
            exitProcess(2)
        }
    }

    private fun compile(files: List<File>, jarFiles: List<File>): CompilationResults {

        val config = FeatherConfiguration.permissive().apply {
            additionalJarFiles.addAll(jarFiles)
        }

        val compiler = FeatherCompiler(config)
        val scripts = files.map { FileFeatherScript(it) }

        return compiler.compile(scripts)
    }

    private fun run(compilationResults: CompilationResults, namedMain: String?, arguments: List<String>) {

        // Find the `main` method
        val mainMethod: Method?
        if (namedMain != null) {
            val klass = try {
                compilationResults.classLoader.loadClass(namedMain)
            } catch (e: Exception) {
                error("Couldn't find/load class '$namedMain'")
                exitProcess(- 1)
            }
            try {
                mainMethod = klass !!.getMethod("main", arrayOf<String>().javaClass)
            } catch (e: Throwable) {
                error("Couldn't find 'func main( args : String... )' on class '$namedMain'")
                exitProcess(- 1)
            }
        } else {
            val matching = compilationResults.findMainFunctions()
            if (matching.size > 1) {
                val classNames = matching.joinToString(separator = "\n") { "    ${it.declaringClass.name}" }
                error("More than 1 `main` function found :\n$classNames")
                exitProcess(- 1)
            } else {
                mainMethod = matching.firstOrNull()
            }
        }

        // If the main method was found, invoke it (i.e. run the script).
        if (mainMethod == null) {
            error("No main method found")
            exitProcess(- 1)

        } else {
            if (! Modifier.isStatic(mainMethod.modifiers)) {
                error("main method on ${mainMethod.declaringClass}' is not static.")
                exitProcess(- 1)

            } else {
                mainMethod.invoke(null, arguments.toTypedArray())
            }
        }
    }

    private fun error(message: String) {
        System.err.println(message)
        System.err.println()
        System.err.println("For help running feather scripts: feather2 --help")
        System.err.println("For help creating jar files: feather2 --help-jar")
        System.err.println()
    }
}