1 /**
2 	Management of packages on the local computer.
3 
4 	Copyright: © 2012-2016 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig, Matthias Dondorff
7 */
8 module dub.packagemanager;
9 
10 import dub.dependency;
11 import dub.internal.utils;
12 import dub.internal.vibecompat.core.file;
13 import dub.internal.vibecompat.core.log;
14 import dub.internal.vibecompat.data.json;
15 import dub.internal.vibecompat.inet.path;
16 import dub.package_;
17 
18 import std.algorithm : countUntil, filter, sort, canFind, remove;
19 import std.array;
20 import std.conv;
21 import std.digest.sha;
22 import std.encoding : sanitize;
23 import std.exception;
24 import std.file;
25 import std.string;
26 import std.zip;
27 
28 
29 /// The PackageManager can retrieve present packages and get / remove
30 /// packages.
31 class PackageManager {
32 	private {
33 		Repository[LocalPackageType] m_repositories;
34 		Path[] m_searchPath;
35 		Package[] m_packages;
36 		Package[] m_temporaryPackages;
37 		bool m_disableDefaultSearchPaths = false;
38 	}
39 
40 	this(Path user_path, Path system_path, bool refresh_packages = true)
41 	{
42 		m_repositories[LocalPackageType.user] = Repository(user_path);
43 		m_repositories[LocalPackageType.system] = Repository(system_path);
44 		if (refresh_packages) refresh(true);
45 	}
46 
47 	/** Gets/sets the list of paths to search for local packages.
48 	*/
49 	@property void searchPath(Path[] paths)
50 	{
51 		if (paths == m_searchPath) return;
52 		m_searchPath = paths.dup;
53 		refresh(false);
54 	}
55 	/// ditto
56 	@property const(Path)[] searchPath() const { return m_searchPath; }
57 
58 	/** Disables searching DUB's predefined search paths.
59 	*/
60 	@property void disableDefaultSearchPaths(bool val)
61 	{
62 		if (val == m_disableDefaultSearchPaths) return;
63 		m_disableDefaultSearchPaths = val;
64 		refresh(true);
65 	}
66 
67 	/** Returns the effective list of search paths, including default ones.
68 	*/
69 	@property const(Path)[] completeSearchPath()
70 	const {
71 		auto ret = appender!(Path[])();
72 		ret.put(m_searchPath);
73 		if (!m_disableDefaultSearchPaths) {
74 			ret.put(m_repositories[LocalPackageType.user].searchPath);
75 			ret.put(m_repositories[LocalPackageType.user].packagePath);
76 			ret.put(m_repositories[LocalPackageType.system].searchPath);
77 			ret.put(m_repositories[LocalPackageType.system].packagePath);
78 		}
79 		return ret.data;
80 	}
81 
82 
83 	/** Looks up a specific package.
84 
85 		Looks up a package matching the given version/path in the set of
86 		registered packages. The lookup order is done according the the
87 		usual rules (see getPackageIterator).
88 
89 		Params:
90 			name = The name of the package
91 			ver = The exact version of the package to query
92 			path = An exact path that the package must reside in. Note that
93 				the package must still be registered in the package manager.
94 			enable_overrides = Apply the local package override list before
95 				returning a package (enabled by default)
96 
97 		Returns:
98 			The matching package or null if no match was found.
99 	*/
100 	Package getPackage(string name, Version ver, bool enable_overrides = true)
101 	{
102 		if (enable_overrides) {
103 			foreach (tp; [LocalPackageType.user, LocalPackageType.system])
104 				foreach (ovr; m_repositories[tp].overrides)
105 					if (ovr.package_ == name && ovr.version_.matches(ver)) {
106 						Package pack;
107 						if (!ovr.targetPath.empty) pack = getPackage(name, ovr.targetPath);
108 						else pack = getPackage(name, ovr.targetVersion, false);
109 						if (pack) return pack;
110 
111 						logWarn("Package override %s %s -> %s %s doesn't reference an existing package.",
112 							ovr.package_, ovr.version_, ovr.targetVersion, ovr.targetPath);
113 					}
114 		}
115 
116 		foreach (p; getPackageIterator(name))
117 			if (p.version_ == ver)
118 				return p;
119 
120 		return null;
121 	}
122 
123 	/// ditto
124 	Package getPackage(string name, string ver, bool enable_overrides = true)
125 	{
126 		return getPackage(name, Version(ver), enable_overrides);
127 	}
128 
129 	/// ditto
130 	Package getPackage(string name, Version ver, Path path)
131 	{
132 		auto ret = getPackage(name, path);
133 		if (!ret || ret.version_ != ver) return null;
134 		return ret;
135 	}
136 
137 	/// ditto
138 	Package getPackage(string name, string ver, Path path)
139 	{
140 		return getPackage(name, Version(ver), path);
141 	}
142 
143 	/// ditto
144 	Package getPackage(string name, Path path)
145 	{
146 		foreach( p; getPackageIterator(name) )
147 			if (p.path.startsWith(path))
148 				return p;
149 		return null;
150 	}
151 
152 
153 	/** Looks up the first package matching the given name.
154 	*/
155 	Package getFirstPackage(string name)
156 	{
157 		foreach (ep; getPackageIterator(name))
158 			return ep;
159 		return null;
160 	}
161 
162 	/** For a given package path, returns the corresponding package.
163 
164 		If the package is already loaded, a reference is returned. Otherwise
165 		the package gets loaded and cached for the next call to this function.
166 
167 		Params:
168 			path = Path to the root directory of the package
169 			recipe_path = Optional path to the recipe file of the package
170 			allow_sub_packages = Also return a sub package if it resides in the given folder
171 
172 		Returns: The packages loaded from the given path
173 		Throws: Throws an exception if no package can be loaded
174 	*/
175 	Package getOrLoadPackage(Path path, Path recipe_path = Path.init, bool allow_sub_packages = false)
176 	{
177 		path.endsWithSlash = true;
178 		foreach (p; getPackageIterator())
179 			if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path)))
180 				return p;
181 		auto pack = Package.load(path, recipe_path);
182 		addPackages(m_temporaryPackages, pack);
183 		return pack;
184 	}
185 
186 
187 	/** Searches for the latest version of a package matching the given dependency.
188 	*/
189 	Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true)
190 	{
191 		Package ret;
192 		foreach (p; getPackageIterator(name))
193 			if (version_spec.matches(p.version_) && (!ret || p.version_ > ret.version_))
194 				ret = p;
195 
196 		if (enable_overrides && ret) {
197 			if (auto ovr = getPackage(name, ret.version_))
198 				return ovr;
199 		}
200 		return ret;
201 	}
202 
203 	/// ditto
204 	Package getBestPackage(string name, string version_spec)
205 	{
206 		return getBestPackage(name, Dependency(version_spec));
207 	}
208 
209 	/** Gets the a specific sub package.
210 
211 		In contrast to `Package.getSubPackage`, this function supports path
212 		based sub packages.
213 
214 		Params:
215 			base_package = The package from which to get a sub package
216 			sub_name = Name of the sub package (not prefixed with the base
217 				package name)
218 			silent_fail = If set to true, the function will return `null` if no
219 				package is found. Otherwise will throw an exception.
220 
221 	*/
222 	Package getSubPackage(Package base_package, string sub_name, bool silent_fail)
223 	{
224 		foreach (p; getPackageIterator(base_package.name~":"~sub_name))
225 			if (p.parentPackage is base_package)
226 				return p;
227 		enforce(silent_fail, "Sub package "~base_package.name~":"~sub_name~" doesn't exist.");
228 		return null;
229 	}
230 
231 
232 	/** Determines if a package is managed by DUB.
233 
234 		Managed packages can be upgraded and removed.
235 	*/
236 	bool isManagedPackage(Package pack)
237 	const {
238 		auto ppath = pack.basePackage.path;
239 		return isManagedPath(ppath);
240 	}
241 
242 	/** Determines if a specifc path is within a DUB managed package folder.
243 
244 		By default, managed folders are "~/.dub/packages" and
245 		"/var/lib/dub/packages".
246 	*/
247 	bool isManagedPath(Path path)
248 	const {
249 		foreach (rep; m_repositories) {
250 			auto rpath = rep.packagePath;
251 			if (path.startsWith(rpath))
252 				return true;
253 		}
254 		return false;
255 	}
256 
257 	/** Enables iteration over all known local packages.
258 
259 		Returns: A delegate suitable for use with `foreach` is returned.
260 	*/
261 	int delegate(int delegate(ref Package)) getPackageIterator()
262 	{
263 		int iterator(int delegate(ref Package) del)
264 		{
265 			foreach (tp; m_temporaryPackages)
266 				if (auto ret = del(tp)) return ret;
267 
268 			// first search local packages
269 			foreach (tp; LocalPackageType.min .. LocalPackageType.max+1)
270 				foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages)
271 					if (auto ret = del(p)) return ret;
272 
273 			// and then all packages gathered from the search path
274 			foreach( p; m_packages )
275 				if( auto ret = del(p) )
276 					return ret;
277 			return 0;
278 		}
279 
280 		return &iterator;
281 	}
282 
283 	/** Enables iteration over all known local packages with a certain name.
284 
285 		Returns: A delegate suitable for use with `foreach` is returned.
286 	*/
287 	int delegate(int delegate(ref Package)) getPackageIterator(string name)
288 	{
289 		int iterator(int delegate(ref Package) del)
290 		{
291 			foreach (p; getPackageIterator())
292 				if (p.name == name)
293 					if (auto ret = del(p)) return ret;
294 			return 0;
295 		}
296 
297 		return &iterator;
298 	}
299 
300 
301 	/** Returns a list of all package overrides for the given scope.
302 	*/
303 	const(PackageOverride)[] getOverrides(LocalPackageType scope_)
304 	const {
305 		return m_repositories[scope_].overrides;
306 	}
307 
308 	/** Adds a new override for the given package.
309 	*/
310 	void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Version target)
311 	{
312 		m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
313 		writeLocalPackageOverridesFile(scope_);
314 	}
315 	/// ditto
316 	void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Path target)
317 	{
318 		m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
319 		writeLocalPackageOverridesFile(scope_);
320 	}
321 
322 	/** Removes an existing package override.
323 	*/
324 	void removeOverride(LocalPackageType scope_, string package_, Dependency version_spec)
325 	{
326 		Repository* rep = &m_repositories[scope_];
327 		foreach (i, ovr; rep.overrides) {
328 			if (ovr.package_ != package_ || ovr.version_ != version_spec)
329 				continue;
330 			rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $];
331 			writeLocalPackageOverridesFile(scope_);
332 			return;
333 		}
334 		throw new Exception(format("No override exists for %s %s", package_, version_spec));
335 	}
336 
337 	/// Extracts the package supplied as a path to it's zip file to the
338 	/// destination and sets a version field in the package description.
339 	Package storeFetchedPackage(Path zip_file_path, Json package_info, Path destination)
340 	{
341 		auto package_name = package_info["name"].get!string;
342 		auto package_version = package_info["version"].get!string;
343 		auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $];
344 
345 		logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'",
346 			package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
347 
348 		if( existsFile(destination) ){
349 			throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination));
350 		}
351 
352 		// open zip file
353 		ZipArchive archive;
354 		{
355 			logDebug("Opening file %s", zip_file_path);
356 			auto f = openFile(zip_file_path, FileMode.read);
357 			scope(exit) f.close();
358 			archive = new ZipArchive(f.readAll());
359 		}
360 
361 		logDebug("Extracting from zip.");
362 
363 		// In a github zip, the actual contents are in a subfolder
364 		Path zip_prefix;
365 		outer: foreach(ArchiveMember am; archive.directory) {
366 			auto path = Path(am.name);
367 			foreach (fil; packageInfoFiles)
368 				if (path.length == 2 && path.head.toString == fil.filename) {
369 					zip_prefix = path[0 .. $-1];
370 					break outer;
371 				}
372 		}
373 
374 		logDebug("zip root folder: %s", zip_prefix);
375 
376 		Path getCleanedPath(string fileName) {
377 			auto path = Path(fileName);
378 			if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path();
379 			return path[zip_prefix.length..path.length];
380 		}
381 
382 		// extract & place
383 		mkdirRecurse(destination.toNativeString());
384 		logDebug("Copying all files...");
385 		int countFiles = 0;
386 		foreach(ArchiveMember a; archive.directory) {
387 			auto cleanedPath = getCleanedPath(a.name);
388 			if(cleanedPath.empty) continue;
389 			auto dst_path = destination~cleanedPath;
390 
391 			logDebug("Creating %s", cleanedPath);
392 			if( dst_path.endsWithSlash ){
393 				if( !existsDirectory(dst_path) )
394 					mkdirRecurse(dst_path.toNativeString());
395 			} else {
396 				if( !existsDirectory(dst_path.parentPath) )
397 					mkdirRecurse(dst_path.parentPath.toNativeString());
398 				auto dstFile = openFile(dst_path, FileMode.createTrunc);
399 				scope(exit) dstFile.close();
400 				dstFile.put(archive.expand(a));
401 				++countFiles;
402 			}
403 		}
404 		logDebug("%s file(s) copied.", to!string(countFiles));
405 
406 		// overwrite dub.json (this one includes a version field)
407 		auto pack = Package.load(destination, Path.init, null, package_info["version"].get!string);
408 
409 		if (pack.recipePath.head != defaultPackageFilename)
410 			// Storeinfo saved a default file, this could be different to the file from the zip.
411 			removeFile(pack.recipePath);
412 		pack.storeInfo();
413 		addPackages(m_packages, pack);
414 		return pack;
415 	}
416 
417 	/// Removes the given the package.
418 	void remove(in Package pack, bool force_remove)
419 	{
420 		logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path);
421 		enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
422 
423 		// remove package from repositories' list
424 		bool found = false;
425 		bool removeFrom(Package[] packs, in Package pack) {
426 			auto packPos = countUntil!("a.path == b.path")(packs, pack);
427 			if(packPos != -1) {
428 				packs = .remove(packs, packPos);
429 				return true;
430 			}
431 			return false;
432 		}
433 		foreach(repo; m_repositories) {
434 			if(removeFrom(repo.localPackages, pack)) {
435 				found = true;
436 				break;
437 			}
438 		}
439 		if(!found)
440 			found = removeFrom(m_packages, pack);
441 		enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
442 
443 		logDebug("About to delete root folder for package '%s'.", pack.path);
444 		rmdirRecurse(pack.path.toNativeString());
445 		logInfo("Removed package: '"~pack.name~"'");
446 	}
447 
448 	Package addLocalPackage(Path path, string verName, LocalPackageType type)
449 	{
450 		path.endsWithSlash = true;
451 		auto pack = Package.load(path);
452 		enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
453 		if (verName.length)
454 			pack.version_ = Version(verName);
455 
456 		// don't double-add packages
457 		Package[]* packs = &m_repositories[type].localPackages;
458 		foreach (p; *packs) {
459 			if (p.path == path) {
460 				enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed.");
461 				logInfo("Package is already registered: %s (version: %s)", p.name, p.version_);
462 				return p;
463 			}
464 		}
465 
466 		addPackages(*packs, pack);
467 
468 		writeLocalPackageList(type);
469 
470 		logInfo("Registered package: %s (version: %s)", pack.name, pack.version_);
471 		return pack;
472 	}
473 
474 	void removeLocalPackage(Path path, LocalPackageType type)
475 	{
476 		path.endsWithSlash = true;
477 
478 		Package[]* packs = &m_repositories[type].localPackages;
479 		size_t[] to_remove;
480 		foreach( i, entry; *packs )
481 			if( entry.path == path )
482 				to_remove ~= i;
483 		enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
484 
485 		string[Version] removed;
486 		foreach_reverse( i; to_remove ) {
487 			removed[(*packs)[i].version_] = (*packs)[i].name;
488 			*packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
489 		}
490 
491 		writeLocalPackageList(type);
492 
493 		foreach(ver, name; removed)
494 			logInfo("Deregistered package: %s (version: %s)", name, ver);
495 	}
496 
497 	/// For the given type add another path where packages will be looked up.
498 	void addSearchPath(Path path, LocalPackageType type)
499 	{
500 		m_repositories[type].searchPath ~= path;
501 		writeLocalPackageList(type);
502 	}
503 
504 	/// Removes a search path from the given type.
505 	void removeSearchPath(Path path, LocalPackageType type)
506 	{
507 		m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
508 		writeLocalPackageList(type);
509 	}
510 
511 	void refresh(bool refresh_existing_packages)
512 	{
513 		logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh_existing_packages);
514 
515 		// load locally defined packages
516 		void scanLocalPackages(LocalPackageType type)
517 		{
518 			Path list_path = m_repositories[type].packagePath;
519 			Package[] packs;
520 			Path[] paths;
521 			if (!m_disableDefaultSearchPaths) try {
522 				auto local_package_file = list_path ~ LocalPackagesFilename;
523 				logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString());
524 				if( !existsFile(local_package_file) ) return;
525 				logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString());
526 				auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
527 				enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array.");
528 				foreach( pentry; packlist ){
529 					try {
530 						auto name = pentry["name"].get!string;
531 						auto path = Path(pentry["path"].get!string);
532 						if (name == "*") {
533 							paths ~= path;
534 						} else {
535 							auto ver = Version(pentry["version"].get!string);
536 
537 							Package pp;
538 							if (!refresh_existing_packages) {
539 								foreach (p; m_repositories[type].localPackages)
540 									if (p.path == path) {
541 										pp = p;
542 										break;
543 									}
544 							}
545 
546 							if (!pp) {
547 								auto infoFile = Package.findPackageFile(path);
548 								if (!infoFile.empty) pp = Package.load(path, infoFile);
549 								else {
550 									logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.",
551 										name, ver, path.toNativeString());
552 									auto info = Json.emptyObject;
553 									info["name"] = name;
554 									pp = new Package(info, path);
555 								}
556 							}
557 
558 							if (pp.name != name)
559 								logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name);
560 							pp.version_ = ver;
561 
562 							addPackages(packs, pp);
563 						}
564 					} catch( Exception e ){
565 						logWarn("Error adding local package: %s", e.msg);
566 					}
567 				}
568 			} catch( Exception e ){
569 				logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
570 			}
571 			m_repositories[type].localPackages = packs;
572 			m_repositories[type].searchPath = paths;
573 		}
574 		scanLocalPackages(LocalPackageType.system);
575 		scanLocalPackages(LocalPackageType.user);
576 
577 		auto old_packages = m_packages;
578 
579 		// rescan the system and user package folder
580 		void scanPackageFolder(Path path)
581 		{
582 			if( path.existsDirectory() ){
583 				logDebug("iterating dir %s", path.toNativeString());
584 				try foreach( pdir; iterateDirectory(path) ){
585 					logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
586 					if (!pdir.isDirectory) continue;
587 
588 					auto pack_path = path ~ (pdir.name ~ "/");
589 
590 					auto packageFile = Package.findPackageFile(pack_path);
591 
592 					if (isManagedPath(path) && packageFile.empty) {
593 						// Search for a single directory within this directory which happen to be a prefix of pdir
594 						// This is to support new folder structure installed over the ancient one.
595 						foreach (subdir; iterateDirectory(path ~ (pdir.name ~ "/")))
596 							if (subdir.isDirectory && pdir.name.startsWith(subdir.name)) {// eg: package vibe-d will be in "vibe-d-x.y.z/vibe-d"
597 								pack_path ~= subdir.name ~ "/";
598 								packageFile = Package.findPackageFile(pack_path);
599 								break;
600 							}
601 					}
602 
603 					if (packageFile.empty) continue;
604 					Package p;
605 					try {
606 						if (!refresh_existing_packages)
607 							foreach (pp; old_packages)
608 								if (pp.path == pack_path) {
609 									p = pp;
610 									break;
611 								}
612 						if (!p) p = Package.load(pack_path, packageFile);
613 						addPackages(m_packages, p);
614 					} catch( Exception e ){
615 						logError("Failed to load package in %s: %s", pack_path, e.msg);
616 						logDiagnostic("Full error: %s", e.toString().sanitize());
617 					}
618 				}
619 				catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
620 			}
621 		}
622 
623 		m_packages = null;
624 		foreach (p; this.completeSearchPath)
625 			scanPackageFolder(p);
626 
627 		void loadOverrides(LocalPackageType type)
628 		{
629 			m_repositories[type].overrides = null;
630 			auto ovrfilepath = m_repositories[type].packagePath ~ LocalOverridesFilename;
631 			if (existsFile(ovrfilepath)) {
632 				foreach (entry; jsonFromFile(ovrfilepath)) {
633 					PackageOverride ovr;
634 					ovr.package_ = entry["name"].get!string;
635 					ovr.version_ = Dependency(entry["version"].get!string);
636 					if (auto pv = "targetVersion" in entry) ovr.targetVersion = Version(pv.get!string);
637 					if (auto pv = "targetPath" in entry) ovr.targetPath = Path(pv.get!string);
638 					m_repositories[type].overrides ~= ovr;
639 				}
640 			}
641 		}
642 		loadOverrides(LocalPackageType.user);
643 		loadOverrides(LocalPackageType.system);
644 	}
645 
646 	alias Hash = ubyte[];
647 	/// Generates a hash value for a given package.
648 	/// Some files or folders are ignored during the generation (like .dub and
649 	/// .svn folders)
650 	Hash hashPackage(Package pack)
651 	{
652 		string[] ignored_directories = [".git", ".dub", ".svn"];
653 		// something from .dub_ignore or what?
654 		string[] ignored_files = [];
655 		SHA1 sha1;
656 		foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
657 			if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString()))
658 				continue;
659 			else if(ignored_files.canFind(Path(file.name).head.toString()))
660 				continue;
661 
662 			sha1.put(cast(ubyte[])Path(file.name).head.toString());
663 			if(file.isDir) {
664 				logDebug("Hashed directory name %s", Path(file.name).head);
665 			}
666 			else {
667 				sha1.put(openFile(Path(file.name)).readAll());
668 				logDebug("Hashed file contents from %s", Path(file.name).head);
669 			}
670 		}
671 		auto hash = sha1.finish();
672 		logDebug("Project hash: %s", hash);
673 		return hash[].dup;
674 	}
675 
676 	private void writeLocalPackageList(LocalPackageType type)
677 	{
678 		Json[] newlist;
679 		foreach (p; m_repositories[type].searchPath) {
680 			auto entry = Json.emptyObject;
681 			entry["name"] = "*";
682 			entry["path"] = p.toNativeString();
683 			newlist ~= entry;
684 		}
685 
686 		foreach (p; m_repositories[type].localPackages) {
687 			if (p.parentPackage) continue; // do not store sub packages
688 			auto entry = Json.emptyObject;
689 			entry["name"] = p.name;
690 			entry["version"] = p.version_.toString();
691 			entry["path"] = p.path.toNativeString();
692 			newlist ~= entry;
693 		}
694 
695 		Path path = m_repositories[type].packagePath;
696 		if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
697 		writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
698 	}
699 
700 	private void writeLocalPackageOverridesFile(LocalPackageType type)
701 	{
702 		Json[] newlist;
703 		foreach (ovr; m_repositories[type].overrides) {
704 			auto jovr = Json.emptyObject;
705 			jovr["name"] = ovr.package_;
706 			jovr["version"] = ovr.version_.versionSpec;
707 			if (!ovr.targetPath.empty) jovr["targetPath"] = ovr.targetPath.toNativeString();
708 			else jovr["targetVersion"] = ovr.targetVersion.toString();
709 			newlist ~= jovr;
710 		}
711 		auto path = m_repositories[type].packagePath;
712 		if (!existsDirectory(path)) mkdirRecurse(path.toNativeString());
713 		writeJsonFile(path ~ LocalOverridesFilename, Json(newlist));
714 	}
715 
716 	/// Adds the package and scans for subpackages.
717 	private void addPackages(ref Package[] dst_repos, Package pack)
718 	const {
719 		// Add the main package.
720 		dst_repos ~= pack;
721 
722 		// Additionally to the internally defined subpackages, whose metadata
723 		// is loaded with the main dub.json, load all externally defined
724 		// packages after the package is available with all the data.
725 		foreach (spr; pack.subPackages) {
726 			Package sp;
727 
728 			if (spr.path.length) {
729 				auto p = Path(spr.path);
730 				p.normalize();
731 				enforce(!p.absolute, "Sub package paths must be sub paths of the parent package.");
732 				auto path = pack.path ~ p;
733 				if (!existsFile(path)) {
734 					logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString());
735 					continue;
736 				}
737 				sp = Package.load(path, Path.init, pack);
738 			} else sp = new Package(spr.recipe, pack.path, pack);
739 
740 			// Add the subpackage.
741 			try {
742 				dst_repos ~= sp;
743 			} catch (Exception e) {
744 				logError("Package '%s': Failed to load sub-package %s: %s", pack.name,
745 					spr.path.length ? spr.path : spr.recipe.name, e.msg);
746 				logDiagnostic("Full error: %s", e.toString().sanitize());
747 			}
748 		}
749 	}
750 }
751 
752 struct PackageOverride {
753 	string package_;
754 	Dependency version_;
755 	Version targetVersion;
756 	Path targetPath;
757 
758 	this(string package_, Dependency version_, Version target_version)
759 	{
760 		this.package_ = package_;
761 		this.version_ = version_;
762 		this.targetVersion = target_version;
763 	}
764 
765 	this(string package_, Dependency version_, Path target_path)
766 	{
767 		this.package_ = package_;
768 		this.version_ = version_;
769 		this.targetPath = target_path;
770 	}
771 }
772 
773 enum LocalPackageType {
774 	user,
775 	system
776 }
777 
778 private enum LocalPackagesFilename = "local-packages.json";
779 private enum LocalOverridesFilename = "local-overrides.json";
780 
781 
782 private struct Repository {
783 	Path path;
784 	Path packagePath;
785 	Path[] searchPath;
786 	Package[] localPackages;
787 	PackageOverride[] overrides;
788 
789 	this(Path path)
790 	{
791 		this.path = path;
792 		this.packagePath = path ~"packages/";
793 	}
794 }