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 dub.compilers.dmd: DMDCompiler; 344 import dub.compilers.ldc: LDCCompiler; 345 import std.algorithm.searching: canFind, startsWith; 346 import std.algorithm.iteration: filter, map; 347 import std.array: array; 348 349 // this is copied from dub's DMDCompiler.invokeLinker since 350 // unfortunately that function modifies the arguments before 351 // calling the linker, but we can't call it either since it 352 // has side-effects. Until dub gets refactored, this has to 353 // be maintained in parallel. Sigh. 354 355 pkg.lflags = pkg.lflags.map!(a => "-L" ~ a).array; 356 357 if(settings.platform.platform.canFind("linux")) 358 pkg.lflags = "-L--no-as-needed" ~ pkg.lflags; 359 360 pkg.lflags ~= settings.platform.compiler == "ldc" 361 ? buildSettings.dflags.filter!(LDCCompiler.isLinkerDFlag).array // ldc2 / ldmd2 362 : buildSettings.dflags.filter!(DMDCompiler.isLinkerDFlag).array; 363 } 364 }