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