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