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 }