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()
}
}