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