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