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