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