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