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