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