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