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 cmdTypeToNinjaRuleName(CommandType commandType, Language language) @safe pure {
10     final switch(commandType) with(CommandType) {
11         case shell: assert(0, "cmdTypeToNinjaRuleName doesn't work for shell");
12         case phony: assert(0, "cmdTypeToNinjaRuleName 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         import std.array: join;
44         import std.range: chain, only;
45         import std.algorithm.iteration: map;
46 
47         return chain(only(mainLine), paramLines.map!(a => "  " ~ a)).join("\n");
48     }
49 }
50 
51 
52 private bool hasDepFile(in CommandType type) @safe pure nothrow {
53     return type == CommandType.compile || type == CommandType.compileAndLink;
54 }
55 
56 private string[] initializeRuleParamLines(in Language language, in string command) @safe pure {
57     version(Windows) {
58         import std.algorithm: among;
59         import std..string: indexOf;
60 
61         // On Windows, the max command line length is ~32K.
62         // Make ninja use a response file for all D/C[++] rules.
63         if (language.among(Language.D, Language.C, Language.Cplusplus)) {
64             const firstSpaceIndex = command.indexOf(' ');
65             if (firstSpaceIndex > 0) {
66                 const program = command[0 .. firstSpaceIndex];
67                 const args = command[firstSpaceIndex+1 .. $];
68                 return [
69                     "command = " ~ program ~ " @$out.rsp",
70                     "rspfile = $out.rsp",
71                     "rspfile_content = " ~ args,
72                 ];
73             }
74         }
75     }
76 
77     return ["command = " ~ command];
78 }
79 
80 /**
81  * Pre-built rules
82  */
83 NinjaEntry[] defaultRules(in Options options) @safe pure {
84 
85     import reggae.build: Command;
86 
87     NinjaEntry createNinjaEntry(in CommandType type, in Language language) @safe pure {
88         const string command = Command.builtinTemplate(type, language, options);
89 
90         string[] paramLines = initializeRuleParamLines(language, command);
91 
92         if(hasDepFile(type)) {
93             version(Windows)
94                 const isMSVC = language == Language.C || language == Language.Cplusplus;
95             else
96                 enum isMSVC = false;
97 
98             if (isMSVC)
99                 paramLines ~= "deps = msvc";
100             else
101                 paramLines ~= ["deps = gcc", "depfile = $out.dep"];
102         }
103 
104         string getDescription() {
105             switch(type) with(CommandType) {
106                 case compile:        return "Compiling $out";
107                 case link:           return "Linking $out";
108                 case compileAndLink: return "Building $out";
109                 default:             return null;
110             }
111         }
112 
113         const description = getDescription();
114         if (description.length)
115             paramLines ~= "description = " ~ description;
116 
117         return NinjaEntry("rule " ~ cmdTypeToNinjaRuleName(type, language), paramLines);
118     }
119 
120     NinjaEntry[] entries;
121     foreach(type; [CommandType.compile, CommandType.link, CommandType.compileAndLink]) {
122         for(Language language = Language.min; language <= Language.max; ++language) {
123             if(hasDepFile(type) && language == Language.unknown) continue;
124             entries ~= createNinjaEntry(type, language);
125         }
126     }
127 
128     string[] phonyParamLines;
129     version(Windows) {
130         phonyParamLines = [`command = cmd.exe /c "$cmd"`, "description = $cmd"];
131     } else {
132         phonyParamLines = ["command = $cmd"];
133     }
134     entries ~= NinjaEntry("rule _phony", phonyParamLines);
135 
136     return entries;
137 }
138 
139 
140 struct Ninja {
141 
142     import reggae.build: Build, Target;
143 
144     NinjaEntry[] buildEntries;
145     NinjaEntry[] ruleEntries;
146 
147     this(Build build, in string projectPath = "") @safe {
148         import reggae.config: options;
149         auto modOptions = options.dup;
150         modOptions.projectPath = projectPath;
151         this(build, modOptions);
152     }
153 
154     this(Build build, in Options options) @safe {
155         _build = build;
156         _options = options;
157         _projectPath = _options.projectPath;
158 
159 
160         foreach(target; _build.range) {
161             target.hasDefaultCommand
162                 ? defaultRule(target)
163                 : target.getCommandType == CommandType.phony
164                 ? phonyRule(target)
165                 : customRule(target);
166         }
167     }
168 
169     //includes rerunning reggae
170     const(NinjaEntry)[] allBuildEntries() @safe {
171         immutable files = flattenEntriesInBuildLine(_options.reggaeFileDependencies);
172         auto paramLines = _options.oldNinja ? [] : ["pool = console"];
173 
174         const(NinjaEntry)[] rerunEntries() {
175             // if exporting the build system, don't include rerunning reggae
176             return _options.export_ ? [] : [NinjaEntry("build build.ninja: _rerun | " ~ files,
177                                                        paramLines)];
178         }
179 
180         const defaultOutputs = _build.defaultTargetsOutputs(_projectPath);
181         const defaultEntry = NinjaEntry("default " ~ flattenEntriesInBuildLine(defaultOutputs));
182 
183         return buildEntries ~ rerunEntries ~ defaultEntry;
184     }
185 
186     //includes rerunning reggae
187     const(NinjaEntry)[] allRuleEntries() @safe pure const {
188         import std.array: join;
189 
190         return ruleEntries ~ defaultRules(_options) ~
191             NinjaEntry("rule _rerun",
192                        ["command = " ~ _options.rerunArgs.join(" "),
193                         "generator = 1",
194                            ]);
195     }
196 
197     string buildOutput() @safe {
198         auto ret = "include rules.ninja\n" ~ output(allBuildEntries);
199         if(_options.export_) ret = _options.eraseProjectPath(ret);
200         return ret;
201     }
202 
203     string rulesOutput() @safe pure const {
204         return output(allRuleEntries);
205     }
206 
207     void writeBuild() @safe {
208         import std.stdio: File;
209         import reggae.path: buildPath;
210 
211         auto buildNinja = File(buildPath(_options.workingDir, "build.ninja"), "w");
212         buildNinja.writeln(buildOutput);
213 
214         auto rulesNinja = File(buildPath(_options.workingDir, "rules.ninja"), "w");
215         rulesNinja.writeln(rulesOutput);
216     }
217 
218 private:
219     Build _build;
220     string _projectPath;
221     const(Options) _options;
222     int _counter = 1;
223 
224     void defaultRule(Target target) @safe {
225         import std.algorithm: canFind, map;
226         import std.array: join, replace;
227 
228         static string flattenShellArgs(in string[] args) {
229             static string quoteArgIfNeeded(string a) {
230                 return !a.canFind(' ') ? a : `"` ~ a.replace(`"`, `\"`) ~ `"`;
231             }
232             return args.map!quoteArgIfNeeded.join(" ");
233         }
234 
235         string[] paramLines;
236         foreach(immutable param; target.commandParamNames) {
237             // skip the DEPFILE parameter, it's already specified in the rule
238             if (param == "DEPFILE") continue;
239             const values = target.getCommandParams(_projectPath, param, []);
240             const flat = flattenShellArgs(values);
241             if(!flat.length) continue;
242             // the flat value still needs to be escaped for Ninja ($ => $$, e.g. for env vars)
243             paramLines ~= param ~ " = " ~ flat.replace("$", "$$");
244         }
245 
246         const ruleName = cmdTypeToNinjaRuleName(target.getCommandType, target.getLanguage);
247         const buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/false);
248 
249         buildEntries ~= NinjaEntry(buildLine, paramLines);
250     }
251 
252     void phonyRule(Target target) @safe {
253         const cmd = target.shellCommand(_options);
254 
255         //no projectPath for phony rules since they don't generate output
256         const outputs = target.expandOutputs("");
257         const inputs = targetDependencies(target);
258         const implicitInputs = target.implicitTargets.length
259             ? target.implicitsInProjectPath(_projectPath)
260             : null;
261         const buildLine = buildLine(outputs, cmd is null ? "phony" : "_phony", inputs, implicitInputs);
262 
263         buildEntries ~= NinjaEntry(buildLine, cmd is null
264                                               ? ["pool = console"]
265                                               : ["cmd = " ~ cmd, "pool = console"]);
266     }
267 
268     void customRule(Target target) @safe {
269 
270         import std.algorithm.searching: canFind;
271 
272         //rawCmdString is used because ninja needs to find where $in and $out are,
273         //so shellCommand wouldn't work
274         immutable shellCommand = target.rawCmdString(_projectPath);
275         immutable implicitInput =  () @trusted { return !shellCommand.canFind("$in");  }();
276         immutable implicitOutput = () @trusted { return !shellCommand.canFind("$out"); }();
277 
278         if(implicitOutput) {
279             implicitOutputRule(target, shellCommand);
280         } else if(implicitInput) {
281             implicitInputRule(target, shellCommand);
282         } else {
283             explicitInOutRule(target, shellCommand);
284         }
285     }
286 
287     void explicitInOutRule(Target target, in string shellCommand, in string implicitInput = "") @safe {
288         import std.regex: regex, match;
289         import std.algorithm.iteration: map;
290         import std.array: empty, join;
291         import std..string: strip;
292         import std.conv: text;
293 
294         auto reg = regex(`^[^ ]+ +(.*?)(\$in|\$out)(.*?)(\$in|\$out)(.*?)$`);
295 
296         auto mat = shellCommand.match(reg);
297         if(mat.captures.empty) { //this is usually bad since we need both $in and $out
298             if(target.dependencyTargets.empty) { //ah, no $in needed then
299                 mat = match(shellCommand ~ " $in", reg); //add a dummy one
300             }
301             else
302                 throw new Exception(text("Could not find both $in and $out.\nCommand: ",
303                                          shellCommand, "\nCaptures: ", mat.captures, "\n",
304                                          "outputs: ", target.rawOutputs.join(" "), "\n",
305                                          "dependencies: ", targetDependencies(target)));
306         }
307 
308         immutable before  = mat.captures[1].strip;
309         immutable first   = mat.captures[2];
310         immutable between = mat.captures[3].strip;
311         immutable last    = mat.captures[4];
312         immutable after   = mat.captures[5].strip;
313 
314         immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, before, first, between, last, after);
315         bool haveToAddRule;
316         immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAddRule);
317 
318         const inputOverride = implicitInput.length ? [implicitInput] : null;
319         const buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/true, inputOverride);
320 
321         string[] buildParamLines;
322         if(!before.empty)  buildParamLines ~= "before = "  ~ before;
323         if(!between.empty) buildParamLines ~= "between = " ~ between;
324         if(!after.empty)   buildParamLines ~= "after = "   ~ after;
325 
326         buildEntries ~= NinjaEntry(buildLine, buildParamLines);
327 
328         if(haveToAddRule) {
329             ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]);
330         }
331     }
332 
333     void implicitOutputRule(Target target, in string shellCommand) @safe {
334         bool haveToAdd;
335         immutable ruleCmdLine = getRuleCommandLine(target, shellCommand, "" /*before*/, "$in");
336         immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd);
337 
338         immutable buildLine = buildLine(target, ruleName, /*includeImplicitInputs=*/false);
339         buildEntries ~= NinjaEntry(buildLine);
340 
341         if(haveToAdd) {
342             ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]);
343         }
344     }
345 
346     void implicitInputRule(Target target, in string shellCommand) @safe {
347 
348         import std.algorithm.searching: canFind;
349         import std.array: replace;
350 
351         string input;
352 
353         immutable cmdLine = () @trusted {
354             string line = shellCommand;
355             auto allDeps = target.dependenciesInProjectPath(_projectPath) ~ target.implicitsInProjectPath(_projectPath);
356             foreach(dep; allDeps) {
357                 if(line.canFind(dep)) {
358                     line = line.replace(dep, "$in");
359                     input = dep;
360                 } else version(Windows) {
361                     const dep_fwd = dep.replace(`\`, "/");
362                     if(line.canFind(dep_fwd)) {
363                         line = line.replace(dep_fwd, "$in");
364                         input = dep;
365                     }
366                 }
367             }
368             return line;
369         }();
370 
371         explicitInOutRule(target, cmdLine, input);
372     }
373 
374     //@trusted because of canFind
375     string getRuleCommandLine(Target target, in string shellCommand,
376                               in string before = "", in string first = "",
377                               in string between = "",
378                               in string last = "", in string after = "") @trusted pure const {
379 
380         import std.array: empty;
381         import std.algorithm.searching: canFind;
382 
383         auto cmdLine = "command = " ~ targetRawCommand(target);
384         if(!before.empty) cmdLine ~= " $before";
385         cmdLine ~= shellCommand.canFind(" " ~ first) ? " " ~ first : first;
386         if(!between.empty) cmdLine ~= " $between";
387         cmdLine ~= shellCommand.canFind(" " ~ last) ? " " ~ last : last;
388         if(!after.empty) cmdLine ~= " $after";
389 
390         return cmdLine;
391     }
392 
393     //Ninja operates on rules, not commands. Since this is supposed to work with
394     //generic build systems, the same command can appear with different parameter
395     //ordering. The first time we create a rule with the same name as the command.
396     //The subsequent times, if any, we append a number to the command to create
397     //a new rule
398     string getRuleName(in string cmd, in string ruleCmdLine, out bool haveToAdd) @safe nothrow {
399         import std.algorithm.searching: canFind, startsWith;
400         import std.algorithm.iteration: filter;
401         import std.array: array, empty, replace;
402         import std.conv: text;
403 
404         immutable ruleMainLine = "rule " ~ cmd;
405         //don't have a rule for this cmd yet, return just the cmd
406         if(!ruleEntries.canFind!(a => a.mainLine == ruleMainLine)) {
407             haveToAdd = true;
408             return cmd;
409         }
410 
411         //so we have a rule for this already. Need to check if the command line
412         //is the same
413 
414         //same cmd: either matches exactly or is cmd_{number}
415         auto isSameCmd = (in NinjaEntry entry) {
416             bool sameMainLine = entry.mainLine.startsWith(ruleMainLine) &&
417                 (entry.mainLine == ruleMainLine || entry.mainLine[ruleMainLine.length] == '_');
418             bool sameCmdLine = entry.paramLines == [ruleCmdLine];
419 
420             return sameMainLine && sameCmdLine;
421         };
422 
423         auto rulesWithSameCmd = ruleEntries.filter!isSameCmd;
424         assert(rulesWithSameCmd.empty || rulesWithSameCmd.array.length == 1);
425 
426         //found a sule with the same cmd and paramLines
427         if(!rulesWithSameCmd.empty)
428             return () @trusted { return rulesWithSameCmd.front.mainLine.replace("rule ", ""); }();
429 
430         //if we got here then it's the first time we see "cmd" with a new
431         //ruleCmdLine, so we add it
432         haveToAdd = true;
433 
434         return cmd ~ "_" ~ (++_counter).text;
435     }
436 
437     string output(const(NinjaEntry)[] entries) @safe pure const nothrow {
438         import reggae.options: banner;
439         import std.algorithm.iteration: map;
440         import std.array: join;
441         return banner ~ entries.map!(a => a.toString).join("\n\n");
442     }
443 
444     string buildLine(Target target, in string rule, in bool includeImplicitInputs,
445                      in string[] inputsOverride = null) @safe pure const {
446         const outputs = target.expandOutputs(_projectPath);
447         const inputs = inputsOverride !is null ? inputsOverride : targetDependencies(target);
448         const implicitInputs = includeImplicitInputs && target.implicitTargets.length
449             ? target.implicitsInProjectPath(_projectPath)
450             : null;
451         return buildLine(outputs, rule, inputs, implicitInputs);
452     }
453 
454     // Creates a Ninja build statement line:
455     // `build <outputs>: <rule> <inputs> | <implicitInputs>`
456     static string buildLine(in string[] outputs, in string rule, in string[] inputs,
457                      in string[] implicitInputs) @safe pure {
458         auto ret = "build " ~ flattenEntriesInBuildLine(outputs) ~ ": " ~ rule ~ " " ~ flattenEntriesInBuildLine(inputs);
459         if (implicitInputs.length)
460             ret ~= " | " ~ flattenEntriesInBuildLine(implicitInputs);
461         return ret;
462     }
463 
464     // Inputs and outputs in build lines need extra escaping of some chars
465     // like colon and space.
466     static string flattenEntriesInBuildLine(in string[] entries) @safe pure {
467         import std.algorithm: map;
468         import std.array: join, replace;
469         return entries
470             .map!(e => e.replace(":", "$:").replace(" ", "$ "))
471             .join(" ");
472     }
473 
474     //@trusted because of splitter
475     private string targetCommand(Target target) @trusted pure const {
476         return targetRawCommand(target).sanitizeCmd;
477     }
478 
479     //@trusted because of splitter
480     private string targetRawCommand(Target target) @trusted pure const {
481         import std.algorithm: splitter;
482         import std.array: front;
483 
484         auto cmd = target.shellCommand(_options);
485         if(cmd == "") return "";
486         return cmd.splitter(" ").front;
487     }
488 
489     private string[] targetDependencies(in Target target) @safe pure const {
490         return target.dependenciesInProjectPath(_projectPath);
491     }
492 
493 }
494 
495 
496 //ninja doesn't like symbols in rule names
497 //@trusted because of replace
498 private string sanitizeCmd(in string cmd) @trusted pure nothrow {
499     import std.path: baseName;
500     import std.array: replace;
501     //only handles c++ compilers so far...
502     return cmd.baseName.replace("+", "p");
503 }