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 }