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