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.workingDir, "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 97 version(unittest) 98 gDubInfos = null; 99 100 if("default" !in gDubInfos) { 101 102 if(!buildPath(options.projectPath, "dub.selections.json").exists) { 103 output.log("Calling `dub upgrade` to create dub.selections.json"); 104 callDub(options, ["dub", "upgrade"]); 105 } 106 107 DubConfigurations getConfigsImpl() { 108 immutable dubBuildArgs = ["dub", "--annotate", "build", "--compiler=" ~ options.dCompiler, 109 "--print-configs", "--build=docs"]; 110 output.log("Querying dub for build configurations"); 111 immutable dubBuildOutput = callDub(options, dubBuildArgs, Yes.maybeNoDeps); 112 return getConfigurations(dubBuildOutput); 113 } 114 115 DubConfigurations getConfigs() { 116 try { 117 return getConfigsImpl; 118 } catch(Exception _) { 119 output.log("Calling `dub fetch` since getting the configuration failed"); 120 dubFetch(output, options); 121 return getConfigsImpl; 122 } 123 } 124 125 const configs = getConfigs(); 126 127 bool oneConfigOk; 128 Exception dubDescribeFailure; 129 130 if(configs.configurations.empty) { 131 output.log("Calling `dub describe`"); 132 const descOutput = callDub(options, ["dub", "describe"], Yes.maybeNoDeps); 133 oneConfigOk = true; 134 gDubInfos["default"] = getDubInfo(descOutput); 135 } else { 136 foreach(config; configs.configurations) { 137 try { 138 output.log("Calling `dub describe` for configuration ", config); 139 const descOutput = callDub(options, ["dub", "describe", "-c", config], Yes.maybeNoDeps); 140 gDubInfos[config] = getDubInfo(descOutput); 141 142 // dub adds certain flags to certain configurations automatically but these flags 143 // don't know up in the output to `dub describe`. Special case them here. 144 145 // unittest should only apply to the main package, hence [0] 146 // this doesn't show up in `dub describe`, it's secret info that dub knows 147 // so we have to add it manually here 148 if(config == "unittest") gDubInfos[config].packages[0].dflags ~= " -unittest"; 149 150 callPreBuildCommands(options, gDubInfos[config]); 151 152 oneConfigOk = true; 153 154 } catch(Exception ex) { 155 if(dubDescribeFailure !is null) dubDescribeFailure = ex; 156 } 157 } 158 159 if(configs.default_ !in gDubInfos) 160 throw new Exception("Non-existent config info for " ~ configs.default_); 161 162 gDubInfos["default"] = gDubInfos[configs.default_]; 163 } 164 165 if(!oneConfigOk) throw dubDescribeFailure; 166 } 167 168 return gDubInfos["default"]; 169 } 170 171 private string callDub(in from!"reggae.options".Options options, 172 in string[] rawArgs, 173 from!"std.typecons".Flag!"maybeNoDeps" maybeNoDeps = from!"std.typecons".No.maybeNoDeps) 174 { 175 import std.process: execute, Config; 176 import std.exception: enforce; 177 import std.conv: text; 178 import std.string: join, split; 179 import std.path: buildPath; 180 import std.file: exists; 181 182 const hasSelections = buildPath(options.projectPath, "dub.selections.json").exists; 183 string[] emptyArgs; 184 const noDepsArgs = hasSelections && maybeNoDeps ? ["--nodeps", "--skip-registry=all"] : emptyArgs; 185 const args = rawArgs ~ noDepsArgs ~ dubEnvArgs; 186 const string[string] env = null; 187 Config config = Config.none; 188 size_t maxOutput = size_t.max; 189 const workDir = options.projectPath; 190 191 const ret = execute(args, env, config, maxOutput, workDir); 192 enforce(ret.status == 0, 193 text("Error calling '", args.join(" "), "' (", ret.status, ")", ":\n", 194 ret.output)); 195 196 return ret.output; 197 } 198 199 private string[] dubEnvArgs() { 200 import std.process: environment; 201 import std.string: split; 202 return environment.get("REGGAE_DUB_ARGS", "").split(" "); 203 } 204 205 private void callPreBuildCommands(in from!"reggae.options".Options options, 206 in from!"reggae.dub.json".DubInfo dubInfo) 207 { 208 import std.process: executeShell, Config; 209 import std.string: replace; 210 import std.exception: enforce; 211 import std.conv: text; 212 import core.exception: RangeError; 213 214 const string[string] env = null; 215 Config config = Config.none; 216 size_t maxOutput = size_t.max; 217 immutable workDir = options.projectPath; 218 219 () @trusted { 220 try { 221 foreach(c; dubInfo.packages[0].preBuildCommands) { 222 auto cmd = c.replace("$project", options.projectPath); 223 immutable ret = executeShell(cmd, env, config, maxOutput, workDir); 224 enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output)); 225 } 226 } catch(RangeError e) { 227 assert(false, "FATAL ERROR: dubInfo has no packages\n" ~ dubInfo.text); 228 } 229 }(); 230 } 231 232 private void dubFetch(T)(auto ref T output, 233 in from!"reggae.options".Options options) 234 @trusted 235 { 236 import reggae.io: log; 237 import std.array: join, replace; 238 import std.stdio: writeln; 239 import std.path: buildPath; 240 import std.json: parseJSON, JSON_TYPE; 241 import std.file: readText; 242 243 const fileName = buildPath(options.projectPath, "dub.selections.json"); 244 auto json = parseJSON(readText(fileName)); 245 246 auto versions = json["versions"]; 247 248 foreach(dubPackage, versionJson; versions.object) { 249 250 // skip the ones with a defined path 251 if(versionJson.type != JSON_TYPE.STRING) continue; 252 253 // versions are usually `==1.2.3`, so strip the sign 254 const version_ = versionJson.str.replace("==", ""); 255 256 if(!needDubFetch(dubPackage, version_)) continue; 257 258 259 const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_] ~ dubEnvArgs; 260 261 output.log("Fetching package with command '", cmd.join(" "), "'"); 262 try 263 callDub(options, cmd); 264 catch(Exception ex) { 265 // local packages can't be fetched, so it's normal to get an error 266 if(!options.dubLocalPackages) 267 throw ex; 268 } 269 } 270 } 271 272 // dub fetch can sometimes take >10s (!) despite the package already being 273 // on disk 274 bool needDubFetch(in string dubPackage, in string version_) { 275 import std.path: buildPath; 276 import std.process: environment; 277 import std.file: exists; 278 279 const packageDir = dubPackage ~ "-" ~ version_; 280 version(Windows) 281 const path = buildPath("C:\\Users", environment["USERNAME"], "AppData", "Roaming", "dub", "packages", packageDir); 282 else 283 const path = buildPath(environment["HOME"], ".dub", "packages", packageDir); 284 285 return !path.exists; 286 } 287 288 289 void writeDubConfig(T)(auto ref T output, 290 in from!"reggae.options".Options options, 291 from!"std.stdio".File file) { 292 import reggae.io: log; 293 import reggae.dub.info: TargetType; 294 import std.stdio: writeln; 295 296 output.log("Writing dub configuration"); 297 298 file.writeln("import reggae.dub.info;"); 299 300 if(options.isDubProject) { 301 302 file.writeln("enum isDubProject = true;"); 303 auto dubInfo = _getDubInfo(output, options); 304 const targetType = dubInfo.packages[0].targetType; 305 306 file.writeln(`const configToDubInfo = assocList([`); 307 308 const keys = () @trusted { return gDubInfos.keys; }(); 309 foreach(config; keys) { 310 file.writeln(` assocEntry("`, config, `", `, gDubInfos[config], `),`); 311 } 312 file.writeln(`]);`); 313 file.writeln; 314 } else { 315 file.writeln("enum isDubProject = false;"); 316 } 317 }