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 }