// // Copyright (c) 2006, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 3 Nov 06 Brian Frank Creation // using compiler ** ** BuildScript is the base class for build scripts - it manages ** the command line interface, argument parsing, environment, and ** target execution. ** ** See `docTools::Build` for details. ** abstract class BuildScript { ////////////////////////////////////////////////////////////////////////// // Env ////////////////////////////////////////////////////////////////////////// ** ** Log used for error reporting and tracing ** BuildLog log := BuildLog() ** ** The source file of this script ** const File scriptFile := File(typeof->sourceFile.toStr.toUri).normalize ** ** The directory containing the this script ** const File scriptDir := scriptFile.parent ** ** Home directory of development installation. By default this ** value is initialized by 'devHome' config prop, otherwise ** `sys::Env.homeDir` is used. ** const File devHomeDir := configDir("devHome", Env.cur.homeDir) ////////////////////////////////////////////////////////////////////////// // Targets ////////////////////////////////////////////////////////////////////////// ** ** Lookup a target by name. If not found and checked is ** false return null, otherwise throw an exception. ** TargetMethod? target(Str name, Bool checked := true) { t := targets.find |t| { t.name == name } if (t != null) return t if (checked) throw Err("Target not found '$name' in $scriptFile") return null } ** ** Get the list of published targets for this script. The ** first target should be the default. The list of targets ** is defined by all the methods with the `Target` facet. ** virtual once TargetMethod[] targets() { acc := TargetMethod[,] typeof.methods.each |m| { if (!m.hasFacet(Target#)) return acc.add(TargetMethod(this, m)) } return acc } ////////////////////////////////////////////////////////////////////////// // Utils ////////////////////////////////////////////////////////////////////////// ** ** Get a config property using the following rules: ** 1. `sys::Env.vars` with 'FAN_BUILD_$name.upper' ** 2. `sys::Env.config` for build pod ** 3. fallback to 'def' parameter ** Str? config(Str name, Str? def := null) { Env.cur.vars["FAN_BUILD_$name.upper"] ?: Env.cur.config(BuildScript#.pod, name, def) } ** ** Get a `config` prop which identifies a directory. ** If the prop isn't configured or doesn't map to a ** valid directory, then return def. ** File? configDir(Str name, File? def := null) { c := config(name) if (c == null) return def try { f := File(c.toUri) if (!f.exists || !f.isDir) throw Err() return f } catch (Err e) log.err("Invalid configDir URI for '$name': $c\n $e") return def } ** ** Resolve a set of URIs to files relative to scriptDir. ** internal File[] resolveFiles(Uri[] uris) { uris.map |uri->File| { f := scriptDir + uri if (!f.exists || f.isDir) throw fatal("Invalid file: $uri") return f } } ** ** Resolve a set of URIs to directories relative to scriptDir. ** internal File[] resolveDirs(Uri[] uris) { uris.map |uri->File| { f := scriptDir + uri if (!f.exists || !f.isDir) throw fatal("Invalid dir: $uri") return f } } ** ** Resolve a set of URIs to files/dirs relative to scriptDir. ** internal File[] resolveFilesOrDirs(Uri[] uris) { uris.map |uri->File| { f := scriptDir + uri if (!f.exists) throw fatal("Invalid file: $uri") return f } } ** ** Dump script environment for debug. ** virtual Void dumpEnv() { log.printLine("---------------") log.printLine(" scriptFile: $scriptFile") log.printLine(" typeof: $typeof.base") log.printLine(" env.homeDir: $Env.cur.homeDir") log.printLine(" env.workDir: $Env.cur.workDir") log.printLine(" devHomeDir: $devHomeDir") typeof.fields.each |f| { if (f.isPublic && !f.isStatic && f.parent != BuildScript#) log.printLine(" " + (f.name+ ":").padr(14) + " " + f.get(this)) } } ** ** Log an error and return a FatalBuildErr instance ** FatalBuildErr fatal(Str msg, Err? err := null) { log.err(msg, err) return FatalBuildErr(msg, err) } ** ** Return this script's source file path. ** override Str toStr() { return typeof->sourceFile.toStr } ////////////////////////////////////////////////////////////////////////// // Arguments ////////////////////////////////////////////////////////////////////////// ** ** Parse the arguments passed from the command line. ** Return true for success or false to end the script. ** private TargetMethod[]? parseArgs(Str[] args) { // check for -? or -dumpEnv if (args.contains("-?") || args.contains("-help")) { usage; return null } if (args.contains("-dumpEnv")) { dumpEnv; return null } success := true toRun := TargetMethod[,] // get published targetss published := targets if (published.isEmpty) { log.err("No targets available for script") return null } // process each argument for (i:=0; i<args.size; ++i) { arg := args[i] if (arg == "-v") { log.level = LogLevel.debug; dumpEnv } else if (arg.startsWith("-")) log.warn("Unknown build option $arg") else { // add target to our run list target := published.find |t| { t.name == arg } if (target == null) { log.err("Unknown build target '$arg'") success = false } else { toRun.add(target) } } } if (!success) return null // if no targets specified, then use the default if (toRun.isEmpty) toRun.add(published.first) return toRun } ** ** Dump usage including all this script's published targets. ** private Void usage() { log.printLine("usage: ") log.printLine(" build [options] <target>*") log.printLine("options:") log.printLine(" -? -help Print usage summary") log.printLine(" -v Verbose debug logging") log.printLine(" -dumpEnv Debug dump of script env") log.printLine("targets:") targets.each |t, i| { n := i == 0 ? "${t.name}*" : "${t.name} " log.print(" ${n.justl(14)} $t.help") log.printLine } } ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// ** ** Run the script with the specified arguments. ** Return 0 on success or -1 on failure. ** Int main(Str[] args := Env.cur.args) { t1 := Duration.now success := false try { targetsToRun := parseArgs(args) if (targetsToRun == null) return 1 targetsToRun.each |t| { t.run } success = true } catch (FatalBuildErr err) { // error should have alredy been logged } catch (Err err) { log.err("Internal build error [$toStr]") err.trace } t2 := Duration.now if (success) { if (log.level <= LogLevel.info) log.out.printLine("BUILD SUCCESS [${(t2-t1).toMillis}ms]!") } else { log.out.printLine("BUILD FAILED [${(t2-t1).toMillis}ms]!") } return success ? 0 : -1 } }