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