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