1 module reggae.backend.ninja; 2 3 4 import reggae.build; 5 import reggae.range; 6 import reggae.rules; 7 import reggae.config; 8 import std.array; 9 import std.range; 10 import std.algorithm; 11 import std.exception: enforce; 12 import std.conv: text; 13 import std.string: strip; 14 import std.path: defaultExtension, absolutePath; 15 16 17 struct NinjaEntry { 18 string mainLine; 19 string[] paramLines; 20 string toString() @safe pure nothrow const { 21 return (mainLine ~ paramLines.map!(a => " " ~ a).array).join("\n"); 22 } 23 } 24 25 26 /** 27 * Pre-built rules 28 */ 29 NinjaEntry[] defaultRules() @safe pure nothrow { 30 return [NinjaEntry("rule _dcompile", 31 ["command = .reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~ 32 dCompiler ~ " $flags $includes $stringImports $in", 33 "deps = gcc", 34 "depfile = $DEPFILE"]), 35 NinjaEntry("rule _link", 36 ["command = " ~ dCompiler ~ " $flags -of$out $in"]), 37 NinjaEntry("rule _cppcompile", 38 ["command = " ~ cppCompiler ~ " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in", 39 "deps = gcc", 40 "depfile = $DEPFILE"]), 41 NinjaEntry("rule _ccompile", 42 ["command = " ~ cCompiler ~ " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in", 43 "deps = gcc", 44 "depfile = $DEPFILE"]), 45 ]; 46 } 47 48 49 struct Ninja { 50 NinjaEntry[] buildEntries; 51 NinjaEntry[] ruleEntries; 52 53 this(Build build, in string projectPath = "") @safe { 54 _build = build; 55 _projectPath = projectPath; 56 57 foreach(topTarget; _build.targets) { 58 foreach(target; DepthFirst(topTarget)) { 59 auto rawCmdLine = target.rawCmdString(_projectPath); 60 rawCmdLine.isDefaultCommand ? defaultRule(target, rawCmdLine) : customRule(target, rawCmdLine); 61 } 62 } 63 } 64 65 const(NinjaEntry)[] allBuildEntries() @safe pure nothrow const { 66 immutable files = [buildFilePath, reggaePath].join(" "); 67 return buildEntries ~ 68 NinjaEntry("build build.ninja: _rerun | " ~ files, 69 ["pool = console"]); 70 } 71 72 const(NinjaEntry)[] allRuleEntries() @safe pure const { 73 immutable _dflags = dflags == "" ? "" : " --dflags='" ~ dflags ~ "'"; 74 75 return ruleEntries ~ defaultRules ~ 76 NinjaEntry("rule _rerun", 77 ["command = " ~ reggaePath ~ " -b ninja" ~ _dflags ~ " " ~ projectPath, 78 "generator = 1"]); 79 } 80 81 string buildOutput() @safe pure nothrow const { 82 return output(allBuildEntries); 83 } 84 85 string rulesOutput() @safe pure const { 86 return output(allRuleEntries); 87 } 88 89 private: 90 Build _build; 91 string _projectPath; 92 int _counter = 1; 93 94 //@trusted because of join 95 void defaultRule(in Target target, in string rawCmdLine) @trusted { 96 immutable rule = rawCmdLine.getDefaultRule; 97 98 string[] paramLines; 99 100 if(rule != "_link") { //i.e. one of the compile rules 101 auto params = ["includes", "flags"]; 102 if(rule == "_dcompile") params ~= "stringImports"; 103 104 foreach(immutable param; params) { 105 immutable value = rawCmdLine.getDefaultRuleParams(param, []).join(" "); 106 paramLines ~= param ~ " = " ~ value; 107 } 108 109 paramLines ~= "DEPFILE = " ~ target.outputs[0] ~ ".dep"; 110 } else { 111 auto params = ["flags"]; 112 113 foreach(immutable param; params) { 114 immutable value = rawCmdLine.getDefaultRuleParams(param, []).join(" "); 115 paramLines ~= param ~ " = " ~ value; 116 } 117 118 } 119 120 buildEntries ~= NinjaEntry("build " ~ target.outputs[0] ~ ": " ~ rule ~ " " ~ 121 target.dependencyFilesString(_projectPath), 122 paramLines); 123 } 124 125 void customRule(in Target target, in string rawCmdLine) @safe { 126 immutable implicitInput = () @trusted { return !rawCmdLine.canFind("$in"); }(); 127 immutable implicitOutput = () @trusted { return !rawCmdLine.canFind("$out"); }(); 128 129 if(implicitOutput) { 130 implicitOutputRule(target, rawCmdLine); 131 } else if(implicitInput) { 132 implicitInputRule(target, rawCmdLine); 133 } else { 134 explicitInOutRule(target, rawCmdLine); 135 } 136 } 137 138 void explicitInOutRule(in Target target, in string rawCmdLine, in string implicitInput = "") @safe { 139 import std.regex; 140 auto reg = regex(`^[^ ]+ +(.*?)(\$in|\$out)(.*?)(\$in|\$out)(.*?)$`); 141 142 auto mat = rawCmdLine.match(reg); 143 enforce(!mat.captures.empty, text("Could not find both $in and $out.\nCommand: ", 144 rawCmdLine, "\nCaptures: ", mat.captures)); 145 immutable before = mat.captures[1].strip; 146 immutable first = mat.captures[2]; 147 immutable between = mat.captures[3].strip; 148 immutable last = mat.captures[4]; 149 immutable after = mat.captures[5].strip; 150 151 immutable ruleCmdLine = getRuleCommandLine(target, rawCmdLine, before, first, between, last, after); 152 bool haveToAdd; 153 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd); 154 155 immutable deps = implicitInput.empty 156 ? target.dependencyFilesString(_projectPath) 157 : implicitInput; 158 159 auto buildLine = "build " ~ target.outputs.join(" ") ~ ": " ~ ruleName ~ 160 " " ~ deps; 161 if(!target.implicits.empty) buildLine ~= " | " ~ target.implicitFilesString(_projectPath); 162 163 string[] buildParamLines; 164 if(!before.empty) buildParamLines ~= "before = " ~ before; 165 if(!between.empty) buildParamLines ~= "between = " ~ between; 166 if(!after.empty) buildParamLines ~= "after = " ~ after; 167 168 buildEntries ~= NinjaEntry(buildLine, buildParamLines); 169 170 if(haveToAdd) { 171 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 172 } 173 } 174 175 void implicitOutputRule(in Target target, in string rawCmdLine) @safe nothrow { 176 bool haveToAdd; 177 immutable ruleCmdLine = getRuleCommandLine(target, rawCmdLine, "" /*before*/, "$in"); 178 immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd); 179 180 immutable buildLine = "build " ~ target.outputs.join(" ") ~ ": " ~ ruleName ~ 181 " " ~ target.dependencyFilesString(_projectPath); 182 buildEntries ~= NinjaEntry(buildLine); 183 184 if(haveToAdd) { 185 ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]); 186 } 187 } 188 189 void implicitInputRule(in Target target, in string rawCmdLine) @safe { 190 string input; 191 192 immutable cmdLine = () @trusted { 193 string line = rawCmdLine; 194 auto allDeps = (target.dependencyFilesString(_projectPath) ~ " " ~ 195 target.implicitFilesString(_projectPath)).splitter(" "); 196 foreach(string dep; allDeps) { 197 if(line.canFind(dep)) { 198 line = line.replace(dep, "$in"); 199 input = dep; 200 } 201 } 202 return line; 203 }(); 204 205 explicitInOutRule(target, cmdLine, input); 206 } 207 208 //@trusted because of canFind 209 string getRuleCommandLine(in Target target, in string rawCmdLine, 210 in string before = "", in string first = "", 211 in string between = "", 212 in string last = "", in string after = "") @trusted pure nothrow const { 213 214 auto cmdLine = "command = " ~ targetRawCommand(target); 215 if(!before.empty) cmdLine ~= " $before"; 216 cmdLine ~= rawCmdLine.canFind(" " ~ first) ? " " ~ first : first; 217 if(!between.empty) cmdLine ~= " $between"; 218 cmdLine ~= rawCmdLine.canFind(" " ~ last) ? " " ~ last : last; 219 if(!after.empty) cmdLine ~= " $after"; 220 return cmdLine; 221 } 222 223 //Ninja operates on rules, not commands. Since this is supposed to work with 224 //generic build systems, the same command can appear with different parameter 225 //ordering. The first time we create a rule with the same name as the command. 226 //The subsequent times, if any, we append a number to the command to create 227 //a new rule 228 string getRuleName(in string cmd, in string ruleCmdLine, out bool haveToAdd) @safe nothrow { 229 immutable ruleMainLine = "rule " ~ cmd; 230 //don't have a rule for this cmd yet, return just the cmd 231 if(!ruleEntries.canFind!(a => a.mainLine == ruleMainLine)) { 232 haveToAdd = true; 233 return cmd; 234 } 235 236 //so we have a rule for this already. Need to check if the command line 237 //is the same 238 239 //same cmd: either matches exactly or is cmd_{number} 240 auto isSameCmd = (in NinjaEntry entry) { 241 bool sameMainLine = entry.mainLine.startsWith(ruleMainLine) && 242 (entry.mainLine == ruleMainLine || entry.mainLine[ruleMainLine.length] == '_'); 243 bool sameCmdLine = entry.paramLines == [ruleCmdLine]; 244 return sameMainLine && sameCmdLine; 245 }; 246 247 auto rulesWithSameCmd = ruleEntries.filter!isSameCmd; 248 assert(rulesWithSameCmd.empty || rulesWithSameCmd.array.length == 1); 249 250 //found a sule with the same cmd and paramLines 251 if(!rulesWithSameCmd.empty) 252 return () @trusted { return rulesWithSameCmd.front.mainLine.replace("rule ", ""); }(); 253 254 //if we got here then it's the first time we see "cmd" with a new 255 //ruleCmdLine, so we add it 256 haveToAdd = true; 257 import std.conv: to; 258 return cmd ~ "_" ~ (++_counter).to!string; 259 } 260 261 string output(const(NinjaEntry)[] entries) @safe pure const nothrow { 262 return entries.map!(a => a.toString).join("\n\n"); 263 } 264 } 265 266 //@trusted because of splitter 267 private string targetCommand(in Target target) @trusted pure nothrow { 268 return targetRawCommand(target).sanitizeCmd; 269 } 270 271 //@trusted because of splitter 272 private string targetRawCommand(in Target target) @trusted pure nothrow { 273 return target.command.splitter(" ").front; 274 } 275 276 //ninja doesn't like symbols in rule names 277 //@trusted because of replace 278 private string sanitizeCmd(in string cmd) @trusted pure nothrow { 279 import std.path; 280 //only handles c++ compilers so far... 281 return cmd.baseName.replace("+", "p"); 282 }