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