1 /** 2 A package manager. 3 4 Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff, Sönke Ludwig 7 */ 8 module dub.dub; 9 10 import dub.compilers.compiler; 11 import dub.dependency; 12 import dub.dependencyresolver; 13 import dub.internal.utils; 14 import dub.internal.vibecompat.core.file; 15 import dub.internal.vibecompat.core.log; 16 import dub.internal.vibecompat.data.json; 17 import dub.internal.vibecompat.inet.url; 18 import dub.package_; 19 import dub.packagemanager; 20 import dub.packagesuppliers; 21 import dub.project; 22 import dub.generators.generator; 23 import dub.init; 24 25 import std.algorithm; 26 import std.array : array, replace; 27 import std.conv : to; 28 import std.exception : enforce; 29 import std.file; 30 import std.process : environment; 31 import std.range : assumeSorted, empty; 32 import std..string; 33 import std.encoding : sanitize; 34 35 // Workaround for libcurl liker errors when building with LDC 36 version (LDC) pragma(lib, "curl"); 37 38 // Set output path and options for coverage reports 39 version (DigitalMars) version (D_Coverage) static if (__VERSION__ >= 2068) 40 { 41 shared static this() 42 { 43 import core.runtime, std.file, std.path, std.stdio; 44 dmd_coverSetMerge(true); 45 auto path = buildPath(dirName(thisExePath()), "../cov"); 46 if (!path.exists) 47 mkdir(path); 48 dmd_coverDestPath(path); 49 } 50 } 51 52 static this() 53 { 54 import dub.compilers.dmd : DMDCompiler; 55 import dub.compilers.gdc : GDCCompiler; 56 import dub.compilers.ldc : LDCCompiler; 57 registerCompiler(new DMDCompiler); 58 registerCompiler(new GDCCompiler); 59 registerCompiler(new LDCCompiler); 60 } 61 62 deprecated("use defaultRegistryURLs") enum defaultRegistryURL = defaultRegistryURLs[0]; 63 64 /// The URL to the official package registry and it's default fallback registries. 65 enum defaultRegistryURLs = [ 66 "https://code.dlang.org/", 67 "https://code-mirror.dlang.io/", 68 "https://dub-registry.herokuapp.com/", 69 ]; 70 71 /** Returns a default list of package suppliers. 72 73 This will contain a single package supplier that points to the official 74 package registry. 75 76 See_Also: `defaultRegistryURLs` 77 */ 78 PackageSupplier[] defaultPackageSuppliers() 79 { 80 logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]); 81 return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)]; 82 } 83 84 /** Returns a registry package supplier according to protocol. 85 86 Allowed protocols are dub+http(s):// and maven+http(s)://. 87 */ 88 PackageSupplier getRegistryPackageSupplier(string url) 89 { 90 switch (url.startsWith("dub+", "mvn+", "file://")) 91 { 92 case 1: 93 return new RegistryPackageSupplier(URL(url[4..$])); 94 case 2: 95 return new MavenRegistryPackageSupplier(URL(url[4..$])); 96 case 3: 97 return new FileSystemPackageSupplier(NativePath(url[7..$])); 98 default: 99 return new RegistryPackageSupplier(URL(url)); 100 } 101 } 102 103 unittest 104 { 105 auto dubRegistryPackageSupplier = getRegistryPackageSupplier("dub+https://code.dlang.org"); 106 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 107 108 dubRegistryPackageSupplier = getRegistryPackageSupplier("https://code.dlang.org"); 109 assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); 110 111 auto mavenRegistryPackageSupplier = getRegistryPackageSupplier("mvn+http://localhost:8040/maven/libs-release/dubpackages"); 112 assert(mavenRegistryPackageSupplier.description.canFind(" http://localhost:8040/maven/libs-release/dubpackages")); 113 114 auto fileSystemPackageSupplier = getRegistryPackageSupplier("file:///etc/dubpackages"); 115 assert(fileSystemPackageSupplier.description.canFind(" " ~ NativePath("/etc/dubpackages").toNativeString)); 116 } 117 118 /** Provides a high-level entry point for DUB's functionality. 119 120 This class provides means to load a certain project (a root package with 121 all of its dependencies) and to perform high-level operations as found in 122 the command line interface. 123 */ 124 class Dub { 125 private { 126 bool m_dryRun = false; 127 PackageManager m_packageManager; 128 PackageSupplier[] m_packageSuppliers; 129 NativePath m_rootPath; 130 SpecialDirs m_dirs; 131 DubConfig m_config; 132 NativePath m_projectPath; 133 Project m_project; 134 NativePath m_overrideSearchPath; 135 string m_defaultCompiler; 136 string m_defaultArchitecture; 137 } 138 139 /** The default placement location of fetched packages. 140 141 This property can be altered, so that packages which are downloaded as part 142 of the normal upgrade process are stored in a certain location. This is 143 how the "--local" and "--system" command line switches operate. 144 */ 145 PlacementLocation defaultPlacementLocation = PlacementLocation.user; 146 147 148 /** Initializes the instance for use with a specific root package. 149 150 Note that a package still has to be loaded using one of the 151 `loadPackage` overloads. 152 153 Params: 154 root_path = Path to the root package 155 additional_package_suppliers = A list of package suppliers to try 156 before the suppliers found in the configurations files and the 157 `defaultPackageSuppliers`. 158 skip_registry = Can be used to skip using the configured package 159 suppliers, as well as the default suppliers. 160 */ 161 this(string root_path = ".", PackageSupplier[] additional_package_suppliers = null, 162 SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none) 163 { 164 m_rootPath = NativePath(root_path); 165 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 166 167 init(); 168 169 if (skip_registry == SkipPackageSuppliers.none) 170 m_packageSuppliers = getPackageSuppliers(additional_package_suppliers); 171 else 172 m_packageSuppliers = getPackageSuppliers(additional_package_suppliers, skip_registry); 173 174 m_packageManager = new PackageManager(m_dirs.localRepository, m_dirs.systemSettings); 175 176 auto ccps = m_config.customCachePaths; 177 if (ccps.length) 178 m_packageManager.customCachePaths = ccps; 179 180 updatePackageSearchPath(); 181 } 182 183 unittest 184 { 185 scope (exit) environment.remove("DUB_REGISTRY"); 186 auto dub = new Dub(".", null, SkipPackageSuppliers.configured); 187 assert(dub.m_packageSuppliers.length == 0); 188 environment["DUB_REGISTRY"] = "http://example.com/"; 189 dub = new Dub(".", null, SkipPackageSuppliers.configured); 190 assert(dub.m_packageSuppliers.length == 1); 191 environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; 192 dub = new Dub(".", null, SkipPackageSuppliers.configured); 193 assert(dub.m_packageSuppliers.length == 2); 194 dub = new Dub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); 195 assert(dub.m_packageSuppliers.length == 3); 196 } 197 198 /** Get the list of package suppliers. 199 200 Params: 201 additional_package_suppliers = A list of package suppliers to try 202 before the suppliers found in the configurations files and the 203 `defaultPackageSuppliers`. 204 skip_registry = Can be used to skip using the configured package 205 suppliers, as well as the default suppliers. 206 */ 207 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry) 208 { 209 PackageSupplier[] ps = additional_package_suppliers; 210 211 if (skip_registry < SkipPackageSuppliers.all) 212 { 213 ps ~= environment.get("DUB_REGISTRY", null) 214 .splitter(";") 215 .map!(url => getRegistryPackageSupplier(url)) 216 .array; 217 } 218 219 if (skip_registry < SkipPackageSuppliers.configured) 220 { 221 ps ~= m_config.registryURLs 222 .map!(url => getRegistryPackageSupplier(url)) 223 .array; 224 } 225 226 if (skip_registry < SkipPackageSuppliers.standard) 227 ps ~= defaultPackageSuppliers(); 228 229 return ps; 230 } 231 232 /// ditto 233 public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers) 234 { 235 return getPackageSuppliers(additional_package_suppliers, m_config.skipRegistry); 236 } 237 238 unittest 239 { 240 scope (exit) environment.remove("DUB_REGISTRY"); 241 auto dub = new Dub(".", null, SkipPackageSuppliers.none); 242 243 dub.m_config = new DubConfig(Json(["skipRegistry": Json("none")]), null); 244 assert(dub.getPackageSuppliers(null).length == 1); 245 246 dub.m_config = new DubConfig(Json(["skipRegistry": Json("configured")]), null); 247 assert(dub.getPackageSuppliers(null).length == 0); 248 249 dub.m_config = new DubConfig(Json(["skipRegistry": Json("standard")]), null); 250 assert(dub.getPackageSuppliers(null).length == 0); 251 252 environment["DUB_REGISTRY"] = "http://example.com/"; 253 assert(dub.getPackageSuppliers(null).length == 1); 254 } 255 256 /** Initializes the instance with a single package search path, without 257 loading a package. 258 259 This constructor corresponds to the "--bare" option of the command line 260 interface. Use 261 */ 262 this(NativePath override_path) 263 { 264 init(); 265 m_overrideSearchPath = override_path; 266 m_packageManager = new PackageManager(NativePath(), NativePath(), false); 267 updatePackageSearchPath(); 268 } 269 270 private void init() 271 { 272 import std.file : tempDir; 273 version(Windows) { 274 m_dirs.systemSettings = NativePath(environment.get("ProgramData")) ~ "dub/"; 275 immutable appDataDir = environment.get("APPDATA"); 276 m_dirs.userSettings = NativePath(appDataDir) ~ "dub/"; 277 m_dirs.localRepository = NativePath(environment.get("LOCALAPPDATA", appDataDir)) ~ "dub"; 278 279 migrateRepositoryFromRoaming(m_dirs.userSettings ~"\\packages", m_dirs.localRepository ~ "\\packages"); 280 281 } else version(Posix){ 282 m_dirs.systemSettings = NativePath("/var/lib/dub/"); 283 m_dirs.userSettings = NativePath(environment.get("HOME")) ~ ".dub/"; 284 if (!m_dirs.userSettings.absolute) 285 m_dirs.userSettings = NativePath(getcwd()) ~ m_dirs.userSettings; 286 m_dirs.localRepository = m_dirs.userSettings; 287 } 288 289 m_dirs.temp = NativePath(tempDir); 290 291 m_config = new DubConfig(jsonFromFile(m_dirs.systemSettings ~ "settings.json", true), m_config); 292 m_config = new DubConfig(jsonFromFile(NativePath(thisExePath).parentPath ~ "../etc/dub/settings.json", true), m_config); 293 m_config = new DubConfig(jsonFromFile(m_dirs.userSettings ~ "settings.json", true), m_config); 294 295 determineDefaultCompiler(); 296 297 m_defaultArchitecture = m_config.defaultArchitecture; 298 } 299 300 version(Windows) 301 private void migrateRepositoryFromRoaming(NativePath roamingDir, NativePath localDir) 302 { 303 immutable roamingDirPath = roamingDir.toNativeString(); 304 if (!existsDirectory(roamingDir)) return; 305 306 immutable localDirPath = localDir.toNativeString(); 307 logInfo("Detected a package cache in " ~ roamingDirPath ~ ". This will be migrated to " ~ localDirPath ~ ". Please wait..."); 308 if (!existsDirectory(localDir)) 309 { 310 mkdirRecurse(localDirPath); 311 } 312 313 runCommand(`xcopy /s /e /y "` ~ roamingDirPath ~ `" "` ~ localDirPath ~ `" > NUL`); 314 rmdirRecurse(roamingDirPath); 315 } 316 317 318 @property void dryRun(bool v) { m_dryRun = v; } 319 320 /** Returns the root path (usually the current working directory). 321 */ 322 @property NativePath rootPath() const { return m_rootPath; } 323 /// ditto 324 @property void rootPath(NativePath root_path) 325 { 326 m_rootPath = root_path; 327 if (!m_rootPath.absolute) m_rootPath = NativePath(getcwd()) ~ m_rootPath; 328 } 329 330 /// Returns the name listed in the dub.json of the current 331 /// application. 332 @property string projectName() const { return m_project.name; } 333 334 @property NativePath projectPath() const { return m_projectPath; } 335 336 @property string[] configurations() const { return m_project.configurations; } 337 338 @property inout(PackageManager) packageManager() inout { return m_packageManager; } 339 340 @property inout(Project) project() inout { return m_project; } 341 342 /** Returns the default compiler binary to use for building D code. 343 344 If set, the "defaultCompiler" field of the DUB user or system 345 configuration file will be used. Otherwise the PATH environment variable 346 will be searched for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2" 347 (in that order, taking into account operating system specific file 348 extensions) and the first match is returned. If no match is found, "dmd" 349 will be used. 350 */ 351 @property string defaultCompiler() const { return m_defaultCompiler; } 352 353 /** Returns the default architecture to use for building D code. 354 355 If set, the "defaultArchitecture" field of the DUB user or system 356 configuration file will be used. Otherwise null will be returned. 357 */ 358 @property string defaultArchitecture() const { return m_defaultArchitecture; } 359 360 /** Loads the package that resides within the configured `rootPath`. 361 */ 362 void loadPackage() 363 { 364 loadPackage(m_rootPath); 365 } 366 367 /// Loads the package from the specified path as the main project package. 368 void loadPackage(NativePath path) 369 { 370 m_projectPath = path; 371 updatePackageSearchPath(); 372 m_project = new Project(m_packageManager, m_projectPath); 373 } 374 375 /// Loads a specific package as the main project package (can be a sub package) 376 void loadPackage(Package pack) 377 { 378 m_projectPath = pack.path; 379 updatePackageSearchPath(); 380 m_project = new Project(m_packageManager, pack); 381 } 382 383 /** Loads a single file package. 384 385 Single-file packages are D files that contain a package receipe comment 386 at their top. A recipe comment must be a nested `/+ ... +/` style 387 comment, containing the virtual recipe file name and a colon, followed by the 388 recipe contents (what would normally be in dub.sdl/dub.json). 389 390 Example: 391 --- 392 /+ dub.sdl: 393 name "test" 394 dependency "vibe-d" version="~>0.7.29" 395 +/ 396 import vibe.http.server; 397 398 void main() 399 { 400 auto settings = new HTTPServerSettings; 401 settings.port = 8080; 402 listenHTTP(settings, &hello); 403 } 404 405 void hello(HTTPServerRequest req, HTTPServerResponse res) 406 { 407 res.writeBody("Hello, World!"); 408 } 409 --- 410 411 The script above can be invoked with "dub --single test.d". 412 */ 413 void loadSingleFilePackage(NativePath path) 414 { 415 import dub.recipe.io : parsePackageRecipe; 416 import std.file : mkdirRecurse, readText; 417 import std.path : baseName, stripExtension; 418 419 path = makeAbsolute(path); 420 421 string file_content = readText(path.toNativeString()); 422 423 if (file_content.startsWith("#!")) { 424 auto idx = file_content.indexOf('\n'); 425 enforce(idx > 0, "The source fine doesn't contain anything but a shebang line."); 426 file_content = file_content[idx+1 .. $]; 427 } 428 429 file_content = file_content.strip(); 430 431 string recipe_content; 432 433 if (file_content.startsWith("/+")) { 434 file_content = file_content[2 .. $]; 435 auto idx = file_content.indexOf("+/"); 436 enforce(idx >= 0, "Missing \"+/\" to close comment."); 437 recipe_content = file_content[0 .. idx].strip(); 438 } else throw new Exception("The source file must start with a recipe comment."); 439 440 auto nidx = recipe_content.indexOf('\n'); 441 442 auto idx = recipe_content.indexOf(':'); 443 enforce(idx > 0 && (nidx < 0 || nidx > idx), 444 "The first line of the recipe comment must list the recipe file name followed by a colon (e.g. \"/+ dub.sdl:\")."); 445 auto recipe_filename = recipe_content[0 .. idx]; 446 recipe_content = recipe_content[idx+1 .. $]; 447 auto recipe_default_package_name = path.toString.baseName.stripExtension.strip; 448 449 auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name); 450 enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files."); 451 enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths."); 452 enforce(recipe.buildSettings.importPaths.length == 0, "Single-file packages are not allowed to specify import paths."); 453 recipe.buildSettings.sourceFiles[""] = [path.toNativeString()]; 454 recipe.buildSettings.sourcePaths[""] = []; 455 recipe.buildSettings.importPaths[""] = []; 456 recipe.buildSettings.mainSourceFile = path.toNativeString(); 457 if (recipe.buildSettings.targetType == TargetType.autodetect) 458 recipe.buildSettings.targetType = TargetType.executable; 459 460 auto pack = new Package(recipe, path.parentPath, null, "~master"); 461 loadPackage(pack); 462 } 463 /// ditto 464 void loadSingleFilePackage(string path) 465 { 466 loadSingleFilePackage(NativePath(path)); 467 } 468 469 /** Disables the default search paths and only searches a specific directory 470 for packages. 471 */ 472 void overrideSearchPath(NativePath path) 473 { 474 if (!path.absolute) path = NativePath(getcwd()) ~ path; 475 m_overrideSearchPath = path; 476 updatePackageSearchPath(); 477 } 478 479 /** Gets the default configuration for a particular build platform. 480 481 This forwards to `Project.getDefaultConfiguration` and requires a 482 project to be loaded. 483 */ 484 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } 485 486 /** Attempts to upgrade the dependency selection of the loaded project. 487 488 Params: 489 options = Flags that control how the upgrade is carried out 490 packages_to_upgrade = Optional list of packages. If this list 491 contains one or more packages, only those packages will 492 be upgraded. Otherwise, all packages will be upgraded at 493 once. 494 */ 495 void upgrade(UpgradeOptions options, string[] packages_to_upgrade = null) 496 { 497 // clear non-existent version selections 498 if (!(options & UpgradeOptions.upgrade)) { 499 next_pack: 500 foreach (p; m_project.selections.selectedPackages) { 501 auto dep = m_project.selections.getSelectedVersion(p); 502 if (!dep.path.empty) { 503 auto path = dep.path; 504 if (!path.absolute) path = this.rootPath ~ path; 505 try if (m_packageManager.getOrLoadPackage(path)) continue; 506 catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } 507 } else { 508 if (m_packageManager.getPackage(p, dep.version_)) continue; 509 foreach (ps; m_packageSuppliers) { 510 try { 511 auto versions = ps.getVersions(p); 512 if (versions.canFind!(v => dep.matches(v))) 513 continue next_pack; 514 } catch (Exception e) { 515 logWarn("Error querying versions for %s, %s: %s", p, ps.description, e.msg); 516 logDebug("Full error: %s", e.toString().sanitize()); 517 } 518 } 519 } 520 521 logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep); 522 m_project.selections.deselectVersion(p); 523 } 524 } 525 526 Dependency[string] versions; 527 auto resolver = new DependencyVersionResolver(this, options); 528 foreach (p; packages_to_upgrade) 529 resolver.addPackageToUpgrade(p); 530 versions = resolver.resolve(m_project.rootPackage, m_project.selections); 531 532 if (options & UpgradeOptions.dryRun) { 533 bool any = false; 534 string rootbasename = getBasePackageName(m_project.rootPackage.name); 535 536 foreach (p, ver; versions) { 537 if (!ver.path.empty) continue; 538 539 auto basename = getBasePackageName(p); 540 if (basename == rootbasename) continue; 541 542 if (!m_project.selections.hasSelectedVersion(basename)) { 543 logInfo("Package %s would be selected with version %s.", 544 basename, ver); 545 any = true; 546 continue; 547 } 548 auto sver = m_project.selections.getSelectedVersion(basename); 549 if (!sver.path.empty) continue; 550 if (ver.version_ <= sver.version_) continue; 551 logInfo("Package %s would be upgraded from %s to %s.", 552 basename, sver, ver); 553 any = true; 554 } 555 if (any) logInfo("Use \"dub upgrade\" to perform those changes."); 556 return; 557 } 558 559 foreach (p; versions.byKey) { 560 auto ver = versions[p]; // Workaround for DMD 2.070.0 AA issue (crashes in aaApply2 if iterating by key+value) 561 assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p); 562 Package pack; 563 if (!ver.path.empty) { 564 try pack = m_packageManager.getOrLoadPackage(ver.path); 565 catch (Exception e) { 566 logDebug("Failed to load path based selection: %s", e.toString().sanitize); 567 continue; 568 } 569 } else { 570 pack = m_packageManager.getBestPackage(p, ver); 571 if (pack && m_packageManager.isManagedPackage(pack) 572 && ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0) 573 { 574 // TODO: only re-install if there is actually a new commit available 575 logInfo("Re-installing branch based dependency %s %s", p, ver.toString()); 576 m_packageManager.remove(pack); 577 pack = null; 578 } 579 } 580 581 FetchOptions fetchOpts; 582 fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; 583 if (!pack) fetch(p, ver, defaultPlacementLocation, fetchOpts, "getting selected version"); 584 if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) { 585 if (ver.path.empty) m_project.selections.selectVersion(p, ver.version_); 586 else { 587 NativePath relpath = ver.path; 588 if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path); 589 m_project.selections.selectVersion(p, relpath); 590 } 591 } 592 } 593 594 string[] missingDependenciesBeforeReinit = m_project.missingDependencies; 595 m_project.reinit(); 596 597 if (!m_project.hasAllDependencies) { 598 auto resolvedDependencies = setDifference( 599 assumeSorted(missingDependenciesBeforeReinit), 600 assumeSorted(m_project.missingDependencies) 601 ); 602 if (!resolvedDependencies.empty) 603 upgrade(options, m_project.missingDependencies); 604 } 605 606 if ((options & UpgradeOptions.select) && !(options & (UpgradeOptions.noSaveSelections | UpgradeOptions.dryRun))) 607 m_project.saveSelections(); 608 } 609 610 /** Generate project files for a specified generator. 611 612 Any existing project files will be overridden. 613 */ 614 void generateProject(string ide, GeneratorSettings settings) 615 { 616 auto generator = createProjectGenerator(ide, m_project); 617 if (m_dryRun) return; // TODO: pass m_dryRun to the generator 618 generator.generate(settings); 619 } 620 621 /** Executes tests on the current project. 622 623 Throws an exception, if unittests failed. 624 */ 625 void testProject(GeneratorSettings settings, string config, NativePath custom_main_file) 626 { 627 if (!custom_main_file.empty && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; 628 629 if (config.length == 0) { 630 // if a custom main file was given, favor the first library configuration, so that it can be applied 631 if (!custom_main_file.empty) config = m_project.getDefaultConfiguration(settings.platform, false); 632 // else look for a "unittest" configuration 633 if (!config.length && m_project.rootPackage.configurations.canFind("unittest")) config = "unittest"; 634 // if not found, fall back to the first "library" configuration 635 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, false); 636 // if still nothing found, use the first executable configuration 637 if (!config.length) config = m_project.getDefaultConfiguration(settings.platform, true); 638 } 639 640 auto generator = createProjectGenerator("build", m_project); 641 642 auto test_config = format("%s-test-%s", m_project.rootPackage.name.replace(".", "-").replace(":", "-"), config); 643 644 BuildSettings lbuildsettings = settings.buildSettings; 645 m_project.addBuildSettings(lbuildsettings, settings, config, null, true); 646 if (lbuildsettings.targetType == TargetType.none) { 647 logInfo(`Configuration '%s' has target type "none". Skipping test.`, config); 648 return; 649 } 650 651 if (lbuildsettings.targetType == TargetType.executable && config == "unittest") { 652 logInfo("Running custom 'unittest' configuration.", config); 653 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 654 settings.config = config; 655 } else if (lbuildsettings.sourceFiles.empty) { 656 logInfo(`No source files found in configuration '%s'. Falling back to "dub -b unittest".`, config); 657 if (!custom_main_file.empty) logWarn("Ignoring custom main file."); 658 settings.config = m_project.getDefaultConfiguration(settings.platform); 659 } else { 660 import std.algorithm : remove; 661 662 logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType); 663 664 BuildSettingsTemplate tcinfo = m_project.rootPackage.recipe.getConfiguration(config).buildSettings; 665 tcinfo.targetType = TargetType.executable; 666 tcinfo.targetName = test_config; 667 // HACK for vibe.d's legacy main() behavior: 668 tcinfo.versions[""] ~= "VibeCustomMain"; 669 m_project.rootPackage.recipe.buildSettings.versions[""] = m_project.rootPackage.recipe.buildSettings.versions.get("", null).remove!(v => v == "VibeDefaultMain"); 670 // TODO: remove this ^ once vibe.d has removed the default main implementation 671 672 auto mainfil = tcinfo.mainSourceFile; 673 if (!mainfil.length) mainfil = m_project.rootPackage.recipe.buildSettings.mainSourceFile; 674 675 string custommodname; 676 if (!custom_main_file.empty) { 677 import std.path; 678 tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.rootPackage.path).toNativeString(); 679 tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); 680 custommodname = custom_main_file.head.toString().baseName(".d"); 681 } 682 683 // prepare the list of tested modules 684 string[] import_modules; 685 foreach (file; lbuildsettings.sourceFiles) { 686 if (file.endsWith(".d")) { 687 auto fname = NativePath(file).head.toString(); 688 if (NativePath(file).relativeTo(m_project.rootPackage.path) == NativePath(mainfil)) { 689 logWarn("Excluding main source file %s from test.", mainfil); 690 tcinfo.excludedSourceFiles[""] ~= mainfil; 691 continue; 692 } 693 if (fname == "package.d") { 694 logWarn("Excluding package.d file from test due to https://issues.dlang.org/show_bug.cgi?id=11847"); 695 continue; 696 } 697 import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, NativePath(file), m_project.rootPackage.path); 698 } 699 } 700 701 // generate main file 702 NativePath mainfile = getTempFile("dub_test_root", ".d"); 703 tcinfo.sourceFiles[""] ~= mainfile.toNativeString(); 704 tcinfo.mainSourceFile = mainfile.toNativeString(); 705 if (!m_dryRun) { 706 auto fil = openFile(mainfile, FileMode.createTrunc); 707 scope(exit) fil.close(); 708 fil.write("module dub_test_root;\n"); 709 fil.write("import std.typetuple;\n"); 710 foreach (mod; import_modules) fil.write(format("static import %s;\n", mod)); 711 fil.write("alias allModules = TypeTuple!("); 712 foreach (i, mod; import_modules) { 713 if (i > 0) fil.write(", "); 714 fil.write(mod); 715 } 716 fil.write(");\n"); 717 if (custommodname.length) { 718 fil.write(format("import %s;\n", custommodname)); 719 } else { 720 fil.write(q{ 721 import std.stdio; 722 import core.runtime; 723 724 void main() { writeln("All unit tests have been run successfully."); } 725 shared static this() { 726 version (Have_tested) { 727 import tested; 728 import core.runtime; 729 import std.exception; 730 Runtime.moduleUnitTester = () => true; 731 //runUnitTests!app(new JsonTestResultWriter("results.json")); 732 enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed."); 733 } 734 } 735 }); 736 } 737 } 738 m_project.rootPackage.recipe.configurations ~= ConfigurationInfo(test_config, tcinfo); 739 m_project = new Project(m_packageManager, m_project.rootPackage); 740 741 settings.config = test_config; 742 } 743 744 generator.generate(settings); 745 } 746 747 /** Prints the specified build settings necessary for building the root package. 748 */ 749 void listProjectData(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) 750 { 751 import std.stdio; 752 import std.ascii : newline; 753 754 // Split comma-separated lists 755 string[] requestedDataSplit = 756 requestedData 757 .map!(a => a.splitter(",").map!strip) 758 .joiner() 759 .array(); 760 761 auto data = m_project.listBuildSettings(settings, requestedDataSplit, list_type); 762 763 string delimiter; 764 final switch (list_type) with (ListBuildSettingsFormat) { 765 case list: delimiter = newline ~ newline; break; 766 case listNul: delimiter = "\0\0"; break; 767 case commandLine: delimiter = " "; break; 768 case commandLineNul: delimiter = "\0\0"; break; 769 } 770 771 write(data.joiner(delimiter)); 772 if (delimiter != "\0\0") writeln(); 773 } 774 775 /// Cleans intermediate/cache files of the given package 776 void cleanPackage(NativePath path) 777 { 778 logInfo("Cleaning package at %s...", path.toNativeString()); 779 enforce(!Package.findPackageFile(path).empty, "No package found.", path.toNativeString()); 780 781 // TODO: clear target files and copy files 782 783 if (existsFile(path ~ ".dub/build")) rmdirRecurse((path ~ ".dub/build").toNativeString()); 784 if (existsFile(path ~ ".dub/obj")) rmdirRecurse((path ~ ".dub/obj").toNativeString()); 785 if (existsFile(path ~ ".dub/metadata_cache.json")) std.file.remove((path ~ ".dub/metadata_cache.json").toNativeString()); 786 787 auto p = Package.load(path); 788 if (p.getBuildSettings().targetType == TargetType.none) { 789 foreach (sp; p.subPackages.filter!(sp => !sp.path.empty)) { 790 cleanPackage(path ~ sp.path); 791 } 792 } 793 } 794 795 /// Fetches the package matching the dependency and places it in the specified location. 796 Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") 797 { 798 Json pinfo; 799 PackageSupplier supplier; 800 foreach(ps; m_packageSuppliers){ 801 try { 802 pinfo = ps.fetchPackageRecipe(packageId, dep, (options & FetchOptions.usePrerelease) != 0); 803 if (pinfo.type == Json.Type.null_) 804 continue; 805 supplier = ps; 806 break; 807 } catch(Exception e) { 808 logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg); 809 logDebug("Full error: %s", e.toString().sanitize()); 810 } 811 } 812 enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString()); 813 string ver = pinfo["version"].get!string; 814 815 NativePath placement; 816 final switch (location) { 817 case PlacementLocation.local: placement = m_rootPath; break; 818 case PlacementLocation.user: placement = m_dirs.localRepository ~ "packages/"; break; 819 case PlacementLocation.system: placement = m_dirs.systemSettings ~ "packages/"; break; 820 } 821 822 // always upgrade branch based versions - TODO: actually check if there is a new commit available 823 Package existing; 824 try existing = m_packageManager.getPackage(packageId, ver, placement); 825 catch (Exception e) { 826 logWarn("Failed to load existing package %s: %s", ver, e.msg); 827 logDiagnostic("Full error: %s", e.toString().sanitize); 828 } 829 830 if (options & FetchOptions.printOnly) { 831 if (existing && existing.version_ != Version(ver)) 832 logInfo("A new version for %s is available (%s -> %s). Run \"dub upgrade %s\" to switch.", 833 packageId, existing.version_, ver, packageId); 834 return null; 835 } 836 837 if (existing) { 838 if (!ver.startsWith("~") || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) { 839 // TODO: support git working trees by performing a "git pull" instead of this 840 logDiagnostic("Package %s %s (%s) is already present with the latest version, skipping upgrade.", 841 packageId, ver, placement); 842 return existing; 843 } else { 844 logInfo("Removing %s %s to prepare replacement with a new version.", packageId, ver); 845 if (!m_dryRun) m_packageManager.remove(existing); 846 } 847 } 848 849 if (reason.length) logInfo("Fetching %s %s (%s)...", packageId, ver, reason); 850 else logInfo("Fetching %s %s...", packageId, ver); 851 if (m_dryRun) return null; 852 853 logDebug("Acquiring package zip file"); 854 855 auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $]; 856 clean_package_version = clean_package_version.replace("+", "_"); // + has special meaning for Optlink 857 if (!placement.existsFile()) 858 mkdirRecurse(placement.toNativeString()); 859 NativePath dstpath = placement ~ (packageId ~ "-" ~ clean_package_version); 860 if (!dstpath.existsFile()) 861 mkdirRecurse(dstpath.toNativeString()); 862 863 // Support libraries typically used with git submodules like ae. 864 // Such libraries need to have ".." as import path but this can create 865 // import path leakage. 866 dstpath = dstpath ~ packageId; 867 868 import std.datetime : seconds; 869 auto lock = lockFile(dstpath.toNativeString() ~ ".lock", 30.seconds); // possibly wait for other dub instance 870 if (dstpath.existsFile()) 871 { 872 m_packageManager.refresh(false); 873 return m_packageManager.getPackage(packageId, ver, dstpath); 874 } 875 876 // repeat download on corrupted zips, see #1336 877 foreach_reverse (i; 0..3) 878 { 879 import std.zip : ZipException; 880 881 auto path = getTempFile(packageId, ".zip"); 882 supplier.fetchPackage(path, packageId, dep, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? 883 scope(exit) std.file.remove(path.toNativeString()); 884 logDiagnostic("Placing to %s...", placement.toNativeString()); 885 886 try { 887 return m_packageManager.storeFetchedPackage(path, pinfo, dstpath); 888 } catch (ZipException e) { 889 logInfo("Failed to extract zip archive for %s %s...", packageId, ver); 890 // rethrow the exception at the end of the loop 891 if (i == 0) 892 throw e; 893 } 894 } 895 assert(0, "Should throw a ZipException instead."); 896 } 897 898 /** Removes a specific locally cached package. 899 900 This will delete the package files from disk and removes the 901 corresponding entry from the list of known packages. 902 903 Params: 904 pack = Package instance to remove 905 */ 906 void remove(in Package pack) 907 { 908 logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); 909 if (!m_dryRun) m_packageManager.remove(pack); 910 } 911 912 /// Compatibility overload. Use the version without a `force_remove` argument instead. 913 void remove(in Package pack, bool force_remove) 914 { 915 remove(pack); 916 } 917 918 /// @see remove(string, string, RemoveLocation) 919 enum RemoveVersionWildcard = "*"; 920 921 /** Removes one or more versions of a locally cached package. 922 923 This will remove a given package with a specified version from the 924 given location. It will remove at most one package, unless `version_` 925 is set to `RemoveVersionWildcard`. 926 927 Params: 928 package_id = Name of the package to be removed 929 location_ = Specifies the location to look for the given package 930 name/version. 931 resolve_version = Callback to select package version. 932 */ 933 void remove(string package_id, PlacementLocation location, 934 scope size_t delegate(in Package[] packages) resolve_version) 935 { 936 enforce(!package_id.empty); 937 if (location == PlacementLocation.local) { 938 logInfo("To remove a locally placed package, make sure you don't have any data" 939 ~ "\nleft in it's directory and then simply remove the whole directory."); 940 throw new Exception("dub cannot remove locally installed packages."); 941 } 942 943 Package[] packages; 944 945 // Retrieve packages to be removed. 946 foreach(pack; m_packageManager.getPackageIterator(package_id)) 947 if (m_packageManager.isManagedPackage(pack)) 948 packages ~= pack; 949 950 // Check validity of packages to be removed. 951 if(packages.empty) { 952 throw new Exception("Cannot find package to remove. (" 953 ~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'" 954 ~ ")"); 955 } 956 957 // Sort package list in ascending version order 958 packages.sort!((a, b) => a.version_ < b.version_); 959 960 immutable idx = resolve_version(packages); 961 if (idx == size_t.max) 962 return; 963 else if (idx != packages.length) 964 packages = packages[idx .. idx + 1]; 965 966 logDebug("Removing %s packages.", packages.length); 967 foreach(pack; packages) { 968 try { 969 remove(pack); 970 logInfo("Removed %s, version %s.", package_id, pack.version_); 971 } catch (Exception e) { 972 logError("Failed to remove %s %s: %s", package_id, pack.version_, e.msg); 973 logInfo("Continuing with other packages (if any)."); 974 } 975 } 976 } 977 978 /// Compatibility overload. Use the version without a `force_remove` argument instead. 979 void remove(string package_id, PlacementLocation location, bool force_remove, 980 scope size_t delegate(in Package[] packages) resolve_version) 981 { 982 remove(package_id, location, resolve_version); 983 } 984 985 /** Removes a specific version of a package. 986 987 Params: 988 package_id = Name of the package to be removed 989 version_ = Identifying a version or a wild card. If an empty string 990 is passed, the package will be removed from the location, if 991 there is only one version retrieved. This will throw an 992 exception, if there are multiple versions retrieved. 993 location_ = Specifies the location to look for the given package 994 name/version. 995 */ 996 void remove(string package_id, string version_, PlacementLocation location) 997 { 998 remove(package_id, location, (in packages) { 999 if (version_ == RemoveVersionWildcard) 1000 return packages.length; 1001 if (version_.empty && packages.length > 1) { 1002 logError("Cannot remove package '" ~ package_id ~ "', there are multiple possibilities at location\n" 1003 ~ "'" ~ to!string(location) ~ "'."); 1004 logError("Available versions:"); 1005 foreach(pack; packages) 1006 logError(" %s", pack.version_); 1007 throw new Exception("Please specify a individual version using --version=... or use the" 1008 ~ " wildcard --version=" ~ RemoveVersionWildcard ~ " to remove all versions."); 1009 } 1010 foreach (i, p; packages) { 1011 if (p.version_ == Version(version_)) 1012 return i; 1013 } 1014 throw new Exception("Cannot find package to remove. (" 1015 ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'" 1016 ~ ")"); 1017 }); 1018 } 1019 1020 /// Compatibility overload. Use the version without a `force_remove` argument instead. 1021 void remove(string package_id, string version_, PlacementLocation location, bool force_remove) 1022 { 1023 remove(package_id, version_, location); 1024 } 1025 1026 /** Adds a directory to the list of locally known packages. 1027 1028 Forwards to `PackageManager.addLocalPackage`. 1029 1030 Params: 1031 path = Path to the package 1032 ver = Optional version to associate with the package (can be left 1033 empty) 1034 system = Make the package known system wide instead of user wide 1035 (requires administrator privileges). 1036 1037 See_Also: `removeLocalPackage` 1038 */ 1039 void addLocalPackage(string path, string ver, bool system) 1040 { 1041 if (m_dryRun) return; 1042 m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); 1043 } 1044 1045 /** Removes a directory from the list of locally known packages. 1046 1047 Forwards to `PackageManager.removeLocalPackage`. 1048 1049 Params: 1050 path = Path to the package 1051 system = Make the package known system wide instead of user wide 1052 (requires administrator privileges). 1053 1054 See_Also: `addLocalPackage` 1055 */ 1056 void removeLocalPackage(string path, bool system) 1057 { 1058 if (m_dryRun) return; 1059 m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1060 } 1061 1062 /** Registers a local directory to search for packages to use for satisfying 1063 dependencies. 1064 1065 Params: 1066 path = Path to a directory containing package directories 1067 system = Make the package known system wide instead of user wide 1068 (requires administrator privileges). 1069 1070 See_Also: `removeSearchPath` 1071 */ 1072 void addSearchPath(string path, bool system) 1073 { 1074 if (m_dryRun) return; 1075 m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1076 } 1077 1078 /** Unregisters a local directory search path. 1079 1080 Params: 1081 path = Path to a directory containing package directories 1082 system = Make the package known system wide instead of user wide 1083 (requires administrator privileges). 1084 1085 See_Also: `addSearchPath` 1086 */ 1087 void removeSearchPath(string path, bool system) 1088 { 1089 if (m_dryRun) return; 1090 m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); 1091 } 1092 1093 /** Queries all package suppliers with the given query string. 1094 1095 Returns a list of tuples, where the first entry is the human readable 1096 name of the package supplier and the second entry is the list of 1097 matched packages. 1098 1099 See_Also: `PackageSupplier.searchPackages` 1100 */ 1101 auto searchPackages(string query) 1102 { 1103 import std.typecons : Tuple, tuple; 1104 Tuple!(string, PackageSupplier.SearchResult[])[] results; 1105 foreach (ps; this.m_packageSuppliers) { 1106 try 1107 results ~= tuple(ps.description, ps.searchPackages(query)); 1108 catch (Exception e) { 1109 logWarn("Searching %s for '%s' failed: %s", ps.description, query, e.msg); 1110 } 1111 } 1112 return results.filter!(tup => tup[1].length); 1113 } 1114 1115 /** Returns a list of all available versions (including branches) for a 1116 particular package. 1117 1118 The list returned is based on the registered package suppliers. Local 1119 packages are not queried in the search for versions. 1120 1121 See_also: `getLatestVersion` 1122 */ 1123 Version[] listPackageVersions(string name) 1124 { 1125 Version[] versions; 1126 foreach (ps; this.m_packageSuppliers) { 1127 try versions ~= ps.getVersions(name); 1128 catch (Exception e) { 1129 logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg); 1130 } 1131 } 1132 return versions.sort().uniq.array; 1133 } 1134 1135 /** Returns the latest available version for a particular package. 1136 1137 This function returns the latest numbered version of a package. If no 1138 numbered versions are available, it will return an available branch, 1139 preferring "~master". 1140 1141 Params: 1142 package_name: The name of the package in question. 1143 prefer_stable: If set to `true` (the default), returns the latest 1144 stable version, even if there are newer pre-release versions. 1145 1146 See_also: `listPackageVersions` 1147 */ 1148 Version getLatestVersion(string package_name, bool prefer_stable = true) 1149 { 1150 auto vers = listPackageVersions(package_name); 1151 enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'."); 1152 auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array; 1153 if (prefer_stable && final_versions.length) return final_versions[$-1]; 1154 else if (vers[$-1].isBranch) return vers[$-1]; 1155 else return vers[$-1]; 1156 } 1157 1158 /** Initializes a directory with a package skeleton. 1159 1160 Params: 1161 path = Path of the directory to create the new package in. The 1162 directory will be created if it doesn't exist. 1163 deps = List of dependencies to add to the package recipe. 1164 type = Specifies the type of the application skeleton to use. 1165 format = Determines the package recipe format to use. 1166 recipe_callback = Optional callback that can be used to 1167 customize the recipe before it gets written. 1168 */ 1169 void createEmptyPackage(NativePath path, string[] deps, string type, 1170 PackageFormat format = PackageFormat.sdl, 1171 scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null) 1172 { 1173 if (!path.absolute) path = m_rootPath ~ path; 1174 path.normalize(); 1175 1176 string[string] depVers; 1177 string[] notFound; // keep track of any failed packages in here 1178 foreach (dep; deps) { 1179 Version ver; 1180 try { 1181 ver = getLatestVersion(dep); 1182 depVers[dep] = ver.isBranch ? ver.toString() : "~>" ~ ver.toString(); 1183 } catch (Exception e) { 1184 notFound ~= dep; 1185 } 1186 } 1187 1188 if(notFound.length > 1){ 1189 throw new Exception(.format("Couldn't find packages: %-(%s, %).", notFound)); 1190 } 1191 else if(notFound.length == 1){ 1192 throw new Exception(.format("Couldn't find package: %-(%s, %).", notFound)); 1193 } 1194 1195 if (m_dryRun) return; 1196 1197 initPackage(path, depVers, type, format, recipe_callback); 1198 1199 //Act smug to the user. 1200 logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); 1201 } 1202 1203 /** Converts the package recipe of the loaded root package to the given format. 1204 1205 Params: 1206 destination_file_ext = The file extension matching the desired 1207 format. Possible values are "json" or "sdl". 1208 print_only = Print the converted recipe instead of writing to disk 1209 */ 1210 void convertRecipe(string destination_file_ext, bool print_only = false) 1211 { 1212 import std.path : extension; 1213 import std.stdio : stdout; 1214 import dub.recipe.io : serializePackageRecipe, writePackageRecipe; 1215 1216 if (print_only) { 1217 auto dst = stdout.lockingTextWriter; 1218 serializePackageRecipe(dst, m_project.rootPackage.rawRecipe, "dub."~destination_file_ext); 1219 return; 1220 } 1221 1222 auto srcfile = m_project.rootPackage.recipePath; 1223 auto srcext = srcfile.head.toString().extension; 1224 if (srcext == "."~destination_file_ext) { 1225 logInfo("Package format is already %s.", destination_file_ext); 1226 return; 1227 } 1228 1229 writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe); 1230 removeFile(srcfile); 1231 } 1232 1233 /** Runs DDOX to generate or serve documentation. 1234 1235 Params: 1236 run = If set to true, serves documentation on a local web server. 1237 Otherwise generates actual HTML files. 1238 generate_args = Additional command line arguments to pass to 1239 "ddox generate-html" or "ddox serve-html". 1240 */ 1241 void runDdox(bool run, string[] generate_args = null) 1242 { 1243 import std.process : browse; 1244 1245 if (m_dryRun) return; 1246 1247 // allow to choose a custom ddox tool 1248 auto tool = m_project.rootPackage.recipe.ddoxTool; 1249 if (tool.empty) tool = "ddox"; 1250 1251 auto tool_pack = m_packageManager.getBestPackage(tool, ">=0.0.0"); 1252 if (!tool_pack) tool_pack = m_packageManager.getBestPackage(tool, "~master"); 1253 if (!tool_pack) { 1254 logInfo("%s is not present, getting and storing it user wide", tool); 1255 tool_pack = fetch(tool, Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); 1256 } 1257 1258 auto ddox_dub = new Dub(null, m_packageSuppliers); 1259 ddox_dub.loadPackage(tool_pack.path); 1260 ddox_dub.upgrade(UpgradeOptions.select); 1261 1262 auto compiler_binary = this.defaultCompiler; 1263 1264 GeneratorSettings settings; 1265 settings.config = "application"; 1266 settings.compiler = getCompiler(compiler_binary); // TODO: not using --compiler ??? 1267 settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary, m_defaultArchitecture); 1268 settings.buildType = "debug"; 1269 settings.run = true; 1270 1271 auto filterargs = m_project.rootPackage.recipe.ddoxFilterArgs.dup; 1272 if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; 1273 1274 settings.runArgs = "filter" ~ filterargs ~ "docs.json"; 1275 ddox_dub.generateProject("build", settings); 1276 1277 auto p = tool_pack.path; 1278 p.endsWithSlash = true; 1279 auto tool_path = p.toNativeString(); 1280 1281 if (run) { 1282 settings.runArgs = ["serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~tool_path~"public"] ~ generate_args; 1283 browse("http://127.0.0.1:8080/"); 1284 } else { 1285 settings.runArgs = ["generate-html", "--navigation-type=ModuleTree", "docs.json", "docs"] ~ generate_args; 1286 } 1287 ddox_dub.generateProject("build", settings); 1288 1289 if (!run) { 1290 // TODO: ddox should copy those files itself 1291 version(Windows) runCommand(`xcopy /S /D "`~tool_path~`public\*" docs\`); 1292 else runCommand("rsync -ru '"~tool_path~"public/' docs/"); 1293 } 1294 } 1295 1296 private void updatePackageSearchPath() 1297 { 1298 if (!m_overrideSearchPath.empty) { 1299 m_packageManager.disableDefaultSearchPaths = true; 1300 m_packageManager.searchPath = [m_overrideSearchPath]; 1301 } else { 1302 auto p = environment.get("DUBPATH"); 1303 NativePath[] paths; 1304 1305 version(Windows) enum pathsep = ";"; 1306 else enum pathsep = ":"; 1307 if (p.length) paths ~= p.split(pathsep).map!(p => NativePath(p))().array(); 1308 m_packageManager.disableDefaultSearchPaths = false; 1309 m_packageManager.searchPath = paths; 1310 } 1311 } 1312 1313 private void determineDefaultCompiler() 1314 { 1315 import std.file : thisExePath; 1316 import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator; 1317 import std.process : environment; 1318 import std.range : front; 1319 1320 m_defaultCompiler = m_config.defaultCompiler.expandTilde; 1321 if (m_defaultCompiler.length && m_defaultCompiler.isAbsolute) 1322 return; 1323 1324 static immutable BinaryPrefix = `$DUB_BINARY_PATH`; 1325 if(m_defaultCompiler.startsWith(BinaryPrefix)) 1326 { 1327 m_defaultCompiler = thisExePath().dirName() ~ m_defaultCompiler[BinaryPrefix.length .. $]; 1328 return; 1329 } 1330 1331 if (!find!isDirSeparator(m_defaultCompiler).empty) 1332 throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ m_defaultCompiler ~ 1333 "\n\nUse \"$DUB_BINARY_PATH/../path/you/want\" instead."); 1334 1335 version (Windows) enum sep = ";", exe = ".exe"; 1336 version (Posix) enum sep = ":", exe = ""; 1337 1338 auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"]; 1339 // If a compiler name is specified, look for it next to dub. 1340 // Otherwise, look for any of the common compilers adjacent to dub. 1341 if (m_defaultCompiler.length) 1342 { 1343 string compilerPath = buildPath(thisExePath().dirName(), m_defaultCompiler ~ exe); 1344 if (existsFile(compilerPath)) 1345 { 1346 m_defaultCompiler = compilerPath; 1347 return; 1348 } 1349 } 1350 else 1351 { 1352 auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe))); 1353 if (!nextFound.empty) 1354 { 1355 m_defaultCompiler = buildPath(thisExePath().dirName(), nextFound.front ~ exe); 1356 return; 1357 } 1358 } 1359 1360 // If nothing found next to dub, search the user's PATH, starting 1361 // with the compiler name from their DUB config file, if specified. 1362 if (m_defaultCompiler.length) 1363 compilers = m_defaultCompiler ~ compilers; 1364 auto paths = environment.get("PATH", "").splitter(sep).map!NativePath; 1365 auto res = compilers.find!(bin => paths.canFind!(p => existsFile(p ~ (bin~exe)))); 1366 m_defaultCompiler = res.empty ? compilers[0] : res.front; 1367 } 1368 1369 private NativePath makeAbsolute(NativePath p) const { return p.absolute ? p : m_rootPath ~ p; } 1370 private NativePath makeAbsolute(string p) const { return makeAbsolute(NativePath(p)); } 1371 } 1372 1373 1374 /// Option flags for `Dub.fetch` 1375 enum FetchOptions 1376 { 1377 none = 0, 1378 forceBranchUpgrade = 1<<0, 1379 usePrerelease = 1<<1, 1380 forceRemove = 1<<2, /// Deprecated, does nothing. 1381 printOnly = 1<<3, 1382 } 1383 1384 /// Option flags for `Dub.upgrade` 1385 enum UpgradeOptions 1386 { 1387 none = 0, 1388 upgrade = 1<<1, /// Upgrade existing packages 1389 preRelease = 1<<2, /// inclde pre-release versions in upgrade 1390 forceRemove = 1<<3, /// Deprecated, does nothing. 1391 select = 1<<4, /// Update the dub.selections.json file with the upgraded versions 1392 dryRun = 1<<5, /// Instead of downloading new packages, just print a message to notify the user of their existence 1393 /*deprecated*/ printUpgradesOnly = dryRun, /// deprecated, use dryRun instead 1394 /*deprecated*/ useCachedResult = 1<<6, /// deprecated, has no effect 1395 noSaveSelections = 1<<7, /// Don't store updated selections on disk 1396 } 1397 1398 /// Determines which of the default package suppliers are queried for packages. 1399 enum SkipPackageSuppliers { 1400 none, /// Uses all configured package suppliers. 1401 standard, /// Does not use the default package suppliers (`defaultPackageSuppliers`). 1402 configured, /// Does not use default suppliers or suppliers configured in DUB's configuration file 1403 all /// Uses only manually specified package suppliers. 1404 } 1405 1406 private class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { 1407 protected { 1408 Dub m_dub; 1409 UpgradeOptions m_options; 1410 Dependency[][string] m_packageVersions; 1411 Package[string] m_remotePackages; 1412 SelectedVersions m_selectedVersions; 1413 Package m_rootPackage; 1414 bool[string] m_packagesToUpgrade; 1415 Package[PackageDependency] m_packages; 1416 TreeNodes[][TreeNode] m_children; 1417 } 1418 1419 1420 this(Dub dub, UpgradeOptions options) 1421 { 1422 m_dub = dub; 1423 m_options = options; 1424 } 1425 1426 void addPackageToUpgrade(string name) 1427 { 1428 m_packagesToUpgrade[name] = true; 1429 } 1430 1431 Dependency[string] resolve(Package root, SelectedVersions selected_versions) 1432 { 1433 m_rootPackage = root; 1434 m_selectedVersions = selected_versions; 1435 return super.resolve(TreeNode(root.name, Dependency(root.version_)), (m_options & UpgradeOptions.printUpgradesOnly) == 0); 1436 } 1437 1438 protected bool isFixedPackage(string pack) 1439 { 1440 return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade; 1441 } 1442 1443 protected override Dependency[] getAllConfigs(string pack) 1444 { 1445 if (auto pvers = pack in m_packageVersions) 1446 return *pvers; 1447 1448 if ((!(m_options & UpgradeOptions.upgrade) || isFixedPackage(pack)) && m_selectedVersions.hasSelectedVersion(pack)) { 1449 auto ret = [m_selectedVersions.getSelectedVersion(pack)]; 1450 logDiagnostic("Using fixed selection %s %s", pack, ret[0]); 1451 m_packageVersions[pack] = ret; 1452 return ret; 1453 } 1454 1455 logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); 1456 Version[] versions; 1457 foreach (p; m_dub.packageManager.getPackageIterator(pack)) 1458 versions ~= p.version_; 1459 1460 foreach (ps; m_dub.m_packageSuppliers) { 1461 try { 1462 auto vers = ps.getVersions(pack); 1463 vers.reverse(); 1464 if (!vers.length) { 1465 logDiagnostic("No versions for %s for %s", pack, ps.description); 1466 continue; 1467 } 1468 1469 versions ~= vers; 1470 break; 1471 } catch (Exception e) { 1472 logWarn("Package %s not found in %s: %s", pack, ps.description, e.msg); 1473 logDebug("Full error: %s", e.toString().sanitize); 1474 } 1475 } 1476 1477 // sort by version, descending, and remove duplicates 1478 versions = versions.sort!"a>b".uniq.array; 1479 1480 // move pre-release versions to the back of the list if no preRelease flag is given 1481 if (!(m_options & UpgradeOptions.preRelease)) 1482 versions = versions.filter!(v => !v.isPreRelease).array ~ versions.filter!(v => v.isPreRelease).array; 1483 1484 // filter out invalid/unreachable dependency specs 1485 versions = versions.filter!((v) { 1486 bool valid = getPackage(pack, Dependency(v)) !is null; 1487 if (!valid) logDiagnostic("Excluding invalid dependency specification %s %s from dependency resolution process.", pack, v); 1488 return valid; 1489 }).array; 1490 1491 if (!versions.length) logDiagnostic("Nothing found for %s", pack); 1492 else logDiagnostic("Return for %s: %s", pack, versions); 1493 1494 auto ret = versions.map!(v => Dependency(v)).array; 1495 m_packageVersions[pack] = ret; 1496 return ret; 1497 } 1498 1499 protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes) 1500 { 1501 if (!nodes.configs.path.empty && getPackage(pack, nodes.configs)) return [nodes.configs]; 1502 else return null; 1503 } 1504 1505 1506 protected override TreeNodes[] getChildren(TreeNode node) 1507 { 1508 if (auto pc = node in m_children) 1509 return *pc; 1510 auto ret = getChildrenRaw(node); 1511 m_children[node] = ret; 1512 return ret; 1513 } 1514 1515 private final TreeNodes[] getChildrenRaw(TreeNode node) 1516 { 1517 import std.array : appender; 1518 auto ret = appender!(TreeNodes[]); 1519 auto pack = getPackage(node.pack, node.config); 1520 if (!pack) { 1521 // this can hapen when the package description contains syntax errors 1522 logDebug("Invalid package in dependency tree: %s %s", node.pack, node.config); 1523 return null; 1524 } 1525 auto basepack = pack.basePackage; 1526 1527 foreach (d; pack.getAllDependenciesRange()) { 1528 auto dbasename = getBasePackageName(d.name); 1529 1530 // detect dependencies to the root package (or sub packages thereof) 1531 if (dbasename == basepack.name) { 1532 auto absdeppath = d.spec.mapToPath(pack.path).path; 1533 absdeppath.endsWithSlash = true; 1534 auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true); 1535 if (subpack) { 1536 auto desireddeppath = d.name == dbasename ? basepack.path : subpack.path; 1537 desireddeppath.endsWithSlash = true; 1538 enforce(d.spec.path.empty || absdeppath == desireddeppath, 1539 format("Dependency from %s to root package references wrong path: %s vs. %s", 1540 node.pack, absdeppath.toNativeString(), desireddeppath.toNativeString())); 1541 } 1542 ret ~= TreeNodes(d.name, node.config); 1543 continue; 1544 } 1545 1546 DependencyType dt; 1547 if (d.spec.optional) { 1548 if (d.spec.default_) dt = DependencyType.optionalDefault; 1549 else dt = DependencyType.optional; 1550 } else dt = DependencyType.required; 1551 1552 Dependency dspec = d.spec.mapToPath(pack.path); 1553 1554 // if not upgrading, use the selected version 1555 if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions && m_selectedVersions.hasSelectedVersion(dbasename)) 1556 dspec = m_selectedVersions.getSelectedVersion(dbasename); 1557 1558 // keep selected optional dependencies and avoid non-selected optional-default dependencies by default 1559 if (m_selectedVersions && !m_selectedVersions.bare) { 1560 if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename)) 1561 dt = DependencyType.optional; 1562 else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename)) 1563 dt = DependencyType.optionalDefault; 1564 } 1565 1566 ret ~= TreeNodes(d.name, dspec, dt); 1567 } 1568 return ret.data; 1569 } 1570 1571 protected override bool matches(Dependency configs, Dependency config) 1572 { 1573 if (!configs.path.empty) return configs.path == config.path; 1574 return configs.merge(config).valid; 1575 } 1576 1577 private Package getPackage(string name, Dependency dep) 1578 { 1579 auto key = PackageDependency(name, dep); 1580 if (auto pp = key in m_packages) 1581 return *pp; 1582 auto p = getPackageRaw(name, dep); 1583 m_packages[key] = p; 1584 return p; 1585 } 1586 1587 private Package getPackageRaw(string name, Dependency dep) 1588 { 1589 auto basename = getBasePackageName(name); 1590 1591 // for sub packages, first try to get them from the base package 1592 if (basename != name) { 1593 auto subname = getSubPackageName(name); 1594 auto basepack = getPackage(basename, dep); 1595 if (!basepack) return null; 1596 if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) { 1597 return sp; 1598 } else if (!basepack.subPackages.canFind!(p => p.path.length)) { 1599 // note: external sub packages are handled further below 1600 auto spr = basepack.getInternalSubPackage(subname); 1601 if (!spr.isNull) { 1602 auto sp = new Package(spr, basepack.path, basepack); 1603 m_remotePackages[sp.name] = sp; 1604 return sp; 1605 } else { 1606 logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep.version_); 1607 return null; 1608 } 1609 } else if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) { 1610 return ret; 1611 } else { 1612 logDiagnostic("External sub package %s %s not found.", name, dep.version_); 1613 return null; 1614 } 1615 } 1616 1617 // shortcut if the referenced package is the root package 1618 if (basename == m_rootPackage.basePackage.name) 1619 return m_rootPackage.basePackage; 1620 1621 if (!dep.path.empty) { 1622 try { 1623 auto ret = m_dub.packageManager.getOrLoadPackage(dep.path); 1624 if (dep.matches(ret.version_)) return ret; 1625 } catch (Exception e) { 1626 logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg); 1627 logDebug("Full error: %s", e.toString().sanitize); 1628 return null; 1629 } 1630 } 1631 1632 if (auto ret = m_dub.m_packageManager.getBestPackage(name, dep)) 1633 return ret; 1634 1635 auto key = name ~ ":" ~ dep.version_.toString(); 1636 if (auto ret = key in m_remotePackages) 1637 return *ret; 1638 1639 auto prerelease = (m_options & UpgradeOptions.preRelease) != 0; 1640 1641 auto rootpack = name.split(":")[0]; 1642 1643 foreach (ps; m_dub.m_packageSuppliers) { 1644 if (rootpack == name) { 1645 try { 1646 auto desc = ps.fetchPackageRecipe(name, dep, prerelease); 1647 if (desc.type == Json.Type.null_) 1648 continue; 1649 auto ret = new Package(desc); 1650 m_remotePackages[key] = ret; 1651 return ret; 1652 } catch (Exception e) { 1653 logDiagnostic("Metadata for %s %s could not be downloaded from %s: %s", name, dep, ps.description, e.msg); 1654 logDebug("Full error: %s", e.toString().sanitize); 1655 } 1656 } else { 1657 logDiagnostic("Package %s not found in base package description (%s). Downloading whole package.", name, dep.version_.toString()); 1658 try { 1659 FetchOptions fetchOpts; 1660 fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; 1661 m_dub.fetch(rootpack, dep, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description"); 1662 auto ret = m_dub.m_packageManager.getBestPackage(name, dep); 1663 if (!ret) { 1664 logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name); 1665 return null; 1666 } 1667 m_remotePackages[key] = ret; 1668 return ret; 1669 } catch (Exception e) { 1670 logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg); 1671 logDebug("Full error: %s", e.toString().sanitize); 1672 } 1673 } 1674 } 1675 1676 m_remotePackages[key] = null; 1677 1678 logWarn("Package %s %s could not be loaded either locally, or from the configured package registries.", name, dep); 1679 return null; 1680 } 1681 } 1682 1683 private struct SpecialDirs { 1684 NativePath temp; 1685 NativePath userSettings; 1686 NativePath systemSettings; 1687 NativePath localRepository; 1688 } 1689 1690 private class DubConfig { 1691 private { 1692 DubConfig m_parentConfig; 1693 Json m_data; 1694 } 1695 1696 this(Json data, DubConfig parent_config) 1697 { 1698 m_data = data; 1699 m_parentConfig = parent_config; 1700 } 1701 1702 @property string[] registryURLs() 1703 { 1704 string[] ret; 1705 if (auto pv = "registryUrls" in m_data) 1706 ret = (*pv).deserializeJson!(string[]); 1707 if (m_parentConfig) ret ~= m_parentConfig.registryURLs; 1708 return ret; 1709 } 1710 1711 @property SkipPackageSuppliers skipRegistry() 1712 { 1713 if(auto pv = "skipRegistry" in m_data) 1714 return to!SkipPackageSuppliers((*pv).get!string); 1715 1716 if (m_parentConfig) 1717 return m_parentConfig.skipRegistry; 1718 1719 return SkipPackageSuppliers.none; 1720 } 1721 1722 @property NativePath[] customCachePaths() 1723 { 1724 import std.algorithm.iteration : map; 1725 import std.array : array; 1726 1727 NativePath[] ret; 1728 if (auto pv = "customCachePaths" in m_data) 1729 ret = (*pv).deserializeJson!(string[]) 1730 .map!(s => NativePath(s)) 1731 .array; 1732 if (m_parentConfig) 1733 ret ~= m_parentConfig.customCachePaths; 1734 return ret; 1735 } 1736 1737 @property string defaultCompiler() 1738 const { 1739 if (auto pv = "defaultCompiler" in m_data) 1740 return pv.get!string; 1741 if (m_parentConfig) return m_parentConfig.defaultCompiler; 1742 return null; 1743 } 1744 1745 @property string defaultArchitecture() 1746 const { 1747 if(auto pv = "defaultArchitecture" in m_data) 1748 return (*pv).get!string; 1749 if (m_parentConfig) return m_parentConfig.defaultArchitecture; 1750 return null; 1751 } 1752 }