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