Newer
Older
dub_jkp / source / dub / packagemanager.d
  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.data.json;
  14. import dub.internal.vibecompat.inet.path;
  15. import dub.internal.logging;
  16. import dub.package_;
  17. import dub.recipe.io;
  18. import configy.Exceptions;
  19. public import configy.Read : StrictMode;
  20.  
  21. import std.algorithm : countUntil, filter, sort, canFind, remove;
  22. import std.array;
  23. import std.conv;
  24. import std.digest.sha;
  25. import std.encoding : sanitize;
  26. import std.exception;
  27. import std.file;
  28. import std.string;
  29. import std.sumtype;
  30. import std.zip;
  31.  
  32.  
  33. /// Indicates where a package has been or should be placed to.
  34. public enum PlacementLocation {
  35. /// Packages retrieved with 'local' will be placed in the current folder
  36. /// using the package name as destination.
  37. local,
  38. /// Packages with 'userWide' will be placed in a folder accessible by
  39. /// all of the applications from the current user.
  40. user,
  41. /// Packages retrieved with 'systemWide' will be placed in a shared folder,
  42. /// which can be accessed by all users of the system.
  43. system,
  44. }
  45.  
  46. /// The PackageManager can retrieve present packages and get / remove
  47. /// packages.
  48. class PackageManager {
  49. private {
  50. Location[] m_repositories;
  51. /// The extra 'internal' location, used by the PackageManager
  52. /// to store path and repository-based dependencies or in bare mode.
  53. Location m_internal;
  54. Package[] m_temporaryPackages;
  55. bool m_disableDefaultSearchPaths = false;
  56. }
  57.  
  58. /**
  59. Instantiate an instance with a single search path
  60.  
  61. This constructor is used when dub is invoked with the '--bar' CLI switch.
  62. The instance will not look up the default repositories
  63. (e.g. ~/.dub/packages), using only `path` instead.
  64.  
  65. Params:
  66. path = Path of the single repository
  67. */
  68. this(NativePath path)
  69. {
  70. this.m_internal.searchPath = [ path ];
  71. this.m_disableDefaultSearchPaths = true;
  72. this.refresh(true);
  73. }
  74.  
  75. deprecated("Use the overload which accepts 3 `NativePath` arguments")
  76. this(NativePath user_path, NativePath system_path, bool refresh_packages = true)
  77. {
  78. m_repositories = [
  79. Location(user_path ~ "packages/"),
  80. Location(system_path ~ "packages/")];
  81.  
  82. if (refresh_packages) refresh(true);
  83. }
  84.  
  85. this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true)
  86. {
  87. m_repositories = [
  88. Location(package_path ~ ".dub/packages/"),
  89. Location(user_path ~ "packages/"),
  90. Location(system_path ~ "packages/")];
  91.  
  92. if (refresh_packages) refresh(true);
  93. }
  94.  
  95. /** Gets/sets the list of paths to search for local packages.
  96. */
  97. @property void searchPath(NativePath[] paths)
  98. {
  99. if (paths == this.m_internal.searchPath) return;
  100. this.m_internal.searchPath = paths.dup;
  101. refresh(false);
  102. }
  103. /// ditto
  104. @property const(NativePath)[] searchPath() const { return this.m_internal.searchPath; }
  105.  
  106. /** Disables searching DUB's predefined search paths.
  107. */
  108. deprecated("Instantiate a PackageManager instance with the single-argument constructor: `new PackageManager(path)`")
  109. @property void disableDefaultSearchPaths(bool val)
  110. {
  111. if (val == m_disableDefaultSearchPaths) return;
  112. m_disableDefaultSearchPaths = val;
  113. refresh(true);
  114. }
  115.  
  116. /** Returns the effective list of search paths, including default ones.
  117. */
  118. @property const(NativePath)[] completeSearchPath()
  119. const {
  120. auto ret = appender!(const(NativePath)[])();
  121. ret.put(this.m_internal.searchPath);
  122. if (!m_disableDefaultSearchPaths) {
  123. foreach (ref repo; m_repositories) {
  124. ret.put(repo.searchPath);
  125. ret.put(repo.packagePath);
  126. }
  127. }
  128. return ret.data;
  129. }
  130.  
  131. /** Sets additional (read-only) package cache paths to search for packages.
  132.  
  133. Cache paths have the same structure as the default cache paths, such as
  134. ".dub/packages/".
  135.  
  136. Note that previously set custom paths will be removed when setting this
  137. property.
  138. */
  139. @property void customCachePaths(NativePath[] custom_cache_paths)
  140. {
  141. import std.algorithm.iteration : map;
  142. import std.array : array;
  143.  
  144. m_repositories.length = PlacementLocation.max+1;
  145. m_repositories ~= custom_cache_paths.map!(p => Location(p)).array;
  146.  
  147. refresh(false);
  148. }
  149.  
  150.  
  151. /** Looks up a specific package.
  152.  
  153. Looks up a package matching the given version/path in the set of
  154. registered packages. The lookup order is done according the the
  155. usual rules (see getPackageIterator).
  156.  
  157. Params:
  158. name = The name of the package
  159. ver = The exact version of the package to query
  160. path = An exact path that the package must reside in. Note that
  161. the package must still be registered in the package manager.
  162. enable_overrides = Apply the local package override list before
  163. returning a package (enabled by default)
  164.  
  165. Returns:
  166. The matching package or null if no match was found.
  167. */
  168. Package getPackage(string name, Version ver, bool enable_overrides = true)
  169. {
  170. if (enable_overrides) {
  171. foreach (ref repo; m_repositories)
  172. foreach (ovr; repo.overrides)
  173. if (ovr.package_ == name && ovr.source.matches(ver)) {
  174. Package pack = ovr.target.match!(
  175. (NativePath path) => getOrLoadPackage(path),
  176. (Version vers) => getPackage(name, vers, false),
  177. );
  178. if (pack) return pack;
  179.  
  180. ovr.target.match!(
  181. (any) {
  182. logWarn("Package override %s %s -> '%s' doesn't reference an existing package.",
  183. ovr.package_, ovr.version_, any);
  184. },
  185. );
  186. }
  187. }
  188.  
  189. foreach (p; getPackageIterator(name))
  190. if (p.version_.matches(ver, isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard))
  191. return p;
  192.  
  193. return null;
  194. }
  195.  
  196. /// ditto
  197. deprecated("Use the overload that accepts a `Version` as second argument")
  198. Package getPackage(string name, string ver, bool enable_overrides = true)
  199. {
  200. return getPackage(name, Version(ver), enable_overrides);
  201. }
  202.  
  203. /// ditto
  204. Package getPackage(string name, Version ver, NativePath path)
  205. {
  206. foreach (p; getPackageIterator(name)) {
  207. auto pvm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard;
  208. if (p.version_.matches(ver, pvm) && p.path.startsWith(path))
  209. return p;
  210. }
  211. return null;
  212. }
  213.  
  214. /// ditto
  215. deprecated("Use the overload that accepts a `Version` as second argument")
  216. Package getPackage(string name, string ver, NativePath path)
  217. {
  218. return getPackage(name, Version(ver), path);
  219. }
  220.  
  221. /// ditto
  222. Package getPackage(string name, NativePath path)
  223. {
  224. foreach( p; getPackageIterator(name) )
  225. if (p.path.startsWith(path))
  226. return p;
  227. return null;
  228. }
  229.  
  230.  
  231. /** Looks up the first package matching the given name.
  232. */
  233. Package getFirstPackage(string name)
  234. {
  235. foreach (ep; getPackageIterator(name))
  236. return ep;
  237. return null;
  238. }
  239.  
  240. /** Looks up the latest package matching the given name.
  241. */
  242. deprecated("Use `getBestPackage` with `name, Dependency.any` instead")
  243. Package getLatestPackage(string name)
  244. {
  245. Package pkg;
  246. foreach (ep; getPackageIterator(name))
  247. if (pkg is null || pkg.version_ < ep.version_)
  248. pkg = ep;
  249. return pkg;
  250. }
  251.  
  252. /** For a given package path, returns the corresponding package.
  253.  
  254. If the package is already loaded, a reference is returned. Otherwise
  255. the package gets loaded and cached for the next call to this function.
  256.  
  257. Params:
  258. path = NativePath to the root directory of the package
  259. recipe_path = Optional path to the recipe file of the package
  260. allow_sub_packages = Also return a sub package if it resides in the given folder
  261. mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
  262.  
  263. Returns: The packages loaded from the given path
  264. Throws: Throws an exception if no package can be loaded
  265. */
  266. Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init,
  267. bool allow_sub_packages = false, StrictMode mode = StrictMode.Ignore)
  268. {
  269. path.endsWithSlash = true;
  270. foreach (p; getPackageIterator())
  271. if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path)))
  272. return p;
  273. auto pack = Package.load(path, recipe_path, null, null, mode);
  274. addPackages(m_temporaryPackages, pack);
  275. return pack;
  276. }
  277.  
  278. /** For a given SCM repository, returns the corresponding package.
  279.  
  280. An SCM repository is provided as its remote URL, the repository is cloned
  281. and in the dependency speicfied commit is checked out.
  282.  
  283. If the target directory already exists, just returns the package
  284. without cloning.
  285.  
  286. Params:
  287. name = Package name
  288. dependency = Dependency that contains the repository URL and a specific commit
  289.  
  290. Returns:
  291. The package loaded from the given SCM repository or null if the
  292. package couldn't be loaded.
  293. */
  294. deprecated("Use the overload that accepts a `dub.dependency : Repository`")
  295. Package loadSCMPackage(string name, Dependency dependency)
  296. in { assert(!dependency.repository.empty); }
  297. do { return this.loadSCMPackage(name, dependency.repository); }
  298.  
  299. /// Ditto
  300. Package loadSCMPackage(string name, Repository repo)
  301. in { assert(!repo.empty); }
  302. do {
  303. Package pack;
  304.  
  305. final switch (repo.kind)
  306. {
  307. case repo.Kind.git:
  308. pack = loadGitPackage(name, repo);
  309. }
  310. if (pack !is null) {
  311. addPackages(m_temporaryPackages, pack);
  312. }
  313. return pack;
  314. }
  315.  
  316. private Package loadGitPackage(string name, in Repository repo)
  317. {
  318. import dub.internal.git : cloneRepository;
  319.  
  320. if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) {
  321. return null;
  322. }
  323.  
  324. string gitReference = repo.ref_.chompPrefix("~");
  325. NativePath destination = getPackagePath(
  326. m_repositories[PlacementLocation.user].packagePath,
  327. name, repo.ref_);
  328. // For libraries leaking their import path
  329. destination ~= name;
  330. destination.endsWithSlash = true;
  331.  
  332. foreach (p; getPackageIterator(name)) {
  333. if (p.path == destination) {
  334. return p;
  335. }
  336. }
  337.  
  338. if (!cloneRepository(repo.remote, gitReference, destination.toNativeString())) {
  339. return null;
  340. }
  341.  
  342. return Package.load(destination);
  343. }
  344.  
  345. /**
  346. * Get the final destination a specific package needs to be stored in.
  347. *
  348. * Note that there needs to be an extra level for libraries like `ae`
  349. * which expects their containing folder to have an exact name and use
  350. * `importPath "../"`.
  351. *
  352. * Hence the final format should be `$BASE/$NAME-$VERSION/$NAME`,
  353. * but this function returns `$BASE/$NAME-$VERSION/`
  354. */
  355. package(dub) static NativePath getPackagePath (NativePath base, string name, string vers)
  356. {
  357. // + has special meaning for Optlink
  358. string clean_vers = vers.chompPrefix("~").replace("+", "_");
  359. NativePath result = base ~ (name ~ "-" ~ clean_vers);
  360. result.endsWithSlash = true;
  361. return result;
  362. }
  363.  
  364. /** Searches for the latest version of a package matching the given dependency.
  365. */
  366. Package getBestPackage(string name, VersionRange range = VersionRange.Any)
  367. {
  368. return this.getBestPackage(name, Dependency(range));
  369. }
  370.  
  371. /// Ditto
  372. Package getBestPackage(string name, Version vers)
  373. {
  374. return this.getBestPackage(name, VersionRange(vers, vers));
  375. }
  376.  
  377. /// Ditto
  378. Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true)
  379. {
  380. Package ret;
  381. foreach (p; getPackageIterator(name)) {
  382. auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard;
  383. if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_))
  384. ret = p;
  385. }
  386.  
  387. if (enable_overrides && ret) {
  388. if (auto ovr = getPackage(name, ret.version_))
  389. return ovr;
  390. }
  391. return ret;
  392. }
  393.  
  394. /// ditto
  395. Package getBestPackage(string name, string version_spec)
  396. {
  397. return getBestPackage(name, Dependency(version_spec));
  398. }
  399.  
  400. /** Gets the a specific sub package.
  401.  
  402. In contrast to `Package.getSubPackage`, this function supports path
  403. based sub packages.
  404.  
  405. Params:
  406. base_package = The package from which to get a sub package
  407. sub_name = Name of the sub package (not prefixed with the base
  408. package name)
  409. silent_fail = If set to true, the function will return `null` if no
  410. package is found. Otherwise will throw an exception.
  411.  
  412. */
  413. Package getSubPackage(Package base_package, string sub_name, bool silent_fail)
  414. {
  415. foreach (p; getPackageIterator(base_package.name~":"~sub_name))
  416. if (p.parentPackage is base_package)
  417. return p;
  418. enforce(silent_fail, "Sub package \""~base_package.name~":"~sub_name~"\" doesn't exist.");
  419. return null;
  420. }
  421.  
  422.  
  423. /** Determines if a package is managed by DUB.
  424.  
  425. Managed packages can be upgraded and removed.
  426. */
  427. bool isManagedPackage(Package pack)
  428. const {
  429. auto ppath = pack.basePackage.path;
  430. return isManagedPath(ppath);
  431. }
  432.  
  433. /** Determines if a specific path is within a DUB managed package folder.
  434.  
  435. By default, managed folders are "~/.dub/packages" and
  436. "/var/lib/dub/packages".
  437. */
  438. bool isManagedPath(NativePath path)
  439. const {
  440. foreach (rep; m_repositories) {
  441. NativePath rpath = rep.packagePath;
  442. if (path.startsWith(rpath))
  443. return true;
  444. }
  445. return false;
  446. }
  447.  
  448. /** Enables iteration over all known local packages.
  449.  
  450. Returns: A delegate suitable for use with `foreach` is returned.
  451. */
  452. int delegate(int delegate(ref Package)) getPackageIterator()
  453. {
  454. int iterator(int delegate(ref Package) del)
  455. {
  456. foreach (tp; m_temporaryPackages)
  457. if (auto ret = del(tp)) return ret;
  458.  
  459. // first search local packages
  460. foreach (ref repo; m_repositories)
  461. foreach (p; repo.localPackages)
  462. if (auto ret = del(p)) return ret;
  463.  
  464. // and then all packages gathered from the search path
  465. foreach( p; this.m_internal.localPackages )
  466. if( auto ret = del(p) )
  467. return ret;
  468. return 0;
  469. }
  470.  
  471. return &iterator;
  472. }
  473.  
  474. /** Enables iteration over all known local packages with a certain name.
  475.  
  476. Returns: A delegate suitable for use with `foreach` is returned.
  477. */
  478. int delegate(int delegate(ref Package)) getPackageIterator(string name)
  479. {
  480. int iterator(int delegate(ref Package) del)
  481. {
  482. foreach (p; getPackageIterator())
  483. if (p.name == name)
  484. if (auto ret = del(p)) return ret;
  485. return 0;
  486. }
  487.  
  488. return &iterator;
  489. }
  490.  
  491.  
  492. /** Returns a list of all package overrides for the given scope.
  493. */
  494. deprecated(OverrideDepMsg)
  495. const(PackageOverride)[] getOverrides(PlacementLocation scope_)
  496. const {
  497. return cast(typeof(return)) this.getOverrides_(scope_);
  498. }
  499.  
  500. package(dub) const(PackageOverride_)[] getOverrides_(PlacementLocation scope_)
  501. const {
  502. return m_repositories[scope_].overrides;
  503. }
  504.  
  505. /** Adds a new override for the given package.
  506. */
  507. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  508. void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target)
  509. {
  510. m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
  511. m_repositories[scope_].writeOverrides();
  512. }
  513. /// ditto
  514. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  515. void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target)
  516. {
  517. m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
  518. m_repositories[scope_].writeOverrides();
  519. }
  520.  
  521. /// Ditto
  522. deprecated(OverrideDepMsg)
  523. void addOverride(PlacementLocation scope_, string package_, VersionRange source, Version target)
  524. {
  525. this.addOverride_(scope_, package_, source, target);
  526. }
  527. /// ditto
  528. deprecated(OverrideDepMsg)
  529. void addOverride(PlacementLocation scope_, string package_, VersionRange source, NativePath target)
  530. {
  531. this.addOverride_(scope_, package_, source, target);
  532. }
  533.  
  534. // Non deprecated version that is used by `commandline`. Do not use!
  535. package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target)
  536. {
  537. m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
  538. m_repositories[scope_].writeOverrides();
  539. }
  540. // Non deprecated version that is used by `commandline`. Do not use!
  541. package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target)
  542. {
  543. m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
  544. m_repositories[scope_].writeOverrides();
  545. }
  546.  
  547. /** Removes an existing package override.
  548. */
  549. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  550. void removeOverride(PlacementLocation scope_, string package_, Dependency version_spec)
  551. {
  552. version_spec.visit!(
  553. (VersionRange src) => this.removeOverride(scope_, package_, src),
  554. (any) { throw new Exception(format("No override exists for %s %s", package_, version_spec)); },
  555. );
  556. }
  557.  
  558. deprecated(OverrideDepMsg)
  559. void removeOverride(PlacementLocation scope_, string package_, VersionRange src)
  560. {
  561. this.removeOverride_(scope_, package_, src);
  562. }
  563.  
  564. package(dub) void removeOverride_(PlacementLocation scope_, string package_, VersionRange src)
  565. {
  566. Location* rep = &m_repositories[scope_];
  567. foreach (i, ovr; rep.overrides) {
  568. if (ovr.package_ != package_ || ovr.source != src)
  569. continue;
  570. rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $];
  571. (*rep).writeOverrides();
  572. return;
  573. }
  574. throw new Exception(format("No override exists for %s %s", package_, src));
  575. }
  576.  
  577. /// Extracts the package supplied as a path to it's zip file to the
  578. /// destination and sets a version field in the package description.
  579. Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination)
  580. {
  581. import std.range : walkLength;
  582.  
  583. auto package_name = package_info["name"].get!string;
  584. auto package_version = package_info["version"].get!string;
  585.  
  586. logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'",
  587. package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
  588.  
  589. if( existsFile(destination) ){
  590. throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination));
  591. }
  592.  
  593. // open zip file
  594. ZipArchive archive;
  595. {
  596. logDebug("Opening file %s", zip_file_path);
  597. auto f = openFile(zip_file_path, FileMode.read);
  598. scope(exit) f.close();
  599. archive = new ZipArchive(f.readAll());
  600. }
  601.  
  602. logDebug("Extracting from zip.");
  603.  
  604. // In a github zip, the actual contents are in a subfolder
  605. alias PSegment = typeof(NativePath.init.head);
  606. PSegment[] zip_prefix;
  607. outer: foreach(ArchiveMember am; archive.directory) {
  608. auto path = NativePath(am.name).bySegment.array;
  609. foreach (fil; packageInfoFiles)
  610. if (path.length == 2 && path[$-1].name == fil.filename) {
  611. zip_prefix = path[0 .. $-1];
  612. break outer;
  613. }
  614. }
  615.  
  616. logDebug("zip root folder: %s", zip_prefix);
  617.  
  618. NativePath getCleanedPath(string fileName) {
  619. auto path = NativePath(fileName);
  620. if (zip_prefix.length && !path.bySegment.startsWith(zip_prefix)) return NativePath.init;
  621. static if (is(typeof(path[0 .. 1]))) return path[zip_prefix.length .. $];
  622. else return NativePath(path.bySegment.array[zip_prefix.length .. $]);
  623. }
  624.  
  625. static void setAttributes(string path, ArchiveMember am)
  626. {
  627. import std.datetime : DosFileTimeToSysTime;
  628.  
  629. auto mtime = DosFileTimeToSysTime(am.time);
  630. setTimes(path, mtime, mtime);
  631. if (auto attrs = am.fileAttributes)
  632. std.file.setAttributes(path, attrs);
  633. }
  634.  
  635. // extract & place
  636. mkdirRecurse(destination.toNativeString());
  637. logDebug("Copying all files...");
  638. int countFiles = 0;
  639. foreach(ArchiveMember a; archive.directory) {
  640. auto cleanedPath = getCleanedPath(a.name);
  641. if(cleanedPath.empty) continue;
  642. auto dst_path = destination ~ cleanedPath;
  643.  
  644. logDebug("Creating %s", cleanedPath);
  645. if( dst_path.endsWithSlash ){
  646. if( !existsDirectory(dst_path) )
  647. mkdirRecurse(dst_path.toNativeString());
  648. } else {
  649. if( !existsDirectory(dst_path.parentPath) )
  650. mkdirRecurse(dst_path.parentPath.toNativeString());
  651. {
  652. auto dstFile = openFile(dst_path, FileMode.createTrunc);
  653. scope(exit) dstFile.close();
  654. dstFile.put(archive.expand(a));
  655. }
  656. setAttributes(dst_path.toNativeString(), a);
  657. ++countFiles;
  658. }
  659. }
  660. logDebug("%s file(s) copied.", to!string(countFiles));
  661.  
  662. // overwrite dub.json (this one includes a version field)
  663. auto pack = Package.load(destination, NativePath.init, null, package_info["version"].get!string);
  664.  
  665. if (pack.recipePath.head != defaultPackageFilename)
  666. // Storeinfo saved a default file, this could be different to the file from the zip.
  667. removeFile(pack.recipePath);
  668. pack.storeInfo();
  669. addPackages(this.m_internal.localPackages, pack);
  670. return pack;
  671. }
  672.  
  673. /// Removes the given the package.
  674. void remove(in Package pack)
  675. {
  676. logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path);
  677. enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
  678.  
  679. // remove package from repositories' list
  680. bool found = false;
  681. bool removeFrom(Package[] packs, in Package pack) {
  682. auto packPos = countUntil!("a.path == b.path")(packs, pack);
  683. if(packPos != -1) {
  684. packs = .remove(packs, packPos);
  685. return true;
  686. }
  687. return false;
  688. }
  689. foreach(repo; m_repositories) {
  690. if(removeFrom(repo.localPackages, pack)) {
  691. found = true;
  692. break;
  693. }
  694. }
  695. if(!found)
  696. found = removeFrom(this.m_internal.localPackages, pack);
  697. enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
  698.  
  699. logDebug("About to delete root folder for package '%s'.", pack.path);
  700. rmdirRecurse(pack.path.toNativeString());
  701. logInfo("Removed", Color.yellow, "%s %s", pack.name.color(Mode.bold), pack.version_);
  702. }
  703.  
  704. /// Compatibility overload. Use the version without a `force_remove` argument instead.
  705. deprecated("Use `remove(pack)` directly instead, the boolean has no effect")
  706. void remove(in Package pack, bool force_remove)
  707. {
  708. remove(pack);
  709. }
  710.  
  711. Package addLocalPackage(NativePath path, string verName, PlacementLocation type)
  712. {
  713. path.endsWithSlash = true;
  714. auto pack = Package.load(path);
  715. enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
  716. if (verName.length)
  717. pack.version_ = Version(verName);
  718.  
  719. // don't double-add packages
  720. Package[]* packs = &m_repositories[type].localPackages;
  721. foreach (p; *packs) {
  722. if (p.path == path) {
  723. enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed.");
  724. logInfo("Package is already registered: %s (version: %s)", p.name, p.version_);
  725. return p;
  726. }
  727. }
  728.  
  729. addPackages(*packs, pack);
  730.  
  731. this.m_repositories[type].writeLocalPackageList();
  732.  
  733. logInfo("Registered package: %s (version: %s)", pack.name, pack.version_);
  734. return pack;
  735. }
  736.  
  737. void removeLocalPackage(NativePath path, PlacementLocation type)
  738. {
  739. path.endsWithSlash = true;
  740.  
  741. Package[]* packs = &m_repositories[type].localPackages;
  742. size_t[] to_remove;
  743. foreach( i, entry; *packs )
  744. if( entry.path == path )
  745. to_remove ~= i;
  746. enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
  747.  
  748. string[Version] removed;
  749. foreach_reverse( i; to_remove ) {
  750. removed[(*packs)[i].version_] = (*packs)[i].name;
  751. *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
  752. }
  753.  
  754. this.m_repositories[type].writeLocalPackageList();
  755.  
  756. foreach(ver, name; removed)
  757. logInfo("Deregistered package: %s (version: %s)", name, ver);
  758. }
  759.  
  760. /// For the given type add another path where packages will be looked up.
  761. void addSearchPath(NativePath path, PlacementLocation type)
  762. {
  763. m_repositories[type].searchPath ~= path;
  764. this.m_repositories[type].writeLocalPackageList();
  765. }
  766.  
  767. /// Removes a search path from the given type.
  768. void removeSearchPath(NativePath path, PlacementLocation type)
  769. {
  770. m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
  771. this.m_repositories[type].writeLocalPackageList();
  772. }
  773.  
  774. void refresh(bool refresh_existing_packages)
  775. {
  776. logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh_existing_packages);
  777.  
  778. if (!m_disableDefaultSearchPaths)
  779. {
  780. this.m_repositories[PlacementLocation.system].scanLocalPackages(refresh_existing_packages, this);
  781. this.m_repositories[PlacementLocation.user].scanLocalPackages(refresh_existing_packages, this);
  782. this.m_repositories[PlacementLocation.local].scanLocalPackages(refresh_existing_packages, this);
  783. }
  784.  
  785. auto old_packages = this.m_internal.localPackages;
  786.  
  787. this.m_internal.localPackages = null;
  788. foreach (p; this.completeSearchPath)
  789. this.m_internal.scanPackageFolder(p, this, refresh_existing_packages, old_packages);
  790.  
  791. if (!m_disableDefaultSearchPaths)
  792. {
  793. this.m_repositories[PlacementLocation.local].loadOverrides();
  794. this.m_repositories[PlacementLocation.user].loadOverrides();
  795. this.m_repositories[PlacementLocation.system].loadOverrides();
  796. }
  797. }
  798.  
  799. alias Hash = ubyte[];
  800. /// Generates a hash digest for a given package.
  801. /// Some files or folders are ignored during the generation (like .dub and
  802. /// .svn folders)
  803. Hash hashPackage(Package pack)
  804. {
  805. string[] ignored_directories = [".git", ".dub", ".svn"];
  806. // something from .dub_ignore or what?
  807. string[] ignored_files = [];
  808. SHA256 hash;
  809. foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
  810. const isDir = file.isDir;
  811. if(isDir && ignored_directories.canFind(NativePath(file.name).head.name))
  812. continue;
  813. else if(ignored_files.canFind(NativePath(file.name).head.name))
  814. continue;
  815.  
  816. hash.put(cast(ubyte[])NativePath(file.name).head.name);
  817. if(isDir) {
  818. logDebug("Hashed directory name %s", NativePath(file.name).head);
  819. }
  820. else {
  821. hash.put(openFile(NativePath(file.name)).readAll());
  822. logDebug("Hashed file contents from %s", NativePath(file.name).head);
  823. }
  824. }
  825. auto digest = hash.finish();
  826. logDebug("Project hash: %s", digest);
  827. return digest[].dup;
  828. }
  829.  
  830. /// Adds the package and scans for subpackages.
  831. private void addPackages(ref Package[] dst_repos, Package pack)
  832. const {
  833. // Add the main package.
  834. dst_repos ~= pack;
  835.  
  836. // Additionally to the internally defined subpackages, whose metadata
  837. // is loaded with the main dub.json, load all externally defined
  838. // packages after the package is available with all the data.
  839. foreach (spr; pack.subPackages) {
  840. Package sp;
  841.  
  842. if (spr.path.length) {
  843. auto p = NativePath(spr.path);
  844. p.normalize();
  845. enforce(!p.absolute, "Sub package paths must be sub paths of the parent package.");
  846. auto path = pack.path ~ p;
  847. if (!existsFile(path)) {
  848. logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString());
  849. continue;
  850. }
  851. sp = Package.load(path, NativePath.init, pack);
  852. } else sp = new Package(spr.recipe, pack.path, pack);
  853.  
  854. // Add the subpackage.
  855. try {
  856. dst_repos ~= sp;
  857. } catch (Exception e) {
  858. logError("Package '%s': Failed to load sub-package %s: %s", pack.name,
  859. spr.path.length ? spr.path : spr.recipe.name, e.msg);
  860. logDiagnostic("Full error: %s", e.toString().sanitize());
  861. }
  862. }
  863. }
  864. }
  865.  
  866. deprecated(OverrideDepMsg)
  867. alias PackageOverride = PackageOverride_;
  868.  
  869. package(dub) struct PackageOverride_ {
  870. private alias ResolvedDep = SumType!(NativePath, Version);
  871. string package_;
  872. VersionRange source;
  873. ResolvedDep target;
  874.  
  875. deprecated("Use `source` instead")
  876. @property inout(Dependency) version_ () inout return @safe {
  877. return Dependency(this.source);
  878. }
  879.  
  880. deprecated("Assign `source` instead")
  881. @property ref PackageOverride version_ (Dependency v) scope return @safe pure {
  882. this.source = v.visit!(
  883. (VersionRange range) => range,
  884. (any) {
  885. int a; if (a) return VersionRange.init; // Trick the compiler
  886. throw new Exception("Cannot use anything else than a `VersionRange` for overrides");
  887. },
  888. );
  889. return this;
  890. }
  891.  
  892. deprecated("Use `target.match` directly instead")
  893. @property inout(Version) targetVersion () inout return @safe pure nothrow @nogc {
  894. return this.target.match!(
  895. (Version v) => v,
  896. (any) => Version.init,
  897. );
  898. }
  899.  
  900. deprecated("Assign `target` directly instead")
  901. @property ref PackageOverride targetVersion (Version v) scope return pure nothrow @nogc {
  902. this.target = v;
  903. return this;
  904. }
  905.  
  906. deprecated("Use `target.match` directly instead")
  907. @property inout(NativePath) targetPath () inout return @safe pure nothrow @nogc {
  908. return this.target.match!(
  909. (NativePath v) => v,
  910. (any) => NativePath.init,
  911. );
  912. }
  913.  
  914. deprecated("Assign `target` directly instead")
  915. @property ref PackageOverride targetPath (NativePath v) scope return pure nothrow @nogc {
  916. this.target = v;
  917. return this;
  918. }
  919.  
  920. deprecated("Use the overload that accepts a `VersionRange` as 2nd argument")
  921. this(string package_, Dependency version_, Version target_version)
  922. {
  923. this.package_ = package_;
  924. this.version_ = version_;
  925. this.target = target_version;
  926. }
  927.  
  928. deprecated("Use the overload that accepts a `VersionRange` as 2nd argument")
  929. this(string package_, Dependency version_, NativePath target_path)
  930. {
  931. this.package_ = package_;
  932. this.version_ = version_;
  933. this.target = target_path;
  934. }
  935.  
  936. this(string package_, VersionRange src, Version target)
  937. {
  938. this.package_ = package_;
  939. this.source = src;
  940. this.target = target;
  941. }
  942.  
  943. this(string package_, VersionRange src, NativePath target)
  944. {
  945. this.package_ = package_;
  946. this.source = src;
  947. this.target = target;
  948. }
  949. }
  950.  
  951. deprecated("Use `PlacementLocation` instead")
  952. enum LocalPackageType : PlacementLocation {
  953. package_ = PlacementLocation.local,
  954. user = PlacementLocation.user,
  955. system = PlacementLocation.system,
  956. }
  957.  
  958. private enum LocalPackagesFilename = "local-packages.json";
  959. private enum LocalOverridesFilename = "local-overrides.json";
  960.  
  961. /// A managed location (see `PlacementLocation`)
  962. private struct Location {
  963. /// The absolute path to the root of the location
  964. NativePath packagePath;
  965.  
  966. /// Configured (extra) search paths for this `Location`
  967. NativePath[] searchPath;
  968.  
  969. /// List of packages at this `Location`
  970. Package[] localPackages;
  971.  
  972. /// List of overrides stored at this `Location`
  973. PackageOverride_[] overrides;
  974.  
  975. this(NativePath path) @safe pure nothrow @nogc
  976. {
  977. this.packagePath = path;
  978. }
  979.  
  980. void loadOverrides()
  981. {
  982. this.overrides = null;
  983. auto ovrfilepath = this.packagePath ~ LocalOverridesFilename;
  984. if (existsFile(ovrfilepath)) {
  985. logWarn("Found local override file: %s", ovrfilepath);
  986. logWarn(OverrideDepMsg);
  987. logWarn("Replace with a path-based dependency in your project or a custom cache path");
  988. foreach (entry; jsonFromFile(ovrfilepath)) {
  989. PackageOverride_ ovr;
  990. ovr.package_ = entry["name"].get!string;
  991. ovr.source = VersionRange.fromString(entry["version"].get!string);
  992. if (auto pv = "targetVersion" in entry) ovr.target = Version(pv.get!string);
  993. if (auto pv = "targetPath" in entry) ovr.target = NativePath(pv.get!string);
  994. this.overrides ~= ovr;
  995. }
  996. }
  997. }
  998.  
  999. private void writeOverrides()
  1000. {
  1001. Json[] newlist;
  1002. foreach (ovr; this.overrides) {
  1003. auto jovr = Json.emptyObject;
  1004. jovr["name"] = ovr.package_;
  1005. jovr["version"] = ovr.source.toString();
  1006. ovr.target.match!(
  1007. (NativePath path) { jovr["targetPath"] = path.toNativeString(); },
  1008. (Version vers) { jovr["targetVersion"] = vers.toString(); },
  1009. );
  1010. newlist ~= jovr;
  1011. }
  1012. auto path = this.packagePath;
  1013. if (!existsDirectory(path)) mkdirRecurse(path.toNativeString());
  1014. writeJsonFile(path ~ LocalOverridesFilename, Json(newlist));
  1015. }
  1016.  
  1017. private void writeLocalPackageList()
  1018. {
  1019. Json[] newlist;
  1020. foreach (p; this.searchPath) {
  1021. auto entry = Json.emptyObject;
  1022. entry["name"] = "*";
  1023. entry["path"] = p.toNativeString();
  1024. newlist ~= entry;
  1025. }
  1026.  
  1027. foreach (p; this.localPackages) {
  1028. if (p.parentPackage) continue; // do not store sub packages
  1029. auto entry = Json.emptyObject;
  1030. entry["name"] = p.name;
  1031. entry["version"] = p.version_.toString();
  1032. entry["path"] = p.path.toNativeString();
  1033. newlist ~= entry;
  1034. }
  1035.  
  1036. NativePath path = this.packagePath;
  1037. if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
  1038. writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
  1039. }
  1040.  
  1041. // load locally defined packages
  1042. void scanLocalPackages(bool refresh_existing_packages, PackageManager manager)
  1043. {
  1044. NativePath list_path = this.packagePath;
  1045. Package[] packs;
  1046. NativePath[] paths;
  1047. try {
  1048. auto local_package_file = list_path ~ LocalPackagesFilename;
  1049. logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString());
  1050. if (!existsFile(local_package_file)) return;
  1051.  
  1052. logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString());
  1053. auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
  1054. enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array.");
  1055. foreach (pentry; packlist) {
  1056. try {
  1057. auto name = pentry["name"].get!string;
  1058. auto path = NativePath(pentry["path"].get!string);
  1059. if (name == "*") {
  1060. paths ~= path;
  1061. } else {
  1062. auto ver = Version(pentry["version"].get!string);
  1063.  
  1064. Package pp;
  1065. if (!refresh_existing_packages) {
  1066. foreach (p; this.localPackages)
  1067. if (p.path == path) {
  1068. pp = p;
  1069. break;
  1070. }
  1071. }
  1072.  
  1073. if (!pp) {
  1074. auto infoFile = Package.findPackageFile(path);
  1075. if (!infoFile.empty) pp = Package.load(path, infoFile);
  1076. else {
  1077. logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.",
  1078. name, ver, path.toNativeString());
  1079. // Store a dummy package
  1080. pp = new Package(PackageRecipe(name), path);
  1081. }
  1082. }
  1083.  
  1084. if (pp.name != name)
  1085. logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name);
  1086. pp.version_ = ver;
  1087. manager.addPackages(packs, pp);
  1088. }
  1089. } catch (Exception e) {
  1090. logWarn("Error adding local package: %s", e.msg);
  1091. }
  1092. }
  1093. } catch (Exception e) {
  1094. logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
  1095. }
  1096. this.localPackages = packs;
  1097. this.searchPath = paths;
  1098. }
  1099.  
  1100. /**
  1101. * Scan the content of a folder (`packagePath` or in `searchPaths`),
  1102. * and add all packages that were found to this location.
  1103. */
  1104. void scanPackageFolder(NativePath path, PackageManager mgr,
  1105. bool refresh_existing_packages, Package[] old_packages)
  1106. {
  1107. if (!path.existsDirectory())
  1108. return;
  1109.  
  1110. logDebug("iterating dir %s", path.toNativeString());
  1111. try foreach (pdir; iterateDirectory(path)) {
  1112. logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
  1113. if (!pdir.isDirectory) continue;
  1114.  
  1115. // Old / flat directory structure, used in non-standard path
  1116. // Packages are stored in $ROOT/$SOMETHING/`
  1117. auto pack_path = path ~ (pdir.name ~ "/");
  1118. auto packageFile = Package.findPackageFile(pack_path);
  1119.  
  1120. // New (since 2015) managed structure:
  1121. // $ROOT/$NAME-$VERSION/$NAME
  1122. // This is the most common code path
  1123. if (mgr.isManagedPath(path) && packageFile.empty) {
  1124. foreach (subdir; iterateDirectory(path ~ (pdir.name ~ "/")))
  1125. if (subdir.isDirectory && pdir.name.startsWith(subdir.name)) {
  1126. pack_path ~= subdir.name ~ "/";
  1127. packageFile = Package.findPackageFile(pack_path);
  1128. break;
  1129. }
  1130. }
  1131.  
  1132. if (packageFile.empty) continue;
  1133. Package p;
  1134. try {
  1135. if (!refresh_existing_packages)
  1136. foreach (pp; old_packages)
  1137. if (pp.path == pack_path) {
  1138. p = pp;
  1139. break;
  1140. }
  1141. if (!p) p = Package.load(pack_path, packageFile);
  1142. mgr.addPackages(this.localPackages, p);
  1143. } catch (ConfigException exc) {
  1144. // Confiy error message already include the path
  1145. logError("Invalid recipe for local package: %S", exc);
  1146. } catch (Exception e) {
  1147. logError("Failed to load package in %s: %s", pack_path, e.msg);
  1148. logDiagnostic("Full error: %s", e.toString().sanitize());
  1149. }
  1150. }
  1151. catch (Exception e)
  1152. logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
  1153. }
  1154. }
  1155.  
  1156. private immutable string OverrideDepMsg =
  1157. "Overrides are deprecated as they are redundant with more fine-grained approaches";