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 
11 enum BuildLanguage {
12     D,
13     Python,
14     Ruby,
15     JavaScript,
16     Lua,
17 }
18 
19 struct Options {
20     Backend backend;
21     string projectPath;
22     string dflags;
23     string ranFromPath;
24     string cCompiler;
25     string cppCompiler;
26     string dCompiler;
27     bool noFetch;
28     bool help;
29     bool perModule;
30     bool isDubProject;
31     bool oldNinja;
32     string[string] userVars;
33 
34     Options dup() @safe pure const nothrow {
35         return Options(backend,
36                        projectPath, dflags, ranFromPath, cCompiler, cppCompiler, dCompiler,
37                        noFetch, help, perModule, isDubProject, oldNinja);
38     }
39 
40     //finished setup
41     void finalize() @safe{
42         ranFromPath = thisExePath();
43 
44         if(!cCompiler)   cCompiler   = "gcc";
45         if(!cppCompiler) cppCompiler = "g++";
46         if(!dCompiler)   dCompiler   = "dmd";
47 
48         isDubProject = _isDubProject;
49 
50         if(isDubProject && backend == Backend.tup) {
51             throw new Exception("dub integration not supported with the tup backend");
52         }
53     }
54 
55     private bool _isDubProject() @safe nothrow {
56         return buildPath(projectPath, "dub.json").exists ||
57             buildPath(projectPath, "package.json").exists;
58     }
59 
60     string reggaeFilePath() @safe const {
61         import std.algorithm, std.array, std.exception, std.conv;
62 
63         auto langs = [dlangFile, pythonFile, rubyFile].
64             filter!exists.
65             map!(a => reggaeFileLanguage(a)).
66             array;
67 
68         enforce(langs.length < 2, text("Reggae builds may only use one language. Found: ",
69                                        langs.map!(to!string).join(", ")));
70 
71         if(dlangFile.exists) return dlangFile;
72         if(pythonFile.exists) return pythonFile;
73         if(rubyFile.exists) return rubyFile;
74         if(jsFile.exists) return jsFile;
75         if(luaFile.exists) return luaFile;
76 
77         immutable path = isDubProject ? "" : projectPath;
78         return buildPath(path, "reggaefile.d").absolutePath;
79     }
80 
81     string dlangFile() @safe const pure nothrow {
82         return projectBuildFile;
83     }
84 
85     string pythonFile() @safe const pure nothrow {
86         return buildPath(projectPath, "reggaefile.py");
87     }
88 
89     string rubyFile() @safe const pure nothrow {
90         return buildPath(projectPath, "reggaefile.rb");
91     }
92 
93     string jsFile() @safe const pure nothrow {
94         return buildPath(projectPath, "reggaefile.js");
95     }
96 
97     string luaFile() @safe const pure nothrow {
98         return buildPath(projectPath, "reggaefile.lua");
99     }
100 
101     string projectBuildFile() @safe const pure nothrow {
102         return buildPath(projectPath, "reggaefile.d");
103     }
104 
105     string toString() @safe const pure {
106         import std.conv: text;
107         import std.traits: isSomeString, isAssociativeArray;
108 
109         string repr = "Options(Backend.";
110 
111         foreach(member; this.tupleof) {
112             static if(isSomeString!(typeof(member)))
113                 repr ~= `"` ~ text(member) ~ `", `;
114             else static if(isAssociativeArray!(typeof(member)))
115                 {}
116             else
117                 repr ~= text(member, ", ");
118         }
119 
120         repr ~= ")";
121         return repr;
122     }
123 
124     string[] rerunArgs() @safe pure const {
125         import std.conv: to;
126 
127         auto args =  [ranFromPath, "-b", backend.to!string, ];
128 
129         if(dflags != "") args ~= ["--dflags='" ~ dflags ~ "'"];
130         if(oldNinja) args ~= "--old_ninja";
131         if(cCompiler != "") args ~=  ["--cc", cCompiler];
132         if(cppCompiler != "") args ~=  ["--cxx", cppCompiler];
133         if(dCompiler != "") args ~=  ["--dc", dCompiler];
134 
135         args ~= projectPath;
136 
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.algorithm;
147 
148         if(fileName.endsWith(".d"))
149             return BuildLanguage.D;
150         else if(fileName.endsWith(".py"))
151             return BuildLanguage.Python;
152         else if(fileName.endsWith(".rb"))
153             return BuildLanguage.Ruby;
154         else if(fileName.endsWith(".js"))
155             return BuildLanguage.JavaScript;
156         else if(fileName.endsWith(".lua"))
157             return BuildLanguage.Lua;
158         else throw new Exception("Unknown language for " ~ fileName);
159     }
160 
161     BuildLanguage reggaeFileLanguage() @safe const {
162         return reggaeFileLanguage(reggaeFilePath);
163     }
164 
165 }
166 
167 
168 //getopt is @system
169 Options getOptions(string[] args) @trusted {
170     import std.getopt;
171 
172     Options options;
173     try {
174         auto helpInfo = getopt(
175             args,
176             "backend|b", "Backend to use (ninja|make). Mandatory.", &options.backend,
177             "dflags", "D compiler flags.", &options.dflags,
178             "d", "User-defined variables (e.g. -d myvar=foo).", &options.userVars,
179             "dc", "D compiler to use (default dmd).", &options.dCompiler,
180             "cc", "C compiler to use (default gcc).", &options.cCompiler,
181             "cxx", "C++ compiler to use (default g++).", &options.cppCompiler,
182             "nofetch", "Assume dub packages are present (no dub fetch).", &options.noFetch,
183             "per_module", "Compile D files per module (default is per package)", &options.perModule,
184             "old_ninja", "Generate a Ninja build compatible with older versions of Ninja", &options.oldNinja,
185             );
186 
187         if(helpInfo.helpWanted) {
188             defaultGetoptPrinter("Usage: reggae -b <ninja|make> </path/to/project>",
189                                  helpInfo.options);
190             options.help = true;
191         }
192 
193     } catch(ConvException ex) {
194         throw new Exception("Unsupported backend, -b must be one of: make|ninja|tup|binary");
195     }
196 
197     if(args.length > 1) options.projectPath = args[1].absolutePath;
198     options.finalize();
199 
200     return options;
201 }