Fantom

 

abstract class

build::BuildScript

sys::Obj
  build::BuildScript
//
// 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
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  **
  ** Construct a new build script.
  **
  new make()
  {
    initEnv
    try
    {
      setup
      validate
      targets = makeTargets.ro
    }
    catch (Err err)
    {
      log.err("Error initializing script [$scriptFile.osPath]")
      throw err
    }
  }

//////////////////////////////////////////////////////////////////////////
// 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)

  ** 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)

  ** {devHomeDir}/bin/
  const File binDir := devHomeDir + `bin/`

  ** {devHomeDir}/lib/
  const File libDir := devHomeDir + `lib/`

  ** {devHomeDir}/lib/fan
  const File libFanDir := devHomeDir + `lib/fan/`

  ** {devHomeDir}/lib/java
  const File libJavaDir := devHomeDir + `lib/java/`

  ** {devHomeDir}/lib/java/ext
  const File libJavaExtDir := devHomeDir + `lib/java/ext/`

  ** {devHomeDir}/lib/java/ext/{Env.cur.platform}
  File libJavaExtPlatformDir := libJavaExtDir + `$Env.cur.platform/`

  ** {devHomeDir}/lib/dotnet
  const File libDotnetDir := devHomeDir + `lib/dotnet/`

  ** Executable extension: ".exe" on Windows and "" on Unix.
  Str exeExt := ""

  **
  ** 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
  }

  ** Initialize the environment
  internal virtual Void initEnv()
  {
    // are we running on a Window's box?
    isWindows = Env.cur.os == "win32"
    exeExt = isWindows ? ".exe" : ""
  }

  @target="Dump env details to help build debugging"
  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")
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  **
  ** Return this script's source file path.
  **
  override Str toStr()
  {
    return typeof->sourceFile.toStr
  }

//////////////////////////////////////////////////////////////////////////
// Targets
//////////////////////////////////////////////////////////////////////////

  **
  ** Return the default target to execute when this script is run.
  ** If this method is not overridden, then the default is to
  ** return the first target declared in the script itself.
  **
  virtual Target defaultTarget()
  {
    targets := makeTargets
    if (targets.isEmpty) throw Err("No targets declared")
    def := targets.find |Target t->Bool| { !t.name.startsWith("dump") }
    return def ?: targets.first
  }

  **
  ** Lookup a target by name.  If not found and checked is
  ** false return null, otherwise throw an exception.  This
  ** method cannot be called until after the script has completed
  ** its constructor.
  **
  Target? target(Str name, Bool checked := true)
  {
    if ((Obj?)targets == null) throw Err("script not setup yet")
    t := targets.find |Target t->Bool| { return t.name == name }
    if (t != null) return t
    if (checked) throw Err("Target not found '$name' in $scriptFile")
    return null
  }

  **
  ** This callback is invoked by the 'BuildScript' constructor after
  ** the call to `setup` to initialize the list of the targets this
  ** script publishes.  The list of  targets is built from all the
  ** methods annotated with the "target" facet.  The "target" facet
  ** should have a string value with a description of what the target
  ** does.
  **
  virtual Target[] makeTargets()
  {
    targets := Target[,]
    typeof.methods.each |Method m|
    {
      description := m.facet(@target)
      if (description == null) return

      if (!(description is Str))
      {
        log.warn("Invalid target facet ${m.qname}@target")
        return
      }

      if (m.params.size > 0 && !m.params.first.hasDefault)
      {
        log.warn("Invalid target method ${m.qname}")
        return
      }

      targets.add(Target(this, m.name, description, toFunc(m)))
    }
    return targets
  }

  private Func toFunc(Method m) { return |->| { m.callOn(this, null) } }

