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