logo

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()
  {
    log = BuildLog.make
    initEnv
    try
    {
      setup
      validate
      targets = makeTargets.ro
    }
    catch (Err err)
    {
      log.error("Error initializing script [$scriptFile.osPath]")
      throw err
    }
  }

//////////////////////////////////////////////////////////////////////////
// Env
//////////////////////////////////////////////////////////////////////////

  ** The source file of this script
  File scriptFile

  ** The directory containing the this script
  File scriptDir

  ** Home directory of development installation.  By default this
  ** value is initialized by Sys.env["fan.build.devHome"], otherwise
  ** Sys.homeDir is used.
  File devHomeDir

  ** {devHomeDir}/bin/
  File binDir

  ** {devHomeDir}/lib/
  File libDir

  ** {devHomeDir}/lib/fan
  File libFanDir

  ** {devHomeDir}/lib/java
  File libJavaDir

  ** {devHomeDir}/lib/net
  File libNetDir

  ** This is the global default version to use when building pods.  It
  ** is initialized by Sys.env["fan.build.globalVersion"], otherwise
  ** "0.0.0" is used as a default.
  Version globalVersion

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

  **
  ** Initialize the environment
  **
  private Void initEnv()
  {
    // init devHomeDir
    devHomeDir = Sys.homeDir
    devHomeProp := Sys.env["fan.build.devHome"]
    if (devHomeProp != null)
    {
      try
      {
        f := File.make(devHomeProp.toUri)
        if (!f.exists || !f.isDir) throw Err.make
        devHomeDir = f
      }
      catch
      {
        log.error("Invalid URI for fan.build.devHome: $devHomeProp")
      }
    }

    // global version
    globalVersion = Version.fromStr("0.0.0")
    globalVersionProp := Sys.env["fan.build.globalVersion"]
    if (globalVersionProp != null)
    {
      try
      {
        globalVersion = Version.fromStr(globalVersionProp)
      }
      catch
      {
        log.error("Invalid Version for fan.build.globalVersion: $globalVersionProp")
      }
    }

    // are we running on a Window's box?
    osName := Sys.env["os.name"]
    isWindows = osName != null && osName.lower.contains("win")

    // exeExt
    exeExt = isWindows ? ".exe" : ""

    // directories
    scriptFile = File.make(type->sourceFile.toStr.toUri)
    scriptDir  = scriptFile.parent
    binDir     = devHomeDir + `bin/`
    libDir     = devHomeDir + `lib/`
    libFanDir  = devHomeDir + `lib/fan/`
    libJavaDir = devHomeDir + `lib/java/`
    libNetDir  = devHomeDir + `lib/net/`

    // debug
    if (log.isDebug)
    {
      log.printLine("BuildScript Environment:")
      log.printLine("  scriptFile:    $scriptFile")
      log.printLine("  scriptDir:     $scriptDir")
      log.printLine("  devHomeDir:    $devHomeDir")
      log.printLine("  binDir:        $binDir")
      log.printLine("  libDir:        $libDir")
      log.printLine("  libFanDir:     $libFanDir")
      log.printLine("  libJavaDir:    $libJavaDir")
      log.printLine("  libNetDir:     $libNetDir")
      log.printLine("  globalVersion: $globalVersion")
    }
  }

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

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

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

  **
  ** Return the default target to execute when this script is run.
  **
  abstract Target defaultTarget()

  **
  ** 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 (targets == null) throw Err.make("script not setup yet")
    t := targets.find |Target t->Bool| { return t.name == name }
    if (t != null) return t
    if (checked) throw Err.make("Target not found: $name")
    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[,]
    type.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.make(this, m.name, description, toFunc(m)))
    }
    return targets
  }

  // TODO: need Func.curry
  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.error("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
      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.error("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 == def ? "${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 := type.field(field).get(this)
    if (val != null) return true
    log.error("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.make("Unexpected null Uri")
        files.add(null)
        return
      }

      file := scriptDir + uri
      if (!file.exists || file.isDir != expectDir )
      {
        ok = false
        if (expectDir)
          log.error("Invalid directory [$uri]")
        else
          log.error("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.error(msg, err)
    return FatalBuildErr.make
  }

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////

  **
  ** Run the script with the specified arguments.
  ** Return 0 on success or -1 on failure.
  **
  Int main(Str[] args := Sys.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.error("Internal build error [$toStr]")
      err.trace
    }
    t2 := Duration.now

    if (success)
      echo("BUILD SUCCESS [${(t2-t1).toMillis}ms]!")
    else
      echo("BUILD FAILED [${(t2-t1).toMillis}ms]!")
    return success ? 0 : -1
  }

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

  ** Log used for error reporting and tracing
  BuildLog log

  ** Targets available on this script (see `makeTargets`)
  readonly Target[] targets

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

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

}