1 module reggae.backend.ninja;
2 
3 
4 import reggae.build;
5 import reggae.range;
6 import reggae.rules;
7 import reggae.config;
8 import std.array;
9 import std.range;
10 import std.algorithm;
11 import std.exception: enforce;
12 import std.conv: text;
13 import std.string: strip;
14 import std.path: defaultExtension, absolutePath;
15 
16 
17 struct NinjaEntry {
18     string mainLine;
19     string[] paramLines;
20     string toString() @safe pure nothrow const {
21         return (mainLine ~ paramLines.map!(a => "  " ~ a).array).join("\n");
22     }
23 }
24 
25 
26 /**
27  * Pre-built rules
28  */
29 NinjaEntry[] defaultRules() @safe pure nothrow {
30     return [NinjaEntry("rule _dcompile",
31                        ["command = .reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~
32                         dCompiler ~ " $flags $includes $stringImports $in",
33                         "deps = gcc",
34                         "depfile = $DEPFILE"]),
35             NinjaEntry("rule _link",
36                        ["command = " ~ dCompiler ~ " $flags -of$out $in"]),
37             NinjaEntry("rule _cppcompile",
38                        ["command = " ~ cppCompiler ~ " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in",
39                         "deps = gcc",
40                         "depfile = $DEPFILE"]),
41             NinjaEntry("rule _ccompile",
42                        ["command = " ~ cCompiler ~ " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in",
43                         "deps = gcc",
44                         "depfile = $DEPFILE"]),
45         ];
46 }
47 
48 
49 struct Ninja {
50     NinjaEntry[] buildEntries;
51     NinjaEntry[] ruleEntries;
52 
53     this(Build build, in string projectPath = "") @safe {
54         _build = build;
55         _projectPath = projectPath;
56 
57         foreach(topTarget; _build.targets) {
58             foreach(target; DepthFirst(topTarget)) {
59                 auto rawCmdLine = target.rawCmdString(_projectPath);
60                 rawCmdLine.isDefaultCommand ? defaultRule(target, rawCmdLine) : customRule(target, rawCmdLine);
61             }
62         }
63     }
64 
65     const(NinjaEntry)[] allBuildEntries() @safe pure nothrow const {
66         immutable files = [buildFilePath, reggaePath].join(" ");
67         return buildEntries ~
68             NinjaEntry("build build.ninja: _rerun | " ~ files,
69                        ["pool = console"]);
70     }
71 
72     const(NinjaEntry)[] allRuleEntries() @safe pure const {
73         immutable _dflags = dflags == "" ? "" : " --dflags='" ~ dflags ~ "'";
74 
75         return ruleEntries ~ defaultRules ~
76             NinjaEntry("rule _rerun",
77                        ["command = " ~ reggaePath ~ " -b ninja" ~ _dflags ~ " " ~ projectPath,
78                         "generator = 1"]);
79     }
80 
81     string buildOutput() @safe pure nothrow const {
82         return output(allBuildEntries);
83     }
84 
85     string rulesOutput() @safe pure const {
86         return output(allRuleEntries);
87     }
88 
89 private:
90     Build _build;
91     string _projectPath;
92     int _counter = 1;
93 
94     //@trusted because of join
95     void defaultRule(in Target target, in string rawCmdLine) @trusted {
96         immutable rule = rawCmdLine.getDefaultRule;
97 
98         string[] paramLines;
99 
100         if(rule != "_link") { //i.e. one of the compile rules
101             auto params = ["includes", "flags"];
102             if(rule == "_dcompile") params ~= "stringImports";
103 
104             foreach(immutable param; params) {
105                 immutable value = rawCmdLine.getDefaultRuleParams(param, []).join(" ");
106                 paramLines ~= param ~ " = " ~ value;
107             }
108 
109             paramLines ~= "DEPFILE = " ~ target.outputs[0] ~ ".dep";
110         } else {
111             auto params = ["flags"];
112 
113             foreach(immutable param; params) {
114                 immutable value = rawCmdLine.getDefaultRuleParams(param, []).join(" ");
115                 paramLines ~= param ~ " = " ~ value;
116             }
117 
118         }
119 
120         buildEntries ~= NinjaEntry("build " ~ target.outputs[0] ~ ": " ~ rule ~ " " ~
121                                    target.dependencyFilesString(_projectPath),
122                                    paramLines);
123     }
124 
125     void customRule(in Target target, in string rawCmdLine) @safe {
126         immutable implicitInput =  () @trusted { return !rawCmdLine.canFind("$in");  }();
127         immutable implicitOutput = () @trusted { return !rawCmdLine.canFind("$out"); }();
128 
129         if(implicitOutput) {
130             implicitOutputRule(target, rawCmdLine);
131         } else if(implicitInput) {
132             implicitInputRule(target, rawCmdLine);
133         } else {
134             explicitInOutRule(target, rawCmdLine);
135         }
136     }
137 
138     void explicitInOutRule(in Target target, in string rawCmdLine, in string implicitInput = "") @safe {
139         import std.regex;
140         auto reg = regex(`^[^ ]+ +(.*?)(\$in|\$out)(.*?)(\$in|\$out)(.*?)$`);
141 
142         auto mat = rawCmdLine.match(reg);
143         enforce(!mat.captures.empty, text("Could not find both $in and $out.\nCommand: ",
144                                           rawCmdLine, "\nCaptures: ", mat.captures));
145         immutable before  = mat.captures[1].strip;
146         immutable first   = mat.captures[2];
147         immutable between = mat.captures[3].strip;
148         immutable last    = mat.captures[4];
149         immutable after   = mat.captures[5].strip;
150 
151         immutable ruleCmdLine = getRuleCommandLine(target, rawCmdLine, before, first, between, last, after);
152         bool haveToAdd;
153         immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd);
154 
155         immutable deps = implicitInput.empty
156             ? target.dependencyFilesString(_projectPath)
157             : implicitInput;
158 
159         auto buildLine = "build " ~ target.outputs.join(" ") ~ ": " ~ ruleName ~
160             " " ~ deps;
161         if(!target.implicits.empty) buildLine ~= " | " ~  target.implicitFilesString(_projectPath);
162 
163         string[] buildParamLines;
164         if(!before.empty)  buildParamLines ~= "before = "  ~ before;
165         if(!between.empty) buildParamLines ~= "between = " ~ between;
166         if(!after.empty)   buildParamLines ~= "after = "   ~ after;
167 
168         buildEntries ~= NinjaEntry(buildLine, buildParamLines);
169 
170         if(haveToAdd) {
171             ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]);
172         }
173     }
174 
175     void implicitOutputRule(in Target target, in string rawCmdLine) @safe nothrow {
176         bool haveToAdd;
177         immutable ruleCmdLine = getRuleCommandLine(target, rawCmdLine, "" /*before*/, "$in");
178         immutable ruleName = getRuleName(targetCommand(target), ruleCmdLine, haveToAdd);
179 
180         immutable buildLine = "build " ~ target.outputs.join(" ") ~ ": " ~ ruleName ~
181             " " ~ target.dependencyFilesString(_projectPath);
182         buildEntries ~= NinjaEntry(buildLine);
183 
184         if(haveToAdd) {
185             ruleEntries ~= NinjaEntry("rule " ~ ruleName, [ruleCmdLine]);
186         }
187     }
188 
189     void implicitInputRule(in Target target, in string rawCmdLine) @safe {
190         string input;
191 
192         immutable cmdLine = () @trusted {
193             string line = rawCmdLine;
194             auto allDeps = (target.dependencyFilesString(_projectPath) ~ " " ~
195                             target.implicitFilesString(_projectPath)).splitter(" ");
196             foreach(string dep; allDeps) {
197                 if(line.canFind(dep)) {
198                     line = line.replace(dep, "$in");
199                     input = dep;
200                 }
201             }
202             return line;
203         }();
204 
205         explicitInOutRule(target, cmdLine, input);
206     }
207 
208     //@trusted because of canFind
209     string getRuleCommandLine(in Target target, in string rawCmdLine,
210                               in string before = "", in string first = "",
211                               in string between = "",
212                               in string last = "", in string after = "") @trusted pure nothrow const {
213 
214         auto cmdLine = "command = " ~ targetRawCommand(target);
215         if(!before.empty) cmdLine ~= " $before";
216         cmdLine ~= rawCmdLine.canFind(" " ~ first) ? " " ~ first : first;
217         if(!between.empty) cmdLine ~= " $between";
218         cmdLine ~= rawCmdLine.canFind(" " ~ last) ? " " ~ last : last;
219         if(!after.empty) cmdLine ~= " $after";
220         return cmdLine;
221     }
222 
223     //Ninja operates on rules, not commands. Since this is supposed to work with
224     //generic build systems, the same command can appear with different parameter
225     //ordering. The first time we create a rule with the same name as the command.
226     //The subsequent times, if any, we append a number to the command to create
227     //a new rule
228     string getRuleName(in string cmd, in string ruleCmdLine, out bool haveToAdd) @safe nothrow {
229         immutable ruleMainLine = "rule " ~ cmd;
230         //don't have a rule for this cmd yet, return just the cmd
231         if(!ruleEntries.canFind!(a => a.mainLine == ruleMainLine)) {
232             haveToAdd = true;
233             return cmd;
234         }
235 
236         //so we have a rule for this already. Need to check if the command line
237         //is the same
238 
239         //same cmd: either matches exactly or is cmd_{number}
240         auto isSameCmd = (in NinjaEntry entry) {
241             bool sameMainLine = entry.mainLine.startsWith(ruleMainLine) &&
242             (entry.mainLine == ruleMainLine || entry.mainLine[ruleMainLine.length] == '_');
243             bool sameCmdLine = entry.paramLines == [ruleCmdLine];
244             return sameMainLine && sameCmdLine;
245         };
246 
247         auto rulesWithSameCmd = ruleEntries.filter!isSameCmd;
248         assert(rulesWithSameCmd.empty || rulesWithSameCmd.array.length == 1);
249 
250         //found a sule with the same cmd and paramLines
251         if(!rulesWithSameCmd.empty)
252             return () @trusted { return rulesWithSameCmd.front.mainLine.replace("rule ", ""); }();
253 
254         //if we got here then it's the first time we see "cmd" with a new
255         //ruleCmdLine, so we add it
256         haveToAdd = true;
257         import std.conv: to;
258         return cmd ~ "_" ~ (++_counter).to!string;
259     }
260 
261     string output(const(NinjaEntry)[] entries) @safe pure const nothrow {
262         return entries.map!(a => a.toString).join("\n\n");
263     }
264 }
265 
266 //@trusted because of splitter
267 private string targetCommand(in Target target) @trusted pure nothrow {
268     return targetRawCommand(target).sanitizeCmd;
269 }
270 
271 //@trusted because of splitter
272 private string targetRawCommand(in Target target) @trusted pure nothrow {
273     return target.command.splitter(" ").front;
274 }
275 
276 //ninja doesn't like symbols in rule names
277 //@trusted because of replace
278 private string sanitizeCmd(in string cmd) @trusted pure nothrow {
279     import std.path;
280     //only handles c++ compilers so far...
281     return cmd.baseName.replace("+", "p");
282 }