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.io.filesystem;
  12. import dub.internal.utils;
  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 dub.recipe.selection;
  19. import dub.internal.configy.Exceptions;
  20. public import dub.internal.configy.Read : StrictMode;
  21.  
  22. import dub.internal.dyaml.stdsumtype;
  23.  
  24. import std.algorithm : countUntil, filter, map, sort, canFind, remove;
  25. import std.array;
  26. import std.conv;
  27. import std.datetime.systime;
  28. import std.digest.sha;
  29. import std.encoding : sanitize;
  30. import std.exception;
  31. import std.range;
  32. import std.string;
  33. import std.typecons;
  34. import std.zip;
  35.  
  36.  
  37. /// Indicates where a package has been or should be placed to.
  38. public enum PlacementLocation {
  39. /// Packages retrieved with 'local' will be placed in the current folder
  40. /// using the package name as destination.
  41. local,
  42. /// Packages with 'userWide' will be placed in a folder accessible by
  43. /// all of the applications from the current user.
  44. user,
  45. /// Packages retrieved with 'systemWide' will be placed in a shared folder,
  46. /// which can be accessed by all users of the system.
  47. system,
  48. }
  49.  
  50. /// Converts a `PlacementLocation` to a string
  51. public string toString (PlacementLocation loc) @safe pure nothrow @nogc
  52. {
  53. final switch (loc) {
  54. case PlacementLocation.local:
  55. return "Local";
  56. case PlacementLocation.user:
  57. return "User";
  58. case PlacementLocation.system:
  59. return "System";
  60. }
  61. }
  62.  
  63. /// A SelectionsFile associated with its file-system path.
  64. struct SelectionsFileLookupResult {
  65. /// The absolute path to the dub.selections.json file
  66. /// (potentially inherited from a parent directory of the root package).
  67. NativePath absolutePath;
  68. /// The parsed dub.selections.json file.
  69. SelectionsFile selectionsFile;
  70. }
  71.  
  72. /// The PackageManager can retrieve present packages and get / remove
  73. /// packages.
  74. class PackageManager {
  75. protected {
  76. /**
  77. * The 'internal' location, for packages not attributable to a location.
  78. *
  79. * There are two uses for this:
  80. * - In `bare` mode, the search paths are set at this scope,
  81. * and packages gathered are stored in `localPackage`;
  82. * - In the general case, any path-based or SCM-based dependency
  83. * is loaded in `fromPath`;
  84. */
  85. Location m_internal;
  86. /**
  87. * List of locations that are managed by this `PackageManager`
  88. *
  89. * The `PackageManager` can be instantiated either in 'bare' mode,
  90. * in which case this array will be empty, or in the normal mode,
  91. * this array will have 3 entries, matching values
  92. * in the `PlacementLocation` enum.
  93. *
  94. * See_Also: `Location`, `PlacementLocation`
  95. */
  96. Location[] m_repositories;
  97. /**
  98. * Whether `refreshLocal` / `refreshCache` has been called or not
  99. *
  100. * User local cache can get pretty large, and we want to avoid our build
  101. * time being dependent on their size. However, in order to support
  102. * local packages and overrides, we were scanning the whole cache prior
  103. * to v1.39.0 (although attempts at fixing this behavior were made
  104. * earlier). Those booleans record whether we have been semi-initialized
  105. * (local packages and overrides have been loaded) or fully initialized
  106. * (all caches have been scanned), the later still being required for
  107. * some API (e.g. `getBestPackage` or `getPackageIterator`).
  108. */
  109. enum InitializationState {
  110. /// No `refresh*` function has been called
  111. none,
  112. /// `refreshLocal` has been called
  113. partial,
  114. /// `refreshCache` (and `refreshLocal`) has been called
  115. full,
  116. }
  117. /// Ditto
  118. InitializationState m_state;
  119. /// The `Filesystem` object, used to interact with directory / files
  120. Filesystem fs;
  121. }
  122.  
  123. /**
  124. Instantiate an instance with a single search path
  125.  
  126. This constructor is used when dub is invoked with the '--bare' CLI switch.
  127. The instance will not look up the default repositories
  128. (e.g. ~/.dub/packages), using only `path` instead.
  129.  
  130. Params:
  131. path = Path of the single repository
  132. */
  133. this(NativePath path)
  134. {
  135. import dub.internal.io.realfs;
  136. this.fs = new RealFS();
  137. this.m_internal.searchPath = [ path ];
  138. this.refresh();
  139. }
  140.  
  141. this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true)
  142. {
  143. import dub.internal.io.realfs;
  144. this(new RealFS(), package_path ~ ".dub/packages/",
  145. user_path ~ "packages/", system_path ~ "packages/");
  146. if (refresh_packages) refresh();
  147. }
  148.  
  149. /**
  150. * Instantiate a `PackageManager` with the provided `Filesystem` and paths
  151. *
  152. * Unlike the other overload, paths are taken as-if, e.g. `packages/` is not
  153. * appended to them.
  154. *
  155. * Params:
  156. * fs = Filesystem abstraction to handle all folder/file I/O.
  157. * local = Path to the local package cache (usually the one in the project),
  158. * whih takes preference over `user` and `system`.
  159. * user = Path to the user package cache (usually ~/.dub/packages/), takes
  160. * precedence over `system` but not over `local`.
  161. * system = Path to the system package cache, this has the least precedence.
  162. */
  163. public this(Filesystem fs, NativePath local, NativePath user, NativePath system)
  164. {
  165. this.fs = fs;
  166. this.m_repositories = [ Location(local), Location(user), Location(system) ];
  167. }
  168.  
  169. /** Gets/sets the list of paths to search for local packages.
  170. */
  171. @property void searchPath(NativePath[] paths)
  172. {
  173. if (paths == this.m_internal.searchPath) return;
  174. this.m_internal.searchPath = paths.dup;
  175. this.refresh();
  176. }
  177. /// ditto
  178. @property const(NativePath)[] searchPath() const { return this.m_internal.searchPath; }
  179.  
  180. /** Returns the effective list of search paths, including default ones.
  181. */
  182. deprecated("Use the `PackageManager` facilities instead")
  183. @property const(NativePath)[] completeSearchPath()
  184. const {
  185. auto ret = appender!(const(NativePath)[])();
  186. ret.put(this.m_internal.searchPath);
  187. foreach (ref repo; m_repositories) {
  188. ret.put(repo.searchPath);
  189. ret.put(repo.packagePath);
  190. }
  191. return ret.data;
  192. }
  193.  
  194. /** Sets additional (read-only) package cache paths to search for packages.
  195.  
  196. Cache paths have the same structure as the default cache paths, such as
  197. ".dub/packages/".
  198.  
  199. Note that previously set custom paths will be removed when setting this
  200. property.
  201. */
  202. @property void customCachePaths(NativePath[] custom_cache_paths)
  203. {
  204. import std.algorithm.iteration : map;
  205. import std.array : array;
  206.  
  207. m_repositories.length = PlacementLocation.max+1;
  208. m_repositories ~= custom_cache_paths.map!(p => Location(p)).array;
  209.  
  210. this.refresh();
  211. }
  212.  
  213. /**
  214. * Looks up a package, first in the list of loaded packages,
  215. * then directly on the file system.
  216. *
  217. * This function allows for lazy loading of packages, without needing to
  218. * first scan all the available locations (as `refresh` does).
  219. *
  220. * Note:
  221. * This function does not take overrides into account. Overrides need
  222. * to be resolved by the caller before `lookup` is called.
  223. * Additionally, if a package of the same version is loaded in multiple
  224. * locations, the first one matching (local > user > system)
  225. * will be returned.
  226. *
  227. * Params:
  228. * name = The full name of the package to look up
  229. * vers = The version the package must match
  230. *
  231. * Returns:
  232. * A `Package` if one was found, `null` if none exists.
  233. */
  234. protected Package lookup (in PackageName name, in Version vers) {
  235. // This is the only place we can get away with lazy initialization,
  236. // since we know exactly what package and version we want.
  237. // However, it is also the most often called API.
  238. this.ensureInitialized(InitializationState.partial);
  239.  
  240. if (auto pkg = this.m_internal.lookup(name, vers))
  241. return pkg;
  242.  
  243. foreach (ref location; this.m_repositories)
  244. if (auto p = location.load(name, vers, this))
  245. return p;
  246.  
  247. return null;
  248. }
  249.  
  250. /** Looks up a specific package.
  251.  
  252. Looks up a package matching the given version/path in the set of
  253. registered packages. The lookup order is done according the the
  254. usual rules (see getPackageIterator).
  255.  
  256. Params:
  257. name = The name of the package
  258. ver = The exact version of the package to query
  259. path = An exact path that the package must reside in. Note that
  260. the package must still be registered in the package manager.
  261. enable_overrides = Apply the local package override list before
  262. returning a package (enabled by default)
  263.  
  264. Returns:
  265. The matching package or null if no match was found.
  266. */
  267. Package getPackage(in PackageName name, in Version ver, bool enable_overrides = true)
  268. {
  269. if (enable_overrides) {
  270. foreach (ref repo; m_repositories)
  271. foreach (ovr; repo.overrides)
  272. if (ovr.package_ == name.toString() && ovr.source.matches(ver)) {
  273. Package pack = ovr.target.match!(
  274. (NativePath path) => getOrLoadPackage(path),
  275. (Version vers) => getPackage(name, vers, false),
  276. );
  277. if (pack) return pack;
  278.  
  279. ovr.target.match!(
  280. (any) {
  281. logWarn("Package override %s %s -> '%s' doesn't reference an existing package.",
  282. ovr.package_, ovr.source, any);
  283. },
  284. );
  285. }
  286. }
  287.  
  288. return this.lookup(name, ver);
  289. }
  290.  
  291. deprecated("Use the overload that accepts a `PackageName` instead")
  292. Package getPackage(string name, Version ver, bool enable_overrides = true)
  293. {
  294. return this.getPackage(PackageName(name), ver, enable_overrides);
  295. }
  296.  
  297. /// ditto
  298. deprecated("Use the overload that accepts a `Version` as second argument")
  299. Package getPackage(string name, string ver, bool enable_overrides = true)
  300. {
  301. return getPackage(name, Version(ver), enable_overrides);
  302. }
  303.  
  304. /// ditto
  305. deprecated("Use the overload that takes a `PlacementLocation`")
  306. Package getPackage(string name, Version ver, NativePath path)
  307. {
  308. foreach (p; getPackageIterator(name)) {
  309. auto pvm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard;
  310. if (p.version_.matches(ver, pvm) && p.path.startsWith(path))
  311. return p;
  312. }
  313. return null;
  314. }
  315.  
  316. /// Ditto
  317. deprecated("Use the overload that accepts a `PackageName` instead")
  318. Package getPackage(string name, Version ver, PlacementLocation loc)
  319. {
  320. return this.getPackage(PackageName(name), ver, loc);
  321. }
  322.  
  323. /// Ditto
  324. Package getPackage(in PackageName name, in Version ver, PlacementLocation loc)
  325. {
  326. // Bare mode
  327. if (loc >= this.m_repositories.length)
  328. return null;
  329. return this.m_repositories[loc].load(name, ver, this);
  330. }
  331.  
  332. /// ditto
  333. deprecated("Use the overload that accepts a `Version` as second argument")
  334. Package getPackage(string name, string ver, NativePath path)
  335. {
  336. return getPackage(name, Version(ver), path);
  337. }
  338.  
  339. /// ditto
  340. deprecated("Use another `PackageManager` API, open an issue if none suits you")
  341. Package getPackage(string name, NativePath path)
  342. {
  343. foreach( p; getPackageIterator(name) )
  344. if (p.path.startsWith(path))
  345. return p;
  346. return null;
  347. }
  348.  
  349.  
  350. /** Looks up the first package matching the given name.
  351. */
  352. deprecated("Use `getBestPackage` instead")
  353. Package getFirstPackage(string name)
  354. {
  355. foreach (ep; getPackageIterator(name))
  356. return ep;
  357. return null;
  358. }
  359.  
  360. /** Looks up the latest package matching the given name.
  361. */
  362. deprecated("Use `getBestPackage` with `name, Dependency.any` instead")
  363. Package getLatestPackage(string name)
  364. {
  365. Package pkg;
  366. foreach (ep; getPackageIterator(name))
  367. if (pkg is null || pkg.version_ < ep.version_)
  368. pkg = ep;
  369. return pkg;
  370. }
  371.  
  372. /** For a given package path, returns the corresponding package.
  373.  
  374. If the package is already loaded, a reference is returned. Otherwise
  375. the package gets loaded and cached for the next call to this function.
  376.  
  377. Params:
  378. path = NativePath to the root directory of the package
  379. recipe_path = Optional path to the recipe file of the package
  380. allow_sub_packages = Also return a sub package if it resides in the given folder
  381. mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
  382.  
  383. Returns: The packages loaded from the given path
  384. Throws: Throws an exception if no package can be loaded
  385. */
  386. Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init,
  387. bool allow_sub_packages = false, StrictMode mode = StrictMode.Ignore)
  388. {
  389. path.endsWithSlash = true;
  390. foreach (p; this.m_internal.fromPath)
  391. if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path)))
  392. return p;
  393. auto pack = this.load(path, recipe_path, null, null, mode);
  394. addPackages(this.m_internal.fromPath, pack);
  395. return pack;
  396. }
  397.  
  398. /**
  399. * Loads a `Package` from the filesystem
  400. *
  401. * This is called when a `Package` needs to be loaded from the path.
  402. * This does not change the internal state of the `PackageManager`,
  403. * it simply loads the `Package` and returns it - it is up to the caller
  404. * to call `addPackages`.
  405. *
  406. * Throws:
  407. * If no package can be found at the `path` / with the `recipe`.
  408. *
  409. * Params:
  410. * path = The directory in which the package resides.
  411. * recipe = Optional path to the package recipe file. If left empty,
  412. * the `path` directory will be searched for a recipe file.
  413. * parent = Reference to the parent package, if the new package is a
  414. * sub package.
  415. * version_ = Optional version to associate to the package instead of
  416. * the one declared in the package recipe, or the one
  417. * determined by invoking the VCS (GIT currently).
  418. * mode = Whether to issue errors, warning, or ignore unknown keys in
  419. * dub.json
  420. *
  421. * Returns: A populated `Package`.
  422. */
  423. protected Package load(NativePath path, NativePath recipe = NativePath.init,
  424. Package parent = null, string version_ = null,
  425. StrictMode mode = StrictMode.Ignore)
  426. {
  427. if (recipe.empty)
  428. recipe = this.findPackageFile(path);
  429.  
  430. enforce(!recipe.empty,
  431. "No package file found in %s, expected one of %s"
  432. .format(path.toNativeString(),
  433. packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
  434.  
  435. const PackageName pname = parent
  436. ? PackageName(parent.name) : PackageName.init;
  437. string text = this.fs.readText(recipe);
  438. auto content = parsePackageRecipe(
  439. text, recipe.toNativeString(), pname, null, mode);
  440. auto ret = new Package(content, path, parent, version_);
  441. ret.m_infoFile = recipe;
  442. return ret;
  443. }
  444.  
  445. /** Searches the given directory for package recipe files.
  446. *
  447. * Params:
  448. * directory = The directory to search
  449. *
  450. * Returns:
  451. * Returns the full path to the package file, if any was found.
  452. * Otherwise returns an empty path.
  453. */
  454. public NativePath findPackageFile(NativePath directory)
  455. {
  456. foreach (file; packageInfoFiles) {
  457. auto filename = directory ~ file.filename;
  458. if (this.fs.existsFile(filename)) return filename;
  459. }
  460. return NativePath.init;
  461. }
  462.  
  463. /** For a given SCM repository, returns the corresponding package.
  464.  
  465. An SCM repository is provided as its remote URL, the repository is cloned
  466. and in the dependency specified commit is checked out.
  467.  
  468. If the target directory already exists, just returns the package
  469. without cloning.
  470.  
  471. Params:
  472. name = Package name
  473. dependency = Dependency that contains the repository URL and a specific commit
  474.  
  475. Returns:
  476. The package loaded from the given SCM repository or null if the
  477. package couldn't be loaded.
  478. */
  479. Package loadSCMPackage(in PackageName name, in Repository repo)
  480. in { assert(!repo.empty); }
  481. do {
  482. Package pack;
  483.  
  484. final switch (repo.kind)
  485. {
  486. case repo.Kind.git:
  487. return this.loadGitPackage(name, repo);
  488. }
  489. }
  490.  
  491. deprecated("Use the overload that accepts a `dub.dependency : Repository`")
  492. Package loadSCMPackage(string name, Dependency dependency)
  493. in { assert(!dependency.repository.empty); }
  494. do { return this.loadSCMPackage(name, dependency.repository); }
  495.  
  496. deprecated("Use `loadSCMPackage(PackageName, Repository)`")
  497. Package loadSCMPackage(string name, Repository repo)
  498. {
  499. return this.loadSCMPackage(PackageName(name), repo);
  500. }
  501.  
  502. private Package loadGitPackage(in PackageName name, in Repository repo)
  503. {
  504. if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) {
  505. return null;
  506. }
  507.  
  508. string gitReference = repo.ref_.chompPrefix("~");
  509. NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_);
  510.  
  511. // Before doing a git clone, let's see if the package exists locally
  512. if (this.fs.existsDirectory(destination)) {
  513. // It exists, check if we already loaded it.
  514. // Either we loaded it on refresh and it's in PlacementLocation.user,
  515. // or we just added it and it's in m_internal.
  516. foreach (p; this.m_internal.fromPath)
  517. if (p.path == destination)
  518. return p;
  519. if (this.m_repositories.length)
  520. foreach (p; this.m_repositories[PlacementLocation.user].fromPath)
  521. if (p.path == destination)
  522. return p;
  523. } else if (!this.gitClone(repo.remote, gitReference, destination))
  524. return null;
  525.  
  526. Package result = this.load(destination);
  527. if (result !is null)
  528. this.addPackages(this.m_internal.fromPath, result);
  529. return result;
  530. }
  531.  
  532. /**
  533. * Perform a `git clone` operation at `dest` using `repo`
  534. *
  535. * Params:
  536. * remote = The remote to clone from
  537. * gitref = The git reference to use
  538. * dest = Where the result of git clone operation is to be stored
  539. *
  540. * Returns:
  541. * Whether or not the clone operation was successfull.
  542. */
  543. protected bool gitClone(string remote, string gitref, in NativePath dest)
  544. {
  545. static import dub.internal.git;
  546. return dub.internal.git.cloneRepository(remote, gitref, dest.toNativeString());
  547. }
  548.  
  549. /**
  550. * Get the final destination a specific package needs to be stored in.
  551. *
  552. * See `Location.getPackagePath`.
  553. */
  554. package(dub) NativePath getPackagePath(PlacementLocation base, in PackageName name, string vers)
  555. {
  556. assert(this.m_repositories.length == 3, "getPackagePath called in bare mode");
  557. return this.m_repositories[base].getPackagePath(name, vers);
  558. }
  559.  
  560. /**
  561. * Searches for the latest version of a package matching the version range.
  562. *
  563. * This will search the local file system only (it doesn't connect
  564. * to the registry) for the "best" (highest version) that matches `range`.
  565. * An overload with a single version exists to search for an exact version.
  566. *
  567. * Params:
  568. * name = Package name to search for
  569. * vers = Exact version to search for
  570. * range = Range of versions to search for, defaults to any
  571. *
  572. * Returns:
  573. * The best package matching the parameters, or `null` if none was found.
  574. */
  575. deprecated("Use the overload that accepts a `PackageName` instead")
  576. Package getBestPackage(string name, Version vers)
  577. {
  578. return this.getBestPackage(PackageName(name), vers);
  579. }
  580.  
  581. /// Ditto
  582. Package getBestPackage(in PackageName name, in Version vers)
  583. {
  584. return this.getBestPackage(name, VersionRange(vers, vers));
  585. }
  586.  
  587. /// Ditto
  588. deprecated("Use the overload that accepts a `PackageName` instead")
  589. Package getBestPackage(string name, VersionRange range = VersionRange.Any)
  590. {
  591. return this.getBestPackage(PackageName(name), range);
  592. }
  593.  
  594. /// Ditto
  595. Package getBestPackage(in PackageName name, in VersionRange range = VersionRange.Any)
  596. {
  597. return this.getBestPackage_(name, Dependency(range));
  598. }
  599.  
  600. /// Ditto
  601. deprecated("Use the overload that accepts a `Version` or a `VersionRange`")
  602. Package getBestPackage(string name, string range)
  603. {
  604. return this.getBestPackage(name, VersionRange.fromString(range));
  605. }
  606.  
  607. /// Ditto
  608. deprecated("`getBestPackage` should only be used with a `Version` or `VersionRange` argument")
  609. Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true)
  610. {
  611. return this.getBestPackage_(PackageName(name), version_spec, enable_overrides);
  612. }
  613.  
  614. // TODO: Merge this into `getBestPackage(string, VersionRange)`
  615. private Package getBestPackage_(in PackageName name, in Dependency version_spec,
  616. bool enable_overrides = true)
  617. {
  618. Package ret;
  619. foreach (p; getPackageIterator(name.toString())) {
  620. auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard;
  621. if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_))
  622. ret = p;
  623. }
  624.  
  625. if (enable_overrides && ret) {
  626. if (auto ovr = getPackage(name, ret.version_))
  627. return ovr;
  628. }
  629. return ret;
  630. }
  631.  
  632. /** Gets the a specific sub package.
  633.  
  634. Params:
  635. base_package = The package from which to get a sub package
  636. sub_name = Name of the sub package (not prefixed with the base
  637. package name)
  638. silent_fail = If set to true, the function will return `null` if no
  639. package is found. Otherwise will throw an exception.
  640.  
  641. */
  642. Package getSubPackage(Package base_package, string sub_name, bool silent_fail)
  643. {
  644. foreach (p; getPackageIterator(base_package.name~":"~sub_name))
  645. if (p.parentPackage is base_package)
  646. return p;
  647. enforce(silent_fail, "Sub package \""~base_package.name~":"~sub_name~"\" doesn't exist.");
  648. return null;
  649. }
  650.  
  651.  
  652. /** Determines if a package is managed by DUB.
  653.  
  654. Managed packages can be upgraded and removed.
  655. */
  656. bool isManagedPackage(const(Package) pack)
  657. const {
  658. auto ppath = pack.basePackage.path;
  659. return isManagedPath(ppath);
  660. }
  661.  
  662. /** Determines if a specific path is within a DUB managed package folder.
  663.  
  664. By default, managed folders are "~/.dub/packages" and
  665. "/var/lib/dub/packages".
  666. */
  667. bool isManagedPath(NativePath path)
  668. const {
  669. foreach (rep; m_repositories)
  670. if (rep.isManaged(path))
  671. return true;
  672. return false;
  673. }
  674.  
  675. /** Enables iteration over all known local packages.
  676.  
  677. Returns: A delegate suitable for use with `foreach` is returned.
  678. */
  679. int delegate(int delegate(ref Package)) getPackageIterator()
  680. {
  681. // This API requires full knowledge of the package cache
  682. this.ensureInitialized(InitializationState.full);
  683.  
  684. int iterator(int delegate(ref Package) del)
  685. {
  686. // Search scope by priority, internal has the highest
  687. foreach (p; this.m_internal.fromPath)
  688. if (auto ret = del(p)) return ret;
  689. foreach (p; this.m_internal.localPackages)
  690. if (auto ret = del(p)) return ret;
  691.  
  692. foreach (ref repo; m_repositories) {
  693. foreach (p; repo.localPackages)
  694. if (auto ret = del(p)) return ret;
  695. foreach (p; repo.fromPath)
  696. if (auto ret = del(p)) return ret;
  697. }
  698. return 0;
  699. }
  700.  
  701. return &iterator;
  702. }
  703.  
  704. /** Enables iteration over all known local packages with a certain name.
  705.  
  706. Returns: A delegate suitable for use with `foreach` is returned.
  707. */
  708. int delegate(int delegate(ref Package)) getPackageIterator(string name)
  709. {
  710. int iterator(int delegate(ref Package) del)
  711. {
  712. foreach (p; getPackageIterator())
  713. if (p.name == name)
  714. if (auto ret = del(p)) return ret;
  715. return 0;
  716. }
  717.  
  718. return &iterator;
  719. }
  720.  
  721.  
  722. /** Returns a list of all package overrides for the given scope.
  723. */
  724. deprecated(OverrideDepMsg)
  725. const(PackageOverride)[] getOverrides(PlacementLocation scope_)
  726. const {
  727. return cast(typeof(return)) this.getOverrides_(scope_);
  728. }
  729.  
  730. package(dub) const(PackageOverride_)[] getOverrides_(PlacementLocation scope_)
  731. const {
  732. return m_repositories[scope_].overrides;
  733. }
  734.  
  735. /** Adds a new override for the given package.
  736. */
  737. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  738. void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target)
  739. {
  740. m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
  741. m_repositories[scope_].writeOverrides(this);
  742. }
  743. /// ditto
  744. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  745. void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target)
  746. {
  747. m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
  748. m_repositories[scope_].writeOverrides(this);
  749. }
  750.  
  751. /// Ditto
  752. deprecated(OverrideDepMsg)
  753. void addOverride(PlacementLocation scope_, string package_, VersionRange source, Version target)
  754. {
  755. this.addOverride_(scope_, package_, source, target);
  756. }
  757. /// ditto
  758. deprecated(OverrideDepMsg)
  759. void addOverride(PlacementLocation scope_, string package_, VersionRange source, NativePath target)
  760. {
  761. this.addOverride_(scope_, package_, source, target);
  762. }
  763.  
  764. // Non deprecated version that is used by `commandline`. Do not use!
  765. package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target)
  766. {
  767. m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
  768. m_repositories[scope_].writeOverrides(this);
  769. }
  770. // Non deprecated version that is used by `commandline`. Do not use!
  771. package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target)
  772. {
  773. m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
  774. m_repositories[scope_].writeOverrides(this);
  775. }
  776.  
  777. /** Removes an existing package override.
  778. */
  779. deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
  780. void removeOverride(PlacementLocation scope_, string package_, Dependency version_spec)
  781. {
  782. version_spec.visit!(
  783. (VersionRange src) => this.removeOverride(scope_, package_, src),
  784. (any) { throw new Exception(format("No override exists for %s %s", package_, version_spec)); },
  785. );
  786. }
  787.  
  788. deprecated(OverrideDepMsg)
  789. void removeOverride(PlacementLocation scope_, string package_, VersionRange src)
  790. {
  791. this.removeOverride_(scope_, package_, src);
  792. }
  793.  
  794. package(dub) void removeOverride_(PlacementLocation scope_, string package_, VersionRange src)
  795. {
  796. Location* rep = &m_repositories[scope_];
  797. foreach (i, ovr; rep.overrides) {
  798. if (ovr.package_ != package_ || ovr.source != src)
  799. continue;
  800. rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $];
  801. (*rep).writeOverrides(this);
  802. return;
  803. }
  804. throw new Exception(format("No override exists for %s %s", package_, src));
  805. }
  806.  
  807. deprecated("Use `store(NativePath source, PlacementLocation dest, string name, Version vers)`")
  808. Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination)
  809. {
  810. import dub.internal.vibecompat.core.file;
  811.  
  812. return this.store_(readFile(zip_file_path), destination,
  813. PackageName(package_info["name"].get!string),
  814. Version(package_info["version"].get!string));
  815. }
  816.  
  817. /**
  818. * Store a zip file stored at `src` into a managed location `destination`
  819. *
  820. * This will extracts the package supplied as (as a zip file) to the
  821. * `destination` and sets a version field in the package description.
  822. * In the future, we should aim not to alter the package description,
  823. * but this is done for backward compatibility.
  824. *
  825. * Params:
  826. * src = The path to the zip file containing the package
  827. * dest = At which `PlacementLocation` the package should be stored
  828. * name = Name of the package being stored
  829. * vers = Version of the package
  830. *
  831. * Returns:
  832. * The `Package` after it has been loaded.
  833. *
  834. * Throws:
  835. * If the package cannot be loaded / the zip is corrupted / the package
  836. * already exists, etc...
  837. */
  838. deprecated("Use the overload that accepts a `PackageName` instead")
  839. Package store(NativePath src, PlacementLocation dest, string name, Version vers)
  840. {
  841. return this.store(src, dest, PackageName(name), vers);
  842. }
  843.  
  844. /// Ditto
  845. Package store(NativePath src, PlacementLocation dest, in PackageName name,
  846. in Version vers)
  847. {
  848. import dub.internal.vibecompat.core.file;
  849.  
  850. auto data = readFile(src);
  851. return this.store(data, dest, name, vers);
  852. }
  853.  
  854. /// Ditto
  855. Package store(ubyte[] data, PlacementLocation dest,
  856. in PackageName name, in Version vers)
  857. {
  858. assert(!name.sub.length, "Cannot store a subpackage, use main package instead");
  859. NativePath dstpath = this.getPackagePath(dest, name, vers.toString());
  860. this.fs.mkdir(dstpath.parentPath());
  861. const lockPath = dstpath.parentPath() ~ ".lock";
  862.  
  863. // possibly wait for other dub instance
  864. import core.time : seconds;
  865. auto lock = lockFile(lockPath.toNativeString(), 30.seconds);
  866. if (this.fs.existsFile(dstpath)) {
  867. return this.getPackage(name, vers, dest);
  868. }
  869. return this.store_(data, dstpath, name, vers);
  870. }
  871.  
  872. /// Backward-compatibility for deprecated overload, simplify once `storeFetchedPatch`
  873. /// is removed
  874. protected Package store_(ubyte[] data, NativePath destination,
  875. in PackageName name, in Version vers)
  876. {
  877. import dub.recipe.json : toJson;
  878. import std.range : walkLength;
  879.  
  880. logDebug("Placing package '%s' version '%s' to location '%s'",
  881. name, vers, destination.toNativeString());
  882.  
  883. enforce(!this.fs.existsFile(destination),
  884. "%s (%s) needs to be removed from '%s' prior placement."
  885. .format(name, vers, destination));
  886.  
  887. ZipArchive archive = new ZipArchive(data);
  888. logDebug("Extracting from zip.");
  889.  
  890. // In a GitHub zip, the actual contents are in a sub-folder
  891. alias PSegment = typeof(NativePath.init.head);
  892. PSegment[] zip_prefix;
  893. outer: foreach(ArchiveMember am; archive.directory) {
  894. auto path = NativePath(am.name).bySegment.array;
  895. foreach (fil; packageInfoFiles)
  896. if (path.length == 2 && path[$-1].name == fil.filename) {
  897. zip_prefix = path[0 .. $-1];
  898. break outer;
  899. }
  900. }
  901.  
  902. logDebug("zip root folder: %s", zip_prefix);
  903.  
  904. NativePath getCleanedPath(string fileName) {
  905. auto path = NativePath(fileName);
  906. if (zip_prefix.length && !path.bySegment.startsWith(zip_prefix)) return NativePath.init;
  907. static if (is(typeof(path[0 .. 1]))) return path[zip_prefix.length .. $];
  908. else return NativePath(path.bySegment.array[zip_prefix.length .. $]);
  909. }
  910.  
  911. void setAttributes(NativePath path, ArchiveMember am)
  912. {
  913. import std.datetime : DosFileTimeToSysTime;
  914.  
  915. auto mtime = DosFileTimeToSysTime(am.time);
  916. this.fs.setTimes(path, mtime, mtime);
  917. if (auto attrs = am.fileAttributes)
  918. this.fs.setAttributes(path, attrs);
  919. }
  920.  
  921. // extract & place
  922. this.fs.mkdir(destination);
  923. logDebug("Copying all files...");
  924. int countFiles = 0;
  925. foreach(ArchiveMember a; archive.directory) {
  926. auto cleanedPath = getCleanedPath(a.name);
  927. if(cleanedPath.empty) continue;
  928. auto dst_path = destination ~ cleanedPath;
  929.  
  930. logDebug("Creating %s", cleanedPath);
  931. if (dst_path.endsWithSlash) {
  932. this.fs.mkdir(dst_path);
  933. } else {
  934. this.fs.mkdir(dst_path.parentPath);
  935. // for symlinks on posix systems, use the symlink function to
  936. // create them. Windows default unzip doesn't handle symlinks,
  937. // so we don't need to worry about it for Windows.
  938. version(Posix) {
  939. import core.sys.posix.sys.stat;
  940. if( S_ISLNK(cast(mode_t)a.fileAttributes) ){
  941. import core.sys.posix.unistd;
  942. // need to convert name and target to zero-terminated string
  943. auto target = toStringz(cast(const(char)[])archive.expand(a));
  944. auto dstFile = toStringz(dst_path.toNativeString());
  945. enforce(symlink(target, dstFile) == 0, "Error creating symlink: " ~ dst_path.toNativeString());
  946. goto symlink_exit;
  947. }
  948. }
  949.  
  950. this.fs.writeFile(dst_path, archive.expand(a));
  951. setAttributes(dst_path, a);
  952. symlink_exit:
  953. ++countFiles;
  954. }
  955. }
  956. logDebug("%s file(s) copied.", to!string(countFiles));
  957.  
  958. // overwrite dub.json (this one includes a version field)
  959. auto pack = this.load(destination, NativePath.init, null, vers.toString());
  960.  
  961. if (pack.recipePath.head != defaultPackageFilename)
  962. // Storeinfo saved a default file, this could be different to the file from the zip.
  963. this.fs.removeFile(pack.recipePath);
  964. auto app = appender!string();
  965. app.writePrettyJsonString(pack.recipe.toJson());
  966. this.fs.writeFile(pack.recipePath.parentPath ~ defaultPackageFilename, app.data);
  967. addPackages(this.m_internal.localPackages, pack);
  968. return pack;
  969. }
  970.  
  971. /// Removes the given the package.
  972. void remove(in Package pack)
  973. {
  974. logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path);
  975. enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
  976. enforce(pack.parentPackage is null, "Cannot remove subpackage %s".format(pack.name));
  977.  
  978. // remove package from repositories' list
  979. bool found = false;
  980. bool removeFrom(Package[] packs, in Package pack) {
  981. auto packPos = countUntil!("a.path == b.path")(packs, pack);
  982. if(packPos != -1) {
  983. packs = .remove(packs, packPos);
  984. return true;
  985. }
  986. return false;
  987. }
  988. foreach(repo; m_repositories) {
  989. if (removeFrom(repo.fromPath, pack)) {
  990. found = true;
  991. break;
  992. }
  993. // Maintain backward compatibility with pre v1.30.0 behavior,
  994. // this is equivalent to remove-local
  995. if (removeFrom(repo.localPackages, pack)) {
  996. found = true;
  997. break;
  998. }
  999. }
  1000. if(!found)
  1001. found = removeFrom(this.m_internal.localPackages, pack);
  1002. enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
  1003.  
  1004. logDebug("About to delete root folder for package '%s'.", pack.path);
  1005. import std.file : rmdirRecurse;
  1006. rmdirRecurse(pack.path.toNativeString());
  1007. logInfo("Removed", Color.yellow, "%s %s", pack.name.color(Mode.bold), pack.version_);
  1008. }
  1009.  
  1010. /// Compatibility overload. Use the version without a `force_remove` argument instead.
  1011. deprecated("Use `remove(pack)` directly instead, the boolean has no effect")
  1012. void remove(in Package pack, bool force_remove)
  1013. {
  1014. remove(pack);
  1015. }
  1016.  
  1017. Package addLocalPackage(NativePath path, string verName, PlacementLocation type)
  1018. {
  1019. // As we iterate over `localPackages` we need it to be populated
  1020. // In theory we could just populate that specific repository,
  1021. // but multiple calls would then become inefficient.
  1022. this.ensureInitialized(InitializationState.full);
  1023.  
  1024. path.endsWithSlash = true;
  1025. auto pack = this.load(path);
  1026. enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
  1027. if (verName.length)
  1028. pack.version_ = Version(verName);
  1029.  
  1030. // don't double-add packages
  1031. Package[]* packs = &m_repositories[type].localPackages;
  1032. foreach (p; *packs) {
  1033. if (p.path == path) {
  1034. enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed.");
  1035. logInfo("Package is already registered: %s (version: %s)", p.name, p.version_);
  1036. return p;
  1037. }
  1038. }
  1039.  
  1040. addPackages(*packs, pack);
  1041.  
  1042. this.m_repositories[type].writeLocalPackageList(this);
  1043.  
  1044. logInfo("Registered package: %s (version: %s)", pack.name, pack.version_);
  1045. return pack;
  1046. }
  1047.  
  1048. void removeLocalPackage(NativePath path, PlacementLocation type)
  1049. {
  1050. // As we iterate over `localPackages` we need it to be populated
  1051. // In theory we could just populate that specific repository,
  1052. // but multiple calls would then become inefficient.
  1053. this.ensureInitialized(InitializationState.full);
  1054.  
  1055. path.endsWithSlash = true;
  1056. Package[]* packs = &m_repositories[type].localPackages;
  1057. size_t[] to_remove;
  1058. foreach( i, entry; *packs )
  1059. if( entry.path == path )
  1060. to_remove ~= i;
  1061. enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
  1062.  
  1063. string[Version] removed;
  1064. foreach (i; to_remove)
  1065. removed[(*packs)[i].version_] = (*packs)[i].name;
  1066.  
  1067. *packs = (*packs).enumerate
  1068. .filter!(en => !to_remove.canFind(en.index))
  1069. .map!(en => en.value).array;
  1070.  
  1071. this.m_repositories[type].writeLocalPackageList(this);
  1072.  
  1073. foreach(ver, name; removed)
  1074. logInfo("Deregistered package: %s (version: %s)", name, ver);
  1075. }
  1076.  
  1077. /// For the given type add another path where packages will be looked up.
  1078. void addSearchPath(NativePath path, PlacementLocation type)
  1079. {
  1080. m_repositories[type].searchPath ~= path;
  1081. this.m_repositories[type].writeLocalPackageList(this);
  1082. }
  1083.  
  1084. /// Removes a search path from the given type.
  1085. void removeSearchPath(NativePath path, PlacementLocation type)
  1086. {
  1087. m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
  1088. this.m_repositories[type].writeLocalPackageList(this);
  1089. }
  1090.  
  1091. deprecated("Use `refresh()` without boolean argument(same as `refresh(false)`")
  1092. void refresh(bool refresh)
  1093. {
  1094. if (refresh)
  1095. logDiagnostic("Refreshing local packages (refresh existing: true)...");
  1096. else
  1097. logDiagnostic("Scanning local packages...");
  1098.  
  1099. this.refreshLocal(refresh);
  1100. this.refreshCache(refresh);
  1101. }
  1102.  
  1103. void refresh()
  1104. {
  1105. logDiagnostic("Scanning local packages...");
  1106. this.refreshLocal(false);
  1107. this.refreshCache(false);
  1108. }
  1109.  
  1110. /// Private API to ensure a level of initialization
  1111. private void ensureInitialized(InitializationState state)
  1112. {
  1113. if (this.m_state >= state)
  1114. return;
  1115. if (state == InitializationState.partial)
  1116. this.refreshLocal(false);
  1117. else
  1118. this.refresh();
  1119. }
  1120.  
  1121. /// Refresh pay-as-you-go: Only load local packages, not the full cache
  1122. private void refreshLocal(bool refresh) {
  1123. foreach (ref repository; this.m_repositories)
  1124. repository.scanLocalPackages(refresh, this);
  1125. this.m_internal.scan(this, refresh);
  1126. foreach (ref repository; this.m_repositories) {
  1127. auto existing = refresh ? null : repository.fromPath;
  1128. foreach (path; repository.searchPath)
  1129. repository.scanPackageFolder(path, this, existing);
  1130. repository.loadOverrides(this);
  1131. }
  1132. if (this.m_state < InitializationState.partial)
  1133. this.m_state = InitializationState.partial;
  1134. }
  1135.  
  1136. /// Refresh the full cache, a potentially expensive operation
  1137. private void refreshCache(bool refresh)
  1138. {
  1139. foreach (ref repository; this.m_repositories)
  1140. repository.scan(this, refresh);
  1141. this.m_state = InitializationState.full;
  1142. }
  1143.  
  1144. alias Hash = ubyte[];
  1145. /// Generates a hash digest for a given package.
  1146. /// Some files or folders are ignored during the generation (like .dub and
  1147. /// .svn folders)
  1148. Hash hashPackage(Package pack)
  1149. {
  1150. import std.file;
  1151. import dub.internal.vibecompat.core.file;
  1152.  
  1153. string[] ignored_directories = [".git", ".dub", ".svn"];
  1154. // something from .dub_ignore or what?
  1155. string[] ignored_files = [];
  1156. SHA256 hash;
  1157. foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
  1158. const isDir = file.isDir;
  1159. if(isDir && ignored_directories.canFind(NativePath(file.name).head.name))
  1160. continue;
  1161. else if(ignored_files.canFind(NativePath(file.name).head.name))
  1162. continue;
  1163.  
  1164. hash.put(cast(ubyte[])NativePath(file.name).head.name);
  1165. if(isDir) {
  1166. logDebug("Hashed directory name %s", NativePath(file.name).head);
  1167. }
  1168. else {
  1169. hash.put(cast(ubyte[]) readFile(NativePath(file.name)));
  1170. logDebug("Hashed file contents from %s", NativePath(file.name).head);
  1171. }
  1172. }
  1173. auto digest = hash.finish();
  1174. logDebug("Project hash: %s", digest);
  1175. return digest[].dup;
  1176. }
  1177.  
  1178. /**
  1179. * Loads the selections file (`dub.selections.json`)
  1180. *
  1181. * The selections file is only used for the root package / project.
  1182. * However, due to it being a filesystem interaction, it is managed
  1183. * from the `PackageManager`.
  1184. *
  1185. * Params:
  1186. * absProjectPath = The absolute path to the root package/project for
  1187. * which to load the selections file.
  1188. *
  1189. * Returns:
  1190. * Either `null` (if no selections file exists or parsing encountered an error),
  1191. * or a `SelectionsFileLookupResult`. Note that the nested `SelectionsFile`
  1192. * might use an unsupported version (see `SelectionsFile` documentation).
  1193. */
  1194. Nullable!SelectionsFileLookupResult readSelections(in NativePath absProjectPath)
  1195. in (absProjectPath.absolute) {
  1196. import dub.internal.configy.Read;
  1197.  
  1198. alias N = typeof(return);
  1199.  
  1200. // check for dub.selections.json in root project dir first, then walk up its
  1201. // parent directories and look for inheritable dub.selections.json files
  1202. const path = this.findSelections(absProjectPath);
  1203. if (path.empty) return N.init;
  1204. const content = this.fs.readText(path);
  1205. // TODO: Remove `StrictMode.Warn` after v1.40 release
  1206. // The default is to error, but as the previous parser wasn't
  1207. // complaining, we should first warn the user.
  1208. auto selections = wrapException(parseConfigString!SelectionsFile(
  1209. content, path.toNativeString(), StrictMode.Warn));
  1210. // Could not parse file
  1211. if (selections.isNull())
  1212. return N.init;
  1213. // Non-inheritable selections found
  1214. if (!path.startsWith(absProjectPath) && !selections.get().inheritable)
  1215. return N.init;
  1216. return N(SelectionsFileLookupResult(path, selections.get()));
  1217. }
  1218.  
  1219. /// Helper function to walk up the filesystem and find `dub.selections.json`
  1220. private NativePath findSelections(in NativePath dir)
  1221. {
  1222. const path = dir ~ "dub.selections.json";
  1223. if (this.fs.existsFile(path))
  1224. return path;
  1225. if (!dir.hasParentPath)
  1226. return NativePath.init;
  1227. return this.findSelections(dir.parentPath);
  1228.  
  1229. }
  1230.  
  1231. /**
  1232. * Writes the selections file (`dub.selections.json`)
  1233. *
  1234. * The selections file is only used for the root package / project.
  1235. * However, due to it being a filesystem interaction, it is managed
  1236. * from the `PackageManager`.
  1237. *
  1238. * Params:
  1239. * project = The root package / project to read the selections file for.
  1240. * selections = The `SelectionsFile` to write.
  1241. * overwrite = Whether to overwrite an existing selections file.
  1242. * True by default.
  1243. */
  1244. public void writeSelections(in Package project, in Selections!1 selections,
  1245. bool overwrite = true)
  1246. {
  1247. const path = project.path ~ "dub.selections.json";
  1248. if (!overwrite && this.fs.existsFile(path))
  1249. return;
  1250. this.fs.writeFile(path, selectionsToString(selections));
  1251. }
  1252.  
  1253. /// Package function to avoid code duplication with deprecated
  1254. /// SelectedVersions.save, merge with `writeSelections` in
  1255. /// the future.
  1256. package static string selectionsToString (in Selections!1 s)
  1257. {
  1258. Json json = selectionsToJSON(s);
  1259. assert(json.type == Json.Type.object);
  1260. assert(json.length == 2 || json.length == 3);
  1261. assert(json["versions"].type != Json.Type.undefined);
  1262.  
  1263. auto result = appender!string();
  1264. result.put("{\n\t\"fileVersion\": ");
  1265. result.writeJsonString(json["fileVersion"]);
  1266. if (s.inheritable)
  1267. result.put(",\n\t\"inheritable\": true");
  1268. result.put(",\n\t\"versions\": {");
  1269. auto vers = json["versions"].get!(Json[string]);
  1270. bool first = true;
  1271. foreach (k; vers.byKey.array.sort()) {
  1272. if (!first) result.put(",");
  1273. else first = false;
  1274. result.put("\n\t\t");
  1275. result.writeJsonString(Json(k));
  1276. result.put(": ");
  1277. result.writeJsonString(vers[k]);
  1278. }
  1279. result.put("\n\t}\n}\n");
  1280. return result.data;
  1281. }
  1282.  
  1283. /// Ditto
  1284. package static Json selectionsToJSON (in Selections!1 s)
  1285. {
  1286. Json serialized = Json.emptyObject;
  1287. serialized["fileVersion"] = s.fileVersion;
  1288. if (s.inheritable)
  1289. serialized["inheritable"] = true;
  1290. serialized["versions"] = Json.emptyObject;
  1291. foreach (p, dep; s.versions)
  1292. serialized["versions"][p] = dep.toJson(true);
  1293. return serialized;
  1294. }
  1295.  
  1296. /// Adds the package and scans for sub-packages.
  1297. protected void addPackages(ref Package[] dst_repos, Package pack)
  1298. {
  1299. // Add the main package.
  1300. dst_repos ~= pack;
  1301.  
  1302. // Additionally to the internally defined sub-packages, whose metadata
  1303. // is loaded with the main dub.json, load all externally defined
  1304. // packages after the package is available with all the data.
  1305. foreach (spr; pack.subPackages) {
  1306. Package sp;
  1307.  
  1308. if (spr.path.length) {
  1309. auto p = NativePath(spr.path);
  1310. p.normalize();
  1311. enforce(!p.absolute, "Sub package paths must be sub paths of the parent package.");
  1312. auto path = pack.path ~ p;
  1313. sp = this.load(path, NativePath.init, pack);
  1314. } else sp = new Package(spr.recipe, pack.path, pack);
  1315.  
  1316. // Add the sub-package.
  1317. try {
  1318. dst_repos ~= sp;
  1319. } catch (Exception e) {
  1320. logError("Package '%s': Failed to load sub-package %s: %s", pack.name,
  1321. spr.path.length ? spr.path : spr.recipe.name, e.msg);
  1322. logDiagnostic("Full error: %s", e.toString().sanitize());
  1323. }
  1324. }
  1325. }
  1326. }
  1327.  
  1328. deprecated(OverrideDepMsg)
  1329. alias PackageOverride = PackageOverride_;
  1330.  
  1331. package(dub) struct PackageOverride_ {
  1332. private alias ResolvedDep = SumType!(NativePath, Version);
  1333. string package_;
  1334. VersionRange source;
  1335. ResolvedDep target;
  1336.  
  1337. deprecated("Use `source` instead")
  1338. @property inout(Dependency) version_ () inout return @safe {
  1339. return Dependency(this.source);
  1340. }
  1341.  
  1342. deprecated("Assign `source` instead")
  1343. @property ref PackageOverride version_ (Dependency v) scope return @safe pure {
  1344. this.source = v.visit!(
  1345. (VersionRange range) => range,
  1346. (any) {
  1347. int a; if (a) return VersionRange.init; // Trick the compiler
  1348. throw new Exception("Cannot use anything else than a `VersionRange` for overrides");
  1349. },
  1350. );
  1351. return this;
  1352. }
  1353.  
  1354. deprecated("Use `target.match` directly instead")
  1355. @property inout(Version) targetVersion () inout return @safe pure nothrow @nogc {
  1356. return this.target.match!(
  1357. (Version v) => v,
  1358. (any) => Version.init,
  1359. );
  1360. }
  1361.  
  1362. deprecated("Assign `target` directly instead")
  1363. @property ref PackageOverride targetVersion (Version v) scope return pure nothrow @nogc {
  1364. this.target = v;
  1365. return this;
  1366. }
  1367.  
  1368. deprecated("Use `target.match` directly instead")
  1369. @property inout(NativePath) targetPath () inout return @safe pure nothrow @nogc {
  1370. return this.target.match!(
  1371. (NativePath v) => v,
  1372. (any) => NativePath.init,
  1373. );
  1374. }
  1375.  
  1376. deprecated("Assign `target` directly instead")
  1377. @property ref PackageOverride targetPath (NativePath v) scope return pure nothrow @nogc {
  1378. this.target = v;
  1379. return this;
  1380. }
  1381.  
  1382. deprecated("Use the overload that accepts a `VersionRange` as 2nd argument")
  1383. this(string package_, Dependency version_, Version target_version)
  1384. {
  1385. this.package_ = package_;
  1386. this.version_ = version_;
  1387. this.target = target_version;
  1388. }
  1389.  
  1390. deprecated("Use the overload that accepts a `VersionRange` as 2nd argument")
  1391. this(string package_, Dependency version_, NativePath target_path)
  1392. {
  1393. this.package_ = package_;
  1394. this.version_ = version_;
  1395. this.target = target_path;
  1396. }
  1397.  
  1398. this(string package_, VersionRange src, Version target)
  1399. {
  1400. this.package_ = package_;
  1401. this.source = src;
  1402. this.target = target;
  1403. }
  1404.  
  1405. this(string package_, VersionRange src, NativePath target)
  1406. {
  1407. this.package_ = package_;
  1408. this.source = src;
  1409. this.target = target;
  1410. }
  1411. }
  1412.  
  1413. deprecated("Use `PlacementLocation` instead")
  1414. enum LocalPackageType : PlacementLocation {
  1415. package_ = PlacementLocation.local,
  1416. user = PlacementLocation.user,
  1417. system = PlacementLocation.system,
  1418. }
  1419.  
  1420. private enum LocalPackagesFilename = "local-packages.json";
  1421. private enum LocalOverridesFilename = "local-overrides.json";
  1422.  
  1423. /**
  1424. * A managed location, with packages, configuration, and overrides
  1425. *
  1426. * There exists three standards locations, listed in `PlacementLocation`.
  1427. * The user one is the default, with the system and local one meeting
  1428. * different needs.
  1429. *
  1430. * Each location has a root, under which the following may be found:
  1431. * - A `packages/` directory, where packages are stored (see `packagePath`);
  1432. * - A `local-packages.json` file, with extra search paths
  1433. * and manually added packages (see `dub add-local`);
  1434. * - A `local-overrides.json` file, with manually added overrides (`dub add-override`);
  1435. *
  1436. * Additionally, each location host a config file,
  1437. * which is not managed by this module, but by dub itself.
  1438. */
  1439. package struct Location {
  1440. /// The absolute path to the root of the location
  1441. NativePath packagePath;
  1442.  
  1443. /// Configured (extra) search paths for this `Location`
  1444. NativePath[] searchPath;
  1445.  
  1446. /**
  1447. * List of manually registered packages at this `Location`
  1448. * and stored in `local-packages.json`
  1449. */
  1450. Package[] localPackages;
  1451.  
  1452. /// List of overrides stored at this `Location`
  1453. PackageOverride_[] overrides;
  1454.  
  1455. /**
  1456. * List of packages stored under `packagePath` and automatically detected
  1457. */
  1458. Package[] fromPath;
  1459.  
  1460. this(NativePath path) @safe pure nothrow @nogc
  1461. {
  1462. this.packagePath = path;
  1463. }
  1464.  
  1465. void loadOverrides(PackageManager mgr)
  1466. {
  1467. this.overrides = null;
  1468. auto ovrfilepath = this.packagePath ~ LocalOverridesFilename;
  1469. if (mgr.fs.existsFile(ovrfilepath)) {
  1470. logWarn("Found local override file: %s", ovrfilepath);
  1471. logWarn(OverrideDepMsg);
  1472. logWarn("Replace with a path-based dependency in your project or a custom cache path");
  1473. const text = mgr.fs.readText(ovrfilepath);
  1474. auto json = parseJsonString(text, ovrfilepath.toNativeString());
  1475. foreach (entry; json) {
  1476. PackageOverride_ ovr;
  1477. ovr.package_ = entry["name"].get!string;
  1478. ovr.source = VersionRange.fromString(entry["version"].get!string);
  1479. if (auto pv = "targetVersion" in entry) ovr.target = Version(pv.get!string);
  1480. if (auto pv = "targetPath" in entry) ovr.target = NativePath(pv.get!string);
  1481. this.overrides ~= ovr;
  1482. }
  1483. }
  1484. }
  1485.  
  1486. private void writeOverrides(PackageManager mgr)
  1487. {
  1488. Json[] newlist;
  1489. foreach (ovr; this.overrides) {
  1490. auto jovr = Json.emptyObject;
  1491. jovr["name"] = ovr.package_;
  1492. jovr["version"] = ovr.source.toString();
  1493. ovr.target.match!(
  1494. (NativePath path) { jovr["targetPath"] = path.toNativeString(); },
  1495. (Version vers) { jovr["targetVersion"] = vers.toString(); },
  1496. );
  1497. newlist ~= jovr;
  1498. }
  1499. auto path = this.packagePath;
  1500. mgr.fs.mkdir(path);
  1501. auto app = appender!string();
  1502. app.writePrettyJsonString(Json(newlist));
  1503. mgr.fs.writeFile(path ~ LocalOverridesFilename, app.data);
  1504. }
  1505.  
  1506. private void writeLocalPackageList(PackageManager mgr)
  1507. {
  1508. Json[] newlist;
  1509. foreach (p; this.searchPath) {
  1510. auto entry = Json.emptyObject;
  1511. entry["name"] = "*";
  1512. entry["path"] = p.toNativeString();
  1513. newlist ~= entry;
  1514. }
  1515.  
  1516. foreach (p; this.localPackages) {
  1517. if (p.parentPackage) continue; // do not store sub packages
  1518. auto entry = Json.emptyObject;
  1519. entry["name"] = p.name;
  1520. entry["version"] = p.version_.toString();
  1521. entry["path"] = p.path.toNativeString();
  1522. newlist ~= entry;
  1523. }
  1524.  
  1525. NativePath path = this.packagePath;
  1526. mgr.fs.mkdir(path);
  1527. auto app = appender!string();
  1528. app.writePrettyJsonString(Json(newlist));
  1529. mgr.fs.writeFile(path ~ LocalPackagesFilename, app.data);
  1530. }
  1531.  
  1532. // load locally defined packages
  1533. void scanLocalPackages(bool refresh, PackageManager manager)
  1534. {
  1535. NativePath list_path = this.packagePath;
  1536. Package[] packs;
  1537. NativePath[] paths;
  1538. try {
  1539. auto local_package_file = list_path ~ LocalPackagesFilename;
  1540. if (!manager.fs.existsFile(local_package_file)) return;
  1541.  
  1542. logDiagnostic("Loading local package map at %s", local_package_file.toNativeString());
  1543. const text = manager.fs.readText(local_package_file);
  1544. auto packlist = parseJsonString(
  1545. text, local_package_file.toNativeString());
  1546. enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array.");
  1547. foreach (pentry; packlist) {
  1548. try {
  1549. auto name = pentry["name"].get!string;
  1550. auto path = NativePath(pentry["path"].get!string);
  1551. if (name == "*") {
  1552. paths ~= path;
  1553. } else {
  1554. auto ver = Version(pentry["version"].get!string);
  1555.  
  1556. Package pp;
  1557. if (!refresh) {
  1558. foreach (p; this.localPackages)
  1559. if (p.path == path) {
  1560. pp = p;
  1561. break;
  1562. }
  1563. }
  1564.  
  1565. if (!pp) {
  1566. auto infoFile = manager.findPackageFile(path);
  1567. if (!infoFile.empty) pp = manager.load(path, infoFile);
  1568. else {
  1569. logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.",
  1570. name, ver, path.toNativeString());
  1571. // Store a dummy package
  1572. pp = new Package(PackageRecipe(name), path);
  1573. }
  1574. }
  1575.  
  1576. if (pp.name != name)
  1577. logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name);
  1578. pp.version_ = ver;
  1579. manager.addPackages(packs, pp);
  1580. }
  1581. } catch (Exception e) {
  1582. logWarn("Error adding local package: %s", e.msg);
  1583. }
  1584. }
  1585. } catch (Exception e) {
  1586. logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
  1587. }
  1588. this.localPackages = packs;
  1589. this.searchPath = paths;
  1590. }
  1591.  
  1592. /**
  1593. * Scan this location
  1594. */
  1595. void scan(PackageManager mgr, bool refresh)
  1596. {
  1597. // If we're asked to refresh, reload the packages from scratch
  1598. auto existing = refresh ? null : this.fromPath;
  1599. if (this.packagePath !is NativePath.init) {
  1600. // For the internal location, we use `fromPath` to store packages
  1601. // loaded by the user (e.g. the project and its sub-packages),
  1602. // so don't clean it.
  1603. this.fromPath = null;
  1604. }
  1605. foreach (path; this.searchPath)
  1606. this.scanPackageFolder(path, mgr, existing);
  1607. if (this.packagePath !is NativePath.init)
  1608. this.scanPackageFolder(this.packagePath, mgr, existing);
  1609. }
  1610.  
  1611. /**
  1612. * Scan the content of a folder (`packagePath` or in `searchPaths`),
  1613. * and add all packages that were found to this location.
  1614. */
  1615. void scanPackageFolder(NativePath path, PackageManager mgr,
  1616. Package[] existing_packages)
  1617. {
  1618. if (!mgr.fs.existsDirectory(path))
  1619. return;
  1620.  
  1621. void loadInternal (NativePath pack_path, NativePath packageFile)
  1622. {
  1623. import std.algorithm.searching : find;
  1624.  
  1625. // If the package has already been loaded, no need to re-load it.
  1626. auto rng = existing_packages.find!(pp => pp.path == pack_path);
  1627. if (!rng.empty)
  1628. return mgr.addPackages(this.fromPath, rng.front);
  1629.  
  1630. try {
  1631. mgr.addPackages(this.fromPath, mgr.load(pack_path, packageFile));
  1632. } catch (ConfigException exc) {
  1633. // Configy error message already include the path
  1634. logError("Invalid recipe for local package: %S", exc);
  1635. } catch (Exception e) {
  1636. logError("Failed to load package in %s: %s", pack_path, e.msg);
  1637. logDiagnostic("Full error: %s", e.toString().sanitize());
  1638. }
  1639. }
  1640.  
  1641. logDebug("iterating dir %s", path.toNativeString());
  1642. try foreach (pdir; mgr.fs.iterateDirectory(path)) {
  1643. logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
  1644. if (!pdir.isDirectory) continue;
  1645.  
  1646. const pack_path = path ~ (pdir.name ~ "/");
  1647. auto packageFile = mgr.findPackageFile(pack_path);
  1648.  
  1649. if (isManaged(path)) {
  1650. // Old / flat directory structure, used in non-standard path
  1651. // Packages are stored in $ROOT/$SOMETHING/`
  1652. if (!packageFile.empty) {
  1653. // Deprecated flat managed directory structure
  1654. logWarn("Package at path '%s' should be under '%s'",
  1655. pack_path.toNativeString().color(Mode.bold),
  1656. (pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold));
  1657. logWarn("The package will no longer be detected starting from v1.42.0");
  1658. loadInternal(pack_path, packageFile);
  1659. } else {
  1660. // New managed structure: $ROOT/$NAME/$VERSION/$NAME
  1661. // This is the most common code path
  1662.  
  1663. // Iterate over versions of a package
  1664. foreach (versdir; mgr.fs.iterateDirectory(pack_path)) {
  1665. if (!versdir.isDirectory) continue;
  1666. auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/");
  1667. if (!mgr.fs.existsDirectory(vers_path)) continue;
  1668. packageFile = mgr.findPackageFile(vers_path);
  1669. loadInternal(vers_path, packageFile);
  1670. }
  1671. }
  1672. } else {
  1673. // Unmanaged directories (dub add-path) are always stored as a
  1674. // flat list of packages, as these are the working copies managed
  1675. // by the user. The nested structure should not be supported,
  1676. // even optionally, because that would lead to bogus "no package
  1677. // file found" errors in case the internal directory structure
  1678. // accidentally matches the $NAME/$VERSION/$NAME scheme
  1679. if (!packageFile.empty)
  1680. loadInternal(pack_path, packageFile);
  1681. }
  1682. }
  1683. catch (Exception e)
  1684. logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
  1685. }
  1686.  
  1687. /**
  1688. * Looks up already-loaded packages at a specific version
  1689. *
  1690. * Looks up a package according to this `Location`'s priority,
  1691. * that is, packages from the search path and local packages
  1692. * have the highest priority.
  1693. *
  1694. * Params:
  1695. * name = The full name of the package to look up
  1696. * ver = The version to look up
  1697. *
  1698. * Returns:
  1699. * A `Package` if one was found, `null` if none exists.
  1700. */
  1701. inout(Package) lookup(in PackageName name, in Version ver) inout {
  1702. foreach (pkg; this.localPackages)
  1703. if (pkg.name == name.toString() &&
  1704. pkg.version_.matches(ver, VersionMatchMode.standard))
  1705. return pkg;
  1706. foreach (pkg; this.fromPath) {
  1707. auto pvm = this.isManaged(pkg.basePackage.path) ?
  1708. VersionMatchMode.strict : VersionMatchMode.standard;
  1709. if (pkg.name == name.toString() && pkg.version_.matches(ver, pvm))
  1710. return pkg;
  1711. }
  1712. return null;
  1713. }
  1714.  
  1715. /**
  1716. * Looks up a package, first in the list of loaded packages,
  1717. * then directly on the file system.
  1718. *
  1719. * This function allows for lazy loading of packages, without needing to
  1720. * first scan all the available locations (as `scan` does).
  1721. *
  1722. * Params:
  1723. * name = The full name of the package to look up
  1724. * vers = The version the package must match
  1725. * mgr = The `PackageManager` to use for adding packages
  1726. *
  1727. * Returns:
  1728. * A `Package` if one was found, `null` if none exists.
  1729. */
  1730. Package load (in PackageName name, Version vers, PackageManager mgr)
  1731. {
  1732. if (auto pkg = this.lookup(name, vers))
  1733. return pkg;
  1734.  
  1735. string versStr = vers.toString();
  1736. const path = this.getPackagePath(name, versStr);
  1737. if (!mgr.fs.existsDirectory(path))
  1738. return null;
  1739.  
  1740. logDiagnostic("Lazily loading package %s:%s from %s", name.main, vers, path);
  1741. auto p = mgr.load(path);
  1742. enforce(
  1743. p.version_ == vers,
  1744. format("Package %s located in %s has a different version than its path: Got %s, expected %s",
  1745. name, path, p.version_, vers));
  1746. mgr.addPackages(this.fromPath, p);
  1747. return p;
  1748. }
  1749.  
  1750. /**
  1751. * Get the final destination a specific package needs to be stored in.
  1752. *
  1753. * Note that there needs to be an extra level for libraries like `ae`
  1754. * which expects their containing folder to have an exact name and use
  1755. * `importPath "../"`.
  1756. *
  1757. * Hence the final format returned is `$BASE/$NAME/$VERSION/$NAME`,
  1758. * `$BASE` is `this.packagePath`.
  1759. *
  1760. * Params:
  1761. * name = The package name - if the name is that of a subpackage,
  1762. * only the path to the main package is returned, as the
  1763. * subpackage path can only be known after reading the recipe.
  1764. * vers = A version string. Typed as a string because git hashes
  1765. * can be used with this function.
  1766. *
  1767. * Returns:
  1768. * An absolute `NativePath` nested in this location.
  1769. */
  1770. NativePath getPackagePath (in PackageName name, string vers)
  1771. {
  1772. NativePath result = this.packagePath ~ name.main.toString() ~ vers ~
  1773. name.main.toString();
  1774. result.endsWithSlash = true;
  1775. return result;
  1776. }
  1777.  
  1778. /// Determines if a specific path is within a DUB managed Location.
  1779. bool isManaged(NativePath path) const {
  1780. return path.startsWith(this.packagePath);
  1781. }
  1782. }
  1783.  
  1784. private immutable string OverrideDepMsg =
  1785. "Overrides are deprecated as they are redundant with more fine-grained approaches";