1 /** 2 A module for providing interop between reggae and dub 3 */ 4 5 module reggae.dub.interop; 6 7 import reggae.options; 8 import reggae.dub.info; 9 import reggae.dub.json; 10 import std.stdio; 11 import std.exception; 12 import std.conv; 13 import std.process; 14 15 16 DubInfo[string] gDubInfos; 17 18 19 @safe: 20 21 struct DubConfigurations { 22 string[] configurations; 23 string default_; 24 } 25 26 27 DubConfigurations getConfigurations(in string output) pure { 28 29 import std.algorithm: splitter, find, canFind, until, map; 30 import std.string: stripLeft; 31 import std.array: array, replace, front, empty; 32 33 auto lines = output.splitter("\n"); 34 auto fromConfigs = lines.find("Available configurations:"). 35 until!(a => a == ""). 36 map!(a => a.stripLeft). 37 array[1..$]; 38 if(fromConfigs.empty) return DubConfigurations(); 39 40 immutable defMarker = " [default]"; 41 auto default_ = fromConfigs.find!(a => a.canFind(defMarker)).front.replace(defMarker, ""); 42 auto configs = fromConfigs.map!(a => a.replace(defMarker, "")).array; 43 44 return DubConfigurations(configs, default_); 45 } 46 47 48 void maybeCreateReggaefile(T)(auto ref T output, in Options options) { 49 import std.file: exists; 50 import std.stdio: writeln; 51 52 if(options.isDubProject && !options.reggaeFilePath.exists) { 53 output.writeln("[Reggae] Creating default dub project reggaefile"); 54 createReggaefile(output, options); 55 } 56 } 57 58 // default build for a dub project when there is no reggaefile 59 void createReggaefile(T)(auto ref T output, in Options options) { 60 import std.stdio: File; 61 import std.path: buildPath; 62 import std.regex: regex, replaceFirst; 63 64 output.writeln("[Reggae] Creating reggaefile.d from dub information"); 65 auto file = File(buildPath(options.workingDir, "reggaefile.d"), "w"); 66 file.writeln(q{ 67 import reggae; 68 enum commonFlags = "-w -g -debug"; 69 mixin build!(dubDefaultTarget!(CompilerFlags(commonFlags)), 70 dubTestTarget!(CompilerFlags(commonFlags))); 71 }.replaceFirst(regex(`^ `), "")); 72 73 if(!options.noFetch) dubFetch(output, options); 74 } 75 76 77 private DubInfo _getDubInfo(T)(auto ref T output, in Options options) { 78 import std.array; 79 import std.file: exists; 80 import std.path: buildPath; 81 import std.stdio: writeln; 82 83 version(unittest) 84 gDubInfos = null; 85 86 if("default" !in gDubInfos) { 87 88 if(!buildPath(options.projectPath, "dub.selections.json").exists) { 89 output.writeln("[Reggae] Calling dub upgrade to create dub.selections.json"); 90 callDub(options, ["dub", "upgrade"]); 91 } 92 93 DubConfigurations getConfigsImpl() { 94 immutable dubBuildArgs = ["dub", "--annotate", "build", "--compiler=" ~ options.dCompiler, 95 "--print-configs", "--build=docs"]; 96 immutable dubBuildOutput = callDub(options, dubBuildArgs); 97 return getConfigurations(dubBuildOutput); 98 } 99 100 DubConfigurations getConfigs() { 101 try { 102 return getConfigsImpl; 103 } catch(Exception _) { 104 output.writeln("[Reggae] Calling 'dub fetch' since getting the configuration failed"); 105 dubFetch(output, options); 106 return getConfigsImpl; 107 } 108 } 109 110 const configs = getConfigs(); 111 112 bool oneConfigOk; 113 Exception dubDescribeFailure; 114 115 if(configs.configurations.empty) { 116 const descOutput = callDub(options, ["dub", "describe"]); 117 oneConfigOk = true; 118 gDubInfos["default"] = getDubInfo(descOutput); 119 } else { 120 foreach(config; configs.configurations) { 121 try { 122 const descOutput = callDub(options, ["dub", "describe", "-c", config]); 123 gDubInfos[config] = getDubInfo(descOutput); 124 125 // dub adds certain flags to certain configurations automatically but these flags 126 // don't know up in the output to `dub describe`. Special case them here. 127 128 // unittest should only apply to the main package, hence [0] 129 // this doesn't show up in `dub describe`, it's secret info that dub knows 130 // so we have to add it manually here 131 if(config == "unittest") gDubInfos[config].packages[0].dflags ~= " -unittest"; 132 133 callPreBuildCommands(options, gDubInfos[config]); 134 135 oneConfigOk = true; 136 137 } catch(Exception ex) { 138 if(dubDescribeFailure !is null) dubDescribeFailure = ex; 139 } 140 } 141 142 if(configs.default_ !in gDubInfos) 143 throw new Exception("Non-existent config info for " ~ configs.default_); 144 145 gDubInfos["default"] = gDubInfos[configs.default_]; 146 } 147 148 if(!oneConfigOk) throw dubDescribeFailure; 149 } 150 151 return gDubInfos["default"]; 152 } 153 154 private string callDub(in Options options, in string[] args) { 155 import std.process; 156 import std.exception: enforce; 157 import std.conv: text; 158 import std.string; 159 160 const string[string] env = null; 161 Config config = Config.none; 162 size_t maxOutput = size_t.max; 163 immutable workDir = options.projectPath; 164 165 immutable ret = execute(args, env, config, maxOutput, workDir); 166 enforce(ret.status == 0, text("Error calling '", args.join(" "), "' (", ret.status, ")", ":\n", 167 ret.output)); 168 return ret.output; 169 } 170 171 private void callPreBuildCommands(in Options options, in DubInfo dubInfo) { 172 import std.process; 173 import std.string: replace; 174 175 const string[string] env = null; 176 Config config = Config.none; 177 size_t maxOutput = size_t.max; 178 immutable workDir = options.projectPath; 179 180 foreach(c; dubInfo.packages[0].preBuildCommands) { 181 auto cmd = c.replace("$project", options.projectPath); 182 immutable ret = executeShell(cmd, env, config, maxOutput, workDir); 183 enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output)); 184 } 185 } 186 187 private void dubFetch(T)(auto ref T output, in Options options) @trusted { 188 import std.array: join, replace; 189 import std.stdio: writeln; 190 import std.path: buildPath; 191 import std.json: parseJSON, JSON_TYPE; 192 import std.file: readText; 193 194 const fileName = buildPath(options.projectPath, "dub.selections.json"); 195 auto json = parseJSON(readText(fileName)); 196 197 auto versions = json["versions"]; 198 199 foreach(dubPackage, versionJson; versions.object) { 200 201 // skip the ones with a defined path 202 if(versionJson.type != JSON_TYPE.STRING) continue; 203 204 // versions are usually `==1.2.3`, so strip the sign 205 const version_ = versionJson.str.replace("==", ""); 206 207 if(!needDubFetch(dubPackage, version_)) continue; 208 209 const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_]; 210 211 output.writeln("[Reggae] Fetching package with command '", cmd.join(" "), "'"); 212 try 213 callDub(options, cmd); 214 catch(Exception ex) { 215 // local packages can't be fetched, so it's normal to get an error 216 if(!options.dubLocalPackages) 217 throw ex; 218 } 219 } 220 } 221 222 // dub fetch can sometimes take >10s (!) despite the package already being 223 // on disk 224 bool needDubFetch(in string dubPackage, in string version_) { 225 import std.path: buildPath; 226 import std.process: environment; 227 import std.file: exists; 228 229 const path = buildPath(environment["HOME"], ".dub", "packages", dubPackage ~ "-" ~ version_); 230 return !path.exists; 231 } 232 233 234 void writeDubConfig(T)(auto ref T output, in Options options, File file) { 235 import reggae.dub.info: TargetType; 236 import std.stdio: writeln; 237 238 output.writeln("[Reggae] Writing dub configuration"); 239 240 file.writeln("import reggae.dub.info;"); 241 242 if(options.isDubProject) { 243 244 file.writeln("enum isDubProject = true;"); 245 auto dubInfo = _getDubInfo(output, options); 246 const targetType = dubInfo.packages[0].targetType; 247 248 file.writeln(`const configToDubInfo = assocList([`); 249 250 const keys = () @trusted { return gDubInfos.keys; }(); 251 foreach(config; keys) { 252 file.writeln(` assocEntry("`, config, `", `, gDubInfos[config], `),`); 253 } 254 file.writeln(`]);`); 255 file.writeln; 256 } else { 257 file.writeln("enum isDubProject = false;"); 258 } 259 }