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 }