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