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 }