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