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