1 /** 2 This module contains the core data definitions that allow a build 3 to be expressed in. $(D Build) is a container struct for top-level 4 targets, $(D Target) is the heart of the system. 5 */ 6 7 module reggae.build; 8 import reggae.ctaa; 9 import reggae.rules.common: Language, getLanguage; 10 11 import std.string: replace; 12 import std.algorithm; 13 import std.path: buildPath, dirSeparator; 14 import std.typetuple: allSatisfy; 15 import std.traits: Unqual, isSomeFunction, ReturnType, arity; 16 import std.array: array, join; 17 import std.conv; 18 import std.exception; 19 import std.typecons; 20 import std.range; 21 import std.typecons; 22 23 24 /** 25 Contains the top-level targets. 26 */ 27 struct Build { 28 static struct TopLevelTarget { 29 Target target; 30 bool optional; 31 } 32 33 private const(TopLevelTarget)[] _targets; 34 35 this(in Target[] targets) { 36 _targets = targets.map!createTopLevelTarget.array; 37 } 38 39 this(T...)(in T targets) { 40 foreach(t; targets) { 41 //the constructor needs to go from Target to TopLevelTarget 42 //and accepts functions that return a parameter as well as parameters themselves 43 //if a function, call it, if not, take the value 44 //if the value is Target, call createTopLevelTarget, if not, take it as is 45 static if(isSomeFunction!(typeof(t)) && is(ReturnType!(typeof(t))) == Target) { 46 _targets ~= createTopLevelTarget(t()); 47 } else static if(is(Unqual!(typeof(t)) == TopLevelTarget)) { 48 _targets ~= t; 49 } else { 50 _targets ~= createTopLevelTarget(t); 51 } 52 } 53 } 54 55 auto targets() @trusted pure nothrow const { 56 return _targets.map!(a => a.target); 57 } 58 59 auto defaultTargets() @trusted pure nothrow const { 60 return _targets.filter!(a => !a.optional).map!(a => a.target); 61 } 62 63 string defaultTargetsString(in string projectPath) @trusted pure const { 64 return defaultTargets.map!(a => a.outputsInProjectPath(projectPath).join(" ")).join(" "); 65 } 66 67 auto range() @safe pure const { 68 import reggae.range; 69 return UniqueDepthFirst(this); 70 } 71 } 72 73 74 /** 75 Designate a target as optional so it won't be built by default. 76 "Compile-time" version that can be aliased 77 */ 78 Build.TopLevelTarget optional(alias targetFunc)() { 79 auto target = targetFunc(); 80 return createTopLevelTarget(target, true); 81 } 82 83 /** 84 Designate a target as optional so it won't be built by default. 85 */ 86 Build.TopLevelTarget optional(in Target target) { 87 return Build.TopLevelTarget(target, true); 88 } 89 90 Build.TopLevelTarget createTopLevelTarget(in Target target, bool optional = false) { 91 return Build.TopLevelTarget(target.inTopLevelObjDirOf(topLevelDirName(target), Yes.topLevel), 92 optional); 93 } 94 95 96 immutable gBuilddir = "$builddir"; 97 immutable gProjdir = "$project"; 98 99 //a directory for each top-level target no avoid name clashes 100 //@trusted because of map -> buildPath -> array 101 Target inTopLevelObjDirOf(in Target target, string dirName, Flag!"topLevel" isTopLevel = No.topLevel) @trusted { 102 //leaf targets only get the $builddir expansion, nothing else 103 //this is because leaf targets are by definition in the project path 104 105 //every other non-top-level target gets its outputs placed in a directory 106 //specific to its top-level parent 107 108 if(target.outputs.any!(a => a.startsWith(gBuilddir) || a.startsWith(gProjdir))) { 109 dirName = topLevelDirName(target); 110 } 111 112 const outputs = isTopLevel 113 ? target.outputs.map!(a => expandBuildDir(a)).array 114 : target.outputs.map!(a => realTargetPath(dirName, target, a)).array; 115 116 return Target(outputs, 117 target._command.expandVariables, 118 target.dependencies.map!(a => a.inTopLevelObjDirOf(dirName)).array, 119 target.implicits.map!(a => a.inTopLevelObjDirOf(dirName)).array); 120 } 121 122 123 string topLevelDirName(in Target target) @safe pure { 124 return buildPath("objs", target.outputs[0].expandBuildDir ~ ".objs"); 125 } 126 127 //targets that have outputs with $builddir or $project in them want to be placed 128 //in a specific place. Those don't get touched. Other targets get 129 //placed in their top-level parent's object directory 130 string realTargetPath(in string dirName, in Target target, in string output) @trusted pure { 131 return target.isLeaf 132 ? expandBuildDir(output) 133 : realTargetPath(dirName, output); 134 } 135 136 137 //targets that have outputs with $builddir or $project in them want to be placed 138 //in a specific place. Those don't get touched. Other targets get 139 //placed in their top-level parent's object directory 140 string realTargetPath(in string dirName, in string output) @trusted pure { 141 import std.algorithm: canFind; 142 143 if(output.startsWith(gProjdir)) return output; 144 145 return output.canFind(gBuilddir) 146 ? output.expandBuildDir 147 : buildPath(dirName, output); 148 } 149 150 //replace $builddir with the current directory 151 string expandBuildDir(in string output) @trusted pure { 152 import std.path: buildNormalizedPath; 153 import std.algorithm; 154 return output. 155 splitter. 156 map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a). 157 join(" "); 158 } 159 160 enum isTarget(alias T) = 161 is(Unqual!(typeof(T)) == Target) || 162 is(Unqual!(typeof(T)) == Build.TopLevelTarget) || 163 isSomeFunction!T && is(ReturnType!T == Target) || 164 isSomeFunction!T && is(ReturnType!T == Build.TopLevelTarget); 165 166 unittest { 167 auto t1 = Target(); 168 const t2 = Target(); 169 static assert(isTarget!t1); 170 static assert(isTarget!t2); 171 const t3 = Build.TopLevelTarget(Target()); 172 static assert(isTarget!t3); 173 } 174 175 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) { 176 Build buildFunc() { 177 return Build(targets); 178 } 179 } 180 181 /** 182 Two variations on a template mixin. When reggae is used as a library, 183 this will essentially build reggae itself as part of the build description. 184 185 When reggae is used as a command-line tool to generate builds, it simply 186 declares the build function that will be called at run-time. The tool 187 will then compile the user's reggaefile.d with the reggae libraries, 188 resulting in a buildgen executable. 189 190 In either case, the compile-time parameters of $(D build) are the 191 build's top-level targets. 192 */ 193 version(reggaelib) { 194 mixin template build(targets...) if(allSatisfy!(isTarget, targets)) { 195 mixin reggaeGen!(targets); 196 } 197 } else { 198 alias build = buildImpl; 199 } 200 201 package template isBuildFunction(alias T) { 202 static if(!isSomeFunction!T) { 203 enum isBuildFunction = false; 204 } else { 205 enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0; 206 } 207 } 208 209 unittest { 210 Build myBuildFunction() { return Build(); } 211 static assert(isBuildFunction!myBuildFunction); 212 float foo; 213 static assert(!isBuildFunction!foo); 214 } 215 216 217 /** 218 The core of reggae's D-based DSL for describing build systems. 219 Targets contain outputs, a command to generate those outputs, 220 explicit dependencies and implicit dependencies. All dependencies 221 are themselves $(D Target) structs. 222 223 The command is given as a string. In this string, certain words 224 have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir). 225 226 $(D $in) gets expanded to all explicit dependencies. 227 $(D $out) gets expanded to all outputs. 228 $(D $project) gets expanded to the project directory (i.e. the directory including 229 the source files to build that was given as a command-line argument). This can be 230 useful when build outputs are to be placed in the source directory, such as 231 automatically generated source files. 232 $(D $builddir) expands to the build directory (i.e. where reggae was run from). 233 */ 234 struct Target { 235 const(string)[] outputs; 236 private const(Command) _command; ///see $(D Command) struct 237 const(Target)[] dependencies; 238 const(Target)[] implicits; 239 240 this(in string output) @safe pure nothrow { 241 this(output, "", null); 242 } 243 244 this(C)(in string output, 245 in C command, 246 in Target dependency, 247 in Target[] implicits = []) @safe pure nothrow { 248 this([output], command, [dependency], implicits); 249 } 250 251 this(C)(in string output, 252 in C command, 253 in Target[] dependencies, 254 in Target[] implicits = []) @safe pure nothrow { 255 this([output], command, dependencies, implicits); 256 } 257 258 this(C)(in string[] outputs, 259 in C command, 260 in Target[] dependencies, 261 in Target[] implicits = []) @safe pure nothrow { 262 263 this.outputs = outputs; 264 this.dependencies = dependencies; 265 this.implicits = implicits; 266 267 static if(is(C == Command)) 268 this._command = command; 269 else 270 this._command = Command(command); 271 } 272 273 @property string dependencyFilesString(in string projectPath = "") @safe pure const { 274 return depFilesStringImpl(dependencies, projectPath); 275 } 276 277 @property string implicitFilesString(in string projectPath = "") @safe pure const { 278 return depFilesStringImpl(implicits, projectPath); 279 } 280 281 bool isLeaf() @safe pure const nothrow { 282 return dependencies is null && implicits is null; 283 } 284 285 string[] outputsInProjectPath(in string projectPath) @safe pure const { 286 string inProjectPath(in string path) { 287 return path.startsWith(gProjdir) 288 ? path 289 : path.startsWith(gBuilddir) 290 ? path.replace(gBuilddir ~ dirSeparator, "") 291 : buildPath(projectPath, path); 292 } 293 294 return outputs.map!(a => isLeaf ? inProjectPath(a) : a). 295 map!(a => a.replace("$project", projectPath)).array; 296 } 297 298 Language getLanguage() @safe pure nothrow const { 299 import reggae.range: Leaves; 300 const leaves = () @trusted { return Leaves(this).array; }(); 301 foreach(language; [Language.D, Language.Cplusplus, Language.C]) { 302 if(leaves.any!(a => reggae.rules.common.getLanguage(a.outputs[0]) == language)) return language; 303 } 304 305 return Language.unknown; 306 } 307 308 ///Replace special variables and return a list of outputs thus modified 309 auto expandOutputs(in string projectPath) @safe pure const { 310 return outputsInProjectPath(projectPath).map!(a => a.replace(gBuilddir ~ dirSeparator, "")); 311 } 312 313 ///replace all special variables with their expansion 314 @property string expandCommand(in string projectPath = "") @trusted pure const { 315 return _command.expand(projectPath, outputs, inputs(projectPath)); 316 } 317 318 //@trusted because of replace 319 string rawCmdString(in string projectPath = "") @trusted pure const { 320 return _command.rawCmdString(projectPath); 321 } 322 323 ///returns a command string to be run by the shell 324 string shellCommand(in string projectPath = "", 325 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 326 return _command.shellCommand(projectPath, getLanguage(), outputs, inputs(projectPath), deps); 327 } 328 329 string[] execute(in string projectPath = "") @safe const { 330 return _command.execute(projectPath, getLanguage(), outputs, inputs(projectPath)); 331 } 332 333 bool hasDefaultCommand() @safe const pure { 334 return _command.isDefaultCommand; 335 } 336 337 CommandType getCommandType() @safe pure const nothrow { 338 return _command.getType; 339 } 340 341 string[] getCommandParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const { 342 return _command.getParams(projectPath, key, ifNotFound); 343 } 344 345 const(string)[] commandParamNames() @safe pure nothrow const { 346 return _command.paramNames; 347 } 348 349 static Target phony(in string output, in string shellCommand, 350 in Target[] dependencies = [], in Target[] implicits = []) @safe pure { 351 return Target(output, Command.phony(shellCommand), dependencies, implicits); 352 } 353 354 string toString() const pure nothrow { 355 try { 356 if(isLeaf) return outputs[0]; 357 immutable outputs = outputs.length == 1 ? `"` ~ outputs[0] ~ `"` : text(outputs); 358 immutable depsStr = dependencies.length == 0 ? "" : text(dependencies); 359 immutable impsStr = implicits.length == 0 ? "" : text(implicits); 360 auto parts = [text(outputs), `"` ~ _command.command ~ `"`]; 361 if(depsStr != "") parts ~= depsStr; 362 if(impsStr != "") parts ~= impsStr; 363 return text("Target(", parts.join(", "), ")"); 364 } catch(Exception) { 365 assert(0); 366 } 367 } 368 369 370 private: 371 372 373 //@trusted because of join 374 string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const { 375 string files; 376 //join doesn't do const, resort to loops 377 foreach(i, dep; deps) { 378 files ~= text(dep.outputsInProjectPath(projectPath).join(" ")); 379 if(i != deps.length - 1) files ~= " "; 380 } 381 return files; 382 } 383 384 string[] inputs(in string projectPath) @safe pure nothrow const { 385 //functional didn't work here, I don't know why so sticking with loops for now 386 string[] inputs; 387 foreach(dep; dependencies) { 388 foreach(output; dep.outputs) { 389 //leaf objects are references to source files in the project path, 390 //those need their path built. Any other dependencies are in the 391 //build path, so they don't need the same treatment 392 inputs ~= dep.isLeaf ? inProjectPath(projectPath, output) : output; 393 } 394 } 395 return inputs; 396 } 397 } 398 399 string inProjectPath(in string projectPath, in string name) @safe pure nothrow { 400 if(name.startsWith(gBuilddir)) return name; 401 return buildPath(projectPath, name); 402 } 403 404 405 enum CommandType { 406 shell, 407 compile, 408 link, 409 compileAndLink, 410 code, 411 phony, 412 } 413 414 alias CommandFunction = void function(in string[], in string[]); 415 416 /** 417 A command to be execute to produce a targets outputs from its inputs. 418 In general this will be a shell command, but the high-level rules 419 use commands with known semantics (compilation, linking, etc) 420 */ 421 struct Command { 422 alias Params = AssocList!(string, string[]); 423 424 private string command; 425 private CommandType type; 426 private Params params; 427 private CommandFunction func; 428 429 ///If constructed with a string, it's a shell command 430 this(string shellCommand) @safe pure nothrow { 431 command = shellCommand; 432 type = CommandType.shell; 433 } 434 435 /**Explicitly request a command of this type with these parameters 436 In general to create one of the builtin high level rules*/ 437 this(CommandType type, Params params = Params()) @safe pure { 438 if(type == CommandType.shell) throw new Exception("Command rule cannot be shell"); 439 this.type = type; 440 this.params = params; 441 } 442 443 ///A D function call command 444 this(CommandFunction func) @safe pure nothrow { 445 type = CommandType.code; 446 this.func = func; 447 } 448 449 static Command phony(in string shellCommand) @safe pure nothrow { 450 Command cmd; 451 cmd.type = CommandType.phony; 452 cmd.command = shellCommand; 453 return cmd; 454 } 455 456 const(string)[] paramNames() @safe pure nothrow const { 457 return params.keys; 458 } 459 460 CommandType getType() @safe pure const nothrow { 461 return type; 462 } 463 464 bool isDefaultCommand() @safe pure const { 465 return type == CommandType.compile || type == CommandType.link || type == CommandType.compileAndLink; 466 } 467 468 string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const { 469 return getParams(projectPath, key, true, ifNotFound); 470 } 471 472 const(Command) expandVariables() @safe pure const { 473 switch(type) with(CommandType) { 474 case shell: 475 auto cmd = Command(expandBuildDir(command)); 476 cmd.type = this.type; 477 return cmd; 478 default: 479 return this; 480 } 481 } 482 483 ///Replace $in, $out, $project with values 484 string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure const { 485 return expandCmd(command, projectPath, outputs, inputs); 486 } 487 488 private static string expandCmd(in string cmd, in string projectPath, 489 in string[] outputs, in string[] inputs) @safe pure { 490 auto replaceIn = cmd.dup.replace("$in", inputs.join(" ")); 491 auto replaceOut = replaceIn.replace("$out", outputs.join(" ")); 492 return replaceOut.replace("$project", projectPath).replace(gBuilddir ~ dirSeparator, ""); 493 } 494 495 //@trusted because of replace 496 string rawCmdString(in string projectPath) @trusted pure const { 497 if(getType != CommandType.shell) 498 throw new Exception("Command type 'code' not supported for ninja backend"); 499 return command.replace("$project", projectPath); 500 } 501 502 //@trusted because of replace 503 private string[] getParams(in string projectPath, in string key, 504 bool useIfNotFound, string[] ifNotFound = []) @safe pure const { 505 return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array; 506 } 507 508 static string builtinTemplate(CommandType type, 509 Language language, 510 Flag!"dependencies" deps = Yes.dependencies) @safe pure { 511 import reggae.config: options; 512 513 final switch(type) with(CommandType) { 514 case phony: 515 assert(0, "builtinTemplate cannot be phony"); 516 517 case shell: 518 assert(0, "builtinTemplate cannot be shell"); 519 520 case link: 521 final switch(language) with(Language) { 522 case D: 523 case unknown: 524 return options.dCompiler ~ " -of$out $flags $in"; 525 case Cplusplus: 526 return options.cppCompiler ~ " -o $out $flags $in"; 527 case C: 528 return options.cCompiler ~ " -o $out $flags $in"; 529 } 530 531 case code: 532 throw new Exception("Command type 'code' has no built-in template"); 533 534 case compile: 535 return compileTemplate(type, language, deps).replace("$out $in", "$out -c $in"); 536 537 case compileAndLink: 538 return compileTemplate(type, language, deps); 539 } 540 } 541 542 private static string compileTemplate(CommandType type, 543 Language language, 544 Flag!"dependencies" deps = Yes.dependencies) @safe pure { 545 import reggae.config: options; 546 547 immutable ccParams = deps 548 ? " $flags $includes -MMD -MT $out -MF $out.dep -o $out $in" 549 : " $flags $includes -o $out $in"; 550 551 final switch(language) with(Language) { 552 case D: 553 return deps 554 ? ".reggae/dcompile --objFile=$out --depFile=$out.dep " ~ 555 options.dCompiler ~ " $flags $includes $stringImports $in" 556 : options.dCompiler ~ " $flags $includes $stringImports -of$out $in"; 557 case Cplusplus: 558 return options.cppCompiler ~ ccParams; 559 case C: 560 return options.cCompiler ~ ccParams; 561 case unknown: 562 throw new Exception("Unsupported language for compiling"); 563 } 564 } 565 566 string defaultCommand(in string projectPath, 567 in Language language, 568 in string[] outputs, 569 in string[] inputs, 570 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 571 assert(isDefaultCommand, text("This command is not a default command: ", this)); 572 auto cmd = builtinTemplate(type, language, deps); 573 foreach(key; params.keys) { 574 immutable var = "$" ~ key; 575 immutable value = getParams(projectPath, key, []).join(" "); 576 cmd = cmd.replace(var, value); 577 } 578 return expandCmd(cmd, projectPath, outputs, inputs); 579 } 580 581 ///returns a command string to be run by the shell 582 string shellCommand(in string projectPath, 583 in Language language, 584 in string[] outputs, 585 in string[] inputs, 586 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 587 return isDefaultCommand 588 ? defaultCommand(projectPath, language, outputs, inputs, deps) 589 : expand(projectPath, outputs, inputs); 590 } 591 592 593 string[] execute(in string projectPath, in Language language, 594 in string[] outputs, in string[] inputs) const @trusted { 595 import std.process; 596 597 final switch(type) with(CommandType) { 598 case shell: 599 case compile: 600 case link: 601 case compileAndLink: 602 case phony: 603 immutable cmd = shellCommand(projectPath, language, outputs, inputs); 604 immutable res = executeShell(cmd); 605 enforce(res.status == 0, "Could not execute phony " ~ cmd ~ ":\n" ~ res.output); 606 return [cmd, res.output]; 607 case code: 608 assert(func !is null, "Command of type code with null function"); 609 func(inputs, outputs); 610 return []; 611 } 612 } 613 614 }