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