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 }