1 module reggae.backend.ninja; 2 3 4 import reggae.build: CommandType; 5 import reggae.rules.common: Language; 6 import reggae.options: Options; 7 8 9 string cmdTypeToNinjaRuleName(CommandType commandType, Language language) @safe pure { 10 final switch(commandType) with(CommandType) { 11 case shell: assert(0, "cmdTypeToNinjaRuleName doesn't work for shell"); 12 case phony: assert(0, "cmdTypeToNinjaRuleName doesn't work for phony"); 13 case code: throw new Exception("Command type 'code' not supported for ninja backend"); 14 case link: 15 final switch(language) with(Language) { 16 case D: return "_dlink"; 17 case Cplusplus: return "_cpplink"; 18 case C: return "_clink"; 19 case unknown: return "_ulink"; 20 } 21 case compile: 22 final switch(language) with(Language) { 23 case D: return "_dcompile"; 24 case Cplusplus: return "_cppcompile"; 25 case C: return "_ccompile"; 26 case unknown: throw new Exception("Unsupported language"); 27 } 28 case compileAndLink: 29 final switch(language) with(Language) { 30 case D: return "_dcompileAndLink"; 31 case Cplusplus: return "_cppcompileAndLink"; 32 case C: return "_ccompileAndLink"; 33 case unknown: throw new Exception("Unsupported language"); 34 } 35 } 36 } 37 38 struct NinjaEntry { 39 string mainLine; 40 string[] paramLines; 41 string toString() @safe pure nothrow const { 42 43 import std.array: join; 44 import std.range: chain, only; 45 import std.algorithm.iteration: map; 46 47 return chain(only(mainLine), paramLines.map!(a => " " ~ a)).join("\n"); 48 } 49 } 50 51 52 private bool hasDepFile(in CommandType type) @safe pure nothrow { 53 return type == CommandType.compile || type == CommandType.compileAndLink; 54 } 55 56 private string[] initializeRuleParamLines(in Language language, in string command) @safe pure { 57 version(Windows) { 58 import std.algorithm: among; 59 import std..string: indexOf; 60 61 // On Windows, the max command line length is ~32K. 62 // Make ninja use a response file for all D/C[++] rules. 63 if (language.among(Language.D, Language.C, Language.Cplusplus)) { 64 const firstSpaceIndex = command.indexOf(' '); 65 if (firstSpaceIndex > 0) { 66 const program = command[0 .. firstSpaceIndex]; 67 const args = command[firstSpaceIndex+1 .. $]; 68 return [ 69 "command = " ~ program ~ " @$out.rsp", 70 "rspfile = $out.rsp", 71 "rspfile_content = " ~ args, 72 ]; 73 } 74 } 75 } 76 77 return ["command = " ~ command]; 78 } 79 80 /** 81 * Pre-built rules 82 */ 83 NinjaEntry[] defaultRules(in Options options) @safe pure { 84 85 import reggae.build: Command; 86 87 NinjaEntry createNinjaEntry(in CommandType type, in Language language) @safe pure { 88 const string command = Command.builtinTemplate(type, language, options); 89 90 string[] paramLines = initializeRuleParamLines(language, command); 91 92 if(hasDepFile(type)) { 93 version(Windows) 94 const isMSVC = language == Language.C || language == Language.Cplusplus; 95 else 96 enum isMSVC = false; 97 98 if (isMSVC) 99 paramLines ~= "deps = msvc"; 100 else 101 paramLines ~= ["deps = gcc", "depfile = $out.dep"]; 102 } 103 104 string getDescription() { 105 switch(type) with(CommandType) { 106 case compile: return "Compiling $out"; 107 case link: return "Linking $out"; 108 case compileAndLink: return "Building $out"; 109 default: return null; 110 } 111 } 112 113 const description = getDescription(); 114 if (description.length) 115 paramLines ~= "description = " ~ description; 116 117 return NinjaEntry("rule " ~ cmdTypeToNinjaRuleName(type, language), paramLines); 118 } 119 120 NinjaEntry[] entries; 121 foreach(type; [CommandType.compile, CommandType.link, CommandType.compileAndLink]) { 122 for(Language language = Language.min; language <= Language.max; ++language) { 123 if(hasDepFile(type) && language == Language.unknown) continue; 124 entries ~= createNinjaEntry(type, language); 125 } 126 } 127 128 string[] phonyParamLines; 129 version(Windows) { 130 phonyParamLines = [`command = cmd.exe /c "$cmd"`, "description = $cmd"]; 131 } else { 132 phonyParamLines = ["command = $cmd"]; 133 } 134 entries ~= NinjaEntry("rule _phony", phonyParamLines); 135 136 return entries; 137 } 138 139 140 struct Ninja { 141 142 import reggae.build: Build, Target; 143 144 NinjaEntry[] buildEntries; 145 NinjaEntry[] ruleEntries; 146 147 this(Build build, in string projectPath = "") @safe { 148 import reggae.config: options; 149 auto modOptions = options.dup; 150 modOptions.projectPath = projectPath; 151 this(build, modOptions); 152 } 153 154 this(Build build, in Options options) @safe { 155 _build = build; 156 _options = options; 157 _projectPath = _options.projectPath; 158 159 160 foreach(target; _build.range) { 161 target.hasDefaultCommand 162 ? defaultRule(target) 163 : target.getCommandType == CommandType.phony 164 ? phonyRule(target) 165 : customRule(target); 166 } 167 } 168 169 //includes rerunning reggae 170 const(NinjaEntry)[] allBuildEntries() @safe { 171 immutable files = flattenEntriesInBuildLine(_options.reggaeFileDependencies); 172 auto paramLines = _options.oldNinja ? [] : ["pool = console"]; 173 174 const(NinjaEntry)[] rerunEntries() { 175 // if exporting the build system, don't include rerunning reggae 176 return _options.export_ ? [] : [NinjaEntry("build build.ninja: _rerun | " ~ files, 177 paramLines)]; 178 } 179 180 const defaultOutputs = _build.defaultTargetsOutputs(_projectPath); 181 const defaultEntry = NinjaEntry("default " ~ flattenEntriesInBuildLine(defaultOutputs)); 182 183 return buildEntries ~ rerunEntries ~ defaultEntry; 184 } 185 186 //includes rerunning reggae 187 const(NinjaEntry)[] allRuleEntries() @safe pure const { 188 import std.array: join; 189 190 return ruleEntries ~ defaultRules(_options) ~ 191 NinjaEntry("rule _rerun", 192 ["command = " ~ _options.rerunArgs.join(" "), 193 "generator = 1", 194 ]); 195 } 196 197 string buildOutput() @safe { 198 auto ret = "include rules.ninja\n" ~ output(allBuildEntries); 199 if(_options.export_) ret = _options.eraseProjectPath(ret); 200 return ret; 201 } 202 203 string rulesOutput() @safe pure const { 204 return output(allRuleEntries); 205 } 206 207 void writeBuild() @safe { 208 import std.stdio: File; 209 import reggae.path: buildPath; 210 211 auto buildNinja = File(buildPath(_options.workingDir, "build.ninja"), "w"); 212 buildNinja.writeln(buildOutput); 213 214 auto rulesNinja = File(buildPath(_options.workingDir, "rules.ninja"), "w"); 215 rulesNinja.writeln(rulesOutput); 216 } 217 218 private: 219 Build _build; 220 string _projectPath; 221 const(Options) _options; 222 int _counter = 1; 223 224 void defaultRule(Target target) @safe { 225 import std.algorithm: canFind, map; 226 import std.array: join, replace; 227 228 static string flattenShellArgs(in string[] args) { 229 static string quoteArgIfNeeded(string a) { 230 return !a.canFind(' ') ? a : `"` ~ a.replace(`"`, `\"`) ~ `"`; 231 } 232 return args.map!quoteArgIfNeeded.join(" "); 233 } 234 235 string[] paramLines; 236 foreach(immutable param; target.commandParamNames) { 237 // skip the DEPFILE parameter, it's already specified in the rule 238 if (param == "DEPFILE") continue; 239 const values = target.getCommandParams(_projectPath, param, []); 240 const flat = flattenShellArgs(values); 241 if(!flat.length) continue; 242 // the flat value still needs to be escaped for Ninja ($ => $$, e.g. for env vars) 243 paramLines ~= param ~ " = " ~ flat.replace("$", "$$"); 244 } 245 246 const ruleName = cmdTypeToNinjaRuleName(target.getCommandType, target.getLanguage); 247 const buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/false); 248 249 buildEntries ~= NinjaEntry(buildLine, paramLines); 250 } 251 252 void phonyRule(Target target) @safe { 253 const cmd = target.shellCommand(_options); 254 255 //no projectPath for phony rules since they don't generate output 256 const outputs = target.expandOutputs(""); 257 const inputs = targetDependencies(target); 258 const implicitInputs = target.implicitTargets.length 259 ? target.implicitsInProjectPath(_projectPath) 260 : null; 261 const buildLine = buildLine(outputs, cmd is null ? "phony" : "_phony", inputs, implicitInputs); 262 263 buildEntries ~= NinjaEntry(buildLine, cmd is null 264 ? ["pool = console"] 265 : ["cmd = " ~ cmd, "pool = console"]); 266 } 267 268 void customRule(Target target) @safe { 269 270 import std.algorithm.searching: canFind; 271 272 //rawCmdString is used because ninja needs to find where $in and $out are, 273 //so shellCommand wouldn't work 274 immutable shellCommand = target.rawCmdString(_projectPath); 275 immutable implicitInput = () @trusted { return !shellCommand.canFind("$in"); }(); 276 immutable implicitOutput = () @trusted { return !shellCommand.canFind("$out"); }(); 277 278 if(implicitOutput) { 279 implicitOutputRule(target, shellCommand); 280 } else if(implicitInput) { 281 implicitInputRule(target, shellCommand); 282 } else { 283 explicitInOutRule(target, shellCommand); 284 } 285 } 286 287 void explicitInOutRule(Target target, in string shellCommand, in string implicitInput = "") @safe { 288 import std.regex: regex, match; 289 import std.algorithm.iteration: map; 290 import std.array: empty, join; 291 import std..string: strip; 292 import std.conv: text; 293 294 auto reg = regex(`^[^ ]+ +(.*?)(\$in|\$out)(.*?)(\$in|\$out)(.*?)$`); 295 296 auto mat = shellCommand.match(reg); 297 if(mat.captures.empty) { //this is usually bad since we need both $in and $out 298 if(target.dependencyTargets.empty) { //ah, no $in needed then 299 mat = match(shellCommand ~ " $in", reg); //add a dummy one 300 } 301 else 302 throw new Exception(text("Could not find both $in and $out.\nCommand: ", 303 shellCommand, "\nCaptures: ", mat.captures, "\n", 304 "outputs: ", target.rawOutputs.join(" "), "\n", 305 "dependencies: ", targetDependencies(target))); 306 } 307 308 immutable before = mat.captures[1].strip; 309 immutable first = mat.captures[2]; 310 immutable between = mat.captures[3].strip; 311 immutable last = mat.captures[4]; 312 immutable after = mat.captures[5].strip; 313 314 immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, before, first, between, last, after); 315 bool haveToAddRule; 316 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAddRule); 317 318 const inputOverride = implicitInput.length ? [implicitInput] : null; 319 const buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/true, inputOverride); 320 321 string[] buildParamLines; 322 if(!before.empty) buildParamLines ~= "before = " ~ before; 323 if(!between.empty) buildParamLines ~= "between = " ~ between; 324 if(!after.empty) buildParamLines ~= "after = " ~ after; 325 326 buildEntries ~= NinjaEntry(buildLine, buildParamLines); 327 328 if(haveToAddRule) { 329 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 330 } 331 } 332 333 void implicitOutputRule(Target target, in string shellCommand) @safe { 334 bool haveToAdd; 335 immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, "" /*before*/, "$in"); 336 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd); 337 338 immutable buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/false); 339 buildEntries ~= NinjaEntry(buildLine); 340 341 if(haveToAdd) { 342 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 343 } 344 } 345 346 void implicitInputRule(Target target, in string shellCommand) @safe { 347 348 import std.algorithm.searching: canFind; 349 import std.array: replace; 350 351 string input; 352 353 immutable cmdLine = () @trusted { 354 string line = shellCommand; 355 auto allDeps = target.dependenciesInProjectPath(_projectPath) ~ target.implicitsInProjectPath(_projectPath); 356 foreach(dep; allDeps) { 357 if(line.canFind(dep)) { 358 line = line.replace(dep, "$in"); 359 input = dep; 360 } else version(Windows) { 361 const dep_fwd = dep.replace(`\`, "/"); 362 if(line.canFind(dep_fwd)) { 363 line = line.replace(dep_fwd, "$in"); 364 input = dep; 365 } 366 } 367 } 368 return line; 369 }(); 370 371 explicitInOutRule(target, cmdLine, input); 372 } 373 374 //@trusted because of canFind 375 string getRuleCommandLine(Target target, in string shellCommand, 376 in string before = "", in string first = "", 377 in string between = "", 378 in string last = "", in string after = "") @trusted pure const { 379 380 import std.array: empty; 381 import std.algorithm.searching: canFind; 382 383 auto cmdLine = "command = " ~ targetRawCommand(target); 384 if(!before.empty) cmdLine ~= " $before"; 385 cmdLine ~= shellCommand.canFind(" " ~ first) ? " " ~ first : first; 386 if(!between.empty) cmdLine ~= " $between"; 387 cmdLine ~= shellCommand.canFind(" " ~ last) ? " " ~ last : last; 388 if(!after.empty) cmdLine ~= " $after"; 389 390 return cmdLine; 391 } 392 393 //Ninja operates on rules, not commands. Since this is supposed to work with 394 //generic build systems, the same command can appear with different parameter 395 //ordering. The first time we create a rule with the same name as the command. 396 //The subsequent times, if any, we append a number to the command to create 397 //a new rule 398 string getRuleName(in string cmd, in string ruleCmdLine, out bool haveToAdd) @safe nothrow { 399 import std.algorithm.searching: canFind, startsWith; 400 import std.algorithm.iteration: filter; 401 import std.array: array, empty, replace; 402 import std.conv: text; 403 404 immutable ruleMainLine = "rule " ~ cmd; 405 //don't have a rule for this cmd yet, return just the cmd 406 if(!ruleEntries.canFind!(a => a.mainLine == ruleMainLine)) { 407 haveToAdd = true; 408 return cmd; 409 } 410 411 //so we have a rule for this already. Need to check if the command line 412 //is the same 413 414 //same cmd: either matches exactly or is cmd_{number} 415 auto isSameCmd = (in NinjaEntry entry) { 416 bool sameMainLine = entry.mainLine.startsWith(ruleMainLine) && 417 (entry.mainLine == ruleMainLine || entry.mainLine[ruleMainLine.length] == '_'); 418 bool sameCmdLine = entry.paramLines == [ruleCmdLine]; 419 420 return sameMainLine && sameCmdLine; 421 }; 422 423 auto rulesWithSameCmd = ruleEntries.filter!isSameCmd; 424 assert(rulesWithSameCmd.empty || rulesWithSameCmd.array.length == 1); 425 426 //found a sule with the same cmd and paramLines 427 if(!rulesWithSameCmd.empty) 428 return () @trusted { return rulesWithSameCmd.front.mainLine.replace("rule ", ""); }(); 429 430 //if we got here then it's the first time we see "cmd" with a new 431 //ruleCmdLine, so we add it 432 haveToAdd = true; 433 434 return cmd ~ "_" ~ (++_counter).text; 435 } 436 437 string output(const(NinjaEntry)[] entries) @safe pure const nothrow { 438 import reggae.options: banner; 439 import std.algorithm.iteration: map; 440 import std.array: join; 441 return banner ~ entries.map!(a => a.toString).join("\n\n"); 442 } 443 444 string buildLine(Target target, in string rule, in bool includeImplicitInputs, 445 in string[] inputsOverride = null) @safe pure const { 446 const outputs = target.expandOutputs(_projectPath); 447 const inputs = inputsOverride !is null ? inputsOverride : targetDependencies(target); 448 const implicitInputs = includeImplicitInputs && target.implicitTargets.length 449 ? target.implicitsInProjectPath(_projectPath) 450 : null; 451 return buildLine(outputs, rule, inputs, implicitInputs); 452 } 453 454 // Creates a Ninja build statement line: 455 // `build <outputs>: <rule> <inputs> | <implicitInputs>` 456 static string buildLine(in string[] outputs, in string rule, in string[] inputs, 457 in string[] implicitInputs) @safe pure { 458 auto ret = "build " ~ flattenEntriesInBuildLine(outputs) ~ ": " ~ rule ~ " " ~ flattenEntriesInBuildLine(inputs); 459 if (implicitInputs.length) 460 ret ~= " | " ~ flattenEntriesInBuildLine(implicitInputs); 461 return ret; 462 } 463 464 // Inputs and outputs in build lines need extra escaping of some chars 465 // like colon and space. 466 static string flattenEntriesInBuildLine(in string[] entries) @safe pure { 467 import std.algorithm: map; 468 import std.array: join, replace; 469 return entries 470 .map!(e => e.replace(":", "$:").replace(" ", "$ ")) 471 .join(" "); 472 } 473 474 //@trusted because of splitter 475 private string targetCommand(Target target) @trusted pure const { 476 return targetRawCommand(target).sanitizeCmd; 477 } 478 479 //@trusted because of splitter 480 private string targetRawCommand(Target target) @trusted pure const { 481 import std.algorithm: splitter; 482 import std.array: front; 483 484 auto cmd = target.shellCommand(_options); 485 if(cmd == "") return ""; 486 return cmd.splitter(" ").front; 487 } 488 489 private string[] targetDependencies(in Target target) @safe pure const { 490 return target.dependenciesInProjectPath(_projectPath); 491 } 492 493 } 494 495 496 //ninja doesn't like symbols in rule names 497 //@trusted because of replace 498 private string sanitizeCmd(in string cmd) @trusted pure nothrow { 499 import std.path: baseName; 500 import std.array: replace; 501 //only handles c++ compilers so far... 502 return cmd.baseName.replace("+", "p"); 503 }