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