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