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