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