1 module reggae.backend.binary;
2 
3 
4 import reggae.build;
5 import reggae.range;
6 import reggae.options;
7 import reggae.dependencies;
8 import std.algorithm;
9 import std.range;
10 import std.file: timeLastModified, 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 
22 @safe:
23 
24 struct BinaryOptions {
25     bool list;
26     private bool _earlyReturn;
27     string[] args;
28 
29     this(string[] args) @trusted {
30         auto optInfo = getopt(
31             args,
32             "list|l", "List available build targets", &list,
33             );
34         if(optInfo.helpWanted) {
35             defaultGetoptPrinter("Usage: build <targets>", optInfo.options);
36             _earlyReturn = true;
37         }
38         if(list) {
39             _earlyReturn = true;
40         }
41 
42         this.args = args[1..$];
43     }
44 
45     bool earlyReturn() const pure nothrow {
46         return _earlyReturn;
47     }
48 }
49 
50 struct Binary {
51     Build build;
52     const(Options) options;
53 
54     this(Build build, in string projectPath) pure {
55         import reggae.config: options;
56         this(build, options);
57     }
58 
59     this(Build build, in Options options) pure {
60         this.build = build;
61         this.options = options;
62     }
63 
64     void run(string[] args) const @system { //@system due to parallel
65         auto binaryOptions = BinaryOptions(args);
66 
67         handleOptions(binaryOptions);
68         if(binaryOptions.earlyReturn) return;
69 
70         bool didAnything = checkReRun();
71 
72         const topTargets = topLevelTargets(binaryOptions.args);
73         if(topTargets.empty)
74             throw new Exception(text("Unknown target(s) ", binaryOptions.args.map!(a => "'" ~ a ~ "'").join(" ")));
75 
76         foreach(topTarget; topTargets) {
77 
78             immutable didPhony = checkChildlessPhony(topTarget);
79             didAnything = didPhony || didAnything;
80             if(didPhony) continue;
81 
82             foreach(level; ByDepthLevel(topTarget)) {
83                 foreach(target; level.parallel) {
84                     const outs = target.outputsInProjectPath(options.projectPath);
85                     immutable depFileName = outs[0] ~ ".dep";
86                     if(depFileName.exists) {
87                         didAnything = checkDeps(target, depFileName) || didAnything;
88                     }
89 
90                     didAnything = checkTimestamps(target) || didAnything;
91                 }
92             }
93         }
94         if(!didAnything) writeln("[build] Nothing to do");
95     }
96 
97     const(Target)[] topLevelTargets(in string[] args) @trusted const pure {
98         return args.empty ?
99             build.defaultTargets.array :
100             build.targets.filter!(a => args.canFind(a.expandOutputs(options.projectPath))).array;
101     }
102 
103     string[] listTargets(BinaryOptions binaryOptions) pure const {
104         string[] result;
105 
106         const defaultTargets = topLevelTargets(binaryOptions.args);
107         foreach(topTarget; defaultTargets)
108             result ~= "- " ~ topTarget.expandOutputs(options.projectPath).join(" ");
109 
110         auto optionalTargets = build.targets.filter!(a => !defaultTargets.canFind(a));
111         foreach(optionalTarget; optionalTargets)
112             result ~= "- " ~ optionalTarget.outputs.map!(a => a.replace("$builddir/", "")).join(" ") ~
113                 " (optional)";
114 
115         return result;
116     }
117 
118 
119 private:
120 
121     void handleOptions(BinaryOptions binaryOptions) const {
122         if(binaryOptions.list) {
123             writeln("List of available top-level targets:");
124             foreach(l; listTargets(binaryOptions)) writeln(l);
125         }
126     }
127 
128     bool checkReRun() const {
129         immutable myPath = thisExePath;
130         if(options.ranFromPath.newerThan(myPath) || options.reggaeFilePath.newerThan(myPath)) {
131             writeln("[build] " ~ options.rerunArgs.join(" "));
132             immutable reggaeRes = execute(options.rerunArgs);
133             enforce(reggaeRes.status == 0,
134                     text("Could not run ", options.rerunArgs.join(" "), " to regenerate build:\n",
135                          reggaeRes.output));
136             writeln(reggaeRes.output);
137 
138             //currently not needed because generating the build also runs it.
139             immutable buildRes = execute([myPath]);
140             enforce(buildRes.status == 0, "Could not redo the build:\n", buildRes.output);
141             writeln(buildRes.output);
142             return true;
143         }
144 
145         return false;
146     }
147 
148     bool checkTimestamps(in Target target) const {
149         foreach(dep; chain(target.dependencies, target.implicits)) {
150 
151             immutable isPhony = target.getCommandType == CommandType.phony;
152             immutable anyNewer = cartesianProduct(dep.outputsInProjectPath(options.projectPath),
153                                                   target.outputsInProjectPath(options.projectPath)).
154                 any!(a => a[0].newerThan(a[1]));
155 
156             if(isPhony || anyNewer) {
157                 executeCommand(target);
158                 return true;
159             }
160         }
161 
162         return false;
163     }
164 
165     //always run phony rules with no dependencies at top-level
166     //ByDepthLevel won't include them
167     bool checkChildlessPhony(in Target target) const {
168         if(target.getCommandType == CommandType.phony &&
169            target.dependencies.empty && target.implicits.empty) {
170             executeCommand(target);
171             return true;
172         }
173         return false;
174     }
175 
176     //Checks dependencies listed in the .dep file created by the compiler
177     bool checkDeps(in Target target, in string depFileName) const @trusted {
178         auto file = File(depFileName);
179         auto dependencies = file.byLine.map!(a => a.to!string).dependenciesFromFile;
180 
181         if(dependencies.any!(a => a.newerThan(target.outputsInProjectPath(options.projectPath)[0]))) {
182             executeCommand(target);
183             return true;
184         }
185 
186         return false;
187     }
188 
189     void executeCommand(in Target target) const @trusted {
190         mkDir(target);
191         const output = target.execute(options.projectPath);
192         writeln("[build] " ~ output[0]);
193         if(target.getCommandType == CommandType.phony)
194             writeln("\n", output[1]);
195     }
196 
197     //@trusted because of mkdirRecurse
198     private void mkDir(in Target target) @trusted const {
199         foreach(output; target.outputsInProjectPath(options.projectPath)) {
200             import std.file: exists, mkdirRecurse;
201             import std.path: dirName;
202             if(!output.dirName.exists) mkdirRecurse(output.dirName);
203         }
204     }
205 }
206 
207 bool newerThan(in string a, in string b) nothrow {
208     try {
209         return a.timeLastModified > b.timeLastModified;
210     } catch(Exception) { //file not there, so newer
211         return true;
212     }
213 }