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     const string[string] env = null;
124     Config config = Config.none;
125     size_t maxOutput = size_t.max;
126     immutable workDir = options.projectPath;
127 
128     foreach(cmd; dubInfo.packages[0].preBuildCommands) {
129         writeln("Calling dub prebuildCommand '", cmd, "'");
130         immutable ret = executeShell(cmd, env, config, maxOutput, workDir);
131         enforce(ret.status == 0, text("Error calling ", cmd, ":\n", ret.output));
132     }
133 }
134 
135 private void dubFetch(in Options options) @trusted {
136     import std.array: join, replace;
137     import std.stdio: writeln;
138     import std.path: buildPath;
139     import std.json: parseJSON, JSON_TYPE;
140     import std.file: readText;
141 
142     const fileName = buildPath(options.projectPath, "dub.selections.json");
143     auto json = parseJSON(readText(fileName));
144 
145     auto versions = json["versions"];
146 
147     foreach(dubPackage, versionJson; versions.object) {
148 
149         // skip the ones with a defined path
150         if(versionJson.type != JSON_TYPE.STRING) continue;
151 
152         // versions are usually `==1.2.3`, so strip the sign
153         const version_ = versionJson.str.replace("==", "");
154 
155         if(!needDubFetch(dubPackage, version_)) continue;
156 
157         const cmd = ["dub", "fetch", dubPackage, "--version=" ~ version_];
158 
159         writeln("[Reggae] Fetching package with command '", cmd.join(" "), "'");
160         try
161             callDub(options, cmd);
162         catch(Exception ex) {
163             // local packages can't be fetched, so it's normal to get an error
164             if(!options.dubLocalPackages)
165                 throw ex;
166         }
167     }
168 }
169 
170 // dub fetch can sometimes take >10s (!) despite the package already being
171 // on disk
172 bool needDubFetch(in string dubPackage, in string version_) {
173     import std.path: buildPath;
174     import std.process: environment;
175     import std.file: exists;
176 
177     const path = buildPath(environment["HOME"], ".dub", "packages", dubPackage ~ "-" ~ version_);
178     return !path.exists;
179 }
180 
181 enum TargetType {
182     executable,
183     library,
184     staticLibrary,
185     sourceLibrary,
186     none,
187 }
188 
189 
190 void writeDubConfig(in Options options, File file) {
191     import std.conv: to;
192     import std.stdio: writeln;
193 
194     writeln("[Reggae] Writing dub configuration");
195 
196     file.writeln("import reggae.dub.info;");
197 
198     if(options.isDubProject) {
199 
200         file.writeln("enum isDubProject = true;");
201         auto dubInfo = _getDubInfo(options);
202         const targetType = dubInfo.packages[0].targetType;
203 
204         try {
205             targetType.to!TargetType;
206         } catch(Exception ex) {
207             throw new Exception(text("Unsupported dub targetType '", targetType, "'"));
208         }
209 
210         file.writeln(`const configToDubInfo = assocList([`);
211 
212         const keys = () @trusted { return gDubInfos.keys; }();
213         foreach(config; keys) {
214             file.writeln(`    assocEntry("`, config, `", `, gDubInfos[config], `),`);
215         }
216         file.writeln(`]);`);
217         file.writeln;
218     } else {
219         file.writeln("enum isDubProject = false;");
220     }
221 }