1 /** 2 A module for providing interop between reggae and dub 3 */ 4 5 module reggae.dub.interop; 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 } 161 162 oneConfigOk = true; 163 164 } catch(Exception ex) { 165 if(dubDescribeFailure !is null) dubDescribeFailure = ex; 166 } 167 } 168 169 if(configs.default_ !in gDubInfos) 170 throw new Exception("Non-existent config info for " ~ configs.default_); 171 172 gDubInfos["default"] = gDubInfos[configs.default_]; 173 } 174 175 if(!oneConfigOk) throw dubDescribeFailure; 176 } 177 178 return gDubInfos["default"]; 179 } 180 181 private string callDub(T)( 182 auto ref T output, 183 in from!"reggae.options".Options options, 184 in string[] rawArgs, 185 from!"std.typecons".Flag!"maybeNoDeps" maybeNoDeps = from!"std.typecons".No.maybeNoDeps) 186 { 187 import reggae.io: log; 188 import std.process: execute, Config; 189 import std.exception: enforce; 190 import std.conv: text; 191 import std.string: join, split; 192 import std.path: buildPath; 193 import std.file: exists; 194 195 const hasSelections = buildPath(options.projectPath, "dub.selections.json").exists; 196 string[] emptyArgs; 197 const noDepsArgs = hasSelections && maybeNoDeps ? ["--nodeps", "--skip-registry=all"] : emptyArgs; 198 const archArg = rawArgs[1] == "fetch" || rawArgs[1] == "upgrade" 199 ? emptyArgs 200 : ["--arch=" ~ options.dubArch.text]; 201 const args = rawArgs ~ noDepsArgs ~ dubEnvArgs ~ archArg; 202 const string[string] env = null; 203 Config config = Config.none; 204 size_t maxOutput = size_t.max; 205 const workDir = options.projectPath; 206 207 output.log("Calling `", args.join(" "), "`"); 208 const ret = execute(args, env, config, maxOutput, workDir); 209 enforce(ret.status == 0, 210 text("Error calling `", args.join(" "), "` (", ret.status, ")", ":\n", 211 ret.output)); 212 213 return ret.output; 214 } 215 216 private string[] dubEnvArgs() { 217 import std.process: environment; 218 import std.string: split; 219 return environment.get("REGGAE_DUB_ARGS", "").split(" "); 220 } 221 222 private void callPreBuildCommands(T)(auto ref T output, 223 in from!"reggae.options".Options options, 224 in from!"reggae.dub.json".DubInfo dubInfo) 225 { 226 import reggae.io: log; 227 import std.process: executeShell, Config; 228 import std.string: replace; 229 import std.exception: enforce; 230 import std.conv: text; 231 232 const string[string] env = null; 233 Config config = Config.none; 234 size_t maxOutput = size_t.max; 235 immutable workDir = options.projectPath; 236 237 if(dubInfo.packages.length == 0) return; 238 239 foreach(const package_; dubInfo.packages) { 240 foreach(const dubCommandString; package_.preBuildCommands) { 241 auto cmd = dubCommandString.replace("$project", options.projectPath); 242 output.log("Executing pre-build command `", cmd, "`"); 243 const ret = executeShell(cmd, env, config, maxOutput, workDir); 244 enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output)); 245 } 246 } 247 } 248 249 private void dubFetch(T)(auto ref T output, 250 in from!"reggae.options".Options options) 251 @trusted 252 { 253 import reggae.io: log; 254 import std.array: join, replace; 255 import std.stdio: writeln; 256 import std.path: buildPath; 257 import std.json: parseJSON, JSONType; 258 import std.file: readText; 259 260 const fileName = buildPath(options.projectPath, "dub.selections.json"); 261 auto json = parseJSON(readText(fileName)); 262 263 auto versions = json["versions"]; 264 265 foreach(dubPackage, versionJson; versions.object) { 266 267 // skip the ones with a defined path 268 if(versionJson.type != JSONType..string) continue; 269 270 // versions are usually `==1.2.3`, so strip the sign 271 const version_ = versionJson.str.replace("==", ""); 272 273 if(!needDubFetch(dubPackage, version_)) continue; 274 275 276 const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_] ~ dubEnvArgs; 277 278 try 279 callDub(output, options, cmd); 280 catch(Exception ex) { 281 // local packages can't be fetched, so it's normal to get an error 282 if(!options.dubLocalPackages) 283 throw ex; 284 } 285 } 286 } 287 288 // dub fetch can sometimes take >10s (!) despite the package already being 289 // on disk 290 bool needDubFetch(in string dubPackage, in string version_) { 291 import reggae.path: dubPackagesDir; 292 import std.path: buildPath; 293 import std.file: exists; 294 295 return !buildPath(dubPackagesDir, 296 dubPackage ~ "-" ~ version_, dubPackage ~ ".lock") 297 .exists; 298 } 299 300 301 void writeDubConfig(T)(auto ref T output, 302 in from!"reggae.options".Options options, 303 from!"std.stdio".File file) { 304 import reggae.io: log; 305 import reggae.dub.info: TargetType; 306 307 output.log("Writing dub configuration"); 308 309 file.writeln("import reggae.dub.info;"); 310 311 if(options.isDubProject) { 312 313 file.writeln("enum isDubProject = true;"); 314 auto dubInfo = _getDubInfo(output, options); 315 const targetType = dubInfo.packages.length 316 ? dubInfo.packages[0].targetType 317 : TargetType.sourceLibrary; 318 319 file.writeln(`const configToDubInfo = assocList([`); 320 321 const keys = () @trusted { return gDubInfos.keys; }(); 322 foreach(config; keys) { 323 file.writeln(` assocEntry("`, config, `", `, gDubInfos[config], `),`); 324 } 325 file.writeln(`]);`); 326 file.writeln; 327 } else { 328 file.writeln("enum isDubProject = false;"); 329 } 330 }