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