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 }