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, 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).array; 230 } 231 232 @property const(Command) command() @safe const pure nothrow { return _command; } 233 234 Language getLanguage() @safe pure nothrow const { 235 return reggae.rules.common.getLanguage(inputs("")[0]); 236 } 237 238 void execute(in string projectPath = "") @safe const { 239 _command.execute(projectPath, outputs, inputs(projectPath)); 240 } 241 242 243 private: 244 245 //@trusted because of join 246 string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow { 247 import std.conv; 248 string files; 249 //join doesn't do const, resort to loops 250 foreach(i, dep; deps) { 251 files ~= text(dep.outputsInProjectPath(projectPath).join(" ")); 252 if(i != deps.length - 1) files ~= " "; 253 } 254 return files; 255 } 256 257 string[] inputs(in string projectPath) @safe pure nothrow const { 258 //functional didn't work here, I don't know why so sticking with loops for now 259 string[] inputs; 260 foreach(dep; dependencies) { 261 foreach(output; dep.outputs) { 262 //leaf objects are references to source files in the project path, 263 //those need their path built. Any other dependencies are in the 264 //build path, so they don't need the same treatment 265 inputs ~= dep.isLeaf ? buildPath(projectPath, output) : output; 266 } 267 } 268 return inputs; 269 } 270 } 271 272 273 enum CommandType { 274 shell, 275 compile, 276 link, 277 code, 278 } 279 280 alias CommandFunction = void function(in string[], in string[]); 281 282 /** 283 A command to be execute to produce a targets outputs from its inputs. 284 In general this will be a shell command, but the high-level rules 285 use commands with known semantics (compilation, linking, etc) 286 */ 287 struct Command { 288 alias Params = AssocList!(string, string[]); 289 290 private string command; 291 private CommandType type; 292 private Params params; 293 private CommandFunction func; 294 295 ///If constructed with a string, it's a shell command 296 this(string shellCommand) @safe pure nothrow { 297 command = shellCommand; 298 type = CommandType.shell; 299 } 300 301 /**Explicitly request a command of this type with these parameters 302 In general to create one of the builtin high level rules*/ 303 this(CommandType type, Params params) @safe pure { 304 if(type == CommandType.shell) throw new Exception("Command rule cannot be shell"); 305 this.type = type; 306 this.params = params; 307 } 308 309 ///A D function call command 310 this(CommandFunction func) @safe pure nothrow { 311 type = CommandType.code; 312 this.func = func; 313 } 314 315 const(string)[] paramNames() @safe pure nothrow const { 316 return params.keys; 317 } 318 319 CommandType getType() @safe pure const { 320 return type; 321 } 322 323 bool isDefaultCommand() @safe pure const { 324 return type != CommandType.shell; 325 } 326 327 string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const { 328 return getParams(projectPath, key, true, ifNotFound); 329 } 330 331 const(Command) expandBuildDir() @safe pure const { 332 switch(type) with(CommandType) { 333 case shell: 334 auto cmd = Command(_expandBuildDir(command)); 335 cmd.type = this.type; 336 return cmd; 337 default: 338 return this; 339 } 340 } 341 342 ///Replace $in, $out, $project with values 343 string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure nothrow const { 344 return expandCmd(command, projectPath, outputs, inputs); 345 } 346 347 private static string expandCmd(in string cmd, in string projectPath, 348 in string[] outputs, in string[] inputs) @safe pure nothrow { 349 auto replaceIn = cmd.dup.replace("$in", inputs.join(" ")); 350 auto replaceOut = replaceIn.replace("$out", outputs.join(" ")); 351 return replaceOut.replace("$project", projectPath); 352 } 353 354 //@trusted because of replace 355 string rawCmdString(in string projectPath) @trusted pure nothrow const { 356 return command.replace("$project", projectPath); 357 } 358 359 //@trusted because of replace 360 private string[] getParams(in string projectPath, in string key, 361 bool useIfNotFound, string[] ifNotFound = []) @safe pure const { 362 return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array; 363 } 364 365 static string builtinTemplate(CommandType type, 366 Language language, 367 Flag!"dependencies" deps = Yes.dependencies) @safe pure { 368 import reggae.config: dCompiler, cppCompiler, cCompiler; 369 370 final switch(type) with(CommandType) { 371 case shell: 372 assert(0, "builtinTemplate cannot be shell"); 373 374 case link: 375 return dCompiler ~ " -of$out $flags $in"; 376 377 case code: 378 throw new Exception("Command type 'code' has no built-in template"); 379 380 case compile: 381 immutable ccParams = deps 382 ? " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in" 383 : " $flags $includes -o $out -c $in"; 384 385 final switch(language) with(Language) { 386 case D: 387 return deps 388 ? ".reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~ 389 dCompiler ~ " $flags $includes $stringImports $in" 390 : dCompiler ~ " $flags $includes $stringImports -of$out -c $in"; 391 case Cplusplus: 392 return cppCompiler ~ ccParams; 393 case C: 394 return cCompiler ~ ccParams; 395 case unknown: 396 throw new Exception("Unsupported language"); 397 } 398 } 399 } 400 401 string defaultCommand(in string projectPath, 402 in string[] outputs, 403 in string[] inputs, 404 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 405 assert(isDefaultCommand, text("This command is not a default command: ", this)); 406 immutable language = getLanguage(inputs[0]); 407 auto cmd = builtinTemplate(type, language, deps); 408 foreach(key; params.keys) { 409 immutable var = "$" ~ key; 410 immutable value = getParams(projectPath, key, []).join(" "); 411 cmd = cmd.replace(var, value); 412 } 413 return expandCmd(cmd, projectPath, outputs, inputs); 414 } 415 416 ///returns a command string to be run by the shell 417 string shellCommand(in string projectPath, 418 in string[] outputs, 419 in string[] inputs, 420 Flag!"dependencies" deps = Yes.dependencies) @safe pure const { 421 return isDefaultCommand 422 ? defaultCommand(projectPath, outputs, inputs, deps) 423 : expand(projectPath, outputs, inputs); 424 } 425 426 427 void execute(in string projectPath, in string[] outputs, in string[] inputs) const @trusted { 428 import std.process; 429 import std.stdio; 430 431 final switch(type) with(CommandType) { 432 case shell: 433 case compile: 434 case link: 435 immutable cmd = shellCommand(projectPath, outputs, inputs); 436 writeln("[build] " ~ cmd); 437 immutable res = executeShell(cmd); 438 enforce(res.status == 0, "Could not execute" ~ cmd ~ ":\n" ~ res.output); 439 break; 440 case code: 441 assert(func !is null, "Command of type code with null function"); 442 func(inputs, outputs); 443 break; 444 } 445 } 446 447 }