1 module reggae.options;
2 
3 import reggae.types;
4 
5 import std.file: thisExePath;
6 import std.path: absolutePath, buildPath;
7 import std.file: exists;
8 
9 enum version_ = "0.5.24+";
10 
11 Options defaultOptions;
12 
13 enum BuildLanguage {
14     D,
15     Python,
16     Ruby,
17     JavaScript,
18     Lua,
19 }
20 
21 enum DubArchitecture {
22     x86,
23     x86_64,
24     x86_mscoff,
25 }
26 
27 struct Options {
28     Backend backend;
29     string projectPath;
30     string dflags;
31     string ranFromPath;
32     string cCompiler;
33     string cppCompiler;
34     string dCompiler;
35     bool noFetch;
36     bool help;
37     bool perModule;
38     bool allAtOnce;
39     bool isDubProject;
40     bool oldNinja;
41     bool noCompilationDB;
42     bool cacheBuildInfo;
43     string[] args;
44     string workingDir;
45     bool version_;
46     bool export_;
47     bool verbose;
48     bool dubLocalPackages;
49     string[] dependencies;
50     string dubObjsDir;
51 
52     version(Windows)
53         DubArchitecture dubArch = DubArchitecture.x86;
54     else
55         DubArchitecture dubArch = DubArchitecture.x86_64;
56 
57     string[string] userVars; //must always be the last member variable
58 
59     Options dup() @safe pure const nothrow {
60         return Options(backend,
61                        projectPath, dflags, ranFromPath, cCompiler, cppCompiler, dCompiler,
62                        noFetch, help, perModule, isDubProject, oldNinja, noCompilationDB, cacheBuildInfo);
63     }
64 
65     //finished setup
66     void finalize(string[] args) @safe {
67         import std.process;
68         import std.path: buildPath;
69 
70         this.args = args;
71         ranFromPath = thisExePath();
72 
73         if(!cCompiler)   cCompiler   = environment.get("CC", "gcc");
74         if(!cppCompiler) cppCompiler = environment.get("CXX", "g++");
75         if(!dCompiler)   dCompiler   = environment.get("DC", "dmd");
76 
77         isDubProject = _dubProjectFile != "";
78 
79         // if there's a dub package file, add it to the list of dependencies so the project
80         // is rebuilt if it changes
81         if(isDubProject) {
82             dependencies ~= _dubProjectFile;
83             dependencies ~= buildPath(projectPath, "dub.selections.json");
84         }
85 
86         if(isDubProject && backend == Backend.tup) {
87             throw new Exception("dub integration not supported with the tup backend");
88         }
89     }
90 
91     package string _dubProjectFile() @safe nothrow {
92         foreach(fileName; ["dub.sdl", "dub.json", "package.json"]) {
93             const name = buildPath(projectPath, fileName);
94             if(name.exists) return name;
95         }
96         return "";
97     }
98 
99     string reggaeFilePath() @safe const {
100         import std.algorithm, std.array, std.exception, std.conv;
101 
102         auto langFiles = [dlangFile, pythonFile, rubyFile, jsFile, luaFile];
103         auto foundFiles = langFiles.filter!exists.array;
104 
105         enforce(foundFiles.length < 2,
106                 text("Reggae builds may only use one language. Found: ",
107                      foundFiles.map!(a => reggaeFileLanguage(a).to!string).join(", ")));
108 
109         if(!foundFiles.empty) return foundFiles.front;
110 
111         return buildPath(projectPath, "reggaefile.d").absolutePath;
112     }
113 
114     string dlangFile() @safe const pure nothrow {
115         return buildPath(projectPath, "reggaefile.d");
116     }
117 
118     string pythonFile() @safe const pure nothrow {
119         return buildPath(projectPath, "reggaefile.py");
120     }
121 
122     string rubyFile() @safe const pure nothrow {
123         return buildPath(projectPath, "reggaefile.rb");
124     }
125 
126     string jsFile() @safe const pure nothrow {
127         return buildPath(projectPath, "reggaefile.js");
128     }
129 
130     string luaFile() @safe const pure nothrow {
131         return buildPath(projectPath, "reggaefile.lua");
132     }
133 
134     string toString() @safe const pure {
135         import std.conv: text;
136         import std.traits: isSomeString, isAssociativeArray, Unqual;
137 
138         string repr = "Options(Backend.";
139 
140         foreach(member; this.tupleof) {
141 
142             static if(isSomeString!(typeof(member)))
143                 repr ~= "`" ~ text(member) ~ "`, ";
144             else static if(isAssociativeArray!(typeof(member)))
145                 {}
146             else static if(is(Unqual!(typeof(member)) == DubArchitecture))
147                 repr ~= `DubArchitecture.` ~ text(member) ~ ", ";
148             else
149                 repr ~= text(member, ", ");
150         }
151 
152         repr ~= ")";
153         return repr;
154     }
155 
156     const (string)[] rerunArgs() @safe pure const {
157         return args;
158     }
159 
160     bool isScriptBuild() @safe const {
161         import reggae.rules.common: getLanguage, Language;
162         return getLanguage(reggaeFilePath) != Language.D;
163     }
164 
165     BuildLanguage reggaeFileLanguage(in string fileName) @safe const {
166         import std.exception;
167         import std.path;
168 
169         with(BuildLanguage) {
170             immutable extToLang = [".d": D, ".py": Python, ".rb": Ruby, ".js": JavaScript, ".lua": Lua];
171             enforce(extension(fileName) in extToLang, "Unsupported build description language in " ~ fileName);
172             return extToLang[extension(fileName)];
173         }
174     }
175 
176     BuildLanguage reggaeFileLanguage() @safe const {
177         return reggaeFileLanguage(reggaeFilePath);
178     }
179 
180     string[] reggaeFileDependencies() @safe const {
181         return [ranFromPath, reggaeFilePath] ~ getReggaeFileDependenciesDlang ~ dependencies;
182     }
183 
184     bool isJsonBuild() @safe const {
185         return reggaeFileLanguage != BuildLanguage.D;
186     }
187 
188     bool earlyExit() @safe pure const nothrow {
189         return help || version_;
190     }
191 
192     string[] compilerVariables() @safe pure nothrow const {
193         return ["CC = " ~ cCompiler, "CXX = " ~ cppCompiler, "DC = " ~ dCompiler];
194     }
195 
196     string eraseProjectPath(in string str) @safe pure nothrow const {
197         import std.string;
198         import std.path;
199         return str.replace(projectPath ~ dirSeparator, "");
200     }
201 }
202 
203 Options getOptions(string[] args) {
204     return getOptions(defaultOptions, args);
205 }
206 
207 //getopt is @system
208 Options getOptions(Options defaultOptions, string[] args) @trusted {
209     import std.getopt;
210     import std.algorithm;
211     import std.array;
212     import std.path;
213     import std.exception: enforce;
214     import std.conv: ConvException;
215 
216     Options options = defaultOptions;
217 
218     //escape spaces so that if we try using these arguments again the shell won't complain
219     auto origArgs = args.map!(a => a.canFind(" ") ? `"` ~ a ~ `"` : a).array;
220 
221     try {
222         auto helpInfo = getopt(
223             args,
224             "backend|b", "Backend to use (ninja|make|binary|tup). Mandatory.", &options.backend,
225             "dflags", "D compiler flags.", &options.dflags,
226             "d", "User-defined variables (e.g. -d myvar=foo).", &options.userVars,
227             "dc", "D compiler to use (default dmd).", &options.dCompiler,
228             "cc", "C compiler to use (default gcc).", &options.cCompiler,
229             "cxx", "C++ compiler to use (default g++).", &options.cppCompiler,
230             "nofetch", "Assume dub packages are present (no dub fetch).", &options.noFetch,
231             "per_module", "Compile D files per module (default is per package)", &options.perModule,
232             "all_at_once", "Compile D files all at once (default is per package)", &options.allAtOnce,
233             "old_ninja", "Generate a Ninja build compatible with older versions of Ninja", &options.oldNinja,
234             "no_comp_db", "Don't generate a JSON compilation database", &options.noCompilationDB,
235             "cache_build_info", "Cache the build information for the binary backend", &options.cacheBuildInfo,
236             "C", "Change directory to run in (similar to make -C and ninja -C)", &options.workingDir,
237             "version", "Prints version information", &options.version_,
238             "export", "Export build system - removes dependencies on reggae itself", &options.export_,
239             "verbose", "Verbose output", &options.verbose,
240             "dub_local", "Project uses dub local packages", &options.dubLocalPackages,
241             "dub-objs-dir", "Directory to place object files for dub dependencies", &options.dubObjsDir,
242             "dub-arch", "Architecture (x86, x86_64, x86_mscoff)", &options.dubArch,
243         );
244 
245         if(helpInfo.helpWanted) {
246             defaultGetoptPrinter("Usage: reggae -b <ninja|make|binary|tup> </path/to/project>",
247                                  helpInfo.options);
248             options.help = true;
249         }
250     } catch(ConvException ex) {
251         import std.algorithm: canFind;
252 
253         if(ex.msg.canFind("Backend"))
254             throw new Exception("Unsupported backend, -b must be one of: make|ninja|tup|binary");
255         else if(ex.msg.canFind("DubArchitecture"))
256             throw new Exception("Unsupported architecture, --dub-arch must be one of: x86|x86_64|x86_mscoff");
257         else
258             assert(0);
259     }
260 
261     enforce(!options.perModule || !options.allAtOnce, "Cannot specify both --per_module and --all_at_once");
262     enforce(options.backend != Backend.none || options.export_, "A backend must be specified with -b/--backend");
263 
264     if(options.version_) {
265         import std.stdio;
266         writeln("reggae v", version_);
267     }
268 
269     immutable argsPath = args.length > 1 ? args[1] : ".";
270     options.projectPath = argsPath.absolutePath.buildNormalizedPath;
271     options.finalize(origArgs);
272 
273     if(options.workingDir == "") {
274         import std.file;
275         options.workingDir = getcwd.absolutePath;
276     } else {
277         options.workingDir = options.workingDir.absolutePath;
278     }
279 
280     return options;
281 }
282 
283 
284 immutable hiddenDir = ".reggae";
285 
286 
287 //returns the list of files that the `reggaefile` depends on
288 //this will usually be empty, but won't be if the reggaefile imports other D files
289 string[] getReggaeFileDependenciesDlang() @trusted {
290     import std.string: chomp;
291     import std.stdio: File;
292     import std.algorithm: splitter;
293     import std.array: array;
294 
295     immutable fileName = buildPath(hiddenDir, "reggaefile.dep");
296     if(!fileName.exists) return [];
297 
298     auto file = File(fileName);
299     file.readln;
300     return file.readln.chomp.splitter(" ").array;
301 }
302 
303 
304 Options withProjectPath(in Options options, in string projectPath) @safe pure nothrow {
305     auto modOptions = options.dup;
306     modOptions.projectPath = projectPath;
307     return modOptions;
308 }
309 
310 
311 string banner() @safe pure nothrow {
312     auto ret = "# Automatically generated by reggae version " ~ version_ ~ "\n";
313     ret ~= "# Do not edit by hand\n";
314     return ret;
315 }