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