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