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