1 module reggae.backend.binary;
2 
3 
4 import reggae.build;
5 import reggae.range;
6 import reggae.options;
7 import reggae.file;
8 import std.algorithm;
9 import std.range;
10 import std.file: thisExePath, exists;
11 import std.process: execute, executeShell;
12 import std.path: absolutePath;
13 import std.typecons: tuple;
14 import std.exception;
15 import std.stdio;
16 import std.parallelism: parallel;
17 import std.conv;
18 import std.array: replace, empty;
19 import std.string: strip;
20 import std.getopt;
21 import std.range.primitives: isInputRange;
22 
23 
24 struct BinaryOptions {
25     bool list;
26     bool norerun;
27     bool singleThreaded;
28     private bool _earlyReturn;
29     string[] args;
30 
31     this(string[] args) @trusted {
32         auto optInfo = getopt(
33             args,
34             "list|l", "List available build targets", &list,
35             "norerun|n", "Don't check for rerun", &norerun,
36             "single|s", "Use only one thread", &singleThreaded,
37             );
38         if(optInfo.helpWanted) {
39             defaultGetoptPrinter("Usage: build <targets>", optInfo.options);
40             _earlyReturn = true;
41         }
42         if(list) {
43             _earlyReturn = true;
44         }
45 
46         this.args = args[1..$];
47     }
48 
49     bool earlyReturn() @safe const pure nothrow {
50         return _earlyReturn;
51     }
52 }
53 
54 auto Binary(Build build, in Options options) @system {
55     version(unittest) {
56         import tests.utils;
57         auto file = new FakeFile;
58         return Binary(build, options, *file);
59     }
60     else
61         return Binary(build, options, stdout);
62 }
63 
64 auto Binary(T)(Build build, in Options options, ref T output) {
65     return BinaryT!(T)(build, options, output);
66 }
67 
68 
69 struct BinaryT(T) {
70     Build build;
71     const(Options) options;
72     T* output;
73 
74     this(Build build, in Options options, ref T output) @trusted {
75         version(unittest) {
76             static if(is(T == File)) {
77                 assert(&output != &stdout,
78                        "stdio not allowed for Binary output in testing, " ~
79                        "use tests.utils.FakeFile instead");
80             }
81         }
82 
83         this.build = build;
84         this.options = options;
85         this.output = &output;
86     }
87 
88     void run(string[] args) @system { //@system due to parallel
89         auto binaryOptions = BinaryOptions(args);
90 
91         handleOptions(binaryOptions);
92         if(binaryOptions.earlyReturn) return;
93 
94         bool didAnything = binaryOptions.norerun ? false : checkReRun();
95 
96         auto topTargets = topLevelTargets(binaryOptions.args);
97         if(topTargets.empty)
98             throw new Exception(text("Unknown target(s) ", binaryOptions.args.map!(a => "'" ~ a ~ "'").join(" ")));
99 
100         if(binaryOptions.singleThreaded)
101             didAnything = mainLoop(topTargets, binaryOptions, didAnything);
102         else
103             didAnything = mainLoop(topTargets.parallel, binaryOptions, didAnything);
104 
105 
106         if(!didAnything) output.writeln("[build] Nothing to do");
107     }
108 
109     Target[] topLevelTargets(string[] args) @trusted pure {
110         return args.empty ?
111             build.defaultTargets.array :
112             build.targets.filter!(a => args.canFind(a.expandOutputs(options.projectPath))).array;
113     }
114 
115     string[] listTargets(BinaryOptions binaryOptions) @safe pure {
116 
117         string targetOutputsString(in Target target) {
118             return "- " ~ target.expandOutputs(options.projectPath).join(" ");
119         }
120 
121         const defaultTargets = topLevelTargets(binaryOptions.args);
122         auto optionalTargets = build.targets.filter!(a => !defaultTargets.canFind(a));
123         return chain(defaultTargets.map!targetOutputsString,
124                      optionalTargets.map!targetOutputsString.map!(a => a ~ " (optional)")).array;
125     }
126 
127 
128 private:
129 
130     bool mainLoop(R)(R topTargets_, in BinaryOptions binaryOptions, bool didAnything) @system {
131         foreach(topTarget; topTargets_) {
132 
133             immutable didPhony = checkChildlessPhony(topTarget);
134             didAnything = didPhony || didAnything;
135             if(didPhony) continue;
136 
137             foreach(level; ByDepthLevel(topTarget)) {
138                 if(binaryOptions.singleThreaded)
139                     foreach(target; level)
140                         handleTarget(target, didAnything);
141                 else
142                     foreach(target; level.parallel)
143                         handleTarget(target, didAnything);
144             }
145         }
146         return didAnything;
147     }
148 
149     void handleTarget(Target target, ref bool didAnything) @safe {
150         const outs = target.expandOutputs(options.projectPath);
151         immutable depFileName = outs[0] ~ ".dep";
152         if(depFileName.exists) {
153             didAnything = checkDeps(target, depFileName) || didAnything;
154         }
155 
156         didAnything = checkTimestamps(target) || didAnything;
157     }
158 
159     void handleOptions(BinaryOptions binaryOptions) @safe {
160         if(binaryOptions.list) {
161             output.writeln("List of available top-level targets:");
162             foreach(l; listTargets(binaryOptions)) output.writeln(l);
163         }
164     }
165 
166     bool checkReRun() @safe {
167         // don't bother if the build system was exported
168         if(options.export_) return false;
169 
170         immutable myPath = thisExePath;
171         if(options.reggaeFileDependencies.any!(a => a.newerThan(myPath))) {
172             output.writeln("[build] " ~ options.rerunArgs.join(" "));
173             immutable reggaeRes = execute(options.rerunArgs);
174             enforce(reggaeRes.status == 0,
175                     text("Could not run ", options.rerunArgs.join(" "), " to regenerate build:\n",
176                          reggaeRes.output));
177             output.writeln(reggaeRes.output);
178 
179             //currently not needed because generating the build also runs it.
180             immutable buildRes = execute([myPath]);
181             enforce(buildRes.status == 0, "Could not redo the build:\n", buildRes.output);
182             output.writeln(buildRes.output);
183             return true;
184         }
185 
186         return false;
187     }
188 
189     bool checkTimestamps(Target target) @safe {
190         auto allDeps = chain(target.dependencyTargets, target.implicitTargets);
191         immutable isPhonyLike = target.getCommandType == CommandType.phony ||
192             allDeps.empty;
193 
194         if(isPhonyLike) {
195             executeCommand(target);
196             return true;
197         }
198 
199         foreach(dep; allDeps) {
200             if(anyNewer(options.projectPath,
201                         dep.expandOutputs(options.projectPath),
202                         target)) {
203                 executeCommand(target);
204                 return true;
205             }
206         }
207 
208         return false;
209     }
210 
211     //always run phony rules with no dependencies at top-level
212     //ByDepthLevel won't include them
213     bool checkChildlessPhony(Target target) @safe {
214         if(target.getCommandType == CommandType.phony &&
215            target.dependencyTargets.empty && target.implicitTargets.empty) {
216             executeCommand(target);
217             return true;
218         }
219         return false;
220     }
221 
222     //Checks dependencies listed in the .dep file created by the compiler
223     bool checkDeps(Target target, in string depFileName) @trusted {
224 
225         auto file = File(depFileName);
226         auto lines = file.byLine.map!(a => a.to!string);
227         auto dependencies = dependenciesFromFile(lines);
228 
229         if(anyNewer(options.projectPath, dependencies, target)) {
230             executeCommand(target);
231             return true;
232         }
233 
234         return false;
235     }
236 
237     void executeCommand(Target target) @trusted {
238         output.writeln("[build] ", target.shellCommand(options));
239 
240         mkDir(target);
241         auto targetOutput = target.execute(options);
242 
243         if(target.getCommandType == CommandType.phony && targetOutput.length > 0)
244             output.writeln("\n", targetOutput[0]);
245     }
246 
247     //@trusted because of mkdirRecurse
248     private void mkDir(Target target) @trusted const {
249         foreach(output; target.expandOutputs(options.projectPath)) {
250             import std.file: exists, mkdirRecurse;
251             import std.path: dirName;
252             if(!output.dirName.exists) mkdirRecurse(output.dirName);
253         }
254     }
255 }
256 
257 
258 
259 bool anyNewer(in string projectPath, in string[] dependencies, in Target target) @safe {
260     return cartesianProduct(dependencies, target.expandOutputs(projectPath)).
261         any!(a => a[0].newerThan(a[1]));
262 }
263 
264 
265 
266 
267 string[] dependenciesFromFile(R)(R lines) if(isInputRange!R) {
268     import std.algorithm: map, filter, find;
269     import std.string: strip;
270     import std.array: empty, join, array, replace, split;
271 
272     if(lines.empty) return [];
273     return lines
274         .map!(a => a.replace(`\`, ``).strip)
275         .join(" ")
276         .find(":")
277         .split(" ")
278         .filter!(a => a != "")
279         .array[1..$];
280 }