/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //TODO add mechanism to add java system properties so apps that need them set can find them //TODO include the systemclass path of the luancher jar, that was not included (either in the classpath or the ng project) //TODO make a fork that uses javaw //TODO bt.kson option to launch a forked or bg process //TODO run bt from a luanched ng cc.drx.Boot //TODO make a bt.kson option lookup for a fast registry like lookup mechanism //TODO make a custom ng boot that launches a cc.drx.ng.Server and friends server if not already launched //TODO maybe revisit the trouble using xsbti.ServerMain (in the meantime decided to use ng server instead) package cc.drx import Repo.{Release,Org} object Jansi{ def apply[A](body: => A):A = { if(OS.kind == OS.Windows) org.fusesource.jansi.AnsiConsole.systemInstall() body } } object BootSbt{ def main(args:Array[String]):Unit = xsbt.boot.Boot.main(args) //TODO if this test ever works that means boots can be forwarded so make it work nice, if it doesn't try the xsbt.boot.Launch methods } object Boot{ def pp(width:Int, key:String, value:String):Unit = println( s"${Color.Blue ansi key}${Color.Yellow ansi ":"}$value" ) class Exit(val code: Int) extends xsbti.Exit def classpathFrom(conf:xsbti.AppConfiguration):Classpath = classpathFrom(conf.provider) def classpathFrom(provider:xsbti.AppProvider):Classpath = { val cpScala = Classpath(provider.scalaProvider.jars map File.apply) val cpBoot = Classpath(provider.mainClasspath map File.apply) val cpLauncher = Classpath.system cpScala ++ cpBoot ++ cpLauncher } case class AppId(org:String, prj:String, ver:String, main:String, cross:String="Disabled", extra:List[String]=Nil, cp:Classpath=Classpath(), scala:String="auto", ctx:StringMap ) extends xsbti.ApplicationID { def classpathExtra:Array[java.io.File] = cp.cp.map{_.file}.toArray def mainComponents = extra.toArray def groupID = org def name = prj def version = ver def mainClass = main def crossVersioned:Boolean = crossVersionedValue != xsbti.CrossValue.Disabled private def isCross:Boolean = crossVersionedValue != xsbti.CrossValue.Disabled def crossVersionedValue:xsbti.CrossValue = xsbti.CrossValue.valueOf(cross.trim.toLowerCase.capitalize) override def toString = { s"$prj ($ver) from $org using main:$main"+ List( if(isCross) Some(s"cross: $crossVersioned") else None, if(cp.cp.nonEmpty) Some(s"cp: ${cp.toString}") else None, if(extra.nonEmpty) Some(s"extra: ${extra mkString ";"}") else None, if(scala!="auto") Some(s"scala: $scala") else None ).flatten.map{"\n " + _}.mkString("") } } case class AppConf(appId:AppId, args:Array[String], base:File){ override def toString = s"AppConf(appId:$appId args:${args mkString " "} base:$base)" def run:xsbti.MainResult = appId.ctx.getString("lang") match { case Some("shell") => { import Implicit.ec val shell = Shell(appId.main +: args, base) println(shell) val res = shell.run.blockWithoutWarning(1.yr) new Boot.Exit(0) } case _ => { new xsbti.Reboot{ def arguments = args//conf.arguments.drop(1) def baseDirectory = base.file //conf.baseDirectory def scalaVersion = appId.scala def app = appId } } } /* def start:xsbti.Server = { new xsbti.Server{ def uri = new java.net.URI("sbt://localhost:9999") // FIXME setup an actual ip and port for the test def awaitTermination = { // val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance run //TODO launch the main blocking class here while(true){Thread.sleep(100)} new Boot.Exit(0) } } } */ } private val javaVer = "java" + OS.java8.getOrElse("8","7") //auto detect java private val scopes = List("scala","java","sbt") /**find a boot conf from a key and launch it with a specified special args*/ def find(conf: xsbti.AppConfiguration, key:String, args:Array[String]):Option[AppConf] = Timer("find boot config"){ val base = File(conf.baseDirectory) val launchedScala = conf.provider.scalaProvider.version val cpBoot = classpathFrom(conf) // Print a message and the arguments to the application println( "Welcome to drx-boot. " + Color.Grey.ansi("A context dependent launcher/build-tool/boot-loader.") ) print(s"Looking for ${Color.Green ansi key wrap "*"} params (using scala:$launchedScala)") val bootKeys = Timer("load kson config"){ BootKeys.load } val ctxOption = bootKeys.getContext(key) //--setup vars for launch mode lazy val boot:Option[AppConf] = for( ctx <- ctxOption; org <- ctx.get[String]("org"); prj <- ctx.get[String]("prj"); ver <- ctx.get[String]("ver") orElse ctx.get[String]("ver."+javaVer); main <- ctx.get[String]("main") //it must define a main method to be launched ) yield { val cross = ctx.get[String]("cross") | "Disabled" val scala:String = ctx.get[String]("scala") getOrElse "2.12.4" //orElse kson.get[String]("scala.scala.ver."+javaVer) getOrElse "2.12.2" //no auto scala, always enforce a specific version that is specified somewhere // val extra:List[String] = ctx.get[String]("extra").map{_.trim.split("[\\s,:]+").toList} getOrElse Nil val extra:List[String] = ctx.split[String]("extra","[\\s,:]+").toList val cp:Classpath = Classpath(ctx.split[File]("cp").toList) val debug:Boolean = ctx.getOrElse[Boolean]("debug", false) val lang:Option[String] = ctx getString "lang" val preArgs:Array[String] = ctx.split[String]("args","\\s+").toArray val fork:Boolean = ctx.getOrElse[Boolean]("fork", false) //val debug = false //FIXME make this debug BootInfo launcher set via a command line or property to remove this debug so the launcher will work //--add custom jvm Props // for((k,v) <- (ctx/'prop).toMap) sys.props(k) = v sys.props ++= (ctx/'prop).toMap if(fork || debug){ println(cpBoot.nice) // hack the main with "cc.drx.BootInfo", and add the actual mainClass to the conf.args so the BootFork can launch the original mainClass and pull it off the argument stack val altMain = if(debug) "cc.drx.BootInfo" else "cc.drx.BootFork" val appId = new AppId(org,prj, ver, main=altMain, cross=cross, extra=extra, cp=cpBoot, scala="2.11.11", ctx) AppConf(appId, Array(main) ++ preArgs ++ args, base) } else{ val appId = new AppId(org,prj, ver, main=main, cross=cross, extra=extra, cp=cp, scala=scala, ctx) println(s"proxy to a app: $appId...") AppConf(appId, preArgs ++ args, base) } } //--select result to return (None on errors) if(ctxOption.isEmpty){ //TODO furuther filter the bootKeys by luanchable types println(s"${Red ansi "Error"}: Could not find launch parameters for key:${Orange ansi key}") val alts = bootKeys.keys filter (_ soundsLike key) if(alts.nonEmpty) println(s" did you mean: " + alts.map{Green ansi _}.mkString(" | ")) else{ println("Available keys are:") bootKeys.keys map (_ fit 20) grouped 5 map {_ mkString ""} foreach println } None } else if(boot.isEmpty){ println(s"${Red ansi "Error"}: invalid (missing) launch configuration for key:${Orange ansi key}") None } else boot //return the first match } //nicer Rich wrappers class ForkConf(val conf: xsbti.AppConfiguration){ val cp = Boot.classpathFrom(conf) val thisArgs = conf.arguments val thisMain = conf.provider.id.mainClass val main = thisArgs.headOption getOrElse thisMain //the passed in val base = File(conf.baseDirectory) val args = thisArgs.drop(1) val shell = Shell(List("java","-cp", cp.toString, main) ++ args) //TODO make a BootFork that does the forked app work override def toString = List( Boot.pp(20, "BaseDirectory:",base.path), Boot.pp(20, "Args:", args.mkString(" ")), Boot.pp(20, "Main:",main), Boot.pp(20, "Classpath:","...\n"+cp.nice.indent) ).mkString("\n") } } class BootFork extends xsbti.AppMain{ def run(appConf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{ println("#BootFork hijacked the boot to collect a claspath that can be forked.") //just print out the information val conf = new Boot.ForkConf(appConf) println(conf) val res = conf.shell.fork //do the fork work println(res) new Boot.Exit(0) } } class BootInfo extends xsbti.AppMain{ def run(appConf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{ Timer("BootInfo"){ println("#BootInfo hijacked the boot for debug purposes.") //just print out the information def pp(title:String, kvs:Map[String,String]):Unit = { println("# " + Color.Green.ansi(title)) val maxWidth = kvs.keys.map{_.size}.max for((k,v) <- kvs.toList.sortBy{_._1.toLowerCase}) Boot.pp(maxWidth, k, v) } BootKeys.main(Array.empty[String]) //list the available bootkeys pp("Environment variables", sys.env ) pp("JVM properties", sys.props.toMap ) //to an immutable map val conf = new Boot.ForkConf(appConf) println(conf) //just print out the information println(conf.shell) //just print out the information new Boot.Exit(0) } } } class BootKeys(kson:Kson){ //--private private lazy val rootPaths = for(line <- kson.lines if line.roots.nonEmpty) yield line.pathList //just the lines with a root key def private lazy val scopeMap = rootPaths.map{p => p.head -> p.tail.reverse.mkString("/")}.toMap def getContext(key:String):Option[StringMap.Scoped] = scopeMap.get(key).map(kson/_/key) //--public lazy val keys = rootPaths.map{_.head}.toSet //keys sorted in the order of the kson config file def print(requestKeys:Seq[String]):Unit = { val missingKeys = requestKeys.filterNot(scopeMap contains _) //--print missing key warning if(missingKeys.nonEmpty) { for(miss <- missingKeys){ val alts = for(key <- keys if miss soundsLike key) yield key println(s"no key found for `$miss`" ) if(alts.nonEmpty) println(" maybe try: " + alts.mkString(", ") ) } } //-- no missing keys else{ for(key <- if(requestKeys.isEmpty) keys else requestKeys; scope <- scopeMap get key; //lookup the scope just for the nice paren printing ctx <- getContext(key); (org,prj,ver) <- ctx.get("org","prj","ver") ) { val scopeParen = "("+scope+")" println(f"$key%-20s $scopeParen%-25s $prj%-25s $ver%-10s $org%-20s") } } } def dep(key:String):Option[(String,String,String)] = { for( scope <- scopeMap get key; //lookup the scope just for the nice paren printing ctx <- getContext(key); orgPrjVer <- ctx.get("org","prj","ver") ) yield orgPrjVer } } //--object BootKeys object BootKeys{ def findBootFile:Option[File] = { val defaultFile = File.cwd/".bt" orElse File.cwd/".bt.kson" orElse File.cwd/"bt.kson" orElse File.cwd/".bt.kson" orElse File.home/".bt" orElse File.home/".bt.kson" orElse File.home/"bt.kson" // println(s"in ${if(defaultFile.isFile) defaultFile else "default"} ") if(defaultFile.isFile) Some(defaultFile) else None } def loadConfig:Kson = { val ksonDefault = Input.resource(file"bt.kson").map{Kson.apply} getOrElse Kson.empty //should always be there val ksonBootFile = findBootFile.map{Kson(_)} getOrElse Kson.empty ksonDefault ++ ksonBootFile } def load:BootKeys = new BootKeys(loadConfig) def apply(kson:Kson):BootKeys = new BootKeys(kson) def main(args:Array[String]):Unit = Jansi{ println("Boot config bt.kson keys") val bootKeys = Timer("load boot keys"){BootKeys.load} Timer("find/list keys"){ bootKeys print args } //TODO use fuzzy finding match for missing keys Console.flush 1.s.sleep//sleep to prevent early stream is cut before finsihing } } class Boot extends xsbti.AppMain{ def run(conf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{ val key = conf.arguments.headOption getOrElse "sbt" //TODO add an auto boot detector app that checks if it is an sbt or gradle or mvn project or undefined project val args = conf.arguments.drop(1) Boot.find(conf,key,args).map{_.run}.getOrElse{new Boot.Exit(0)} //TODO add console/shell runner if empty args??? } } /**Boot console to loop and load the boot keys onces, and TODO provide key completion and path executible complesion and lookup (system independent)*/ class BootConsole extends xsbti.AppMain{ private val exitKeys = Set("exit","quit",":q", ":exit", ":quite", "exit()") def run(conf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{ // import Implicit.ec // Console.flush def exitBoot():Unit = println(Red ansi "exiting Boot console") //--define the prompt @tailrec def prompt():Unit = { print(s"\n${Green ansi "bt"}${Grey ansi "> "}") // val line:String = ??? //scala.io.StdIn.readLine().trim //TODO use a char buffer to support tab complete and offer a generic 2.10 solution val line:String = Input.std.promptLine.trim if(exitKeys contains line) exitBoot() else { val arguments = line.split("""\s+""") val key = arguments.headOption getOrElse "sbt" //TODO add an auto boot detector app that checks if it is an sbt or gradle or mvn project or undefined project val args = arguments.drop(1) val boot = Boot.find(conf, key, args) // println(s"console boot: $boot") // boot.map{_.run}.getOrElse{new Boot.Exit(0)} boot.foreach{bt => // println(conf.provider.newMain()) // sbt.boot.Launch bt.run //FIXME how to really run the boot? from the sbtLauncher??? need to makea new provider?? } // conf.provider.loader prompt() //recursivly show the prompt } } //--recursively read/eval/execute the prompt prompt() new Boot.Exit(0) } } /* //run a shell command (passing through system independent; i.e. fixes for windows) object BootShell{ def main(args:Array[String]):Unit = Jansi{ import Implicit.ec val shell = Shell(args) val res = shell.run.blockWithoutWarning(1.yr) Console.flush // res getOrElse 1 //1 is an error } } */