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 }