//////////////////////////////////////////////////////////////////////////
// Arguments
//////////////////////////////////////////////////////////////////////////

  **
  ** Parse the arguments passed from the command line.
  ** Return true for success or false to end the script.
  **
  private Bool parseArgs(Str[] args)
  {
    // check for usage
    if (args.contains("-?") || args.contains("-help"))
    {
      usage
      return false
    }

    success := true
    toRun = Target[,]

    // get published targetss
    published := targets
    if (published.isEmpty)
    {
      log.err("No targets available for script")
      return false
    }

    // 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 |Target t->Bool| { return t.name == arg }
        if (target == null)
        {
          log.err("Unknown build target '$arg'")
          success = false
        }
        else
        {
          toRun.add(target)
        }
      }
    }

    // if no targets specified, then use the default
    if (toRun.isEmpty)
      toRun.add(defaultTarget)

    // return success flag
    return success
  }

  **
  ** 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("targets:")
    def := defaultTarget
    targets.each |Target t, Int i|
    {
      n := t.name == def.name ? "${t.name}*" : "${t.name} "
      log.print("  ${n.justl(14)} $t.description")
      log.printLine
    }
  }

//////////////////////////////////////////////////////////////////////////
// Setup
//////////////////////////////////////////////////////////////////////////

  **
  ** The setup callback is invoked before creating or processing of
  ** any targets to ensure that the BuildScript is correctly initialized.
  ** If the script cannot be setup then report errors via the log and
  ** throw FatalBuildErr to terminate the script.
  **
  virtual Void setup()
  {
  }

  **
  ** Internal callback to validate setup
  **
  internal virtual Void validate()
  {
  }

  **
  ** Check that the specified field is non-null, if not
  ** then log an error and return false.
  **
  internal Bool validateReqField(Str field)
  {
    val := typeof.field(field).get(this)
    if (val != null) return true
    log.err("Required field not set: '$field' [$toStr]")
    return false
  }

  **
  ** Convert a Uri to a directory and verify it exists.
  **
  internal File? resolveDir(Uri? uri, Bool nullOk := false)
  {
    return resolveUris([uri], nullOk, true)[0]
  }

  **
  ** Convert a Uri to a file and verify it exists.
  **
  internal File? resolveFile(Uri? uri, Bool nullOk := false)
  {
    return resolveUris([uri], nullOk, false)[0]
  }

  **
  ** Convert a list of Uris to directories and verify they all exist.
  **
  internal File?[] resolveDirs(Uri?[]? uris, Bool nullOk := false)
  {
    return resolveUris(uris, nullOk, true)
  }

  **
  ** Convert a list of Uris to files and verify they all exist.
  **
  internal File?[] resolveFiles(Uri?[]? uris, Bool nullOk := false)
  {
    return resolveUris(uris, nullOk, false)
  }

  private File?[] resolveUris(Uri?[]? uris, Bool nullOk, Bool expectDir)
  {
    files := File?[,]
    if (uris == null) return files

    files.capacity = uris.size
    ok := true
    uris.each |Uri? uri|
    {
      if (uri == null)
      {
        if (!nullOk) throw FatalBuildErr("Unexpected null Uri")
        files.add(null)
        return
      }

      file := scriptDir + uri
      if (!file.exists || file.isDir != expectDir )
      {
        ok = false
        if (expectDir)
          log.err("Invalid directory [$uri]")
        else
          log.err("Invalid file [$uri]")
      }
      files.add(file)
    }
    if (!ok) throw FatalBuildErr.make
    return files
  }

//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////

  **
  ** Log an error and return a FatalBuildErr instance
  **
  FatalBuildErr fatal(Str msg, Err? err := null)
  {
    log.err(msg, err)
    return FatalBuildErr(msg, err)
  }

//////////////////////////////////////////////////////////////////////////
// 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
    {
      if (!parseArgs(args)) return -1
      toRun.each |Target 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
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  ** Targets available on this script (see `makeTargets`)
  readonly Target[] targets := Target#.emptyList

  ** Targets specified to run by command line
  Target[]? toRun

  ** Are we running on a Window's box
  internal Bool isWindows

}