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