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 cmdTypeToNinjaString(CommandType commandType, Language language) @safe pure { 10 final switch(commandType) with(CommandType) { 11 case shell: assert(0, "cmdTypeToNinjaString doesn't work for shell"); 12 case phony: assert(0, "cmdTypeToNinjaString 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 // ninja freaks out if there's a Windows drive in the path 44 // because of the colon 45 string noDrive(string path) { 46 version(Posix) 47 return path; 48 else { 49 import std.string: replace; 50 import std.algorithm: canFind; 51 52 foreach(c; 'A' .. 'Z' + 1) { 53 const drive = cast(char)c ~ `:\`; 54 if(path.canFind(drive)) return path.replace(drive, `\`); 55 } 56 return path; 57 } 58 } 59 60 import std.array: join; 61 import std.range: chain, only; 62 import std.algorithm.iteration: map; 63 64 return chain(only(mainLine), paramLines.map!(a => " " ~ noDrive(a))).join("\n"); 65 } 66 } 67 68 private string escapeEnvVars(in string line) @safe pure nothrow { 69 import std.string: replace; 70 return line.replace("$", "$$"); 71 } 72 73 74 private bool hasDepFile(in CommandType type) @safe pure nothrow { 75 return type == CommandType.compile || type == CommandType.compileAndLink; 76 } 77 78 /** 79 * Pre-built rules 80 */ 81 NinjaEntry[] defaultRules(in Options options) @safe pure { 82 83 import reggae.build: Command; 84 85 NinjaEntry createNinjaEntry(in CommandType type, in Language language) @safe pure { 86 string[] paramLines = ["command = " ~ Command.builtinTemplate(type, language, options)]; 87 if(hasDepFile(type)) paramLines ~= ["deps = gcc", "depfile = $out.dep"]; 88 return NinjaEntry("rule " ~ cmdTypeToNinjaString(type, language), paramLines); 89 } 90 91 NinjaEntry[] entries; 92 foreach(type; [CommandType.compile, CommandType.link, CommandType.compileAndLink]) { 93 for(Language language = Language.min; language <= Language.max; ++language) { 94 if(hasDepFile(type) && language == Language.unknown) continue; 95 entries ~= createNinjaEntry(type, language); 96 } 97 } 98 99 entries ~= NinjaEntry("rule _phony", ["command = $cmd"]); 100 101 return entries; 102 } 103 104 105 struct Ninja { 106 107 import reggae.build: Build, Target; 108 109 NinjaEntry[] buildEntries; 110 NinjaEntry[] ruleEntries; 111 112 this(Build build, in string projectPath = "") @safe { 113 import reggae.config: options; 114 auto modOptions = options.dup; 115 modOptions.projectPath = projectPath; 116 this(build, modOptions); 117 } 118 119 this(Build build, in Options options) @safe { 120 _build = build; 121 _options = options; 122 _projectPath = _options.projectPath; 123 124 125 foreach(target; _build.range) { 126 target.hasDefaultCommand 127 ? defaultRule(target) 128 : target.getCommandType == CommandType.phony 129 ? phonyRule(target) 130 : customRule(target); 131 } 132 } 133 134 //includes rerunning reggae 135 const(NinjaEntry)[] allBuildEntries() @safe { 136 import std.path: stripDrive; 137 import std.algorithm.iteration: map; 138 import std.array: join; 139 140 immutable files = _options.reggaeFileDependencies.map!stripDrive.join(" "); 141 auto paramLines = _options.oldNinja ? [] : ["pool = console"]; 142 143 const(NinjaEntry)[] rerunEntries() { 144 // if exporting the build system, don't include rerunning reggae 145 return _options.export_ ? [] : [NinjaEntry("build build.ninja: _rerun | " ~ files, 146 paramLines)]; 147 } 148 149 return buildEntries ~ rerunEntries ~ NinjaEntry("default " ~ _build.defaultTargetsString(_projectPath)); 150 } 151 152 //includes rerunning reggae 153 const(NinjaEntry)[] allRuleEntries() @safe pure const { 154 import std.array: join; 155 156 return ruleEntries ~ defaultRules(_options) ~ 157 NinjaEntry("rule _rerun", 158 ["command = " ~ _options.rerunArgs.join(" "), 159 "generator = 1", 160 ]); 161 } 162 163 string buildOutput() @safe { 164 auto ret = "include rules.ninja\n" ~ output(allBuildEntries); 165 if(_options.export_) ret = _options.eraseProjectPath(ret); 166 return ret; 167 } 168 169 string rulesOutput() @safe pure const { 170 return output(allRuleEntries); 171 } 172 173 void writeBuild() @safe { 174 import std.stdio; 175 import std.path; 176 177 auto buildNinja = File(buildPath(_options.workingDir, "build.ninja"), "w"); 178 buildNinja.writeln(buildOutput); 179 180 auto rulesNinja = File(buildPath(_options.workingDir, "rules.ninja"), "w"); 181 rulesNinja.writeln(rulesOutput); 182 } 183 184 private: 185 Build _build; 186 string _projectPath; 187 const(Options) _options; 188 int _counter = 1; 189 190 //@trusted because of join 191 void defaultRule(Target target) @trusted { 192 import std.array: join; 193 194 string[] paramLines; 195 196 foreach(immutable param; target.commandParamNames) { 197 immutable value = target.getCommandParams(_projectPath, param, []).join(" "); 198 if(value == "") continue; 199 paramLines ~= param ~ " = " ~ value.escapeEnvVars; 200 } 201 202 immutable language = target.getLanguage; 203 204 buildEntries ~= NinjaEntry(buildLine(target) ~ 205 cmdTypeToNinjaString(target.getCommandType, language) ~ 206 " " ~ targetDependencies(target), 207 paramLines); 208 } 209 210 void phonyRule(Target target) @safe { 211 import std.array: join, empty; 212 213 //no projectPath for phony rules since they don't generate output 214 immutable outputs = target.expandOutputs("").join(" "); 215 auto buildLine = "build " ~ outputs ~ ": _phony " ~ targetDependencies(target); 216 if(!target.implicitTargets.empty) buildLine ~= " | " ~ target.implicitsInProjectPath(_projectPath).join(" "); 217 buildEntries ~= NinjaEntry(buildLine, 218 ["cmd = " ~ target.shellCommand(_options), 219 "pool = console"]); 220 } 221 222 void customRule(Target target) @safe { 223 224 import std.algorithm.searching: canFind; 225 226 //rawCmdString is used because ninja needs to find where $in and $out are, 227 //so shellCommand wouldn't work 228 immutable shellCommand = target.rawCmdString(_projectPath); 229 immutable implicitInput = () @trusted { return !shellCommand.canFind("$in"); }(); 230 immutable implicitOutput = () @trusted { return !shellCommand.canFind("$out"); }(); 231 232 if(implicitOutput) { 233 implicitOutputRule(target, shellCommand); 234 } else if(implicitInput) { 235 implicitInputRule(target, shellCommand); 236 } else { 237 explicitInOutRule(target, shellCommand); 238 } 239 } 240 241 void explicitInOutRule(Target target, in string shellCommand, in string implicitInput = "") @safe { 242 import std.regex: regex, match; 243 import std.array: empty, join; 244 import std.string: strip; 245 import std.conv: text; 246 247 auto reg = regex(`^[^ ]+ +(.*?)(\$in|\$out)(.*?)(\$in|\$out)(.*?)$`); 248 249 auto mat = shellCommand.match(reg); 250 if(mat.captures.empty) { //this is usually bad since we need both $in and $out 251 if(target.dependencyTargets.empty) { //ah, no $in needed then 252 mat = match(shellCommand ~ " $in", reg); //add a dummy one 253 } 254 else 255 throw new Exception(text("Could not find both $in and $out.\nCommand: ", 256 shellCommand, "\nCaptures: ", mat.captures, "\n", 257 "outputs: ", target.rawOutputs.join(" "), "\n", 258 "dependencies: ", targetDependencies(target))); 259 } 260 261 immutable before = mat.captures[1].strip; 262 immutable first = mat.captures[2]; 263 immutable between = mat.captures[3].strip; 264 immutable last = mat.captures[4]; 265 immutable after = mat.captures[5].strip; 266 267 immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, before, first, between, last, after); 268 bool haveToAddRule; 269 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAddRule); 270 271 immutable deps = implicitInput.empty 272 ? targetDependencies(target) 273 : implicitInput; 274 275 auto buildLine = buildLine(target) ~ ruleName ~ " " ~ deps; 276 if(!target.implicitTargets.empty) buildLine ~= " | " ~ target.implicitsInProjectPath(_projectPath).join(" "); 277 278 string[] buildParamLines; 279 if(!before.empty) buildParamLines ~= "before = " ~ before; 280 if(!between.empty) buildParamLines ~= "between = " ~ between; 281 if(!after.empty) buildParamLines ~= "after = " ~ after; 282 283 buildEntries ~= NinjaEntry(buildLine, buildParamLines); 284 285 if(haveToAddRule) { 286 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 287 } 288 } 289 290 void implicitOutputRule(Target target, in string shellCommand) @safe { 291 bool haveToAdd; 292 immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, "" /*before*/, "$in"); 293 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd); 294 295 immutable buildLine = buildLine(target) ~ ruleName ~ 296 " " ~ targetDependencies(target); 297 buildEntries ~= NinjaEntry(buildLine); 298 299 if(haveToAdd) { 300 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 301 } 302 } 303 304 void implicitInputRule(Target target, in string shellCommand) @safe { 305 306 import std.algorithm.searching: canFind; 307 import std.array: replace; 308 309 string input; 310 311 immutable cmdLine = () @trusted { 312 string line = shellCommand; 313 auto allDeps = target.dependenciesInProjectPath(_projectPath) ~ target.implicitsInProjectPath(_projectPath); 314 foreach(dep; allDeps) { 315 if(line.canFind(dep)) { 316 line = line.replace(dep, "$in"); 317 input = dep; 318 } 319 } 320 return line; 321 }(); 322 323 explicitInOutRule(target, cmdLine, input); 324 } 325 326 //@trusted because of canFind 327 string getRuleCommandLine(Target target, in string shellCommand, 328 in string before = "", in string first = "", 329 in string between = "", 330 in string last = "", in string after = "") @trusted pure const { 331 332 import std.array: empty; 333 import std.algorithm.searching: canFind; 334 335 auto cmdLine = "command = " ~ targetRawCommand(target); 336 if(!before.empty) cmdLine ~= " $before"; 337 cmdLine ~= shellCommand.canFind(" " ~ first) ? " " ~ first : first; 338 if(!between.empty) cmdLine ~= " $between"; 339 cmdLine ~= shellCommand.canFind(" " ~ last) ? " " ~ last : last; 340 if(!after.empty) cmdLine ~= " $after"; 341 342 return cmdLine; 343 } 344 345 //Ninja operates on rules, not commands. Since this is supposed to work with 346 //generic build systems, the same command can appear with different parameter 347 //ordering. The first time we create a rule with the same name as the command. 348 //The subsequent times, if any, we append a number to the command to create 349 //a new rule 350 string getRuleName(in string cmd, in string ruleCmdLine, out bool haveToAdd) @safe nothrow { 351 import std.algorithm.searching: canFind, startsWith; 352 import std.algorithm.iteration: filter; 353 import std.array: array, empty, replace; 354 import std.conv: text; 355 356 immutable ruleMainLine = "rule " ~ cmd; 357 //don't have a rule for this cmd yet, return just the cmd 358 if(!ruleEntries.canFind!(a => a.mainLine == ruleMainLine)) { 359 haveToAdd = true; 360 return cmd; 361 } 362 363 //so we have a rule for this already. Need to check if the command line 364 //is the same 365 366 //same cmd: either matches exactly or is cmd_{number} 367 auto isSameCmd = (in NinjaEntry entry) { 368 bool sameMainLine = entry.mainLine.startsWith(ruleMainLine) && 369 (entry.mainLine == ruleMainLine || entry.mainLine[ruleMainLine.length] == '_'); 370 bool sameCmdLine = entry.paramLines == [ruleCmdLine]; 371 372 return sameMainLine && sameCmdLine; 373 }; 374 375 auto rulesWithSameCmd = ruleEntries.filter!isSameCmd; 376 assert(rulesWithSameCmd.empty || rulesWithSameCmd.array.length == 1); 377 378 //found a sule with the same cmd and paramLines 379 if(!rulesWithSameCmd.empty) 380 return () @trusted { return rulesWithSameCmd.front.mainLine.replace("rule ", ""); }(); 381 382 //if we got here then it's the first time we see "cmd" with a new 383 //ruleCmdLine, so we add it 384 haveToAdd = true; 385 386 return cmd ~ "_" ~ (++_counter).text; 387 } 388 389 string output(const(NinjaEntry)[] entries) @safe pure const nothrow { 390 import reggae.options: banner; 391 import std.algorithm.iteration: map; 392 import std.array: join; 393 return banner ~ entries.map!(a => a.toString).join("\n\n"); 394 } 395 396 string buildLine(Target target) @safe pure const { 397 import std.algorithm.iteration: map; 398 import std.array: join; 399 400 string fix(string path) { 401 version(Windows) { 402 import std.path: isRooted; 403 return path.isRooted && path.length >=2 && path[1] == ':' 404 ? path[0] ~ "$:" ~ path[2..$] // ninja doesn't like colons in the path 405 : path; 406 } else 407 return path; 408 } 409 410 const outputs = target 411 .expandOutputs(_projectPath) 412 .map!fix 413 .join(" "); 414 415 return "build " ~ outputs ~ ": "; 416 } 417 418 //@trusted because of splitter 419 private string targetCommand(Target target) @trusted pure const { 420 return targetRawCommand(target).sanitizeCmd; 421 } 422 423 //@trusted because of splitter 424 private string targetRawCommand(Target target) @trusted pure const { 425 import std.algorithm: splitter; 426 import std.array: front; 427 428 auto cmd = target.shellCommand(_options); 429 if(cmd == "") return ""; 430 return cmd.splitter(" ").front; 431 } 432 433 private string targetDependencies(in Target target) @safe pure const { 434 import std.path: stripDrive; 435 import std.algorithm.iteration: map; 436 import std.array: join; 437 438 return target.dependenciesInProjectPath(_projectPath).map!stripDrive.join(" "); 439 } 440 441 } 442 443 444 //ninja doesn't like symbols in rule names 445 //@trusted because of replace 446 private string sanitizeCmd(in string cmd) @trusted pure nothrow { 447 import std.path: baseName; 448 import std.array: replace; 449 //only handles c++ compilers so far... 450 return cmd.baseName.replace("+", "p"); 451 }