1 /**
2    A module for providing interop between reggae and dub
3 */
4 module reggae.dub.interop;
5 
6 
7 import reggae.from;
8 public import reggae.dub.interop.reggaefile;
9 
10 
11 from!"reggae.dub.info".DubInfo[string] gDubInfos;
12 
13 
14 void writeDubConfig(T)(auto ref T output,
15                        in from!"reggae.options".Options options,
16                        from!"std.stdio".File file) {
17     import reggae.io: log;
18     import reggae.dub.info: TargetType;
19     import reggae.dub.interop.fetch: dubFetch;
20     import reggae.dub.interop.dublib: Dub;
21 
22     output.log("Writing dub configuration");
23     scope(exit) output.log("Finished writing dub configuration");
24 
25     if(!options.isDubProject) {
26         file.writeln("enum isDubProject = false;");
27         return;
28     }
29 
30     // must check for dub.selections.json before creating dub instance
31     const dubSelectionsJson = ensureDubSelectionsJson(output, options);
32 
33     auto dub = Dub(options);
34 
35     dubFetch(output, dub, options, dubSelectionsJson);
36 
37     file.writeln("import reggae.dub.info;");
38     file.writeln("enum isDubProject = true;");
39 
40     output.log("    Getting dub build information");
41     auto dubInfo = getDubInfo(output, dub, options);
42     output.log("    Got     dub build information");
43 
44     const targetType = dubInfo.packages.length
45         ? dubInfo.packages[0].targetType
46         : TargetType.sourceLibrary;
47 
48     file.writeln(`const configToDubInfo = assocList([`);
49 
50     const keys = () @trusted { return gDubInfos.keys; }();
51     foreach(config; keys) {
52         file.writeln(`    assocEntry("`, config, `", `, gDubInfos[config], `),`);
53     }
54     file.writeln(`]);`);
55     file.writeln;
56 }
57 
58 
59 private string ensureDubSelectionsJson
60     (O)
61     (ref O output, in from!"reggae.options".Options options)
62     @safe
63 {
64     import reggae.dub.interop.exec: callDub, dubEnvArgs;
65     import reggae.io: log;
66     import reggae.path: buildPath;
67     import std.file: exists;
68     import std.exception: enforce;
69 
70     const path = buildPath(options.projectPath, "dub.selections.json");
71 
72     if(!path.exists) {
73         output.log("Creating dub.selections.json");
74         const cmd = ["dub", "upgrade"] ~ dubEnvArgs;
75         callDub(output, options, cmd);
76     }
77 
78     enforce(path.exists, "Could not create dub.selections.json");
79 
80     return path;
81 }
82 
83 
84 
85 private from!"reggae.dub.info".DubInfo getDubInfo
86     (T)
87     (auto ref T output,
88      ref from!"reggae.dub.interop.dublib".Dub dub,
89      in from!"reggae.options".Options options)
90 {
91     import reggae.dub.interop: gDubInfos;
92     import reggae.io: log;
93     import reggae.path: buildPath;
94     import std.file: exists;
95     import std.exception: enforce;
96 
97     version(unittest) gDubInfos = null;
98 
99     if("default" !in gDubInfos) {
100 
101         enforce(buildPath(options.projectPath, "dub.selections.json").exists,
102                 "Cannot find dub.selections.json");
103 
104         auto settings = dub.getGeneratorSettings(options);
105         const configs = dubConfigurations(output, dub, options, settings);
106         bool atLeastOneConfigOk;
107         Exception dubInfoFailure;
108 
109         foreach(config; configs.configurations) {
110             try {
111                 handleDubConfig(output, dub, options, settings, config);
112                 atLeastOneConfigOk = true;
113             } catch(Exception ex) {
114                 output.log("ERROR: Could not get info for configuration ", config, ": ", ex.msg);
115                 if(dubInfoFailure is null) dubInfoFailure = ex;
116             }
117         }
118 
119         gDubInfos["default"] = gDubInfos[configs.default_];
120 
121         if(!atLeastOneConfigOk) {
122             assert(dubInfoFailure !is null,
123                    "Internal error: no configurations worked and no exception to throw");
124             throw dubInfoFailure;
125         }
126     }
127 
128     return gDubInfos["default"];
129 }
130 
131 private from!"reggae.dub.interop.configurations".DubConfigurations
132 dubConfigurations
133     (O)
134     (ref O output,
135      ref from!"reggae.dub.interop.dublib".Dub dub,
136      in from!"reggae.options".Options options,
137      from!"dub.generators.generator".GeneratorSettings settings)
138 {
139     import reggae.dub.interop.configurations: DubConfigurations;
140     import reggae.io: log;
141 
142     if(options.dubConfig == "") {
143 
144         output.log("Getting dub configurations");
145         auto ret = dub.getConfigs(settings.platform);
146         output.log("Number of dub configurations: ", ret.configurations.length);
147 
148         // this happens e.g. the targetType is "none"
149         if(ret.configurations.length == 0)
150             return DubConfigurations([""], "");
151 
152         return ret;
153     } else {
154         return DubConfigurations([options.dubConfig], options.dubConfig);
155     }
156 }
157 
158 private void handleDubConfig
159     (O)
160     (ref O output,
161      ref from!"reggae.dub.interop.dublib".Dub dub,
162      in from!"reggae.options".Options options,
163      from!"dub.generators.generator".GeneratorSettings settings,
164      in string config)
165 {
166     import reggae.io: log;
167     import std.conv: text;
168 
169     output.log("Querying dub configuration '", config, "'");
170     gDubInfos[config] = dub.configToDubInfo(settings, config);
171 
172     // dub adds certain flags to certain configurations automatically but these flags
173     // don't know up in the output to `dub describe`. Special case them here.
174 
175     // unittest should only apply to the main package, hence [0].
176     // This doesn't show up in `dub describe`, it's secret info that dub knows
177     // so we have to add it manually here.
178     if(config == "unittest") {
179         if(config !in gDubInfos)
180             throw new Exception(
181                 text("Configuration `", config, "` not found in ",
182                      () @trusted { return gDubInfos.keys; }()));
183         if(gDubInfos[config].packages.length == 0)
184             throw new Exception(
185                 text("No main package in `", config, "` configuration"));
186         gDubInfos[config].packages[0].dflags ~= "-unittest";
187     }
188 
189     try
190         callPreBuildCommands(output, options, gDubInfos[config]);
191     catch(Exception e) {
192         output.log("Error calling prebuild commands: ", e.msg);
193         throw e;
194     }
195 }
196 
197 
198 private void callPreBuildCommands(T)(auto ref T output,
199                                      in from!"reggae.options".Options options,
200                                      in from!"reggae.dub.info".DubInfo dubInfo)
201     @safe
202 {
203     import reggae.io: log;
204     import std.process: executeShell, Config;
205     import std..string: replace;
206     import std.exception: enforce;
207     import std.conv: text;
208 
209     const string[string] env = null;
210     Config config = Config.none;
211     size_t maxOutput = size_t.max;
212     immutable workDir = options.projectPath;
213 
214     if(dubInfo.packages.length == 0) return;
215 
216     foreach(const package_; dubInfo.packages) {
217         foreach(const dubCommandString; package_.preBuildCommands) {
218             auto cmd = dubCommandString.replace("$project", options.projectPath);
219             output.log("Executing pre-build command `", cmd, "`");
220             const ret = executeShell(cmd, env, config, maxOutput, workDir);
221             enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output));
222         }
223     }
224 }