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