logo

abstract class

build::BuildScript

sys::Obj
  build::BuildScript
   1  //
   2  // Copyright (c) 2006, Brian Frank and Andy Frank
   3  // Licensed under the Academic Free License version 3.0
   4  //
   5  // History:
   6  //   3 Nov 06  Brian Frank  Creation
   7  //
   8  
   9  using compiler
  10  
  11  **
  12  ** BuildScript is the base class for build scripts - it manages
  13  ** the command line interface, argument parsing, environment, and
  14  ** target execution.
  15  **
  16  ** See `docTools::Build` for details.
  17  **
  18  abstract class BuildScript
  19  {
  20  
  21  //////////////////////////////////////////////////////////////////////////
  22  // Construction
  23  //////////////////////////////////////////////////////////////////////////
  24  
  25    **
  26    ** Construct a new build script.
  27    **
  28    new make()
  29    {
  30      log = BuildLog.make
  31      initEnv
  32      try
  33      {
  34        setup
  35        validate
  36        targets = makeTargets.ro
  37      }
  38      catch (Err err)
  39      {
  40        log.error("Error initializing script [$scriptFile.osPath]")
  41        throw err
  42      }
  43    }
  44  
  45  //////////////////////////////////////////////////////////////////////////
  46  // Env
  47  //////////////////////////////////////////////////////////////////////////
  48  
  49    ** The source file of this script
  50    File scriptFile
  51  
  52    ** The directory containing the this script
  53    File scriptDir
  54  
  55    ** Home directory of development installation.  By default this
  56    ** value is initialized by Sys.env["fan.build.devHome"], otherwise
  57    ** Sys.homeDir is used.
  58    File devHomeDir
  59  
  60    ** {devHomeDir}/bin/{os}/
  61    File binDir
  62  
  63    ** {devHomeDir}/lib/
  64    File libDir
  65  
  66    ** {devHomeDir}/lib/fan
  67    File libFanDir
  68  
  69    ** {devHomeDir}/lib/java
  70    File libJavaDir
  71  
  72    ** {devHomeDir}/lib/net
  73    File libNetDir
  74  
  75    ** This is the global default version to use when building pods.  It
  76    ** is initialized by Sys.env["fan.build.globalVersion"], otherwise
  77    ** "0.0.0" is used as a default.
  78    Version globalVersion
  79  
  80    **
  81    ** Initialize the environment
  82    **
  83    private Void initEnv()
  84    {
  85      // init devHomeDir
  86      devHomeDir = Sys.homeDir
  87      devHomeProp := Sys.env["fan.build.devHome"]
  88      if (devHomeProp != null)
  89      {
  90        try
  91        {
  92          f := File.make(devHomeProp.toUri)
  93          if (!f.exists || !f.isDir) throw Err.make
  94          devHomeDir = f
  95        }
  96        catch
  97        {
  98          log.error("Invalid URI for fan.build.devHome: $devHomeProp")
  99        }
 100      }
 101  
 102      // global version
 103      globalVersion = Version.fromStr("0.0.0")
 104      globalVersionProp := Sys.env["fan.build.globalVersion"]
 105      if (devHomeProp != null)
 106      {
 107        try
 108        {
 109          globalVersion = Version.fromStr(globalVersionProp)
 110        }
 111        catch
 112        {
 113          log.error("Invalid Version for fan.build.globalVersion: $globalVersionProp")
 114        }
 115      }
 116  
 117      // directories
 118      scriptFile = File.make(type->sourceFile.toStr.toUri)
 119      scriptDir  = scriptFile.parent
 120      binDir     = devHomeDir + `bin/win/`
 121      libDir     = devHomeDir + `lib/`
 122      libFanDir  = devHomeDir + `lib/fan/`
 123      libJavaDir = devHomeDir + `lib/java/`
 124      libNetDir  = devHomeDir + `lib/net/`
 125  
 126      // debug
 127      if (log.isDebug)
 128      {
 129        log.printLine("BuildScript Environment:")
 130        log.printLine("  scriptFile:    $scriptFile")
 131        log.printLine("  scriptDir:     $scriptDir")
 132        log.printLine("  devHomeDir:    $devHomeDir")
 133        log.printLine("  binDir:        $binDir")
 134        log.printLine("  libDir:        $libDir")
 135        log.printLine("  libFanDir:     $libFanDir")
 136        log.printLine("  libJavaDir:    $libJavaDir")
 137        log.printLine("  libNetDir:     $libNetDir")
 138        log.printLine("  globalVersion: $globalVersion")
 139      }
 140    }
 141  
 142  //////////////////////////////////////////////////////////////////////////
 143  // Identity
 144  //////////////////////////////////////////////////////////////////////////
 145  
 146    **
 147    ** Return this script's source file path.
 148    **
 149    override Str toStr()
 150    {
 151      return type->sourceFile.toStr
 152    }
 153  
 154  //////////////////////////////////////////////////////////////////////////
 155  // Targets
 156  //////////////////////////////////////////////////////////////////////////
 157  
 158    **
 159    ** Return the default target to execute when this script is run.
 160    **
 161    abstract Target defaultTarget()
 162  
 163    **
 164    ** Lookup a target by name.  If not found and checked is
 165    ** false return null, otherwise throw an exception.  This
 166    ** method cannot be called until after the script has completed
 167    ** its constructor.
 168    **
 169    Target target(Str name, Bool checked := true)
 170    {
 171      if (targets == null) throw Err.make("script not setup yet")
 172      t := targets.find |Target t->Bool| { return t.name == name }
 173      if (t != null) return t
 174      if (checked) throw Err.make("Target not found: $name")
 175      return null
 176    }
 177  
 178    **
 179    ** This callback is invoked by the 'BuildScript' constructor after
 180    ** the call to `setup` to initialize the list of the targets this
 181    ** script publishes.  The list of  targets is built from all the
 182    ** methods annotated with the "target" facet.  The "target" facet
 183    ** should have a string value with a description of what the target
 184    ** does.
 185    **
 186    virtual Target[] makeTargets()
 187    {
 188      targets := Target[,]
 189      type.methods.each |Method m|
 190      {
 191        description := m.facet("target")
 192        if (description == null) return
 193  
 194        if (!(description is Str))
 195        {
 196          log.warn("Invalid target facet ${m.qname}@target")
 197          return
 198        }
 199  
 200        if (m.params.size > 0 && !m.params.first.hasDefault)
 201        {
 202          log.warn("Invalid target method ${m.qname}")
 203          return
 204        }
 205  
 206        targets.add(Target.make(this, m.name, description, toFunc(m)))
 207      }
 208      return targets
 209    }
 210  
 211    // TODO: need Func.curry
 212    private Func toFunc(Method m) { return |,| { m.callOn(this, null) } }
 213  
 214  //////////////////////////////////////////////////////////////////////////
 215  // Arguments
 216  //////////////////////////////////////////////////////////////////////////
 217  
 218    **
 219    ** Parse the arguments passed from the command line.
 220    ** Return true for success or false to end the script.
 221    **
 222    private Bool parseArgs(Str[] args)
 223    {
 224      // check for usage
 225      if (args.contains("-?") || args.contains("-help"))
 226      {
 227        usage
 228        return false
 229      }
 230  
 231      success := true
 232      toRun = Target[,]
 233  
 234      // get published targetss
 235      published := targets
 236      if (published.isEmpty)
 237      {
 238        log.error("No targets available for script")
 239        return false
 240      }
 241  
 242      // process each argument
 243      for (i:=0; i<args.size; ++i)
 244      {
 245        arg := args[i]
 246        if (arg == "-v") log.level = LogLevel.debug
 247        else if (arg.startsWith("-")) log.warn("Unknown build option $arg")
 248        else
 249        {
 250          // add target to our run list
 251          target := published.find |Target t->Bool| { return t.name == arg }
 252          if (target == null)
 253          {
 254            log.error("Unknown build target '$arg'")
 255            success = false
 256          }
 257          else
 258          {
 259            toRun.add(target)
 260          }
 261        }
 262      }
 263  
 264      // if no targets specified, then use the default
 265      if (toRun.isEmpty)
 266        toRun.add(defaultTarget)
 267  
 268      // return success flag
 269      return success
 270    }
 271  
 272    **
 273    ** Dump usage including all this script's published targets.
 274    **
 275    private Void usage()
 276    {
 277      log.printLine("usage: ")
 278      log.printLine("  build [options] <target>*")
 279      log.printLine("options:")
 280      log.printLine("  -? -help       print usage summary")
 281      log.printLine("  -v             verbose debug logging")
 282      log.printLine("targets:")
 283      def := defaultTarget
 284      targets.each |Target t, Int i|
 285      {
 286        n := t == def ? "${t.name}*" : "${t.name} "
 287        log.print("  ${n.justl(14)} $t.description")
 288        log.printLine
 289      }
 290    }
 291  
 292  //////////////////////////////////////////////////////////////////////////
 293  // Setup
 294  //////////////////////////////////////////////////////////////////////////
 295  
 296    **
 297    ** The setup callback is invoked before creating or processing of
 298    ** any targets to ensure that the BuildScript is correctly initialized.
 299    ** If the script cannot be setup then report errors via the log and
 300    ** throw FatalBuildErr to terminate the script.
 301    **
 302    virtual Void setup()
 303    {
 304    }
 305  
 306    **
 307    ** Internal callback to validate setup
 308    **
 309    internal virtual Void validate()
 310    {
 311    }
 312  
 313    **
 314    ** Check that the specified field is non-null, if not
 315    ** then log an error and return false.
 316    **
 317    internal Bool validateReqField(Str field)
 318    {
 319      val := type.field(field).get(this)
 320      if (val != null) return true
 321      log.error("Required field not set: '$field' [$toStr]")
 322      return false
 323    }
 324  
 325    **
 326    ** Convert a Uri to a directory and verify it exists.
 327    **
 328    internal File resolveDir(Uri uri, Bool nullOk := false)
 329    {
 330      return resolveUris([uri], nullOk, true)[0]
 331    }
 332  
 333    **
 334    ** Convert a Uri to a file and verify it exists.
 335    **
 336    internal File resolveFile(Uri uri, Bool nullOk := false)
 337    {
 338      return resolveUris([uri], nullOk, false)[0]
 339    }
 340  
 341    **
 342    ** Convert a list of Uris to directories and verify they all exist.
 343    **
 344    internal File[] resolveDirs(Uri[] uris, Bool nullOk := false)
 345    {
 346      return resolveUris(uris, nullOk, true)
 347    }
 348  
 349    **
 350    ** Convert a list of Uris to files and verify they all exist.
 351    **
 352    internal File[] resolveFiles(Uri[] uris, Bool nullOk := false)
 353    {
 354      return resolveUris(uris, nullOk, false)
 355    }
 356  
 357    private File[] resolveUris(Uri[] uris, Bool nullOk, Bool expectDir)
 358    {
 359      files := File[,]
 360      if (uris == null) return files
 361  
 362      files.capacity = uris.size
 363      ok := true
 364      uris.each |Uri uri|
 365      {
 366        if (uri == null)
 367        {
 368          if (!nullOk) throw FatalBuildErr.make("Unexpected null Uri")
 369          files.add(null)
 370          return
 371        }
 372  
 373        file := scriptDir + uri
 374        if (!file.exists || file.isDir != expectDir )
 375        {
 376          ok = false
 377          if (expectDir)
 378            log.error("Invalid directory [$uri]")
 379          else
 380            log.error("Invalid file [$uri]")
 381        }
 382        files.add(file)
 383      }
 384      if (!ok) throw FatalBuildErr.make
 385      return files
 386    }
 387  
 388  //////////////////////////////////////////////////////////////////////////
 389  // Utils
 390  //////////////////////////////////////////////////////////////////////////
 391  
 392    **
 393    ** Log an error and return a FatalBuildErr instance
 394    **
 395    FatalBuildErr fatal(Str msg, Err err := null)
 396    {
 397      log.error(msg, err)
 398      return FatalBuildErr.make
 399    }
 400  
 401  //////////////////////////////////////////////////////////////////////////
 402  // Main
 403  //////////////////////////////////////////////////////////////////////////
 404  
 405    **
 406    ** Run the script with the specified arguments.
 407    ** Return 0 on success or -1 on failure.
 408    **
 409    Int main(Str[] args := Sys.args)
 410    {
 411      t1 := Duration.now
 412      success := false
 413      try
 414      {
 415        if (!parseArgs(args)) return -1
 416        toRun.each |Target t| { t.run }
 417        success = true
 418      }
 419      catch (FatalBuildErr err)
 420      {
 421        // error should have alredy been logged
 422      }
 423      catch (Err err)
 424      {
 425        log.error("Internal build error [$toStr]")
 426        err.trace
 427      }
 428      t2 := Duration.now
 429  
 430      if (success)
 431        echo("BUILD SUCCESS [${(t2-t1).toMillis}ms]!")
 432      else
 433        echo("BUILD FAILED [${(t2-t1).toMillis}ms]!")
 434      return success ? 0 : -1
 435    }
 436  
 437  //////////////////////////////////////////////////////////////////////////
 438  // Fields
 439  //////////////////////////////////////////////////////////////////////////
 440  
 441    ** Log used for error reporting and tracing
 442    BuildLog log
 443  
 444    ** Targets available on this script (see `makeTargets`)
 445    readonly Target[] targets
 446  
 447    ** Targets specified to run by command line
 448    Target[] toRun
 449  
 450  }