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 }