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 }