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