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; 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 21 Target createTopLevelTarget(in Target target) { 22 return Target(target.outputs, 23 target._command.expandBuildDir, 24 target.dependencies.map!(a => a.enclose(target)).array, 25 target.implicits.map!(a => a.enclose(target)).array); 26 } 27 28 /** 29 Contains the top-level targets. 30 */ 31 struct Build { 32 const(Target)[] targets; 33 34 this(in Target[] targets) { 35 this.targets = targets.map!createTopLevelTarget.array; 36 } 37 38 this(T...)(in T targets) { 39 foreach(t; targets) { 40 static if(isSomeFunction!(typeof(t))) { 41 const target = t(); 42 } else { 43 const target = t; 44 } 45 46 this.targets ~= createTopLevelTarget(target); 47 } 48 } 49 } 50 51 //a directory for each top-level target no avoid name clashes 52 //@trusted because of map -> buildPath -> array 53 Target enclose(in Target target, in Target topLevel) @trusted { 54 //leaf targets only get the $builddir expansion, nothing else 55 if(target.isLeaf) return Target(target.outputs.map!(a => a._expandBuildDir).array, 56 target._command.expandBuildDir, 57 target.dependencies, 58 target.implicits); 59 60 //every other non-top-level target gets its outputs placed in a directory 61 //specific to its top-level parent 62 immutable dirName = buildPath("objs", topLevel.outputs[0] ~ ".objs"); 63 return Target(target.outputs.map!(a => realTargetPath(dirName, a)).array, 64 target._command.expandBuildDir, 65 target.dependencies.map!(a => a.enclose(topLevel)).array, 66 target.implicits.map!(a => a.enclose(topLevel)).array); 67 } 68 69 immutable gBuilddir = "$builddir"; 70 71 72 //targets that have outputs with $builddir in them want to be placed 73 //in a specific place. Those don't get touched. Other targets get 74 //placed in their top-level parent's object directory 75 private string realTargetPath(in string dirName, in string output) @trusted pure { 76 import std.algorithm: canFind; 77 78 return output.canFind(gBuilddir) 79 ? output._expandBuildDir 80 : buildPath(dirName, output); 81 } 82 83 private string _expandBuildDir(in string output) @trusted pure { 84 import std.path: buildNormalizedPath; 85 import std.algorithm; 86 return output. 87 splitter. 88 map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a). 89 join(" "); 90 } 91 92 enum isTarget(alias T) = is(Unqual!(typeof(T)) == Target) || 93 isSomeFunction!T && is(ReturnType!T == Target); 94 95 unittest { 96 auto t1 = Target(); 97 const t2 = Target(); 98 static assert(isTarget!t1); 99 static assert(isTarget!t2); 100 } 101 102 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) { 103 Build buildFunc() { 104 return Build(targets); 105 } 106 } 107 108 /** 109 Two variations on a template mixin. When reggae is used as a library, 110 this will essentially build reggae itself as part of the build description. 111 112 When reggae is used as a command-line tool to generate builds, it simply 113 declares the build function that will be called at run-time. The tool 114 will then compile the user's reggaefile.d with the reggae libraries, 115 resulting in a buildgen executable. 116 117 In either case, the compile-time parameters of $(D build) are the 118 build's top-level targets. 119 */ 120 version(reggaelib) { 121 mixin template build(targets...) if(allSatisfy!(isTarget, targets)) { 122 mixin reggaeGen!(targets); 123 } 124 } else { 125 alias build = buildImpl; 126 } 127 128 package template isBuildFunction(alias T) { 129 static if(!isSomeFunction!T) { 130 enum isBuildFunction = false; 131 } else { 132 enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0; 133 } 134 } 135 136 unittest { 137 Build myBuildFunction() { return Build(); } 138 static assert(isBuildFunction!myBuildFunction); 139 float foo; 140 static assert(!isBuildFunction!foo); 141 } 142 143 144 /** 145 The core of reggae's D-based DSL for describing build systems. 146 Targets contain outputs, a command to generate those outputs, 147 explicit dependencies and implicit dependencies. All dependencies 148 are themselves $(D Target) structs. 149 150 The command is given as a string. In this string, certain words 151 have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir). 152 153 $(D $in) gets expanded to all explicit dependencies. 154 $(D $out) gets expanded to all outputs. 155 $(D $project) gets expanded to the project directory (i.e. the directory including 156 the source files to build that was given as a command-line argument). This can be 157 useful when build outputs are to be placed in the source directory, such as 158 automatically generated source files. 159 $(D $builddir) expands to the build directory (i.e. where reggae was run from). 160 */ 161 struct Target { 162 const(string)[] outputs; 163 private const(Command) _command; ///see $(D Command) struct 164 const(Target)[] dependencies; 165 const(Target)[] implicits; 166 167 this(in string output) @safe pure nothrow { 168 this(output, "", null); 169 } 170 171 this(C)(in string output, 172 in C command, 173 in Target dependency, 174 in Target[] implicits = []) @safe pure nothrow { 175 this([output], command, [dependency], implicits); 176 } 177 178 this(C)(in string output, 179 in C command, 180 in Target[] dependencies, 181 in Target[] implicits = []) @safe pure nothrow { 182 this([output], command, dependencies, implicits); 183 } 184 185 this(C)(in string[] outputs, 186 in C command, 187 in Target[] dependencies, 188 in Target[] implicits = []) @safe pure nothrow { 189 190 this.outputs = outputs; 191 this.dependencies = dependencies; 192 this.implicits = implicits; 193 194 static if(is(C == Command)) 195 this._command = command; 196 else 197 this._command = Command(command); 198 } 199 200 @property string dependencyFilesString(in string projectPath = "") @safe pure const nothrow { 201 return depFilesStringImpl(dependencies, projectPath); 202 } 203 204 @property string implicitFilesString(in string projectPath = "") @safe pure const nothrow { 205 return depFilesStringImpl(implicits, projectPath); 206 } 207 208 ///replace all special variables with their expansion 209 @property string expandCommand(in string projectPath = "") @trusted pure const nothrow { 210 return _command.expand(projectPath, outputs, inputs(projectPath)); 211 } 212 213 bool isLeaf() @safe pure const nothrow { 214 return dependencies is null && implicits is null; 215 } 216 217 //@trusted because of replace 218 string rawCmdString(in string projectPath) @trusted pure nothrow const { 219 return _command.rawCmdString(projectPath); 220 } 221 222 ///returns a command string to be run by the shell 223 string shellCommand(in string projectPath = "", 224 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 225 return _command.shellCommand(projectPath, getLanguage(), outputs, inputs(projectPath), deps); 226 } 227 228 string[] outputsInProjectPath(in string projectPath) @safe pure nothrow const { 229 return outputs.map!(a => isLeaf ? buildPath(projectPath, a) : a). 230 map!(a => a.replace("$project", projectPath)).array; 231 } 232 233 @property const(Command) command() @safe const pure nothrow { return _command; } 234 235 Language getLanguage() @safe pure nothrow const { 236 import reggae.range: Leaves; 237 const leaves = () @trusted { return Leaves(this).array; }(); 238 foreach(language; [Language.D, Language.Cplusplus, Language.C]) { 239 if(leaves.any!(a => reggae.rules.common.getLanguage(a.outputs[0]) == language)) return language; 240 } 241 242 return Language.unknown; 243 } 244 245 void execute(in string projectPath = "") @safe const { 246 _command.execute(projectPath, getLanguage(), outputs, inputs(projectPath)); 247 } 248 249 250 private: 251 252 //@trusted because of join 253 string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow { 254 import std.conv; 255 string files; 256 //join doesn't do const, resort to loops 257 foreach(i, dep; deps) { 258 files ~= text(dep.outputsInProjectPath(projectPath).join(" ")); 259 if(i != deps.length - 1) files ~= " "; 260 } 261 return files; 262 } 263 264 string[] inputs(in string projectPath) @safe pure nothrow const { 265 //functional didn't work here, I don't know why so sticking with loops for now 266 string[] inputs; 267 foreach(dep; dependencies) { 268 foreach(output; dep.outputs) { 269 //leaf objects are references to source files in the project path, 270 //those need their path built. Any other dependencies are in the 271 //build path, so they don't need the same treatment 272 inputs ~= dep.isLeaf ? buildPath(projectPath, output) : output; 273 } 274 } 275 return inputs; 276 } 277 } 278 279 280 enum CommandType { 281 shell, 282 compile, 283 link, 284 code, 285 } 286 287 alias CommandFunction = void function(in string[], in string[]); 288 289 /** 290 A command to be execute to produce a targets outputs from its inputs. 291 In general this will be a shell command, but the high-level rules 292 use commands with known semantics (compilation, linking, etc) 293 */ 294 struct Command { 295 alias Params = AssocList!(string, string[]); 296 297 private string command; 298 private CommandType type; 299 private Params params; 300 private CommandFunction func; 301 302 ///If constructed with a string, it's a shell command 303 this(string shellCommand) @safe pure nothrow { 304 command = shellCommand; 305 type = CommandType.shell; 306 } 307 308 /**Explicitly request a command of this type with these parameters 309 In general to create one of the builtin high level rules*/ 310 this(CommandType type, Params params) @safe pure { 311 if(type == CommandType.shell) throw new Exception("Command rule cannot be shell"); 312 this.type = type; 313 this.params = params; 314 } 315 316 ///A D function call command 317 this(CommandFunction func) @safe pure nothrow { 318 type = CommandType.code; 319 this.func = func; 320 } 321 322 const(string)[] paramNames() @safe pure nothrow const { 323 return params.keys; 324 } 325 326 CommandType getType() @safe pure const { 327 return type; 328 } 329 330 bool isDefaultCommand() @safe pure const { 331 return type != CommandType.shell; 332 } 333 334 string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const { 335 return getParams(projectPath, key, true, ifNotFound); 336 } 337 338 const(Command) expandBuildDir() @safe pure const { 339 switch(type) with(CommandType) { 340 case shell: 341 auto cmd = Command(_expandBuildDir(command)); 342 cmd.type = this.type; 343 return cmd; 344 default: 345 return this; 346 } 347 } 348 349 ///Replace $in, $out, $project with values 350 string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure nothrow const { 351 return expandCmd(command, projectPath, outputs, inputs); 352 } 353 354 private static string expandCmd(in string cmd, in string projectPath, 355 in string[] outputs, in string[] inputs) @safe pure nothrow { 356 auto replaceIn = cmd.dup.replace("$in", inputs.join(" ")); 357 auto replaceOut = replaceIn.replace("$out", outputs.join(" ")); 358 return replaceOut.replace("$project", projectPath); 359 } 360 361 //@trusted because of replace 362 string rawCmdString(in string projectPath) @trusted pure nothrow const { 363 return command.replace("$project", projectPath); 364 } 365 366 //@trusted because of replace 367 private string[] getParams(in string projectPath, in string key, 368 bool useIfNotFound, string[] ifNotFound = []) @safe pure const { 369 return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array; 370 } 371 372 static string builtinTemplate(CommandType type, 373 Language language, 374 Flag!"dependencies" deps = Yes.dependencies) @safe pure { 375 import reggae.config: dCompiler, cppCompiler, cCompiler; 376 377 final switch(type) with(CommandType) { 378 case shell: 379 assert(0, "builtinTemplate cannot be shell"); 380 381 case link: 382 final switch(language) with(Language) { 383 import std.stdio; 384 debug writeln("builtinTemplate called with language ", language); 385 case D: 386 case unknown: 387 return dCompiler ~ " -of$out $flags $in"; 388 case Cplusplus: 389 return cppCompiler ~ " -o $out $flags $in"; 390 case C: 391 return cCompiler ~ " -o $out $flags $in"; 392 } 393 394 case code: 395 throw new Exception("Command type 'code' has no built-in template"); 396 397 case compile: 398 immutable ccParams = deps 399 ? " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in" 400 : " $flags $includes -o $out -c $in"; 401 402 final switch(language) with(Language) { 403 case D: 404 return deps 405 ? ".reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~ 406 dCompiler ~ " $flags $includes $stringImports $in" 407 : dCompiler ~ " $flags $includes $stringImports -of$out -c $in"; 408 case Cplusplus: 409 return cppCompiler ~ ccParams; 410 case C: 411 return cCompiler ~ ccParams; 412 case unknown: 413 throw new Exception("Unsupported language for compiling"); 414 } 415 } 416 } 417 418 string defaultCommand(in string projectPath, 419 in Language language, 420 in string[] outputs, 421 in string[] inputs, 422 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 423 assert(isDefaultCommand, text("This command is not a default command: ", this)); 424 auto cmd = builtinTemplate(type, language, deps); 425 foreach(key; params.keys) { 426 immutable var = "$" ~ key; 427 immutable value = getParams(projectPath, key, []).join(" "); 428 cmd = cmd.replace(var, value); 429 } 430 return expandCmd(cmd, projectPath, outputs, inputs); 431 } 432 433 ///returns a command string to be run by the shell 434 string shellCommand(in string projectPath, 435 in Language language, 436 in string[] outputs, 437 in string[] inputs, 438 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 439 return isDefaultCommand 440 ? defaultCommand(projectPath, language, outputs, inputs, deps) 441 : expand(projectPath, outputs, inputs); 442 } 443 444 445 void execute(in string projectPath, in Language language, 446 in string[] outputs, in string[] inputs) const @trusted { 447 import std.process; 448 import std.stdio; 449 450 final switch(type) with(CommandType) { 451 case shell: 452 case compile: 453 case link: 454 immutable cmd = shellCommand(projectPath, language, outputs, inputs); 455 writeln("[build] " ~ cmd); 456 immutable res = executeShell(cmd); 457 enforce(res.status == 0, "Could not execute" ~ cmd ~ ":\n" ~ res.output); 458 break; 459 case code: 460 assert(func !is null, "Command of type code with null function"); 461 func(inputs, outputs); 462 break; 463 } 464 } 465 466 }