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