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.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                     }
161 
162                     oneConfigOk = true;
163 
164                 } catch(Exception ex) {
165                     if(dubDescribeFailure !is null) dubDescribeFailure = ex;
166                 }
167             }
168 
169             if(configs.default_ !in gDubInfos)
170                 throw new Exception("Non-existent config info for " ~ configs.default_);
171 
172             gDubInfos["default"] = gDubInfos[configs.default_];
173        }
174 
175         if(!oneConfigOk) throw dubDescribeFailure;
176     }
177 
178     return gDubInfos["default"];
179 }
180 
181 private string callDub(T)(
182     auto ref T output,
183     in from!"reggae.options".Options options,
184     in string[] rawArgs,
185     from!"std.typecons".Flag!"maybeNoDeps" maybeNoDeps = from!"std.typecons".No.maybeNoDeps)
186 {
187     import reggae.io: log;
188     import std.process: execute, Config;
189     import std.exception: enforce;
190     import std.conv: text;
191     import std.string: join, split;
192     import std.path: buildPath;
193     import std.file: exists;
194 
195     const hasSelections = buildPath(options.projectPath, "dub.selections.json").exists;
196     string[] emptyArgs;
197     const noDepsArgs = hasSelections && maybeNoDeps ? ["--nodeps", "--skip-registry=all"] : emptyArgs;
198     const archArg = rawArgs[1] == "fetch" || rawArgs[1] == "upgrade"
199         ? emptyArgs
200         : ["--arch=" ~ options.dubArch.text];
201     const args = rawArgs ~ noDepsArgs ~ dubEnvArgs ~ archArg;
202     const string[string] env = null;
203     Config config = Config.none;
204     size_t maxOutput = size_t.max;
205     const workDir = options.projectPath;
206 
207     output.log("Calling `", args.join(" "), "`");
208     const ret = execute(args, env, config, maxOutput, workDir);
209     enforce(ret.status == 0,
210             text("Error calling `", args.join(" "), "` (", ret.status, ")", ":\n",
211                  ret.output));
212 
213     return ret.output;
214 }
215 
216 private string[] dubEnvArgs() {
217     import std.process: environment;
218     import std.string: split;
219     return environment.get("REGGAE_DUB_ARGS", "").split(" ");
220 }
221 
222 private void callPreBuildCommands(T)(auto ref T output,
223                                      in from!"reggae.options".Options options,
224                                      in from!"reggae.dub.json".DubInfo dubInfo)
225 {
226     import reggae.io: log;
227     import std.process: executeShell, Config;
228     import std.string: replace;
229     import std.exception: enforce;
230     import std.conv: text;
231 
232     const string[string] env = null;
233     Config config = Config.none;
234     size_t maxOutput = size_t.max;
235     immutable workDir = options.projectPath;
236 
237     if(dubInfo.packages.length == 0) return;
238 
239     foreach(const package_; dubInfo.packages) {
240         foreach(const dubCommandString; package_.preBuildCommands) {
241             auto cmd = dubCommandString.replace("$project", options.projectPath);
242             output.log("Executing pre-build command `", cmd, "`");
243             const ret = executeShell(cmd, env, config, maxOutput, workDir);
244             enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output));
245         }
246     }
247 }
248 
249 private void dubFetch(T)(auto ref T output,
250                          in from!"reggae.options".Options options)
251     @trusted
252 {
253     import reggae.io: log;
254     import std.array: join, replace;
255     import std.stdio: writeln;
256     import std.path: buildPath;
257     import std.json: parseJSON, JSONType;
258     import std.file: readText;
259 
260     const fileName = buildPath(options.projectPath, "dub.selections.json");
261     auto json = parseJSON(readText(fileName));
262 
263     auto versions = json["versions"];
264 
265     foreach(dubPackage, versionJson; versions.object) {
266 
267         // skip the ones with a defined path
268         if(versionJson.type != JSONType..string) continue;
269 
270         // versions are usually `==1.2.3`, so strip the sign
271         const version_ = versionJson.str.replace("==", "");
272 
273         if(!needDubFetch(dubPackage, version_)) continue;
274 
275 
276         const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_] ~ dubEnvArgs;
277 
278         try
279             callDub(output, options, cmd);
280         catch(Exception ex) {
281             // local packages can't be fetched, so it's normal to get an error
282             if(!options.dubLocalPackages)
283                 throw ex;
284         }
285     }
286 }
287 
288 // dub fetch can sometimes take >10s (!) despite the package already being
289 // on disk
290 bool needDubFetch(in string dubPackage, in string version_) {
291     import reggae.path: dubPackagesDir;
292     import std.path: buildPath;
293     import std.file: exists;
294 
295     return !buildPath(dubPackagesDir,
296                       dubPackage ~ "-" ~ version_, dubPackage ~ ".lock")
297         .exists;
298 }
299 
300 
301 void writeDubConfig(T)(auto ref T output,
302                        in from!"reggae.options".Options options,
303                        from!"std.stdio".File file) {
304     import reggae.io: log;
305     import reggae.dub.info: TargetType;
306 
307     output.log("Writing dub configuration");
308 
309     file.writeln("import reggae.dub.info;");
310 
311     if(options.isDubProject) {
312 
313         file.writeln("enum isDubProject = true;");
314         auto dubInfo = _getDubInfo(output, options);
315         const targetType = dubInfo.packages.length
316             ? dubInfo.packages[0].targetType
317             : TargetType.sourceLibrary;
318 
319         file.writeln(`const configToDubInfo = assocList([`);
320 
321         const keys = () @trusted { return gDubInfos.keys; }();
322         foreach(config; keys) {
323             file.writeln(`    assocEntry("`, config, `", `, gDubInfos[config], `),`);
324         }
325         file.writeln(`]);`);
326         file.writeln;
327     } else {
328         file.writeln("enum isDubProject = false;");
329     }
330 }