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