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 
31 
32 version(minimal) {
33     //empty stubs for minimal version of reggae
34     void maybeCreateReggaefile(T...)(T) {}
35     void writeDubConfig(T...)(T) {}
36 } else {
37     import reggae.dub.interop;
38 }
39 
40 mixin template reggaeGen(targets...) {
41     mixin buildImpl!targets;
42     mixin ReggaeMain;
43 }
44 
45 mixin template ReggaeMain() {
46     import reggae.options: getOptions;
47     import std.stdio: stderr;
48 
49     int main(string[] args) {
50         try {
51             run(getOptions(args));
52         } catch(Exception ex) {
53             stderr.writeln(ex.msg);
54             return 1;
55         }
56 
57         return 0;
58     }
59 }
60 
61 void run(in Options options) {
62     if(options.help) return;
63     enforce(options.projectPath != "", "A project path must be specified");
64 
65     if(options.reggaeFileLanguage != BuildLanguage.D) {
66         immutable haveToReturn = jsonBuild(options, options.reggaeFileLanguage);
67         if(haveToReturn) return;
68     }
69 
70     maybeCreateReggaefile(options);
71     createBuild(options);
72 }
73 
74 //get JSON description of the build from a scripting language
75 //return true if no D files are present
76 bool jsonBuild(in Options options, in BuildLanguage language) {
77     enforce(options.backend != Backend.binary, "Binary backend not supported via JSON");
78 
79     immutable jsonOutput = getJsonOutput(options, language);
80 
81     import reggae.json_build;
82     import reggae.buildgen;
83     import reggae.rules.common: Language;
84 
85     const build = jsonToBuild(options.projectPath, jsonOutput);
86     generateBuild(build, options);
87 
88     //true -> exit early
89     return !build.targets.canFind!(a => a.getLanguage == Language.D);
90 }
91 
92 private string getJsonOutput(in Options options, in BuildLanguage language) @safe {
93     const args = getJsonOutputArgs(options, language);
94     const nodePaths = environment.get("NODE_PATH", "").split(":");
95     const luaPaths = environment.get("LUA_PATH", "").split(";");
96     auto env = ["NODE_PATH": (nodePaths ~ options.projectPath).join(":"),
97                 "LUA_PATH": (luaPaths ~ buildPath(options.projectPath, "?.lua")).join(";")];
98     immutable res = execute(args, env);
99     enforce(res.status == 0, text("Could not execute ", args.join(" "), ":\n", res.output));
100     return res.output;
101 }
102 
103 private string[] getJsonOutputArgs(in Options options, in BuildLanguage language) @safe pure nothrow {
104     final switch(language) {
105 
106     case BuildLanguage.D:
107         assert(0, "Cannot obtain JSON build for builds written in D");
108 
109     case BuildLanguage.Python:
110         return ["python", "-m", "reggae.json_build", options.projectPath];
111 
112     case BuildLanguage.Ruby:
113         return ["ruby", "-S", "-I" ~ options.projectPath, "reggae_json_build.rb"];
114 
115     case BuildLanguage.Lua:
116         return ["reggae_json_build.lua"];
117 
118     case BuildLanguage.JavaScript:
119         return ["reggae_json_build.js"];
120     }
121 }
122 
123 enum coreFiles = [
124     "options.d",
125     "buildgen_main.d", "buildgen.d",
126     "build.d",
127     "backend/package.d", "backend/binary.d",
128     "package.d", "range.d", "reflect.d",
129     "dependencies.d", "types.d", "dcompile.d",
130     "ctaa.d", "sorting.d",
131     "rules/package.d",
132     "rules/common.d",
133     "rules/d.d",
134     "rules/c_and_cpp.d",
135     "core/package.d", "core/rules/package.d",
136     ];
137 enum otherFiles = [
138     "backend/ninja.d", "backend/make.d", "backend/tup.d",
139     "dub/info.d", "rules/dub.d",
140     ];
141 
142 //all files that need to be written out and compiled
143 private string[] fileNames() @safe pure nothrow {
144     version(minimal) return coreFiles;
145     else return coreFiles ~ otherFiles;
146 }
147 
148 
149 private void createBuild(in Options options) {
150 
151     enforce(options.reggaeFilePath.exists, text("Could not find ", options.reggaeFilePath));
152 
153     //write out the library source files to be compiled with the user's
154     //build description
155     writeSrcFiles(options);
156 
157     //compile the binaries (the build generator and dcompile)
158     immutable buildGenName = compileBinaries(options);
159 
160     //binary backend has no build generator, it _is_ the build
161     if(options.backend == Backend.binary) return;
162 
163     //only got here to build .dcompile
164     if(options.isScriptBuild) return;
165 
166     //actually run the build generator
167     writeln("[Reggae] Running the created binary to generate the build");
168     immutable retRunBuildgen = execute([buildPath(".", buildGenName)]);
169     enforce(retRunBuildgen.status == 0,
170             text("Couldn't execute the produced ", buildGenName, " binary:\n", retRunBuildgen.output));
171 
172     writeln(retRunBuildgen.output);
173 }
174 
175 
176 private immutable hiddenDir = ".reggae";
177 
178 private auto compileBinaries(in Options options) {
179 
180     immutable buildGenName = getBuildGenName(options);
181     const compileBuildGenCmd = getCompileBuildGenCmd(options);
182 
183     immutable dcompileName = buildPath(hiddenDir, "dcompile");
184     immutable dcompileCmd = ["dmd",
185                              "-I.reggae/src",
186                              "-of" ~ dcompileName,
187                              reggaeSrcFileName("dcompile.d"),
188                              reggaeSrcFileName("dependencies.d")];
189 
190 
191     static struct Binary { string name; const(string)[] cmd; }
192 
193     auto binaries = options.isScriptBuild
194         ? [Binary(dcompileName, dcompileCmd)]
195         : [Binary(buildGenName, compileBuildGenCmd), Binary(dcompileName, dcompileCmd)];
196     foreach(bin; binaries) writeln("[Reggae] Compiling metabuild binary ", bin.name);
197 
198     import std.parallelism;
199 
200     foreach(bin; binaries.parallel) {
201         immutable res = execute(bin.cmd);
202         enforce(res.status == 0, text("Couldn't execute ", bin.cmd.join(" "), ":\n", res.output,
203                                       "\n", "bin.name: ", bin.name, ", bin.cmd: ", bin.cmd.join(" ")));
204     }
205 
206     return buildGenName;
207 }
208 
209 const (string[]) getCompileBuildGenCmd(in Options options) @safe {
210     const reggaeSrcs = ("config.d" ~ fileNames).
211         filter!(a => a != "dcompile.d").
212         map!(a => a.reggaeSrcFileName).array;
213 
214     immutable buildBinFlags = options.backend == Backend.binary
215         ? ["-O"]
216         : [];
217     immutable commonBefore = ["dmd",
218                               "-I" ~ options.projectPath,
219                               "-I" ~ buildPath(hiddenDir, "src"),
220                               "-g", "-debug",
221                               "-of" ~ getBuildGenName(options)];
222     const commonAfter = buildBinFlags ~
223         options.reggaeFilePath ~ reggaeSrcs;
224     version(minimal) return commonBefore ~ "-version=minimal" ~ commonAfter;
225     else return commonBefore ~ commonAfter;
226 }
227 
228 string getBuildGenName(in Options options) @safe pure nothrow {
229     return options.backend == Backend.binary ? "build" : buildPath(hiddenDir, "buildgen");
230 }
231 
232 immutable reggaeSrcDirName = buildPath(hiddenDir, "src", "reggae");
233 
234 private string filesTupleString() @safe pure nothrow {
235     return "TypeTuple!(" ~ fileNames.map!(a => `"` ~ a ~ `"`).join(",") ~ ")";
236 }
237 
238 template FileNames() {
239     mixin("alias FileNames = " ~ filesTupleString ~ ";");
240 }
241 
242 
243 private void writeSrcFiles(in Options options) {
244     writeln("[Reggae] Writing reggae source files");
245 
246     import std.file: mkdirRecurse;
247     if(!reggaeSrcDirName.exists) {
248         mkdirRecurse(reggaeSrcDirName);
249         mkdirRecurse(buildPath(reggaeSrcDirName, "dub"));
250         mkdirRecurse(buildPath(reggaeSrcDirName, "rules"));
251         mkdirRecurse(buildPath(reggaeSrcDirName, "backend"));
252         mkdirRecurse(buildPath(reggaeSrcDirName, "core", "rules"));
253     }
254 
255 
256     //this foreach has to happen at compile time due
257     //to the string import below.
258     foreach(fileName; FileNames!()) {
259         auto file = File(reggaeSrcFileName(fileName), "w");
260         file.write(import(fileName));
261     }
262 
263     writeConfig(options);
264 }
265 
266 
267 private void writeConfig(in Options options) {
268     auto file = File(reggaeSrcFileName("config.d"), "w");
269 
270     file.writeln(q{
271         module reggae.config;
272         import reggae.ctaa;
273         import reggae.types;
274         import reggae.options;
275     });
276 
277     file.writeln("immutable options = ", options, ";");
278 
279     file.writeln("enum userVars = AssocList!(string, string)([");
280     foreach(key, value; options.userVars) {
281         file.writeln("assocEntry(`", key, "`, `", value, "`), ");
282     }
283     file.writeln("]);");
284 
285     writeDubConfig(options, file);
286 }
287 
288 
289 
290 private string reggaeSrcFileName(in string fileName) @safe pure nothrow {
291     return buildPath(reggaeSrcDirName, fileName);
292 }