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