1 /** 2 The main entry point for the reggae tool. Its tasks are: 3 $(UL 4 $(LI Verify that a $(D reggafile.d) exists in the selected directory) 5 $(LI Generate a $(D reggaefile.d) for dub projects) 6 $(LI Write out the reggae library files and $(D config.d)) 7 $(LI Compile the build description with the reggae library files to produce $(D buildgen)) 8 $(LI Produce $(D dcompile), a binary to call the D compiler to obtain dependencies during compilation) 9 $(LI Call the produced $(D buildgen) binary) 10 ) 11 */ 12 13 14 module reggae.reggae; 15 16 import std.stdio; 17 import std.process: execute, environment; 18 import std.array: array, join, empty, split; 19 import std.path: absolutePath, buildPath, relativePath; 20 import std.typetuple; 21 import std.file; 22 import std.conv: text; 23 import std.exception: enforce; 24 import std.conv: to; 25 import std.algorithm; 26 27 import reggae.options; 28 import reggae.ctaa; 29 import reggae.types; 30 import reggae.file; 31 32 33 version(minimal) { 34 //empty stubs for minimal version of reggae 35 void maybeCreateReggaefile(T...)(T) {} 36 void writeDubConfig(T...)(T) {} 37 } else { 38 import reggae.dub.interop; 39 } 40 41 mixin template reggaeGen(targets...) { 42 mixin buildImpl!targets; 43 mixin ReggaeMain; 44 } 45 46 mixin template ReggaeMain() { 47 import reggae.options: getOptions; 48 import std.stdio: stdout, stderr; 49 50 int main(string[] args) { 51 try { 52 run(stdout, args); 53 } catch(Exception ex) { 54 stderr.writeln(ex.msg); 55 return 1; 56 } 57 58 return 0; 59 } 60 } 61 62 void run(T)(T output, string[] args) { 63 auto options = getOptions(args); 64 run(output, options); 65 } 66 67 void run(T)(T output, Options options) { 68 if(options.earlyExit) return; 69 enforce(options.projectPath != "", "A project path must be specified"); 70 71 // write out the library source files to be compiled/interpreted 72 // with the user's build description 73 writeSrcFiles(output, options); 74 75 if(options.isJsonBuild) { 76 immutable haveToReturn = jsonBuild(options); 77 if(haveToReturn) return; 78 } 79 80 maybeCreateReggaefile(options); 81 createBuild(output, options); 82 } 83 84 //get JSON description of the build from a scripting language 85 //and transform it into a build description 86 //return true if no D files are present 87 bool jsonBuild(Options options) { 88 immutable jsonOutput = getJsonOutput(options); 89 return jsonBuild(options, jsonOutput); 90 } 91 92 //transform JSON description into a Build struct 93 //return true if no D files are present 94 bool jsonBuild(Options options, in string jsonOutput) { 95 enforce(options.backend != Backend.binary, "Binary backend not supported via JSON"); 96 97 version(minimal) 98 assert(0, "JSON builds not supported in minimal version"); 99 else { 100 import reggae.json_build; 101 import reggae.buildgen; 102 import reggae.rules.common: Language; 103 104 auto build = jsonToBuild(options.projectPath, jsonOutput); 105 doBuild(build, jsonToOptions(options, jsonOutput)); 106 107 import reggae.buildgen:writeCompilationDB; 108 if(!options.noCompilationDB) writeCompilationDB(build, options); 109 110 //true -> exit early 111 return !build.targets.canFind!(a => a.getLanguage == Language.D); 112 } 113 } 114 115 116 private string getJsonOutput(in Options options) @safe { 117 const args = getJsonOutputArgs(options); 118 const path = environment.get("PATH", "").split(":"); 119 const pythonPaths = environment.get("PYTHONPATH", "").split(":"); 120 const nodePaths = environment.get("NODE_PATH", "").split(":"); 121 const luaPaths = environment.get("LUA_PATH", "").split(";"); 122 const srcDir = buildPath(options.workingDir, hiddenDir, "src"); 123 const binDir = buildPath(srcDir, "reggae"); 124 auto env = ["PATH": (path ~ binDir).join(":"), 125 "PYTHONPATH": (pythonPaths ~ srcDir).join(":"), 126 "NODE_PATH": (nodePaths ~ options.projectPath ~ binDir).join(":"), 127 "LUA_PATH": (luaPaths ~ buildPath(options.projectPath, "?.lua") ~ buildPath(binDir, "?.lua")).join(";")]; 128 immutable res = execute(args, env); 129 enforce(res.status == 0, text("Could not execute ", args.join(" "), ":\n", res.output)); 130 return res.output; 131 } 132 133 private string[] getJsonOutputArgs(in Options options) @safe { 134 final switch(options.reggaeFileLanguage) { 135 136 case BuildLanguage.D: 137 assert(0, "Cannot obtain JSON build for builds written in D"); 138 139 case BuildLanguage.Python: 140 141 auto optionsString = () @trusted { 142 import std.json; 143 import std.traits; 144 JSONValue jsonVal = parseJSON(`{}`); 145 foreach(member; __traits(allMembers, typeof(options))) { 146 static if(is(typeof(mixin(`options.` ~ member)) == const(Backend)) || 147 is(typeof(mixin(`options.` ~ member)) == const(string)) || 148 is(typeof(mixin(`options.` ~ member)) == const(bool)) || 149 is(typeof(mixin(`options.` ~ member)) == const(string[string])) || 150 is(typeof(mixin(`options.` ~ member)) == const(string[]))) 151 jsonVal.object[member] = mixin(`options.` ~ member); 152 } 153 return jsonVal.toString; 154 }(); 155 156 return ["/usr/bin/env", "python", "-m", "reggae.reggae_json_build", 157 "--options", optionsString, 158 options.projectPath]; 159 160 case BuildLanguage.Ruby: 161 return ["ruby", "-S", 162 "-I" ~ options.projectPath, 163 "-I" ~ buildPath(options.workingDir, hiddenDir, "src", "reggae"), 164 "reggae_json_build.rb"]; 165 166 case BuildLanguage.Lua: 167 return ["lua", buildPath(options.workingDir, hiddenDir, "src", "reggae", "reggae_json_build.lua")]; 168 169 case BuildLanguage.JavaScript: 170 return ["node", buildPath(options.workingDir, hiddenDir, "src", "reggae", "reggae_json_build.js")]; 171 } 172 } 173 174 enum coreFiles = [ 175 "options.d", 176 "buildgen_main.d", "buildgen.d", 177 "build.d", 178 "backend/package.d", "backend/binary.d", 179 "package.d", "range.d", "reflect.d", 180 "dependencies.d", "types.d", "dcompile.d", 181 "ctaa.d", "sorting.d", "file.d", 182 "rules/package.d", 183 "rules/common.d", 184 "rules/d.d", 185 "rules/c_and_cpp.d", 186 "core/package.d", "core/rules/package.d", 187 ]; 188 enum otherFiles = [ 189 "backend/ninja.d", "backend/make.d", "backend/tup.d", 190 "dub/info.d", "rules/dub.d", 191 ]; 192 193 version(minimal) { 194 enum string[] foreignFiles = []; 195 } else { 196 enum foreignFiles = [ 197 "__init__.py", "build.py", "reflect.py", "rules.py", "reggae_json_build.py", 198 "reggae.rb", "reggae_json_build.rb", 199 "reggae-js.js", "reggae_json_build.js", 200 "JSON.lua", "reggae.lua", "reggae_json_build.lua", 201 ]; 202 } 203 204 //all files that need to be written out and compiled 205 private string[] fileNames() @safe pure nothrow { 206 version(minimal) 207 return coreFiles; 208 else 209 return coreFiles ~ otherFiles; 210 } 211 212 213 private void createBuild(T)(T output, in Options options) { 214 215 enforce(options.reggaeFilePath.exists, text("Could not find ", options.reggaeFilePath)); 216 217 //compile the binaries (the build generator and dcompile) 218 immutable buildGenName = compileBinaries(output, options); 219 220 //binary backend has no build generator, it _is_ the build 221 if(options.backend == Backend.binary) return; 222 223 //only got here to build .dcompile 224 if(options.isScriptBuild) return; 225 226 //actually run the build generator 227 output.writeln("[Reggae] Running the created binary to generate the build"); 228 immutable retRunBuildgen = execute([buildPath(options.workingDir, hiddenDir, buildGenName)]); 229 enforce(retRunBuildgen.status == 0, 230 text("Couldn't execute the produced ", buildGenName, " binary:\n", retRunBuildgen.output)); 231 232 output.writeln(retRunBuildgen.output); 233 } 234 235 236 struct Binary { 237 string name; 238 const(string)[] cmd; 239 } 240 241 242 private string compileBinaries(T)(T output, in Options options) { 243 buildDCompile(output, options); 244 245 immutable buildGenName = getBuildGenName(options); 246 if(options.isScriptBuild) return buildGenName; 247 248 const buildGenCmd = getCompileBuildGenCmd(options); 249 immutable buildObjName = "build.o"; 250 buildBinary(output, options, Binary(buildObjName, buildGenCmd)); 251 252 const reggaeFileDeps = getReggaeFileDependenciesDlang; 253 auto objFiles = [buildObjName]; 254 if(!reggaeFileDeps.empty) { 255 immutable rest = "rest.o"; 256 buildBinary(output, 257 options, 258 Binary(rest, 259 [options.dCompiler, 260 "-c", 261 "-of" ~ "rest.o"] ~ 262 importPaths(options) ~ 263 reggaeFileDeps)); 264 objFiles ~= rest; 265 } 266 buildBinary(output, options, Binary(buildGenName, [options.dCompiler, "-of" ~ buildGenName] ~ objFiles)); 267 268 return buildGenName; 269 } 270 271 void buildDCompile(T)(T output, in Options options) { 272 if(!thisExePath.newerThan(buildPath(options.workingDir, hiddenDir, "dcompile"))) 273 return; 274 275 immutable cmd = [options.dCompiler, 276 "-Isrc", 277 "-ofdcompile", 278 buildPath(options.workingDir, hiddenDir, reggaeSrcRelDirName, "dcompile.d"), 279 buildPath(options.workingDir, hiddenDir, reggaeSrcRelDirName, "dependencies.d")]; 280 281 buildBinary(output, options, Binary("dcompile", cmd)); 282 } 283 284 private bool isExecutable(in char[] path) @trusted nothrow @nogc //TODO: @safe 285 { 286 import core.sys.posix.unistd; 287 import std.internal.cstring; 288 return (access(path.tempCString(), X_OK) == 0); 289 } 290 291 private void buildBinary(T)(T output, in Options options, in Binary bin) { 292 import std.process; 293 string[string] env; 294 auto config = Config.none; 295 auto maxOutput = size_t.max; 296 auto workDir = buildPath(options.workingDir, hiddenDir); 297 std.stdio.write("[Reggae] Compiling metabuild binary ", bin.name); 298 if(options.verbose) std.stdio.write(" with ", bin.cmd.join(" ")); 299 output.writeln; 300 // std.process.execute has a bug where using workDir and a relative path 301 // don't work (https://issues.dlang.org/show_bug.cgi?id=15915) 302 // so executeShell is used instead 303 immutable res = executeShell(bin.cmd.join(" "), env, config, maxOutput, workDir); 304 enforce(res.status == 0, text("Couldn't execute ", bin.cmd.join(" "), "\nin ", workDir, 305 ":\n", res.output, 306 "\n", "bin.name: ", bin.name, ", bin.cmd: ", bin.cmd.join(" "))); 307 308 } 309 310 311 private const(string)[] getCompileBuildGenCmd(in Options options) @safe { 312 import reggae.rules.common: objExt; 313 314 const reggaeSrcs = ("config.d" ~ fileNames). 315 filter!(a => a != "dcompile.d"). 316 map!(a => buildPath(reggaeSrcRelDirName, a)).array; 317 318 immutable buildBinFlags = options.backend == Backend.binary 319 ? ["-O", "-inline"] 320 : []; 321 const commonBefore = ["./dcompile", 322 "--objFile=" ~ "build.o", 323 "--depFile=" ~ "reggaefile.dep", 324 options.dCompiler] ~ 325 importPaths(options) ~ 326 ["-g", 327 "-debug"]; 328 const commonAfter = buildBinFlags ~ 329 options.reggaeFilePath ~ reggaeSrcs; 330 version(minimal) return commonBefore ~ "-version=minimal" ~ commonAfter; 331 else return commonBefore ~ commonAfter; 332 } 333 334 private string[] importPaths(in Options options) @safe nothrow { 335 import std.file; 336 337 immutable srcDir = "-I" ~ buildPath("src"); 338 // if compiling phobos, the includes for the reggaefile.d compilation 339 // will pick up the new phobos if we include the src path 340 return "std".exists ? [srcDir] : ["-I" ~ options.projectPath, srcDir]; 341 } 342 343 private string getBuildGenName(in Options options) @safe pure nothrow { 344 return options.backend == Backend.binary ? buildPath("..", "build") : "buildgen"; 345 } 346 347 348 immutable reggaeSrcRelDirName = buildPath("src", "reggae"); 349 350 string reggaeSrcDirName(in Options options) @safe pure nothrow { 351 return buildPath(options.workingDir, hiddenDir, reggaeSrcRelDirName); 352 } 353 354 355 void writeSrcFiles(T)(T output, in Options options) { 356 output.writeln("[Reggae] Writing reggae source files"); 357 358 import std.file: mkdirRecurse; 359 immutable reggaeSrcDirName = reggaeSrcDirName(options); 360 if(!reggaeSrcDirName.exists) { 361 mkdirRecurse(reggaeSrcDirName); 362 mkdirRecurse(buildPath(reggaeSrcDirName, "dub")); 363 mkdirRecurse(buildPath(reggaeSrcDirName, "rules")); 364 mkdirRecurse(buildPath(reggaeSrcDirName, "backend")); 365 mkdirRecurse(buildPath(reggaeSrcDirName, "core", "rules")); 366 } 367 368 //this foreach has to happen at compile time due 369 //to the string import below. 370 foreach(fileName; aliasSeqOf!(fileNames ~ foreignFiles)) { 371 auto file = File(reggaeSrcFileName(options, fileName), "w"); 372 file.write(import(fileName)); 373 } 374 375 output.writeln("[Reggae] Writing reggae configuration"); 376 writeConfig(options); 377 } 378 379 380 private void writeConfig(in Options options) { 381 auto file = File(reggaeSrcFileName(options, "config.d"), "w"); 382 383 file.writeln(q{ 384 module reggae.config; 385 import reggae.ctaa; 386 import reggae.types; 387 import reggae.options; 388 }); 389 390 version(minimal) file.writeln("enum isDubProject = false;"); 391 file.writeln("immutable options = ", options, ";"); 392 393 file.writeln("enum userVars = AssocList!(string, string)(["); 394 foreach(key, value; options.userVars) { 395 file.writeln("assocEntry(`", key, "`, `", value, "`), "); 396 } 397 file.writeln("]);"); 398 399 try { 400 writeDubConfig(options, file); 401 } catch(Exception ex) { 402 stderr.writeln("Could not write dub configuration, try 'dub upgrade': ", ex.msg); 403 throw ex; 404 } 405 } 406 407 408 private string reggaeSrcFileName(in Options options, in string fileName) @safe pure nothrow { 409 return buildPath(reggaeSrcDirName(options), fileName); 410 }