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 }