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 		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[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(NativePath[] paths)
50 	{
51 		if (paths == m_searchPath) return;
52 		m_searchPath = paths.dup;
53 		refresh(false);
54 	}
55 	/// ditto
56 	@property const(NativePath)[] 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(NativePath)[] completeSearchPath()
70 	const {
71 		auto ret = appender!(NativePath[])();
72 		ret.put(cast(NativePath[])m_searchPath); // work around Phobos 17251
73 		if (!m_disableDefaultSearchPaths) {
74 			ret.put(cast(NativePath[])m_repositories[LocalPackageType.user].searchPath);
75 			ret.put(cast(NativePath)m_repositories[LocalPackageType.user].packagePath);
76 			ret.put(cast(NativePath[])m_repositories[LocalPackageType.system].searchPath);
77 			ret.put(cast(NativePath)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 = getOrLoadPackage(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, NativePath 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, NativePath path)
139 	{
140 		return getPackage(name, Version(ver), path);
141 	}
142 
143 	/// ditto
144 	Package getPackage(string name, NativePath 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 = NativePath 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(NativePath path, NativePath recipe_path = NativePath.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 specific 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(NativePath path)
248 	const {
249 		foreach (rep; m_repositories) {
250 			NativePath 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, NativePath 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(NativePath zip_file_path, Json package_info, NativePath destination)
340 	{
341 		import std.range : walkLength;
342 
343 		auto package_name = package_info["name"].get!string;
344 		auto package_version = package_info["version"].get!string;
345 		auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $];
346 
347 		logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'",
348 			package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
349 
350 		if( existsFile(destination) ){
351 			throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination));
352 		}
353 
354 		// open zip file
355 		ZipArchive archive;
356 		{
357 			logDebug("Opening file %s", zip_file_path);
358 			auto f = openFile(zip_file_path, FileMode.read);
359 			scope(exit) f.close();
360 			archive = new ZipArchive(f.readAll());
361 		}
362 
363 		logDebug("Extracting from zip.");
364 
365 		// In a github zip, the actual contents are in a subfolder
366 		alias PSegment = typeof(NativePath.init.head);
367 		PSegment[] zip_prefix;
368 		outer: foreach(ArchiveMember am; archive.directory) {
369 			auto path = NativePath(am.name).bySegment.array;
370 			foreach (fil; packageInfoFiles)
371 				if (path.length == 2 && path[$-1].toString == fil.filename) {
372 					zip_prefix = path[0 .. $-1];
373 					break outer;
374 				}
375 		}
376 
377 		logDebug("zip root folder: %s", zip_prefix);
378 
379 		NativePath getCleanedPath(string fileName) {
380 			auto path = NativePath(fileName);
381 			if (zip_prefix.length && !path.bySegment.startsWith(zip_prefix)) return NativePath.init;
382 			static if (is(typeof(path[0 .. 1]))) return path[zip_prefix.length .. $];
383 			else return NativePath(path.bySegment.array[zip_prefix.length .. $]);
384 		}
385 
386 		static void setAttributes(string path, ArchiveMember am)
387 		{
388 			import std.datetime : DosFileTimeToSysTime;
389 
390 			auto mtime = DosFileTimeToSysTime(am.time);
391 			setTimes(path, mtime, mtime);
392 			if (auto attrs = am.fileAttributes)
393 				std.file.setAttributes(path, attrs);
394 		}
395 
396 		// extract & place
397 		mkdirRecurse(destination.toNativeString());
398 		logDebug("Copying all files...");
399 		int countFiles = 0;
400 		foreach(ArchiveMember a; archive.directory) {
401 			auto cleanedPath = getCleanedPath(a.name);
402 			if(cleanedPath.empty) continue;
403 			auto dst_path = destination ~ cleanedPath;
404 
405 			logDebug("Creating %s", cleanedPath);
406 			if( dst_path.endsWithSlash ){
407 				if( !existsDirectory(dst_path) )
408 					mkdirRecurse(dst_path.toNativeString());
409 			} else {
410 				if( !existsDirectory(dst_path.parentPath) )
411 					mkdirRecurse(dst_path.parentPath.toNativeString());
412 				{
413 					auto dstFile = openFile(dst_path, FileMode.createTrunc);
414 					scope(exit) dstFile.close();
415 					dstFile.put(archive.expand(a));
416 				}
417 				setAttributes(dst_path.toNativeString(), a);
418 				++countFiles;
419 			}
420 		}
421 		logDebug("%s file(s) copied.", to!string(countFiles));
422 
423 		// overwrite dub.json (this one includes a version field)
424 		auto pack = Package.load(destination, NativePath.init, null, package_info["version"].get!string);
425 
426 		if (pack.recipePath.head != defaultPackageFilename)
427 			// Storeinfo saved a default file, this could be different to the file from the zip.
428 			removeFile(pack.recipePath);
429 		pack.storeInfo();
430 		addPackages(m_packages, pack);
431 		return pack;
432 	}
433 
434 	/// Removes the given the package.
435 	void remove(in Package pack)
436 	{
437 		logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path);
438 		enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
439 
440 		// remove package from repositories' list
441 		bool found = false;
442 		bool removeFrom(Package[] packs, in Package pack) {
443 			auto packPos = countUntil!("a.path == b.path")(packs, pack);
444 			if(packPos != -1) {
445 				packs = .remove(packs, packPos);
446 				return true;
447 			}
448 			return false;
449 		}
450 		foreach(repo; m_repositories) {
451 			if(removeFrom(repo.localPackages, pack)) {
452 				found = true;
453 				break;
454 			}
455 		}
456 		if(!found)
457 			found = removeFrom(m_packages, pack);
458 		enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
459 
460 		logDebug("About to delete root folder for package '%s'.", pack.path);
461 		rmdirRecurse(pack.path.toNativeString());
462 		logInfo("Removed package: '"~pack.name~"'");
463 	}
464 
465 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
466 	void remove(in Package pack, bool force_remove)
467 	{
468 		remove(pack);
469 	}
470 
471 	Package addLocalPackage(NativePath path, string verName, LocalPackageType type)
472 	{
473 		path.endsWithSlash = true;
474 		auto pack = Package.load(path);
475 		enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
476 		if (verName.length)
477 			pack.version_ = Version(verName);
478 
479 		// don't double-add packages
480 		Package[]* packs = &m_repositories[type].localPackages;
481 		foreach (p; *packs) {
482 			if (p.path == path) {
483 				enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed.");
484 				logInfo("Package is already registered: %s (version: %s)", p.name, p.version_);
485 				return p;
486 			}
487 		}
488 
489 		addPackages(*packs, pack);
490 
491 		writeLocalPackageList(type);
492 
493 		logInfo("Registered package: %s (version: %s)", pack.name, pack.version_);
494 		return pack;
495 	}
496 
497 	void removeLocalPackage(NativePath path, LocalPackageType type)
498 	{
499 		path.endsWithSlash = true;
500 
501 		Package[]* packs = &m_repositories[type].localPackages;
502 		size_t[] to_remove;
503 		foreach( i, entry; *packs )
504 			if( entry.path == path )
505 				to_remove ~= i;
506 		enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
507 
508 		string[Version] removed;
509 		foreach_reverse( i; to_remove ) {
510 			removed[(*packs)[i].version_] = (*packs)[i].name;
511 			*packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
512 		}
513 
514 		writeLocalPackageList(type);
515 
516 		foreach(ver, name; removed)
517 			logInfo("Deregistered package: %s (version: %s)", name, ver);
518 	}
519 
520 	/// For the given type add another path where packages will be looked up.
521 	void addSearchPath(NativePath path, LocalPackageType type)
522 	{
523 		m_repositories[type].searchPath ~= path;
524 		writeLocalPackageList(type);
525 	}
526 
527 	/// Removes a search path from the given type.
528 	void removeSearchPath(NativePath path, LocalPackageType type)
529 	{
530 		m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
531 		writeLocalPackageList(type);
532 	}
533 
534 	void refresh(bool refresh_existing_packages)
535 	{
536 		logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh_existing_packages);
537 
538 		// load locally defined packages
539 		void scanLocalPackages(LocalPackageType type)
540 		{
541 			NativePath list_path = m_repositories[type].packagePath;
542 			Package[] packs;
543 			NativePath[] paths;
544 			if (!m_disableDefaultSearchPaths) try {
545 				auto local_package_file = list_path ~ LocalPackagesFilename;
546 				logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString());
547 				if( !existsFile(local_package_file) ) return;
548 				logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString());
549 				auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
550 				enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array.");
551 				foreach( pentry; packlist ){
552 					try {
553 						auto name = pentry["name"].get!string;
554 						auto path = NativePath(pentry["path"].get!string);
555 						if (name == "*") {
556 							paths ~= path;
557 						} else {
558 							auto ver = Version(pentry["version"].get!string);
559 
560 							Package pp;
561 							if (!refresh_existing_packages) {
562 								foreach (p; m_repositories[type].localPackages)
563 									if (p.path == path) {
564 										pp = p;
565 										break;
566 									}
567 							}
568 
569 							if (!pp) {
570 								auto infoFile = Package.findPackageFile(path);
571 								if (!infoFile.empty) pp = Package.load(path, infoFile);
572 								else {
573 									logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.",
574 										name, ver, path.toNativeString());
575 									auto info = Json.emptyObject;
576 									info["name"] = name;
577 									pp = new Package(info, path);
578 								}
579 							}
580 
581 							if (pp.name != name)
582 								logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name);
583 							pp.version_ = ver;
584 
585 							addPackages(packs, pp);
586 						}
587 					} catch( Exception e ){
588 						logWarn("Error adding local package: %s", e.msg);
589 					}
590 				}
591 			} catch( Exception e ){
592 				logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
593 			}
594 			m_repositories[type].localPackages = packs;
595 			m_repositories[type].searchPath = paths;
596 		}
597 		scanLocalPackages(LocalPackageType.system);
598 		scanLocalPackages(LocalPackageType.user);
599 
600 		auto old_packages = m_packages;
601 
602 		// rescan the system and user package folder
603 		void scanPackageFolder(NativePath path)
604 		{
605 			if( path.existsDirectory() ){
606 				logDebug("iterating dir %s", path.toNativeString());
607 				try foreach( pdir; iterateDirectory(path) ){
608 					logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
609 					if (!pdir.isDirectory) continue;
610 
611 					auto pack_path = path ~ (pdir.name ~ "/");
612 
613 					auto packageFile = Package.findPackageFile(pack_path);
614 
615 					if (isManagedPath(path) && packageFile.empty) {
616 						// Search for a single directory within this directory which happen to be a prefix of pdir
617 						// This is to support new folder structure installed over the ancient one.
618 						foreach (subdir; iterateDirectory(path ~ (pdir.name ~ "/")))
619 							if (subdir.isDirectory && pdir.name.startsWith(subdir.name)) {// eg: package vibe-d will be in "vibe-d-x.y.z/vibe-d"
620 								pack_path ~= subdir.name ~ "/";
621 								packageFile = Package.findPackageFile(pack_path);
622 								break;
623 							}
624 					}
625 
626 					if (packageFile.empty) continue;
627 					Package p;
628 					try {
629 						if (!refresh_existing_packages)
630 							foreach (pp; old_packages)
631 								if (pp.path == pack_path) {
632 									p = pp;
633 									break;
634 								}
635 						if (!p) p = Package.load(pack_path, packageFile);
636 						addPackages(m_packages, p);
637 					} catch( Exception e ){
638 						logError("Failed to load package in %s: %s", pack_path, e.msg);
639 						logDiagnostic("Full error: %s", e.toString().sanitize());
640 					}
641 				}
642 				catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
643 			}
644 		}
645 
646 		m_packages = null;
647 		foreach (p; this.completeSearchPath)
648 			scanPackageFolder(p);
649 
650 		void loadOverrides(LocalPackageType type)
651 		{
652 			m_repositories[type].overrides = null;
653 			auto ovrfilepath = m_repositories[type].packagePath ~ LocalOverridesFilename;
654 			if (existsFile(ovrfilepath)) {
655 				foreach (entry; jsonFromFile(ovrfilepath)) {
656 					PackageOverride ovr;
657 					ovr.package_ = entry["name"].get!string;
658 					ovr.version_ = Dependency(entry["version"].get!string);
659 					if (auto pv = "targetVersion" in entry) ovr.targetVersion = Version(pv.get!string);
660 					if (auto pv = "targetPath" in entry) ovr.targetPath = NativePath(pv.get!string);
661 					m_repositories[type].overrides ~= ovr;
662 				}
663 			}
664 		}
665 		loadOverrides(LocalPackageType.user);
666 		loadOverrides(LocalPackageType.system);
667 	}
668 
669 	alias Hash = ubyte[];
670 	/// Generates a hash value for a given package.
671 	/// Some files or folders are ignored during the generation (like .dub and
672 	/// .svn folders)
673 	Hash hashPackage(Package pack)
674 	{
675 		string[] ignored_directories = [".git", ".dub", ".svn"];
676 		// something from .dub_ignore or what?
677 		string[] ignored_files = [];
678 		SHA1 sha1;
679 		foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
680 			if(file.isDir && ignored_directories.canFind(NativePath(file.name).head.toString()))
681 				continue;
682 			else if(ignored_files.canFind(NativePath(file.name).head.toString()))
683 				continue;
684 
685 			sha1.put(cast(ubyte[])NativePath(file.name).head.toString());
686 			if(file.isDir) {
687 				logDebug("Hashed directory name %s", NativePath(file.name).head);
688 			}
689 			else {
690 				sha1.put(openFile(NativePath(file.name)).readAll());
691 				logDebug("Hashed file contents from %s", NativePath(file.name).head);
692 			}
693 		}
694 		auto hash = sha1.finish();
695 		logDebug("Project hash: %s", hash);
696 		return hash[].dup;
697 	}
698 
699 	private void writeLocalPackageList(LocalPackageType type)
700 	{
701 		Json[] newlist;
702 		foreach (p; m_repositories[type].searchPath) {
703 			auto entry = Json.emptyObject;
704 			entry["name"] = "*";
705 			entry["path"] = p.toNativeString();
706 			newlist ~= entry;
707 		}
708 
709 		foreach (p; m_repositories[type].localPackages) {
710 			if (p.parentPackage) continue; // do not store sub packages
711 			auto entry = Json.emptyObject;
712 			entry["name"] = p.name;
713 			entry["version"] = p.version_.toString();
714 			entry["path"] = p.path.toNativeString();
715 			newlist ~= entry;
716 		}
717 
718 		NativePath path = m_repositories[type].packagePath;
719 		if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
720 		writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
721 	}
722 
723 	private void writeLocalPackageOverridesFile(LocalPackageType type)
724 	{
725 		Json[] newlist;
726 		foreach (ovr; m_repositories[type].overrides) {
727 			auto jovr = Json.emptyObject;
728 			jovr["name"] = ovr.package_;
729 			jovr["version"] = ovr.version_.versionSpec;
730 			if (!ovr.targetPath.empty) jovr["targetPath"] = ovr.targetPath.toNativeString();
731 			else jovr["targetVersion"] = ovr.targetVersion.toString();
732 			newlist ~= jovr;
733 		}
734 		auto path = m_repositories[type].packagePath;
735 		if (!existsDirectory(path)) mkdirRecurse(path.toNativeString());
736 		writeJsonFile(path ~ LocalOverridesFilename, Json(newlist));
737 	}
738 
739 	/// Adds the package and scans for subpackages.
740 	private void addPackages(ref Package[] dst_repos, Package pack)
741 	const {
742 		// Add the main package.
743 		dst_repos ~= pack;
744 
745 		// Additionally to the internally defined subpackages, whose metadata
746 		// is loaded with the main dub.json, load all externally defined
747 		// packages after the package is available with all the data.
748 		foreach (spr; pack.subPackages) {
749 			Package sp;
750 
751 			if (spr.path.length) {
752 				auto p = NativePath(spr.path);
753 				p.normalize();
754 				enforce(!p.absolute, "Sub package paths must be sub paths of the parent package.");
755 				auto path = pack.path ~ p;
756 				if (!existsFile(path)) {
757 					logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString());
758 					continue;
759 				}
760 				sp = Package.load(path, NativePath.init, pack);
761 			} else sp = new Package(spr.recipe, pack.path, pack);
762 
763 			// Add the subpackage.
764 			try {
765 				dst_repos ~= sp;
766 			} catch (Exception e) {
767 				logError("Package '%s': Failed to load sub-package %s: %s", pack.name,
768 					spr.path.length ? spr.path : spr.recipe.name, e.msg);
769 				logDiagnostic("Full error: %s", e.toString().sanitize());
770 			}
771 		}
772 	}
773 }
774 
775 struct PackageOverride {
776 	string package_;
777 	Dependency version_;
778 	Version targetVersion;
779 	NativePath targetPath;
780 
781 	this(string package_, Dependency version_, Version target_version)
782 	{
783 		this.package_ = package_;
784 		this.version_ = version_;
785 		this.targetVersion = target_version;
786 	}
787 
788 	this(string package_, Dependency version_, NativePath target_path)
789 	{
790 		this.package_ = package_;
791 		this.version_ = version_;
792 		this.targetPath = target_path;
793 	}
794 }
795 
796 enum LocalPackageType {
797 	user,
798 	system
799 }
800 
801 private enum LocalPackagesFilename = "local-packages.json";
802 private enum LocalOverridesFilename = "local-overrides.json";
803 
804 
805 private struct Repository {
806 	NativePath path;
807 	NativePath packagePath;
808 	NativePath[] searchPath;
809 	Package[] localPackages;
810 	PackageOverride[] overrides;
811 
812 	this(NativePath path)
813 	{
814 		this.path = path;
815 		this.packagePath = path ~"packages/";
816 	}
817 }