1 /** 2 A module for providing interop between reggae and dub 3 */ 4 module reggae.dub.interop; 5 6 7 import reggae.from; 8 9 10 from!"reggae.dub.info".DubInfo[string] gDubInfos; 11 12 13 @safe: 14 15 struct DubConfigurations { 16 string[] configurations; 17 string default_; 18 } 19 20 21 DubConfigurations getConfigurations(in string rawOutput) pure { 22 23 import std.algorithm: findSkip, filter, map, canFind, startsWith; 24 import std.string: splitLines, stripLeft; 25 import std.array: array, replace; 26 27 string output = rawOutput; // findSkip mutates output 28 const found = output.findSkip("Available configurations:"); 29 assert(found, "Could not find configurations in:\n" ~ rawOutput); 30 auto configs = output 31 .splitLines 32 .filter!(a => a.startsWith(" ")) 33 .map!stripLeft 34 .array; 35 36 if(configs.length == 0) return DubConfigurations(); 37 38 string default_; 39 foreach(ref config; configs) { 40 const defaultMarker = " [default]"; 41 if(config.canFind(defaultMarker)) { 42 assert(default_ is null); 43 config = config.replace(defaultMarker, ""); 44 default_ = config; 45 break; 46 } 47 } 48 49 return DubConfigurations(configs, default_); 50 } 51 52 53 void maybeCreateReggaefile(T)(auto ref T output, 54 in from!"reggae.options".Options options) 55 { 56 import std.file: exists; 57 58 if(options.isDubProject && !options.reggaeFilePath.exists) { 59 createReggaefile(output, options); 60 } 61 } 62 63 // default build for a dub project when there is no reggaefile 64 void createReggaefile(T)(auto ref T output, 65 in from!"reggae.options".Options options) 66 { 67 import reggae.io: log; 68 import std.stdio: File; 69 import std.path: buildPath; 70 import std.regex: regex, replaceFirst; 71 72 output.log("Creating reggaefile.d from dub information"); 73 auto file = File(buildPath(options.projectPath, "reggaefile.d"), "w"); 74 75 file.writeln(q{ 76 import reggae; 77 enum commonFlags = "-w -g -debug"; 78 mixin build!(dubDefaultTarget!(CompilerFlags(commonFlags)), 79 dubTestTarget!(CompilerFlags(commonFlags))); 80 }.replaceFirst(regex(`^ `), "")); 81 82 if(!options.noFetch) dubFetch(output, options); 83 } 84 85 86 private from!"reggae.dub.info".DubInfo _getDubInfo(T)(auto ref T output, 87 in from!"reggae.options".Options options) 88 { 89 import reggae.io: log; 90 import reggae.dub.json: getDubInfo; 91 import std.array; 92 import std.file: exists; 93 import std.path: buildPath; 94 import std.stdio: writeln; 95 import std.typecons: Yes; 96 import std.conv: text; 97 98 version(unittest) 99 gDubInfos = null; 100 101 if("default" !in gDubInfos) { 102 103 if(!buildPath(options.projectPath, "dub.selections.json").exists) { 104 callDub(output, options, ["dub", "upgrade"]); 105 } 106 107 DubConfigurations tryGetConfigs() { 108 immutable dubBuildArgs = ["dub", "--annotate", "build", "--compiler=" ~ options.dCompiler, 109 "--print-configs", "--build=docs"]; 110 immutable dubBuildOutput = callDub(output, options, dubBuildArgs, Yes.maybeNoDeps); 111 return getConfigurations(dubBuildOutput); 112 } 113 114 DubConfigurations getConfigs() { 115 try { 116 return tryGetConfigs; 117 } catch(Exception _) { 118 output.log("Calling `dub fetch` since getting the configuration failed"); 119 dubFetch(output, options); 120 return tryGetConfigs; 121 } 122 } 123 124 const configs = getConfigs(); 125 126 bool oneConfigOk; 127 Exception dubDescribeFailure; 128 129 if(configs.configurations.empty) { 130 const descOutput = callDub(output, options, ["dub", "describe"], Yes.maybeNoDeps); 131 oneConfigOk = true; 132 gDubInfos["default"] = getDubInfo(descOutput); 133 } else { 134 foreach(config; configs.configurations) { 135 try { 136 const descOutput = callDub(output, options, ["dub", "describe", "-c", config], Yes.maybeNoDeps); 137 gDubInfos[config] = getDubInfo(descOutput); 138 139 // dub adds certain flags to certain configurations automatically but these flags 140 // don't know up in the output to `dub describe`. Special case them here. 141 142 // unittest should only apply to the main package, hence [0]. 143 // This doesn't show up in `dub describe`, it's secret info that dub knows 144 // so we have to add it manually here. 145 if(config == "unittest") { 146 if(config !in gDubInfos) 147 throw new Exception( 148 text("Configuration `", config, "` not found in ", 149 () @trusted { return gDubInfos.keys; }())); 150 if(gDubInfos[config].packages.length == 0) 151 throw new Exception( 152 text("No main package in `", config, "` configuration")); 153 gDubInfos[config].packages[0].dflags ~= " -unittest"; 154 } 155 156 try 157 callPreBuildCommands(output, options, gDubInfos[config]); 158 catch(Exception e) { 159 output.log("Error calling prebuild commands: ", e.msg); 160 throw e; 161 } 162 163 oneConfigOk = true; 164 165 } catch(Exception ex) { 166 output.log("ERROR: exception in calling dub describe: ", ex.msg); 167 if(dubDescribeFailure is null) dubDescribeFailure = ex; 168 } 169 } 170 171 if(configs.default_ !in gDubInfos) 172 throw new Exception("Non-existent config info for " ~ configs.default_); 173 174 gDubInfos["default"] = gDubInfos[configs.default_]; 175 } 176 177 if(!oneConfigOk) { 178 assert(dubDescribeFailure !is null, 179 "Internal error: no configurations worked and no exception to throw"); 180 throw dubDescribeFailure; 181 } 182 } 183 184 return gDubInfos["default"]; 185 } 186 187 private string callDub(T)( 188 auto ref T output, 189 in from!"reggae.options".Options options, 190 in string[] rawArgs, 191 from!"std.typecons".Flag!"maybeNoDeps" maybeNoDeps = from!"std.typecons".No.maybeNoDeps) 192 { 193 import reggae.io: log; 194 import std.process: execute, Config; 195 import std.exception: enforce; 196 import std.conv: text; 197 import std.string: join, split; 198 import std.path: buildPath; 199 import std.file: exists; 200 201 const hasSelections = buildPath(options.projectPath, "dub.selections.json").exists; 202 string[] emptyArgs; 203 const noDepsArgs = hasSelections && maybeNoDeps ? ["--nodeps", "--skip-registry=all"] : emptyArgs; 204 const archArg = rawArgs[1] == "fetch" || rawArgs[1] == "upgrade" 205 ? emptyArgs 206 : ["--arch=" ~ options.dubArch.text]; 207 const args = rawArgs ~ noDepsArgs ~ dubEnvArgs ~ archArg; 208 const string[string] env = null; 209 Config config = Config.none; 210 size_t maxOutput = size_t.max; 211 const workDir = options.projectPath; 212 213 output.log("Calling `", args.join(" "), "`"); 214 const ret = execute(args, env, config, maxOutput, workDir); 215 enforce(ret.status == 0, 216 text("Error calling `", args.join(" "), "` (", ret.status, ")", ":\n", 217 ret.output)); 218 219 return ret.output; 220 } 221 222 private string[] dubEnvArgs() { 223 import std.process: environment; 224 import std.string: split; 225 return environment.get("REGGAE_DUB_ARGS", "").split(" "); 226 } 227 228 private void callPreBuildCommands(T)(auto ref T output, 229 in from!"reggae.options".Options options, 230 in from!"reggae.dub.json".DubInfo dubInfo) 231 { 232 import reggae.io: log; 233 import std.process: executeShell, Config; 234 import std.string: replace; 235 import std.exception: enforce; 236 import std.conv: text; 237 238 const string[string] env = null; 239 Config config = Config.none; 240 size_t maxOutput = size_t.max; 241 immutable workDir = options.projectPath; 242 243 if(dubInfo.packages.length == 0) return; 244 245 foreach(const package_; dubInfo.packages) { 246 foreach(const dubCommandString; package_.preBuildCommands) { 247 auto cmd = dubCommandString.replace("$project", options.projectPath); 248 output.log("Executing pre-build command `", cmd, "`"); 249 const ret = executeShell(cmd, env, config, maxOutput, workDir); 250 enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output)); 251 } 252 } 253 } 254 255 private void dubFetch(T)(auto ref T output, 256 in from!"reggae.options".Options options) 257 @trusted 258 { 259 import reggae.io: log; 260 import std.array: join, replace; 261 import std.stdio: writeln; 262 import std.path: buildPath; 263 import std.json: parseJSON, JSONType; 264 import std.file: readText; 265 266 const fileName = buildPath(options.projectPath, "dub.selections.json"); 267 auto json = parseJSON(readText(fileName)); 268 269 auto versions = json["versions"]; 270 271 foreach(dubPackage, versionJson; versions.object) { 272 273 // skip the ones with a defined path 274 if(versionJson.type != JSONType..string) continue; 275 276 // versions are usually `==1.2.3`, so strip the sign 277 const version_ = versionJson.str.replace("==", ""); 278 279 if(!needDubFetch(dubPackage, version_)) continue; 280 281 282 const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_] ~ dubEnvArgs; 283 284 try 285 callDub(output, options, cmd); 286 catch(Exception ex) { 287 // local packages can't be fetched, so it's normal to get an error 288 if(!options.dubLocalPackages) 289 throw ex; 290 } 291 } 292 } 293 294 // dub fetch can sometimes take >10s (!) despite the package already being 295 // on disk 296 bool needDubFetch(in string dubPackage, in string version_) { 297 import reggae.path: dubPackagesDir; 298 import std.path: buildPath; 299 import std.file: exists; 300 301 return !buildPath(dubPackagesDir, 302 dubPackage ~ "-" ~ version_, dubPackage ~ ".lock") 303 .exists; 304 } 305 306 307 void writeDubConfig(T)(auto ref T output, 308 in from!"reggae.options".Options options, 309 from!"std.stdio".File file) { 310 import reggae.io: log; 311 import reggae.dub.info: TargetType; 312 313 output.log("Writing dub configuration"); 314 315 file.writeln("import reggae.dub.info;"); 316 317 if(options.isDubProject) { 318 319 file.writeln("enum isDubProject = true;"); 320 auto dubInfo = _getDubInfo(output, options); 321 const targetType = dubInfo.packages.length 322 ? dubInfo.packages[0].targetType 323 : TargetType.sourceLibrary; 324 325 file.writeln(`const configToDubInfo = assocList([`); 326 327 const keys = () @trusted { return gDubInfos.keys; }(); 328 foreach(config; keys) { 329 file.writeln(` assocEntry("`, config, `", `, gDubInfos[config], `),`); 330 } 331 file.writeln(`]);`); 332 file.writeln; 333 } else { 334 file.writeln("enum isDubProject = false;"); 335 } 336 }