1 /** 2 Management of packages on the local computer. 3 4 Copyright: © 2012-2016 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.packagemanager; 9 10 import dub.dependency; 11 import dub.internal.io.filesystem; 12 import dub.internal.utils; 13 import dub.internal.vibecompat.data.json; 14 import dub.internal.vibecompat.inet.path; 15 import dub.internal.logging; 16 import dub.package_; 17 import dub.recipe.io; 18 import dub.recipe.selection; 19 import dub.internal.configy.exceptions; 20 public import dub.internal.configy.read : StrictMode; 21 22 import dub.internal.dyaml.stdsumtype; 23 24 import std.algorithm : countUntil, filter, map, sort, canFind, remove; 25 import std.array; 26 import std.conv; 27 import std.datetime.systime; 28 import std.digest.sha; 29 import std.encoding : sanitize; 30 import std.exception; 31 import std.range; 32 import std.string; 33 import std.typecons; 34 import std.zip; 35 36 37 /// Indicates where a package has been or should be placed to. 38 public enum PlacementLocation { 39 /// Packages retrieved with 'local' will be placed in the current folder 40 /// using the package name as destination. 41 local, 42 /// Packages with 'userWide' will be placed in a folder accessible by 43 /// all of the applications from the current user. 44 user, 45 /// Packages retrieved with 'systemWide' will be placed in a shared folder, 46 /// which can be accessed by all users of the system. 47 system, 48 } 49 50 /// Converts a `PlacementLocation` to a string 51 public string toString (PlacementLocation loc) @safe pure nothrow @nogc 52 { 53 final switch (loc) { 54 case PlacementLocation.local: 55 return "Local"; 56 case PlacementLocation.user: 57 return "User"; 58 case PlacementLocation.system: 59 return "System"; 60 } 61 } 62 63 /// A SelectionsFile associated with its file-system path. 64 struct SelectionsFileLookupResult { 65 /// The absolute path to the dub.selections.json file 66 /// (potentially inherited from a parent directory of the root package). 67 NativePath absolutePath; 68 /// The parsed dub.selections.json file. 69 SelectionsFile selectionsFile; 70 } 71 72 /// The PackageManager can retrieve present packages and get / remove 73 /// packages. 74 class PackageManager { 75 protected { 76 /** 77 * The 'internal' location, for packages not attributable to a location. 78 * 79 * There are two uses for this: 80 * - In `bare` mode, the search paths are set at this scope, 81 * and packages gathered are stored in `localPackage`; 82 * - In the general case, any path-based or SCM-based dependency 83 * is loaded in `fromPath`; 84 */ 85 Location m_internal; 86 /** 87 * List of locations that are managed by this `PackageManager` 88 * 89 * The `PackageManager` can be instantiated either in 'bare' mode, 90 * in which case this array will be empty, or in the normal mode, 91 * this array will have 3 entries, matching values 92 * in the `PlacementLocation` enum. 93 * 94 * See_Also: `Location`, `PlacementLocation` 95 */ 96 Location[] m_repositories; 97 /** 98 * Whether `refreshLocal` / `refreshCache` has been called or not 99 * 100 * User local cache can get pretty large, and we want to avoid our build 101 * time being dependent on their size. However, in order to support 102 * local packages and overrides, we were scanning the whole cache prior 103 * to v1.39.0 (although attempts at fixing this behavior were made 104 * earlier). Those booleans record whether we have been semi-initialized 105 * (local packages and overrides have been loaded) or fully initialized 106 * (all caches have been scanned), the later still being required for 107 * some API (e.g. `getBestPackage` or `getPackageIterator`). 108 */ 109 enum InitializationState { 110 /// No `refresh*` function has been called 111 none, 112 /// `refreshLocal` has been called 113 partial, 114 /// `refreshCache` (and `refreshLocal`) has been called 115 full, 116 } 117 /// Ditto 118 InitializationState m_state; 119 /// The `Filesystem` object, used to interact with directory / files 120 Filesystem fs; 121 } 122 123 /** 124 Instantiate an instance with a single search path 125 126 This constructor is used when dub is invoked with the '--bare' CLI switch. 127 The instance will not look up the default repositories 128 (e.g. ~/.dub/packages), using only `path` instead. 129 130 Params: 131 path = Path of the single repository 132 */ 133 this(NativePath path, Filesystem fs = null) 134 { 135 import dub.internal.io.realfs; 136 this.fs = fs !is null ? fs : new RealFS(); 137 this.m_internal.searchPath = [ path ]; 138 this.refresh(); 139 } 140 141 deprecated("Use the overload that accepts a `Filesystem`") 142 this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true) 143 { 144 import dub.internal.io.realfs; 145 this(new RealFS(), package_path ~ ".dub/packages/", 146 user_path ~ "packages/", system_path ~ "packages/"); 147 if (refresh_packages) refresh(); 148 } 149 150 /** 151 * Instantiate a `PackageManager` with the provided `Filesystem` and paths 152 * 153 * Unlike the other overload, paths are taken as-if, e.g. `packages/` is not 154 * appended to them. 155 * 156 * Params: 157 * fs = Filesystem abstraction to handle all folder/file I/O. 158 * local = Path to the local package cache (usually the one in the project), 159 * whih takes preference over `user` and `system`. 160 * user = Path to the user package cache (usually ~/.dub/packages/), takes 161 * precedence over `system` but not over `local`. 162 * system = Path to the system package cache, this has the least precedence. 163 */ 164 public this(Filesystem fs, NativePath local, NativePath user, NativePath system) 165 { 166 this.fs = fs; 167 this.m_repositories = [ Location(local), Location(user), Location(system) ]; 168 } 169 170 /** Gets/sets the list of paths to search for local packages. 171 */ 172 @property void searchPath(NativePath[] paths) 173 { 174 if (paths == this.m_internal.searchPath) return; 175 this.m_internal.searchPath = paths.dup; 176 this.refresh(); 177 } 178 /// ditto 179 @property const(NativePath)[] searchPath() const { return this.m_internal.searchPath; } 180 181 /** Returns the effective list of search paths, including default ones. 182 */ 183 deprecated("Use the `PackageManager` facilities instead") 184 @property const(NativePath)[] completeSearchPath() 185 const { 186 auto ret = appender!(const(NativePath)[])(); 187 ret.put(this.m_internal.searchPath); 188 foreach (ref repo; m_repositories) { 189 ret.put(repo.searchPath); 190 ret.put(repo.packagePath); 191 } 192 return ret.data; 193 } 194 195 /** Sets additional (read-only) package cache paths to search for packages. 196 197 Cache paths have the same structure as the default cache paths, such as 198 ".dub/packages/". 199 200 Note that previously set custom paths will be removed when setting this 201 property. 202 */ 203 @property void customCachePaths(NativePath[] custom_cache_paths) 204 { 205 import std.algorithm.iteration : map; 206 import std.array : array; 207 208 m_repositories.length = PlacementLocation.max+1; 209 m_repositories ~= custom_cache_paths.map!(p => Location(p)).array; 210 211 this.refresh(); 212 } 213 214 /** 215 * Looks up a package, first in the list of loaded packages, 216 * then directly on the file system. 217 * 218 * This function allows for lazy loading of packages, without needing to 219 * first scan all the available locations (as `refresh` does). 220 * 221 * Note: 222 * This function does not take overrides into account. Overrides need 223 * to be resolved by the caller before `lookup` is called. 224 * Additionally, if a package of the same version is loaded in multiple 225 * locations, the first one matching (local > user > system) 226 * will be returned. 227 * 228 * Params: 229 * name = The full name of the package to look up 230 * vers = The version the package must match 231 * 232 * Returns: 233 * A `Package` if one was found, `null` if none exists. 234 */ 235 protected Package lookup (in PackageName name, in Version vers) { 236 // This is the only place we can get away with lazy initialization, 237 // since we know exactly what package and version we want. 238 // However, it is also the most often called API. 239 this.ensureInitialized(InitializationState.partial); 240 241 if (auto pkg = this.m_internal.lookup(name, vers)) 242 return pkg; 243 244 foreach (ref location; this.m_repositories) 245 if (auto p = location.load(name, vers, this)) 246 return p; 247 248 return null; 249 } 250 251 /** Looks up a specific package. 252 253 Looks up a package matching the given version/path in the set of 254 registered packages. The lookup order is done according the the 255 usual rules (see getPackageIterator). 256 257 Params: 258 name = The name of the package 259 ver = The exact version of the package to query 260 path = An exact path that the package must reside in. Note that 261 the package must still be registered in the package manager. 262 enable_overrides = Apply the local package override list before 263 returning a package (enabled by default) 264 265 Returns: 266 The matching package or null if no match was found. 267 */ 268 Package getPackage(in PackageName name, in Version ver, bool enable_overrides = true) 269 { 270 if (enable_overrides) { 271 foreach (ref repo; m_repositories) 272 foreach (ovr; repo.overrides) 273 if (ovr.package_ == name.toString() && ovr.source.matches(ver)) { 274 Package pack = ovr.target.match!( 275 (NativePath path) => getOrLoadPackage(path, NativePath.init, name), 276 (Version vers) => getPackage(name, vers, false), 277 ); 278 if (pack) return pack; 279 280 ovr.target.match!( 281 (any) { 282 logWarn("Package override %s %s -> '%s' doesn't reference an existing package.", 283 ovr.package_, ovr.source, any); 284 }, 285 ); 286 } 287 } 288 289 return this.lookup(name, ver); 290 } 291 292 deprecated("Use the overload that accepts a `PackageName` instead") 293 Package getPackage(string name, Version ver, bool enable_overrides = true) 294 { 295 return this.getPackage(PackageName(name), ver, enable_overrides); 296 } 297 298 /// ditto 299 deprecated("Use the overload that accepts a `Version` as second argument") 300 Package getPackage(string name, string ver, bool enable_overrides = true) 301 { 302 return getPackage(name, Version(ver), enable_overrides); 303 } 304 305 /// ditto 306 deprecated("Use the overload that takes a `PlacementLocation`") 307 Package getPackage(string name, Version ver, NativePath path) 308 { 309 foreach (p; getPackageIterator(name)) { 310 auto pvm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard; 311 if (p.version_.matches(ver, pvm) && p.path.startsWith(path)) 312 return p; 313 } 314 return null; 315 } 316 317 /// Ditto 318 deprecated("Use the overload that accepts a `PackageName` instead") 319 Package getPackage(string name, Version ver, PlacementLocation loc) 320 { 321 return this.getPackage(PackageName(name), ver, loc); 322 } 323 324 /// Ditto 325 Package getPackage(in PackageName name, in Version ver, PlacementLocation loc) 326 { 327 // Bare mode 328 if (loc >= this.m_repositories.length) 329 return null; 330 return this.m_repositories[loc].load(name, ver, this); 331 } 332 333 /// ditto 334 deprecated("Use the overload that accepts a `Version` as second argument") 335 Package getPackage(string name, string ver, NativePath path) 336 { 337 return getPackage(name, Version(ver), path); 338 } 339 340 /// ditto 341 deprecated("Use another `PackageManager` API, open an issue if none suits you") 342 Package getPackage(string name, NativePath path) 343 { 344 foreach( p; getPackageIterator(name) ) 345 if (p.path.startsWith(path)) 346 return p; 347 return null; 348 } 349 350 351 /** Looks up the first package matching the given name. 352 */ 353 deprecated("Use `getBestPackage` instead") 354 Package getFirstPackage(string name) 355 { 356 foreach (ep; getPackageIterator(name)) 357 return ep; 358 return null; 359 } 360 361 /** Looks up the latest package matching the given name. 362 */ 363 deprecated("Use `getBestPackage` with `name, Dependency.any` instead") 364 Package getLatestPackage(string name) 365 { 366 Package pkg; 367 foreach (ep; getPackageIterator(name)) 368 if (pkg is null || pkg.version_ < ep.version_) 369 pkg = ep; 370 return pkg; 371 } 372 373 /** For a given package path, returns the corresponding package. 374 375 If the package is already loaded, a reference is returned. Otherwise 376 the package gets loaded and cached for the next call to this function. 377 378 Params: 379 path = NativePath to the root directory of the package 380 recipe_path = Optional path to the recipe file of the package 381 name = Optional (sub-)package name if known in advance. Required if a sub-package is to be returned. 382 mode = Whether to issue errors, warning, or ignore unknown keys in dub.json 383 384 Returns: The packages loaded from the given path 385 Throws: Throws an exception if no package can be loaded 386 */ 387 Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init, 388 PackageName name = PackageName.init, StrictMode mode = StrictMode.Ignore) 389 { 390 path.endsWithSlash = true; 391 const nameString = name.toString(); 392 393 foreach (p; this.m_internal.fromPath) { 394 if (!nameString.empty) { 395 if (p.name == nameString && (p.path == path || p.basePackage.path == path)) 396 return p; 397 } else { 398 if (p.path == path && !p.parentPackage) 399 return p; 400 } 401 } 402 403 auto pack = this.load(path, recipe_path, null, null, mode); 404 auto nameToResolve = PackageName(pack.name); 405 406 if (!nameString.empty) { 407 nameToResolve = PackageName(nameString); 408 const loadedName = PackageName(pack.name); 409 enforce(loadedName == nameToResolve || loadedName == nameToResolve.main, 410 "Package %s loaded from '%s' does not match expected name %s".format( 411 loadedName, path.toNativeString(), nameToResolve)); 412 } 413 414 pack = addPackagesAndResolveSubPackage(this.m_internal.fromPath, pack, nameToResolve); 415 enforce(pack !is null, "No sub-package %s in parent package loaded from '%s'".format( 416 nameToResolve, path.toNativeString())); 417 return pack; 418 } 419 420 /** 421 * Loads a `Package` from the filesystem 422 * 423 * This is called when a `Package` needs to be loaded from the path. 424 * This does not change the internal state of the `PackageManager`, 425 * it simply loads the `Package` and returns it - it is up to the caller 426 * to call `addPackages`. 427 * 428 * Throws: 429 * If no package can be found at the `path` / with the `recipe`. 430 * 431 * Params: 432 * path = The directory in which the package resides. 433 * recipe = Optional path to the package recipe file. If left empty, 434 * the `path` directory will be searched for a recipe file. 435 * parent = Reference to the parent package, if the new package is a 436 * sub package. 437 * version_ = Optional version to associate to the package instead of 438 * the one declared in the package recipe, or the one 439 * determined by invoking the VCS (GIT currently). 440 * mode = Whether to issue errors, warning, or ignore unknown keys in 441 * dub.json 442 * 443 * Returns: A populated `Package`. 444 */ 445 protected Package load(NativePath path, NativePath recipe = NativePath.init, 446 Package parent = null, string version_ = null, 447 StrictMode mode = StrictMode.Ignore) 448 { 449 if (recipe.empty) 450 recipe = this.findPackageFile(path); 451 452 enforce(!recipe.empty, 453 "No package file found in %s, expected one of %s" 454 .format(path.toNativeString(), 455 packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); 456 457 const PackageName pname = parent 458 ? PackageName(parent.name) : PackageName.init; 459 string text = this.fs.readText(recipe); 460 auto content = parsePackageRecipe( 461 text, recipe.toNativeString(), pname, null, mode); 462 auto ret = new Package(content, path, parent, version_); 463 ret.m_infoFile = recipe; 464 return ret; 465 } 466 467 /** Searches the given directory for package recipe files. 468 * 469 * Params: 470 * directory = The directory to search 471 * 472 * Returns: 473 * Returns the full path to the package file, if any was found. 474 * Otherwise returns an empty path. 475 */ 476 public NativePath findPackageFile(NativePath directory) 477 { 478 foreach (file; packageInfoFiles) { 479 auto filename = directory ~ file.filename; 480 if (this.fs.existsFile(filename)) return filename; 481 } 482 return NativePath.init; 483 } 484 485 /** For a given SCM repository, returns the corresponding package. 486 487 An SCM repository is provided as its remote URL, the repository is cloned 488 and in the dependency specified commit is checked out. 489 490 If the target directory already exists, just returns the package 491 without cloning. 492 493 Params: 494 name = Package name 495 dependency = Dependency that contains the repository URL and a specific commit 496 497 Returns: 498 The package loaded from the given SCM repository or null if the 499 package couldn't be loaded. 500 */ 501 Package loadSCMPackage(in PackageName name, in Repository repo) 502 in { assert(!repo.empty); } 503 do { 504 Package pack; 505 506 final switch (repo.kind) 507 { 508 case repo.Kind.git: 509 return this.loadGitPackage(name, repo); 510 } 511 } 512 513 deprecated("Use the overload that accepts a `dub.dependency : Repository`") 514 Package loadSCMPackage(string name, Dependency dependency) 515 in { assert(!dependency.repository.empty); } 516 do { return this.loadSCMPackage(name, dependency.repository); } 517 518 deprecated("Use `loadSCMPackage(PackageName, Repository)`") 519 Package loadSCMPackage(string name, Repository repo) 520 { 521 return this.loadSCMPackage(PackageName(name), repo); 522 } 523 524 private Package loadGitPackage(in PackageName name, in Repository repo) 525 { 526 if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) { 527 return null; 528 } 529 530 string gitReference = repo.ref_.chompPrefix("~"); 531 NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); 532 533 // Before doing a git clone, let's see if the package exists locally 534 if (this.fs.existsDirectory(destination)) { 535 bool isMatch(Package p) { 536 return p.name == name.toString() && p.basePackage.path == destination; 537 } 538 539 // It exists, check if we already loaded it. 540 // Either we loaded it on refresh and it's in PlacementLocation.user, 541 // or we just added it and it's in m_internal. 542 foreach (p; this.m_internal.fromPath) 543 if (isMatch(p)) 544 return p; 545 if (this.m_repositories.length) 546 foreach (p; this.m_repositories[PlacementLocation.user].fromPath) 547 if (isMatch(p)) 548 return p; 549 } else if (!this.gitClone(repo.remote, gitReference, destination)) 550 return null; 551 552 Package p = this.load(destination); 553 if (p is null) return null; 554 return this.addPackagesAndResolveSubPackage(this.m_internal.fromPath, p, name); 555 } 556 557 /** 558 * Perform a `git clone` operation at `dest` using `repo` 559 * 560 * Params: 561 * remote = The remote to clone from 562 * gitref = The git reference to use 563 * dest = Where the result of git clone operation is to be stored 564 * 565 * Returns: 566 * Whether or not the clone operation was successfull. 567 */ 568 protected bool gitClone(string remote, string gitref, in NativePath dest) 569 { 570 static import dub.internal.git; 571 return dub.internal.git.cloneRepository(remote, gitref, dest.toNativeString()); 572 } 573 574 /** 575 * Get the final destination a specific package needs to be stored in. 576 * 577 * See `Location.getPackagePath`. 578 */ 579 package(dub) NativePath getPackagePath(PlacementLocation base, in PackageName name, string vers) 580 { 581 assert(this.m_repositories.length == 3, "getPackagePath called in bare mode"); 582 return this.m_repositories[base].getPackagePath(name, vers); 583 } 584 585 /** 586 * Searches for the latest version of a package matching the version range. 587 * 588 * This will search the local file system only (it doesn't connect 589 * to the registry) for the "best" (highest version) that matches `range`. 590 * An overload with a single version exists to search for an exact version. 591 * 592 * Params: 593 * name = Package name to search for 594 * vers = Exact version to search for 595 * range = Range of versions to search for, defaults to any 596 * 597 * Returns: 598 * The best package matching the parameters, or `null` if none was found. 599 */ 600 deprecated("Use the overload that accepts a `PackageName` instead") 601 Package getBestPackage(string name, Version vers) 602 { 603 return this.getBestPackage(PackageName(name), vers); 604 } 605 606 /// Ditto 607 Package getBestPackage(in PackageName name, in Version vers) 608 { 609 return this.getBestPackage(name, VersionRange(vers, vers)); 610 } 611 612 /// Ditto 613 deprecated("Use the overload that accepts a `PackageName` instead") 614 Package getBestPackage(string name, VersionRange range = VersionRange.Any) 615 { 616 return this.getBestPackage(PackageName(name), range); 617 } 618 619 /// Ditto 620 Package getBestPackage(in PackageName name, in VersionRange range = VersionRange.Any) 621 { 622 return this.getBestPackage_(name, Dependency(range)); 623 } 624 625 /// Ditto 626 deprecated("Use the overload that accepts a `Version` or a `VersionRange`") 627 Package getBestPackage(string name, string range) 628 { 629 return this.getBestPackage(name, VersionRange.fromString(range)); 630 } 631 632 /// Ditto 633 deprecated("`getBestPackage` should only be used with a `Version` or `VersionRange` argument") 634 Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true) 635 { 636 return this.getBestPackage_(PackageName(name), version_spec, enable_overrides); 637 } 638 639 // TODO: Merge this into `getBestPackage(string, VersionRange)` 640 private Package getBestPackage_(in PackageName name, in Dependency version_spec, 641 bool enable_overrides = true) 642 { 643 Package ret; 644 foreach (p; getPackageIterator(name.toString())) { 645 auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard; 646 if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_)) 647 ret = p; 648 } 649 650 if (enable_overrides && ret) { 651 if (auto ovr = getPackage(name, ret.version_)) 652 return ovr; 653 } 654 return ret; 655 } 656 657 /** Gets the a specific sub package. 658 659 Params: 660 base_package = The package from which to get a sub package 661 sub_name = Name of the sub package (not prefixed with the base 662 package name) 663 silent_fail = If set to true, the function will return `null` if no 664 package is found. Otherwise will throw an exception. 665 666 */ 667 Package getSubPackage(Package base_package, string sub_name, bool silent_fail) 668 { 669 foreach (p; getPackageIterator(base_package.name~":"~sub_name)) 670 if (p.parentPackage is base_package) 671 return p; 672 enforce(silent_fail, "Sub package \""~base_package.name~":"~sub_name~"\" doesn't exist."); 673 return null; 674 } 675 676 677 /** Determines if a package is managed by DUB. 678 679 Managed packages can be upgraded and removed. 680 */ 681 bool isManagedPackage(const(Package) pack) 682 const { 683 auto ppath = pack.basePackage.path; 684 return isManagedPath(ppath); 685 } 686 687 /** Determines if a specific path is within a DUB managed package folder. 688 689 By default, managed folders are "~/.dub/packages" and 690 "/var/lib/dub/packages". 691 */ 692 bool isManagedPath(NativePath path) 693 const { 694 foreach (rep; m_repositories) 695 if (rep.isManaged(path)) 696 return true; 697 return false; 698 } 699 700 /** Enables iteration over all known local packages. 701 702 Returns: A delegate suitable for use with `foreach` is returned. 703 */ 704 int delegate(int delegate(ref Package)) getPackageIterator() 705 { 706 // This API requires full knowledge of the package cache 707 this.ensureInitialized(InitializationState.full); 708 709 int iterator(int delegate(ref Package) del) 710 { 711 // Search scope by priority, internal has the highest 712 foreach (p; this.m_internal.fromPath) 713 if (auto ret = del(p)) return ret; 714 foreach (p; this.m_internal.localPackages) 715 if (auto ret = del(p)) return ret; 716 717 foreach (ref repo; m_repositories) { 718 foreach (p; repo.localPackages) 719 if (auto ret = del(p)) return ret; 720 foreach (p; repo.fromPath) 721 if (auto ret = del(p)) return ret; 722 } 723 return 0; 724 } 725 726 return &iterator; 727 } 728 729 /** Enables iteration over all known local packages with a certain name. 730 731 Returns: A delegate suitable for use with `foreach` is returned. 732 */ 733 int delegate(int delegate(ref Package)) getPackageIterator(string name) 734 { 735 int iterator(int delegate(ref Package) del) 736 { 737 foreach (p; getPackageIterator()) 738 if (p.name == name) 739 if (auto ret = del(p)) return ret; 740 return 0; 741 } 742 743 return &iterator; 744 } 745 746 747 /** Returns a list of all package overrides for the given scope. 748 */ 749 deprecated(OverrideDepMsg) 750 const(PackageOverride)[] getOverrides(PlacementLocation scope_) 751 const { 752 return cast(typeof(return)) this.getOverrides_(scope_); 753 } 754 755 package(dub) const(PackageOverride_)[] getOverrides_(PlacementLocation scope_) 756 const { 757 return m_repositories[scope_].overrides; 758 } 759 760 /** Adds a new override for the given package. 761 */ 762 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 763 void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target) 764 { 765 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 766 m_repositories[scope_].writeOverrides(this); 767 } 768 /// ditto 769 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 770 void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target) 771 { 772 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 773 m_repositories[scope_].writeOverrides(this); 774 } 775 776 /// Ditto 777 deprecated(OverrideDepMsg) 778 void addOverride(PlacementLocation scope_, string package_, VersionRange source, Version target) 779 { 780 this.addOverride_(scope_, package_, source, target); 781 } 782 /// ditto 783 deprecated(OverrideDepMsg) 784 void addOverride(PlacementLocation scope_, string package_, VersionRange source, NativePath target) 785 { 786 this.addOverride_(scope_, package_, source, target); 787 } 788 789 // Non deprecated version that is used by `commandline`. Do not use! 790 package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target) 791 { 792 m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); 793 m_repositories[scope_].writeOverrides(this); 794 } 795 // Non deprecated version that is used by `commandline`. Do not use! 796 package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target) 797 { 798 m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); 799 m_repositories[scope_].writeOverrides(this); 800 } 801 802 /** Removes an existing package override. 803 */ 804 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 805 void removeOverride(PlacementLocation scope_, string package_, Dependency version_spec) 806 { 807 version_spec.visit!( 808 (VersionRange src) => this.removeOverride(scope_, package_, src), 809 (any) { throw new Exception(format("No override exists for %s %s", package_, version_spec)); }, 810 ); 811 } 812 813 deprecated(OverrideDepMsg) 814 void removeOverride(PlacementLocation scope_, string package_, VersionRange src) 815 { 816 this.removeOverride_(scope_, package_, src); 817 } 818 819 package(dub) void removeOverride_(PlacementLocation scope_, string package_, VersionRange src) 820 { 821 Location* rep = &m_repositories[scope_]; 822 foreach (i, ovr; rep.overrides) { 823 if (ovr.package_ != package_ || ovr.source != src) 824 continue; 825 rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $]; 826 (*rep).writeOverrides(this); 827 return; 828 } 829 throw new Exception(format("No override exists for %s %s", package_, src)); 830 } 831 832 deprecated("Use `store(NativePath source, PlacementLocation dest, string name, Version vers)`") 833 Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination) 834 { 835 import dub.internal.vibecompat.core.file; 836 837 return this.store_(readFile(zip_file_path), destination, 838 PackageName(package_info["name"].get!string), 839 Version(package_info["version"].get!string)); 840 } 841 842 /** 843 * Store a zip file stored at `src` into a managed location `destination` 844 * 845 * This will extracts the package supplied as (as a zip file) to the 846 * `destination` and sets a version field in the package description. 847 * In the future, we should aim not to alter the package description, 848 * but this is done for backward compatibility. 849 * 850 * Params: 851 * src = The path to the zip file containing the package 852 * dest = At which `PlacementLocation` the package should be stored 853 * name = Name of the package being stored 854 * vers = Version of the package 855 * 856 * Returns: 857 * The `Package` after it has been loaded. 858 * 859 * Throws: 860 * If the package cannot be loaded / the zip is corrupted / the package 861 * already exists, etc... 862 */ 863 deprecated("Use the overload that accepts a `PackageName` instead") 864 Package store(NativePath src, PlacementLocation dest, string name, Version vers) 865 { 866 return this.store(src, dest, PackageName(name), vers); 867 } 868 869 /// Ditto 870 Package store(NativePath src, PlacementLocation dest, in PackageName name, 871 in Version vers) 872 { 873 import dub.internal.vibecompat.core.file; 874 875 auto data = readFile(src); 876 return this.store(data, dest, name, vers); 877 } 878 879 /// Ditto 880 Package store(ubyte[] data, PlacementLocation dest, 881 in PackageName name, in Version vers) 882 { 883 assert(!name.sub.length, "Cannot store a subpackage, use main package instead"); 884 NativePath dstpath = this.getPackagePath(dest, name, vers.toString()); 885 this.fs.mkdir(dstpath.parentPath()); 886 const lockPath = dstpath.parentPath() ~ ".lock"; 887 888 // possibly wait for other dub instance 889 import core.time : seconds; 890 auto lock = lockFile(lockPath.toNativeString(), 30.seconds); 891 if (this.fs.existsFile(dstpath)) { 892 return this.getPackage(name, vers, dest); 893 } 894 return this.store_(data, dstpath, name, vers); 895 } 896 897 /// Backward-compatibility for deprecated overload, simplify once `storeFetchedPatch` 898 /// is removed 899 protected Package store_(ubyte[] data, NativePath destination, 900 in PackageName name, in Version vers) 901 { 902 import dub.recipe.json : toJson; 903 import std.range : walkLength; 904 905 logDebug("Placing package '%s' version '%s' to location '%s'", 906 name, vers, destination.toNativeString()); 907 908 enforce(!this.fs.existsFile(destination), 909 "%s (%s) needs to be removed from '%s' prior placement." 910 .format(name, vers, destination)); 911 912 ZipArchive archive = new ZipArchive(data); 913 logDebug("Extracting from zip."); 914 915 // In a GitHub zip, the actual contents are in a sub-folder 916 alias PSegment = typeof(PosixPath.init.head); 917 PSegment[] zip_prefix; 918 outer: foreach(ArchiveMember am; archive.directory) { 919 auto path = PosixPath(am.name).bySegment.array; 920 foreach (fil; packageInfoFiles) 921 if (path.length == 2 && path[$-1].name == fil.filename) { 922 zip_prefix = path[0 .. $-1]; 923 break outer; 924 } 925 } 926 927 logDebug("zip root folder: %s", zip_prefix); 928 929 PosixPath getCleanedPath(string fileName) { 930 auto path = PosixPath(fileName); 931 if (zip_prefix.length && !path.bySegment.startsWith(zip_prefix)) return PosixPath.init; 932 static if (is(typeof(path[0 .. 1]))) return path[zip_prefix.length .. $]; 933 else return PosixPath(path.bySegment.array[zip_prefix.length .. $]); 934 } 935 936 void setAttributes(NativePath path, ArchiveMember am) 937 { 938 import std.datetime : DosFileTimeToSysTime; 939 940 auto mtime = DosFileTimeToSysTime(am.time); 941 this.fs.setTimes(path, mtime, mtime); 942 if (auto attrs = am.fileAttributes) 943 this.fs.setAttributes(path, attrs); 944 } 945 946 // extract & place 947 this.fs.mkdir(destination); 948 logDebug("Copying all files..."); 949 int countFiles = 0; 950 foreach(ArchiveMember a; archive.directory) { 951 auto cleanedPath = getCleanedPath(a.name); 952 if(cleanedPath.empty) continue; 953 auto dst_path = destination ~ cleanedPath; 954 955 logDebug("Creating %s", cleanedPath); 956 if (dst_path.endsWithSlash) { 957 this.fs.mkdir(dst_path); 958 } else { 959 this.fs.mkdir(dst_path.parentPath); 960 // for symlinks on posix systems, use the symlink function to 961 // create them. Windows default unzip doesn't handle symlinks, 962 // so we don't need to worry about it for Windows. 963 version(Posix) { 964 import core.sys.posix.sys.stat; 965 if( S_ISLNK(cast(mode_t)a.fileAttributes) ){ 966 import core.sys.posix.unistd; 967 // need to convert name and target to zero-terminated string 968 auto target = toStringz(cast(const(char)[])archive.expand(a)); 969 auto dstFile = toStringz(dst_path.toNativeString()); 970 enforce(symlink(target, dstFile) == 0, "Error creating symlink: " ~ dst_path.toNativeString()); 971 goto symlink_exit; 972 } 973 } 974 975 this.fs.writeFile(dst_path, archive.expand(a)); 976 setAttributes(dst_path, a); 977 symlink_exit: 978 ++countFiles; 979 } 980 } 981 logDebug("%s file(s) copied.", to!string(countFiles)); 982 983 // overwrite dub.json (this one includes a version field) 984 auto pack = this.load(destination, NativePath.init, null, vers.toString()); 985 986 if (pack.recipePath.head != defaultPackageFilename) 987 // Storeinfo saved a default file, this could be different to the file from the zip. 988 this.fs.removeFile(pack.recipePath); 989 auto app = appender!string(); 990 app.writePrettyJsonString(pack.recipe.toJson()); 991 this.fs.writeFile(pack.recipePath.parentPath ~ defaultPackageFilename, app.data); 992 addPackages(this.m_internal.localPackages, pack); 993 return pack; 994 } 995 996 /// Removes the given the package. 997 void remove(in Package pack) 998 { 999 logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path); 1000 enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); 1001 enforce(pack.parentPackage is null, "Cannot remove subpackage %s".format(pack.name)); 1002 1003 // remove package from repositories' list 1004 bool found = false; 1005 bool removeFrom(Package[] packs, in Package pack) { 1006 auto packPos = countUntil!("a.path == b.path")(packs, pack); 1007 if(packPos != -1) { 1008 packs = .remove(packs, packPos); 1009 return true; 1010 } 1011 return false; 1012 } 1013 foreach(repo; m_repositories) { 1014 if (removeFrom(repo.fromPath, pack)) { 1015 found = true; 1016 break; 1017 } 1018 // Maintain backward compatibility with pre v1.30.0 behavior, 1019 // this is equivalent to remove-local 1020 if (removeFrom(repo.localPackages, pack)) { 1021 found = true; 1022 break; 1023 } 1024 } 1025 if(!found) 1026 found = removeFrom(this.m_internal.localPackages, pack); 1027 enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); 1028 1029 logDebug("About to delete root folder for package '%s'.", pack.path); 1030 this.fs.removeDir(pack.path, true); 1031 logInfo("Removed", Color.yellow, "%s %s", pack.name.color(Mode.bold), pack.version_); 1032 } 1033 1034 /// Compatibility overload. Use the version without a `force_remove` argument instead. 1035 deprecated("Use `remove(pack)` directly instead, the boolean has no effect") 1036 void remove(in Package pack, bool force_remove) 1037 { 1038 remove(pack); 1039 } 1040 1041 Package addLocalPackage(NativePath path, string verName, PlacementLocation type) 1042 { 1043 // As we iterate over `localPackages` we need it to be populated 1044 // In theory we could just populate that specific repository, 1045 // but multiple calls would then become inefficient. 1046 this.ensureInitialized(InitializationState.full); 1047 1048 path.endsWithSlash = true; 1049 auto pack = this.load(path); 1050 enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); 1051 if (verName.length) 1052 pack.version_ = Version(verName); 1053 1054 // don't double-add packages 1055 Package[]* packs = &m_repositories[type].localPackages; 1056 foreach (p; *packs) { 1057 if (p.path == path) { 1058 enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed."); 1059 logInfo("Package is already registered: %s (version: %s)", p.name, p.version_); 1060 return p; 1061 } 1062 } 1063 1064 addPackages(*packs, pack); 1065 1066 this.m_repositories[type].writeLocalPackageList(this); 1067 1068 logInfo("Registered package: %s (version: %s)", pack.name, pack.version_); 1069 return pack; 1070 } 1071 1072 void removeLocalPackage(NativePath path, PlacementLocation type) 1073 { 1074 // As we iterate over `localPackages` we need it to be populated 1075 // In theory we could just populate that specific repository, 1076 // but multiple calls would then become inefficient. 1077 this.ensureInitialized(InitializationState.full); 1078 1079 path.endsWithSlash = true; 1080 Package[]* packs = &m_repositories[type].localPackages; 1081 size_t[] to_remove; 1082 foreach( i, entry; *packs ) 1083 if( entry.path == path ) 1084 to_remove ~= i; 1085 enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); 1086 1087 string[Version] removed; 1088 foreach (i; to_remove) 1089 removed[(*packs)[i].version_] = (*packs)[i].name; 1090 1091 *packs = (*packs).enumerate 1092 .filter!(en => !to_remove.canFind(en.index)) 1093 .map!(en => en.value).array; 1094 1095 this.m_repositories[type].writeLocalPackageList(this); 1096 1097 foreach(ver, name; removed) 1098 logInfo("Deregistered package: %s (version: %s)", name, ver); 1099 } 1100 1101 /// For the given type add another path where packages will be looked up. 1102 void addSearchPath(NativePath path, PlacementLocation type) 1103 { 1104 m_repositories[type].searchPath ~= path; 1105 this.m_repositories[type].writeLocalPackageList(this); 1106 } 1107 1108 /// Removes a search path from the given type. 1109 void removeSearchPath(NativePath path, PlacementLocation type) 1110 { 1111 m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); 1112 this.m_repositories[type].writeLocalPackageList(this); 1113 } 1114 1115 deprecated("Use `refresh()` without boolean argument(same as `refresh(false)`") 1116 void refresh(bool refresh) 1117 { 1118 if (refresh) 1119 logDiagnostic("Refreshing local packages (refresh existing: true)..."); 1120 else 1121 logDiagnostic("Scanning local packages..."); 1122 1123 this.refreshLocal(refresh); 1124 this.refreshCache(refresh); 1125 } 1126 1127 void refresh() 1128 { 1129 logDiagnostic("Scanning local packages..."); 1130 this.refreshLocal(false); 1131 this.refreshCache(false); 1132 } 1133 1134 /// Private API to ensure a level of initialization 1135 private void ensureInitialized(InitializationState state) 1136 { 1137 if (this.m_state >= state) 1138 return; 1139 if (state == InitializationState.partial) 1140 this.refreshLocal(false); 1141 else 1142 this.refresh(); 1143 } 1144 1145 /// Refresh pay-as-you-go: Only load local packages, not the full cache 1146 private void refreshLocal(bool refresh) { 1147 foreach (ref repository; this.m_repositories) 1148 repository.scanLocalPackages(refresh, this); 1149 this.m_internal.scan(this, refresh); 1150 foreach (ref repository; this.m_repositories) { 1151 auto existing = refresh ? null : repository.fromPath; 1152 foreach (path; repository.searchPath) 1153 repository.scanPackageFolder(path, this, existing); 1154 repository.loadOverrides(this); 1155 } 1156 if (this.m_state < InitializationState.partial) 1157 this.m_state = InitializationState.partial; 1158 } 1159 1160 /// Refresh the full cache, a potentially expensive operation 1161 private void refreshCache(bool refresh) 1162 { 1163 foreach (ref repository; this.m_repositories) 1164 repository.scan(this, refresh); 1165 this.m_state = InitializationState.full; 1166 } 1167 1168 alias Hash = ubyte[]; 1169 /// Generates a hash digest for a given package. 1170 /// Some files or folders are ignored during the generation (like .dub and 1171 /// .svn folders) 1172 Hash hashPackage(Package pack) 1173 { 1174 import std.file; 1175 import dub.internal.vibecompat.core.file; 1176 1177 string[] ignored_directories = [".git", ".dub", ".svn"]; 1178 // something from .dub_ignore or what? 1179 string[] ignored_files = []; 1180 SHA256 hash; 1181 foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) { 1182 const isDir = file.isDir; 1183 if(isDir && ignored_directories.canFind(NativePath(file.name).head.name)) 1184 continue; 1185 else if(ignored_files.canFind(NativePath(file.name).head.name)) 1186 continue; 1187 1188 hash.put(cast(ubyte[])NativePath(file.name).head.name); 1189 if(isDir) { 1190 logDebug("Hashed directory name %s", NativePath(file.name).head); 1191 } 1192 else { 1193 hash.put(cast(ubyte[]) readFile(NativePath(file.name))); 1194 logDebug("Hashed file contents from %s", NativePath(file.name).head); 1195 } 1196 } 1197 auto digest = hash.finish(); 1198 logDebug("Project hash: %s", digest); 1199 return digest[].dup; 1200 } 1201 1202 /** 1203 * Loads the selections file (`dub.selections.json`) 1204 * 1205 * The selections file is only used for the root package / project. 1206 * However, due to it being a filesystem interaction, it is managed 1207 * from the `PackageManager`. 1208 * 1209 * Params: 1210 * absProjectPath = The absolute path to the root package/project for 1211 * which to load the selections file. 1212 * 1213 * Returns: 1214 * Either `null` (if no selections file exists or parsing encountered an error), 1215 * or a `SelectionsFileLookupResult`. Note that the nested `SelectionsFile` 1216 * might use an unsupported version (see `SelectionsFile` documentation). 1217 */ 1218 Nullable!SelectionsFileLookupResult readSelections(in NativePath absProjectPath) 1219 in (absProjectPath.absolute) { 1220 import dub.internal.configy.easy; 1221 1222 alias N = typeof(return); 1223 1224 // check for dub.selections.json in root project dir first, then walk up its 1225 // parent directories and look for inheritable dub.selections.json files 1226 const path = this.findSelections(absProjectPath); 1227 if (path.empty) return N.init; 1228 const content = this.fs.readText(path); 1229 // TODO: Remove `StrictMode.Warn` after v1.40 release 1230 // The default is to error, but as the previous parser wasn't 1231 // complaining, we should first warn the user. 1232 auto selections = wrapException(parseConfigString!SelectionsFile( 1233 content, path.toNativeString(), StrictMode.Warn)); 1234 // Could not parse file 1235 if (selections.isNull()) 1236 return N.init; 1237 // Non-inheritable selections found 1238 if (!path.startsWith(absProjectPath) && !selections.get().inheritable) 1239 return N.init; 1240 return N(SelectionsFileLookupResult(path, selections.get())); 1241 } 1242 1243 /// Helper function to walk up the filesystem and find `dub.selections.json` 1244 private NativePath findSelections(in NativePath dir) 1245 { 1246 const path = dir ~ "dub.selections.json"; 1247 if (this.fs.existsFile(path)) 1248 return path; 1249 if (!dir.hasParentPath) 1250 return NativePath.init; 1251 return this.findSelections(dir.parentPath); 1252 1253 } 1254 1255 /** 1256 * Writes the selections file (`dub.selections.json`) 1257 * 1258 * The selections file is only used for the root package / project. 1259 * However, due to it being a filesystem interaction, it is managed 1260 * from the `PackageManager`. 1261 * 1262 * Params: 1263 * project = The root package / project to read the selections file for. 1264 * selections = The `SelectionsFile` to write. 1265 * overwrite = Whether to overwrite an existing selections file. 1266 * True by default. 1267 */ 1268 public void writeSelections(in Package project, in Selections!1 selections, 1269 bool overwrite = true) 1270 { 1271 const path = project.path ~ "dub.selections.json"; 1272 if (!overwrite && this.fs.existsFile(path)) 1273 return; 1274 this.fs.writeFile(path, selectionsToString(selections)); 1275 } 1276 1277 /// Package function to avoid code duplication with deprecated 1278 /// SelectedVersions.save, merge with `writeSelections` in 1279 /// the future. 1280 package static string selectionsToString (in Selections!1 s) 1281 { 1282 Json json = selectionsToJSON(s); 1283 assert(json.type == Json.Type.object); 1284 assert(json.length == 2 || json.length == 3); 1285 assert(json["versions"].type != Json.Type.undefined); 1286 1287 auto result = appender!string(); 1288 result.put("{\n\t\"fileVersion\": "); 1289 result.writeJsonString(json["fileVersion"]); 1290 if (s.inheritable) 1291 result.put(",\n\t\"inheritable\": true"); 1292 result.put(",\n\t\"versions\": {"); 1293 auto vers = json["versions"].get!(Json[string]); 1294 bool first = true; 1295 foreach (k; vers.byKey.array.sort()) { 1296 if (!first) result.put(","); 1297 else first = false; 1298 result.put("\n\t\t"); 1299 result.writeJsonString(Json(k)); 1300 result.put(": "); 1301 result.writeJsonString(vers[k]); 1302 } 1303 result.put("\n\t}\n}\n"); 1304 return result.data; 1305 } 1306 1307 /// Ditto 1308 package static Json selectionsToJSON (in Selections!1 s) 1309 { 1310 Json serialized = Json.emptyObject; 1311 serialized["fileVersion"] = s.fileVersion; 1312 if (s.inheritable) 1313 serialized["inheritable"] = true; 1314 serialized["versions"] = Json.emptyObject; 1315 foreach (p, dep; s.versions) 1316 serialized["versions"][p] = dep.toJsonDep(); 1317 return serialized; 1318 } 1319 1320 /// Adds the package and its sub-packages. 1321 protected void addPackages(ref Package[] dst_repos, Package pack) 1322 { 1323 // Add the main package. 1324 dst_repos ~= pack; 1325 1326 // Additionally to the internally defined sub-packages, whose metadata 1327 // is loaded with the main dub.json, load all externally defined 1328 // packages after the package is available with all the data. 1329 foreach (spr; pack.subPackages) { 1330 Package sp; 1331 1332 if (spr.path.length) { 1333 auto p = NativePath(spr.path); 1334 p.normalize(); 1335 enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); 1336 auto path = pack.path ~ p; 1337 sp = this.load(path, NativePath.init, pack); 1338 } else sp = new Package(spr.recipe, pack.path, pack); 1339 1340 // Add the sub-package. 1341 try { 1342 dst_repos ~= sp; 1343 } catch (Exception e) { 1344 logError("Package '%s': Failed to load sub-package %s: %s", pack.name, 1345 spr.path.length ? spr.path : spr.recipe.name, e.msg); 1346 logDiagnostic("Full error: %s", e.toString().sanitize()); 1347 } 1348 } 1349 } 1350 1351 /// Adds the package and its sub-packages, and returns the added package matching 1352 /// the specified name (of the package itself or a sub-package). 1353 /// Returns null if the sub-package doesn't exist. 1354 private Package addPackagesAndResolveSubPackage(ref Package[] dst_repos, Package pack, 1355 in PackageName nameToResolve) 1356 in(pack.name == nameToResolve.main.toString(), 1357 "nameToResolve must be the added package or one of its sub-packages") 1358 { 1359 this.addPackages(dst_repos, pack); 1360 1361 if (nameToResolve.sub.empty) 1362 return pack; 1363 1364 // available sub-packages have been appended 1365 foreach_reverse (sp; dst_repos) { 1366 if (sp.parentPackage is pack && sp.name == nameToResolve.toString()) 1367 return sp; 1368 } 1369 1370 logDiagnostic("Sub-package %s not found in parent package", nameToResolve); 1371 return null; 1372 } 1373 } 1374 1375 deprecated(OverrideDepMsg) 1376 alias PackageOverride = PackageOverride_; 1377 1378 package(dub) struct PackageOverride_ { 1379 private alias ResolvedDep = SumType!(NativePath, Version); 1380 string package_; 1381 VersionRange source; 1382 ResolvedDep target; 1383 1384 deprecated("Use `source` instead") 1385 @property inout(Dependency) version_ () inout return @safe { 1386 return Dependency(this.source); 1387 } 1388 1389 deprecated("Assign `source` instead") 1390 @property ref PackageOverride version_ (Dependency v) scope return @safe pure { 1391 this.source = v.visit!( 1392 (VersionRange range) => range, 1393 (any) { 1394 int a; if (a) return VersionRange.init; // Trick the compiler 1395 throw new Exception("Cannot use anything else than a `VersionRange` for overrides"); 1396 }, 1397 ); 1398 return this; 1399 } 1400 1401 deprecated("Use `target.match` directly instead") 1402 @property inout(Version) targetVersion () inout return @safe pure nothrow @nogc { 1403 return this.target.match!( 1404 (Version v) => v, 1405 (any) => Version.init, 1406 ); 1407 } 1408 1409 deprecated("Assign `target` directly instead") 1410 @property ref PackageOverride targetVersion (Version v) scope return pure nothrow @nogc { 1411 this.target = v; 1412 return this; 1413 } 1414 1415 deprecated("Use `target.match` directly instead") 1416 @property inout(NativePath) targetPath () inout return @safe pure nothrow @nogc { 1417 return this.target.match!( 1418 (NativePath v) => v, 1419 (any) => NativePath.init, 1420 ); 1421 } 1422 1423 deprecated("Assign `target` directly instead") 1424 @property ref PackageOverride targetPath (NativePath v) scope return pure nothrow @nogc { 1425 this.target = v; 1426 return this; 1427 } 1428 1429 deprecated("Use the overload that accepts a `VersionRange` as 2nd argument") 1430 this(string package_, Dependency version_, Version target_version) 1431 { 1432 this.package_ = package_; 1433 this.version_ = version_; 1434 this.target = target_version; 1435 } 1436 1437 deprecated("Use the overload that accepts a `VersionRange` as 2nd argument") 1438 this(string package_, Dependency version_, NativePath target_path) 1439 { 1440 this.package_ = package_; 1441 this.version_ = version_; 1442 this.target = target_path; 1443 } 1444 1445 this(string package_, VersionRange src, Version target) 1446 { 1447 this.package_ = package_; 1448 this.source = src; 1449 this.target = target; 1450 } 1451 1452 this(string package_, VersionRange src, NativePath target) 1453 { 1454 this.package_ = package_; 1455 this.source = src; 1456 this.target = target; 1457 } 1458 } 1459 1460 deprecated("Use `PlacementLocation` instead") 1461 enum LocalPackageType : PlacementLocation { 1462 package_ = PlacementLocation.local, 1463 user = PlacementLocation.user, 1464 system = PlacementLocation.system, 1465 } 1466 1467 private enum LocalPackagesFilename = "local-packages.json"; 1468 private enum LocalOverridesFilename = "local-overrides.json"; 1469 1470 /** 1471 * A managed location, with packages, configuration, and overrides 1472 * 1473 * There exists three standards locations, listed in `PlacementLocation`. 1474 * The user one is the default, with the system and local one meeting 1475 * different needs. 1476 * 1477 * Each location has a root, under which the following may be found: 1478 * - A `packages/` directory, where packages are stored (see `packagePath`); 1479 * - A `local-packages.json` file, with extra search paths 1480 * and manually added packages (see `dub add-local`); 1481 * - A `local-overrides.json` file, with manually added overrides (`dub add-override`); 1482 * 1483 * Additionally, each location host a config file, 1484 * which is not managed by this module, but by dub itself. 1485 */ 1486 package struct Location { 1487 /// The absolute path to the root of the location 1488 NativePath packagePath; 1489 1490 /// Configured (extra) search paths for this `Location` 1491 NativePath[] searchPath; 1492 1493 /** 1494 * List of manually registered packages at this `Location` 1495 * and stored in `local-packages.json` 1496 */ 1497 Package[] localPackages; 1498 1499 /// List of overrides stored at this `Location` 1500 PackageOverride_[] overrides; 1501 1502 /** 1503 * List of packages stored under `packagePath` and automatically detected 1504 */ 1505 Package[] fromPath; 1506 1507 this(NativePath path) @safe pure nothrow @nogc 1508 { 1509 this.packagePath = path; 1510 } 1511 1512 void loadOverrides(PackageManager mgr) 1513 { 1514 this.overrides = null; 1515 auto ovrfilepath = this.packagePath ~ LocalOverridesFilename; 1516 if (mgr.fs.existsFile(ovrfilepath)) { 1517 logWarn("Found local override file: %s", ovrfilepath); 1518 logWarn(OverrideDepMsg); 1519 logWarn("Replace with a path-based dependency in your project or a custom cache path"); 1520 const text = mgr.fs.readText(ovrfilepath); 1521 auto json = parseJsonString(text, ovrfilepath.toNativeString()); 1522 foreach (entry; json) { 1523 PackageOverride_ ovr; 1524 ovr.package_ = entry["name"].get!string; 1525 ovr.source = VersionRange.fromString(entry["version"].get!string); 1526 if (auto pv = "targetVersion" in entry) ovr.target = Version(pv.get!string); 1527 if (auto pv = "targetPath" in entry) ovr.target = NativePath(pv.get!string); 1528 this.overrides ~= ovr; 1529 } 1530 } 1531 } 1532 1533 private void writeOverrides(PackageManager mgr) 1534 { 1535 Json[] newlist; 1536 foreach (ovr; this.overrides) { 1537 auto jovr = Json.emptyObject; 1538 jovr["name"] = ovr.package_; 1539 jovr["version"] = ovr.source.toString(); 1540 ovr.target.match!( 1541 (NativePath path) { jovr["targetPath"] = path.toNativeString(); }, 1542 (Version vers) { jovr["targetVersion"] = vers.toString(); }, 1543 ); 1544 newlist ~= jovr; 1545 } 1546 auto path = this.packagePath; 1547 mgr.fs.mkdir(path); 1548 auto app = appender!string(); 1549 app.writePrettyJsonString(Json(newlist)); 1550 mgr.fs.writeFile(path ~ LocalOverridesFilename, app.data); 1551 } 1552 1553 private void writeLocalPackageList(PackageManager mgr) 1554 { 1555 Json[] newlist; 1556 foreach (p; this.searchPath) { 1557 auto entry = Json.emptyObject; 1558 entry["name"] = "*"; 1559 entry["path"] = p.toNativeString(); 1560 newlist ~= entry; 1561 } 1562 1563 foreach (p; this.localPackages) { 1564 if (p.parentPackage) continue; // do not store sub packages 1565 auto entry = Json.emptyObject; 1566 entry["name"] = p.name; 1567 entry["version"] = p.version_.toString(); 1568 entry["path"] = p.path.toNativeString(); 1569 newlist ~= entry; 1570 } 1571 1572 NativePath path = this.packagePath; 1573 mgr.fs.mkdir(path); 1574 auto app = appender!string(); 1575 app.writePrettyJsonString(Json(newlist)); 1576 mgr.fs.writeFile(path ~ LocalPackagesFilename, app.data); 1577 } 1578 1579 // load locally defined packages 1580 void scanLocalPackages(bool refresh, PackageManager manager) 1581 { 1582 NativePath list_path = this.packagePath; 1583 Package[] packs; 1584 NativePath[] paths; 1585 try { 1586 auto local_package_file = list_path ~ LocalPackagesFilename; 1587 if (!manager.fs.existsFile(local_package_file)) return; 1588 1589 logDiagnostic("Loading local package map at %s", local_package_file.toNativeString()); 1590 const text = manager.fs.readText(local_package_file); 1591 auto packlist = parseJsonString( 1592 text, local_package_file.toNativeString()); 1593 enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array."); 1594 foreach (pentry; packlist) { 1595 try { 1596 auto name = pentry["name"].get!string; 1597 auto path = NativePath(pentry["path"].get!string); 1598 if (name == "*") { 1599 paths ~= path; 1600 } else { 1601 auto ver = Version(pentry["version"].get!string); 1602 1603 Package pp; 1604 if (!refresh) { 1605 foreach (p; this.localPackages) 1606 if (p.path == path) { 1607 pp = p; 1608 break; 1609 } 1610 } 1611 1612 if (!pp) { 1613 auto infoFile = manager.findPackageFile(path); 1614 if (!infoFile.empty) pp = manager.load(path, infoFile); 1615 else { 1616 logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.", 1617 name, ver, path.toNativeString()); 1618 // Store a dummy package 1619 pp = new Package(PackageRecipe(name), path); 1620 } 1621 } 1622 1623 if (pp.name != name) 1624 logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name); 1625 pp.version_ = ver; 1626 manager.addPackages(packs, pp); 1627 } 1628 } catch (Exception e) { 1629 logWarn("Error adding local package: %s", e.msg); 1630 } 1631 } 1632 } catch (Exception e) { 1633 logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); 1634 } 1635 this.localPackages = packs; 1636 this.searchPath = paths; 1637 } 1638 1639 /** 1640 * Scan this location 1641 */ 1642 void scan(PackageManager mgr, bool refresh) 1643 { 1644 // If we're asked to refresh, reload the packages from scratch 1645 auto existing = refresh ? null : this.fromPath; 1646 if (this.packagePath !is NativePath.init) { 1647 // For the internal location, we use `fromPath` to store packages 1648 // loaded by the user (e.g. the project and its sub-packages), 1649 // so don't clean it. 1650 this.fromPath = null; 1651 } 1652 foreach (path; this.searchPath) 1653 this.scanPackageFolder(path, mgr, existing); 1654 if (this.packagePath !is NativePath.init) 1655 this.scanPackageFolder(this.packagePath, mgr, existing); 1656 } 1657 1658 /** 1659 * Scan the content of a folder (`packagePath` or in `searchPaths`), 1660 * and add all packages that were found to this location. 1661 */ 1662 void scanPackageFolder(NativePath path, PackageManager mgr, 1663 Package[] existing_packages) 1664 { 1665 if (!mgr.fs.existsDirectory(path)) 1666 return; 1667 1668 void loadInternal (NativePath pack_path, NativePath packageFile) 1669 { 1670 import std.algorithm.searching : find; 1671 1672 // If the package has already been loaded, no need to re-load it. 1673 auto rng = existing_packages.find!(pp => pp.path == pack_path); 1674 if (!rng.empty) 1675 return mgr.addPackages(this.fromPath, rng.front); 1676 1677 try { 1678 mgr.addPackages(this.fromPath, mgr.load(pack_path, packageFile)); 1679 } catch (ConfigException exc) { 1680 // Configy error message already include the path 1681 logError("Invalid recipe for local package: %S", exc); 1682 } catch (Exception e) { 1683 logError("Failed to load package in %s: %s", pack_path, e.msg); 1684 logDiagnostic("Full error: %s", e.toString().sanitize()); 1685 } 1686 } 1687 1688 logDebug("iterating dir %s", path.toNativeString()); 1689 try foreach (pdir; mgr.fs.iterateDirectory(path)) { 1690 logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); 1691 if (!pdir.isDirectory) continue; 1692 1693 const pack_path = path ~ (pdir.name ~ "/"); 1694 auto packageFile = mgr.findPackageFile(pack_path); 1695 1696 if (isManaged(path)) { 1697 // Old / flat directory structure, used in non-standard path 1698 // Packages are stored in $ROOT/$SOMETHING/` 1699 if (!packageFile.empty) { 1700 // Deprecated flat managed directory structure 1701 logWarn("Package at path '%s' should be under '%s'", 1702 pack_path.toNativeString().color(Mode.bold), 1703 (pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold)); 1704 logWarn("The package will no longer be detected starting from v1.42.0"); 1705 loadInternal(pack_path, packageFile); 1706 } else { 1707 // New managed structure: $ROOT/$NAME/$VERSION/$NAME 1708 // This is the most common code path 1709 1710 // Iterate over versions of a package 1711 foreach (versdir; mgr.fs.iterateDirectory(pack_path)) { 1712 if (!versdir.isDirectory) continue; 1713 auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/"); 1714 if (!mgr.fs.existsDirectory(vers_path)) continue; 1715 packageFile = mgr.findPackageFile(vers_path); 1716 loadInternal(vers_path, packageFile); 1717 } 1718 } 1719 } else { 1720 // Unmanaged directories (dub add-path) are always stored as a 1721 // flat list of packages, as these are the working copies managed 1722 // by the user. The nested structure should not be supported, 1723 // even optionally, because that would lead to bogus "no package 1724 // file found" errors in case the internal directory structure 1725 // accidentally matches the $NAME/$VERSION/$NAME scheme 1726 if (!packageFile.empty) 1727 loadInternal(pack_path, packageFile); 1728 } 1729 } 1730 catch (Exception e) 1731 logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString()); 1732 } 1733 1734 /** 1735 * Looks up already-loaded packages at a specific version 1736 * 1737 * Looks up a package according to this `Location`'s priority, 1738 * that is, packages from the search path and local packages 1739 * have the highest priority. 1740 * 1741 * Params: 1742 * name = The full name of the package to look up 1743 * ver = The version to look up 1744 * 1745 * Returns: 1746 * A `Package` if one was found, `null` if none exists. 1747 */ 1748 inout(Package) lookup(in PackageName name, in Version ver) inout { 1749 foreach (pkg; this.localPackages) 1750 if (pkg.name == name.toString() && 1751 pkg.version_.matches(ver, VersionMatchMode.standard)) 1752 return pkg; 1753 foreach (pkg; this.fromPath) { 1754 auto pvm = this.isManaged(pkg.basePackage.path) ? 1755 VersionMatchMode.strict : VersionMatchMode.standard; 1756 if (pkg.name == name.toString() && pkg.version_.matches(ver, pvm)) 1757 return pkg; 1758 } 1759 return null; 1760 } 1761 1762 /** 1763 * Looks up a package, first in the list of loaded packages, 1764 * then directly on the file system. 1765 * 1766 * This function allows for lazy loading of packages, without needing to 1767 * first scan all the available locations (as `scan` does). 1768 * 1769 * Params: 1770 * name = The full name of the package to look up 1771 * vers = The version the package must match 1772 * mgr = The `PackageManager` to use for adding packages 1773 * 1774 * Returns: 1775 * A `Package` if one was found, `null` if none exists. 1776 */ 1777 Package load (in PackageName name, Version vers, PackageManager mgr) 1778 { 1779 if (auto pkg = this.lookup(name, vers)) 1780 return pkg; 1781 1782 string versStr = vers.toString(); 1783 const path = this.getPackagePath(name, versStr); 1784 if (!mgr.fs.existsDirectory(path)) 1785 return null; 1786 1787 logDiagnostic("Lazily loading package %s:%s from %s", name.main, vers, path); 1788 auto p = mgr.load(path); 1789 enforce( 1790 p.version_ == vers, 1791 format("Package %s located in %s has a different version than its path: Got %s, expected %s", 1792 name.main, path, p.version_, vers)); 1793 return mgr.addPackagesAndResolveSubPackage(this.fromPath, p, name); 1794 } 1795 1796 /** 1797 * Get the final destination a specific package needs to be stored in. 1798 * 1799 * Note that there needs to be an extra level for libraries like `ae` 1800 * which expects their containing folder to have an exact name and use 1801 * `importPath "../"`. 1802 * 1803 * Hence the final format returned is `$BASE/$NAME/$VERSION/$NAME`, 1804 * `$BASE` is `this.packagePath`. 1805 * 1806 * Params: 1807 * name = The package name - if the name is that of a subpackage, 1808 * only the path to the main package is returned, as the 1809 * subpackage path can only be known after reading the recipe. 1810 * vers = A version string. Typed as a string because git hashes 1811 * can be used with this function. 1812 * 1813 * Returns: 1814 * An absolute `NativePath` nested in this location. 1815 */ 1816 NativePath getPackagePath (in PackageName name, string vers) 1817 { 1818 NativePath result = this.packagePath ~ name.main.toString() ~ vers ~ 1819 name.main.toString(); 1820 result.endsWithSlash = true; 1821 return result; 1822 } 1823 1824 /// Determines if a specific path is within a DUB managed Location. 1825 bool isManaged(NativePath path) const { 1826 return path.startsWith(this.packagePath); 1827 } 1828 } 1829 1830 private immutable string OverrideDepMsg = 1831 "Overrides are deprecated as they are redundant with more fine-grained approaches";