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";