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