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.path; 61 output.writeln("[Reggae] Creating reggaefile.d from dub information"); 62 auto file = File(buildPath(options.workingDir, "reggaefile.d"), "w"); 63 file.writeln(q{import reggae;}); 64 file.writeln(q{mixin build!(dubDefaultTarget!(), dubTestTarget!());}); 65 66 if(!options.noFetch) dubFetch(output, options); 67 } 68 69 70 private DubInfo _getDubInfo(T)(auto ref T output, in Options options) { 71 import std.array; 72 import std.file: exists; 73 import std.path: buildPath; 74 import std.stdio: writeln; 75 76 version(unittest) 77 gDubInfos = null; 78 79 if("default" !in gDubInfos) { 80 81 if(!buildPath(options.projectPath, "dub.selections.json").exists) { 82 output.writeln("[Reggae] Calling dub upgrade to create dub.selections.json"); 83 callDub(options, ["dub", "upgrade"]); 84 } 85 86 DubConfigurations getConfigsImpl() { 87 immutable dubBuildArgs = ["dub", "--annotate", "build", "--compiler=" ~ options.dCompiler, 88 "--print-configs", "--build=docs"]; 89 immutable dubBuildOutput = callDub(options, dubBuildArgs); 90 return getConfigurations(dubBuildOutput); 91 } 92 93 DubConfigurations getConfigs() { 94 try { 95 return getConfigsImpl; 96 } catch(Exception _) { 97 output.writeln("[Reggae] Calling 'dub fetch' since getting the configuration failed"); 98 dubFetch(output, options); 99 return getConfigsImpl; 100 } 101 } 102 103 const configs = getConfigs(); 104 105 bool oneConfigOk; 106 Exception dubDescribeFailure; 107 108 if(configs.configurations.empty) { 109 const descOutput = callDub(options, ["dub", "describe"]); 110 oneConfigOk = true; 111 gDubInfos["default"] = getDubInfo(descOutput); 112 } else { 113 foreach(config; configs.configurations) { 114 try { 115 const descOutput = callDub(options, ["dub", "describe", "-c", config]); 116 oneConfigOk = true; 117 gDubInfos[config] = getDubInfo(descOutput); 118 119 //dub adds certain flags to certain configurations automatically but these flags 120 //don't know up in the output to `dub describe`. Special case them here. 121 122 //unittest should only apply to the main package, hence [0] 123 if(config == "unittest") gDubInfos[config].packages[0].dflags ~= " -unittest"; 124 125 callPreBuildCommands(options, gDubInfos[config]); 126 127 128 } catch(Exception ex) { 129 dubDescribeFailure = ex; 130 } 131 } 132 133 gDubInfos["default"] = gDubInfos[configs.default_]; 134 } 135 136 if(!oneConfigOk) throw dubDescribeFailure; 137 } 138 139 return gDubInfos["default"]; 140 } 141 142 private string callDub(in Options options, in string[] args) { 143 import std.process; 144 import std.exception: enforce; 145 import std.conv: text; 146 import std.string; 147 148 const string[string] env = null; 149 Config config = Config.none; 150 size_t maxOutput = size_t.max; 151 immutable workDir = options.projectPath; 152 153 immutable ret = execute(args, env, config, maxOutput, workDir); 154 enforce(ret.status == 0, text("Error calling '", args.join(" "), "' (", ret.status, ")", ":\n", 155 ret.output)); 156 return ret.output; 157 } 158 159 private void callPreBuildCommands(in Options options, in DubInfo dubInfo) { 160 import std.process; 161 import std.string: replace; 162 163 const string[string] env = null; 164 Config config = Config.none; 165 size_t maxOutput = size_t.max; 166 immutable workDir = options.projectPath; 167 168 foreach(c; dubInfo.packages[0].preBuildCommands) { 169 auto cmd = c.replace("$project", options.projectPath); 170 immutable ret = executeShell(cmd, env, config, maxOutput, workDir); 171 enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output)); 172 } 173 } 174 175 private void dubFetch(T)(auto ref T output, in Options options) @trusted { 176 import std.array: join, replace; 177 import std.stdio: writeln; 178 import std.path: buildPath; 179 import std.json: parseJSON, JSON_TYPE; 180 import std.file: readText; 181 182 const fileName = buildPath(options.projectPath, "dub.selections.json"); 183 auto json = parseJSON(readText(fileName)); 184 185 auto versions = json["versions"]; 186 187 foreach(dubPackage, versionJson; versions.object) { 188 189 // skip the ones with a defined path 190 if(versionJson.type != JSON_TYPE.STRING) continue; 191 192 // versions are usually `==1.2.3`, so strip the sign 193 const version_ = versionJson.str.replace("==", ""); 194 195 if(!needDubFetch(dubPackage, version_)) continue; 196 197 const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_]; 198 199 output.writeln("[Reggae] Fetching package with command '", cmd.join(" "), "'"); 200 try 201 callDub(options, cmd); 202 catch(Exception ex) { 203 // local packages can't be fetched, so it's normal to get an error 204 if(!options.dubLocalPackages) 205 throw ex; 206 } 207 } 208 } 209 210 // dub fetch can sometimes take >10s (!) despite the package already being 211 // on disk 212 bool needDubFetch(in string dubPackage, in string version_) { 213 import std.path: buildPath; 214 import std.process: environment; 215 import std.file: exists; 216 217 const path = buildPath(environment["HOME"], ".dub", "packages", dubPackage ~ "-" ~ version_); 218 return !path.exists; 219 } 220 221 222 void writeDubConfig(T)(auto ref T output, in Options options, File file) { 223 import reggae.dub.info: TargetType; 224 import std.stdio: writeln; 225 226 output.writeln("[Reggae] Writing dub configuration"); 227 228 file.writeln("import reggae.dub.info;"); 229 230 if(options.isDubProject) { 231 232 file.writeln("enum isDubProject = true;"); 233 auto dubInfo = _getDubInfo(output, options); 234 const targetType = dubInfo.packages[0].targetType; 235 236 file.writeln(`const configToDubInfo = assocList([`); 237 238 const keys = () @trusted { return gDubInfos.keys; }(); 239 foreach(config; keys) { 240 file.writeln(` assocEntry("`, config, `", `, gDubInfos[config], `),`); 241 } 242 file.writeln(`]);`); 243 file.writeln; 244 } else { 245 file.writeln("enum isDubProject = false;"); 246 } 247 }