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