1 /**
2    Extract build information by using dub as a library
3  */
4 module reggae.dub.interop.dublib;
5 
6 
7 import reggae.from;
8 import dub.generators.generator: ProjectGenerator;
9 
10 
11 // Not shared because, for unknown reasons, dub registers compilers
12 // in thread-local storage so we register the compilers in all
13 // threads. In normal dub usage it's done in of one dub's static
14 // constructors. In one thread.
15 static this() nothrow {
16     import dub.compilers.compiler: registerCompiler;
17     import dub.compilers.dmd: DMDCompiler;
18     import dub.compilers.ldc: LDCCompiler;
19     import dub.compilers.gdc: GDCCompiler;
20 
21     try {
22         registerCompiler(new DMDCompiler);
23         registerCompiler(new LDCCompiler);
24         registerCompiler(new GDCCompiler);
25     } catch(Exception e) {
26         import std.stdio: stderr;
27         try
28             stderr.writeln("ERROR: ", e);
29         catch(Exception _) {}
30     }
31 }
32 
33 
34 struct Dub {
35     import reggae.dub.interop.configurations: DubConfigurations;
36     import reggae.dub.info: DubInfo;
37     import reggae.options: Options;
38     import dub.project: Project;
39 
40     private Project _project;
41 
42     this(in Options options) @safe {
43         import std.exception: enforce;
44         import std.path: buildPath;
45         import std.file: exists;
46 
47         const path = buildPath(options.projectPath, "dub.selections.json");
48         enforce(path.exists, "Cannot create dub instance without dub.selections.json");
49 
50         _project = project(ProjectPath(options.projectPath));
51     }
52 
53     auto getPackage(in string dubPackage, in string version_) @trusted /*dub*/ {
54         import dub.dependency: Version;
55         return _project.packageManager.getPackage(dubPackage, Version(version_));
56     }
57 
58     DubConfigurations getConfigs(in from!"reggae.options".Options options) {
59         auto settings = generatorSettings(options.dCompiler.toCompiler);
60         return DubConfigurations(_project.configurations, _project.getDefaultConfiguration(settings.platform));
61     }
62 
63     DubInfo configToDubInfo
64         (in from!"reggae.options".Options options, in string config)
65         @trusted  // dub
66     {
67         auto generator = new InfoGenerator(_project);
68         generator.generate(generatorSettings(options.dCompiler.toCompiler, config));
69         return DubInfo(generator.dubPackages);
70     }
71 
72     void reinit() @trusted {
73         _project.reinit;
74     }
75 }
76 
77 
78 /// What it says on the tin
79 struct ProjectPath {
80     string value;
81 }
82 
83 /// Normally ~/.dub
84 struct UserPackagesPath {
85     string value = "/dev/null";
86 }
87 
88 /// Normally ~/.dub
89 UserPackagesPath userPackagesPath() @safe {
90     import std.process: environment;
91     import std.path: buildPath, isAbsolute;
92     import std.file: getcwd;
93 
94     version(Windows) {
95         immutable appDataDir = environment.get("APPDATA");
96         const path = buildPath(environment.get("LOCALAPPDATA", appDataDir), "dub");
97     } else version(Posix) {
98         string path = buildPath(environment.get("HOME"), ".dub/");
99         if(!path.isAbsolute)
100             path = buildPath(getcwd(), path);
101     } else
102           static assert(false, "Unknown system");
103 
104     return UserPackagesPath(path);
105 }
106 
107 struct SystemPackagesPath {
108     string value = "/dev/null";
109 }
110 
111 
112 SystemPackagesPath systemPackagesPath() @safe {
113     import std.process: environment;
114     import std.path: buildPath;
115 
116     version(Windows)
117         const path = buildPath(environment.get("ProgramData"), "dub/");
118     else version(Posix)
119         const path = "/var/lib/dub/";
120     else
121         static assert(false, "Unknown system");
122 
123     return SystemPackagesPath(path);
124 }
125 
126 enum Compiler {
127     dmd,
128     ldc,
129     gdc,
130     ldmd,
131 }
132 
133 
134 package from!"reggae.dub.info".DubInfo configToDubInfo
135     (O)
136     (auto ref O output, in from!"reggae.options".Options options, in string config)
137     @trusted  // dub
138 {
139     import reggae.dub.info: DubInfo;
140     import reggae.dub.interop.dublib: project, generatorSettings, InfoGenerator,
141         systemPackagesPath, userPackagesPath, ProjectPath, Compiler;
142     import std.conv: to;
143 
144     auto proj = project(
145         ProjectPath(options.projectPath),
146         systemPackagesPath,
147         userPackagesPath,
148     );
149 
150     auto generator = new InfoGenerator(proj);
151     generator.generate(generatorSettings(options.dCompiler.toCompiler, config));
152 
153     return DubInfo(generator.dubPackages);
154 }
155 
156 
157 Compiler toCompiler(in string compiler) @safe pure {
158     import std.conv: to;
159     if(compiler == "ldc2") return Compiler.ldc;
160     return compiler.to!Compiler;
161 }
162 
163 
164 struct Path {
165     string value;
166 }
167 
168 struct JSONString {
169     string value;
170 }
171 
172 
173 struct DubPackages {
174 
175     import dub.packagemanager: PackageManager;
176 
177     private PackageManager _packageManager;
178     private string _userPackagesPath;
179 
180     this(in ProjectPath projectPath,
181          in SystemPackagesPath systemPackagesPath,
182          in UserPackagesPath userPackagesPath)
183         @safe
184     {
185         _packageManager = packageManager(projectPath, systemPackagesPath, userPackagesPath);
186         _userPackagesPath = userPackagesPath.value;
187     }
188 
189     /**
190        Takes a path to a zipped dub package and stores it in the appropriate
191        user packages path.
192        The metadata is usually taken from the dub registry via an HTTP
193        API call.
194      */
195     void storeZip(in Path zip, in JSONString metadata) @safe {
196         import dub.internal.vibecompat.data.json: parseJson;
197         import dub.internal.vibecompat.inet.path: NativePath;
198         import std.path: buildPath;
199 
200         auto metadataString = metadata.value.idup;
201         auto metadataJson = () @trusted { return parseJson(metadataString); }();
202         const name = () @trusted { return cast(string) metadataJson["name"]; }();
203         const version_ = () @trusted { return cast(string) metadataJson["version"]; }();
204 
205         () @trusted {
206             _packageManager.storeFetchedPackage(
207                 NativePath(zip.value),
208                 metadataJson,
209                 NativePath(buildPath(_userPackagesPath, "packages", name ~ "-" ~ version_, name)),
210             );
211         }();
212     }
213 }
214 
215 
216 auto generatorSettings(in Compiler compiler = Compiler.dmd, in string config = "") @safe {
217     import dub.compilers.compiler: getCompiler;
218     import dub.generators.generator: GeneratorSettings;
219     import dub.platform: determineBuildPlatform;
220     import std.conv: text;
221 
222     GeneratorSettings ret;
223 
224     ret.buildType = "debug";  // FIXME
225     const compilerName = compiler.text;
226     ret.compiler = () @trusted { return getCompiler(compilerName); }();
227     ret.platform.compilerBinary = compilerName;  // FIXME? (absolute path?)
228     ret.config = config;
229     ret.platform = () @trusted { return determineBuildPlatform; }();
230 
231     return ret;
232 }
233 
234 
235 auto project(in ProjectPath projectPath) @safe {
236     return project(projectPath, systemPackagesPath, userPackagesPath);
237 }
238 
239 
240 auto project(in ProjectPath projectPath,
241              in SystemPackagesPath systemPackagesPath,
242              in UserPackagesPath userPackagesPath)
243     @trusted
244 {
245     import dub.project: Project;
246     import dub.internal.vibecompat.inet.path: NativePath;
247 
248     auto pkgManager = packageManager(projectPath, systemPackagesPath, userPackagesPath);
249 
250     return new Project(pkgManager, NativePath(projectPath.value));
251 }
252 
253 
254 private auto dubPackage(in ProjectPath projectPath) @trusted {
255     import dub.internal.vibecompat.inet.path: NativePath;
256     import dub.package_: Package;
257     return new Package(recipe(projectPath), NativePath(projectPath.value));
258 }
259 
260 
261 private auto recipe(in ProjectPath projectPath) @safe {
262     import dub.recipe.packagerecipe: PackageRecipe;
263     import dub.recipe.json: parseJson;
264     import dub.recipe.sdl: parseSDL;
265     static import dub.internal.vibecompat.data.json;
266     import std.file: readText, exists;
267     import std.path: buildPath;
268 
269     PackageRecipe recipe;
270 
271     string inProjectPath(in string path) {
272         return buildPath(projectPath.value, path);
273     }
274 
275     if(inProjectPath("dub.sdl").exists) {
276         const text = readText(inProjectPath("dub.sdl"));
277         () @trusted { parseSDL(recipe, text, "parent", "dub.sdl"); }();
278         return recipe;
279     } else if(inProjectPath("dub.json").exists) {
280         auto text = readText(inProjectPath("dub.json"));
281         auto json = () @trusted { return dub.internal.vibecompat.data.json.parseJson(text); }();
282         () @trusted { parseJson(recipe, json, "" /*parent*/); }();
283         return recipe;
284     } else
285         throw new Exception("Could not find dub.sdl or dub.json in " ~ projectPath.value);
286 }
287 
288 
289 auto packageManager(in ProjectPath projectPath,
290                     in SystemPackagesPath systemPackagesPath,
291                     in UserPackagesPath userPackagesPath)
292     @trusted
293 {
294     import dub.internal.vibecompat.inet.path: NativePath;
295     import dub.packagemanager: PackageManager;
296 
297     const userPath = NativePath(userPackagesPath.value);
298     const systemPath = NativePath(systemPackagesPath.value);
299     const refreshPackages = false;
300 
301     auto pkgManager = new PackageManager(userPath, systemPath, refreshPackages);
302     // In dub proper, this initialisation is done in commandline.d
303     // in the function runDubCommandLine. If not not, subpackages
304     // won't work.
305     pkgManager.getOrLoadPackage(NativePath(projectPath.value));
306 
307     return pkgManager;
308 }
309 
310 
311 class InfoGenerator: ProjectGenerator {
312     import reggae.dub.info: DubPackage;
313     import dub.project: Project;
314     import dub.generators.generator: GeneratorSettings;
315     import dub.compilers.buildsettings: BuildSettings;
316 
317     DubPackage[] dubPackages;
318 
319     this(Project project) @trusted {
320         super(project);
321     }
322 
323     /** Copied from the dub documentation:
324 
325         Overridden in derived classes to implement the actual generator functionality.
326 
327         The function should go through all targets recursively. The first target
328         (which is guaranteed to be there) is
329         $(D targets[m_project.rootPackage.name]). The recursive descent is then
330         done using the $(D TargetInfo.linkDependencies) list.
331 
332         This method is also potentially responsible for running the pre and post
333         build commands, while pre and post generate commands are already taken
334         care of by the $(D generate) method.
335 
336         Params:
337             settings = The generator settings used for this run
338             targets = A map from package name to TargetInfo that contains all
339                 binary targets to be built.
340     */
341     override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) @trusted {
342 
343         import dub.compilers.buildsettings: BuildSetting;
344 
345         DubPackage nameToDubPackage(in string targetName, in bool isFirstPackage = false) {
346             const targetInfo = targets[targetName];
347             auto newBuildSettings = targetInfo.buildSettings.dup;
348             settings.compiler.prepareBuildSettings(newBuildSettings,
349                                                    BuildSetting.noOptions /*???*/);
350             DubPackage pkg;
351 
352             pkg.name = targetInfo.pack.name;
353             pkg.path = targetInfo.pack.path.toNativeString;
354             pkg.targetFileName = newBuildSettings.targetName;
355             pkg.files = newBuildSettings.sourceFiles.dup;
356             pkg.targetType = cast(typeof(pkg.targetType)) newBuildSettings.targetType;
357             pkg.dependencies = targetInfo.dependencies.dup;
358 
359             enum sameNameProperties = [
360                 "mainSourceFile", "dflags", "lflags", "importPaths",
361                 "stringImportPaths", "versions", "libs",
362                 "preBuildCommands", "postBuildCommands",
363             ];
364             static foreach(prop; sameNameProperties) {
365                 mixin(`pkg.`, prop, ` = newBuildSettings.`, prop, `;`);
366             }
367 
368             if(isFirstPackage)  // unfortunately due to dub's `invokeLinker`
369                 adjustMainPackage(pkg, settings, newBuildSettings);
370 
371             return pkg;
372         }
373 
374 
375         bool[string] visited;
376 
377         const rootName = m_project.rootPackage.name;
378         dubPackages ~= nameToDubPackage(rootName, true);
379 
380         foreach(i, dep; targets[rootName].linkDependencies) {
381             if (dep in visited) continue;
382             visited[dep] = true;
383             dubPackages ~= nameToDubPackage(dep);
384         }
385     }
386 
387     private static adjustMainPackage(ref DubPackage pkg,
388                                      in GeneratorSettings settings,
389                                      in BuildSettings buildSettings)
390     {
391         import std.algorithm.searching: canFind, startsWith;
392         import std.algorithm.iteration: filter, map;
393         import std.array: array;
394 
395         // this is copied from dub's DMDCompiler.invokeLinker since
396         // unfortunately that function modifies the arguments before
397         // calling the linker, but we can't call it either since it
398         // has side-effects. Until dub gets refactored, this has to
399         // be maintained in parallel. Sigh.
400 
401         pkg.lflags = pkg.lflags.map!(a => "-L" ~ a).array;
402 
403         if(settings.platform.platform.canFind("linux"))
404             pkg.lflags = "-L--no-as-needed" ~ pkg.lflags;
405 
406         static bool isLinkerDFlag(in string arg) {
407             switch (arg) {
408             default:
409                 if (arg.startsWith("-defaultlib=")) return true;
410                 return false;
411             case "-g", "-gc", "-m32", "-m64", "-shared", "-lib", "-m32mscoff":
412                 return true;
413             }
414         }
415 
416         pkg.lflags ~= buildSettings.dflags.filter!isLinkerDFlag.array;
417     }
418 }