
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 }