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