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