Newer
Older
dub_jkp / source / dub / recipe / selection.d
  1. /**
  2. * Contains type definition for the selections file
  3. *
  4. * The selections file, commonly known by its file name
  5. * `dub.selections.json`, is used by Dub to store resolved
  6. * dependencies. Its purpose is identical to other package
  7. * managers' lock file.
  8. */
  9. module dub.recipe.selection;
  10.  
  11. import dub.dependency;
  12. import dub.internal.vibecompat.inet.path : NativePath;
  13.  
  14. import dub.internal.configy.Attributes;
  15. import dub.internal.dyaml.stdsumtype;
  16.  
  17. import std.exception;
  18.  
  19. deprecated("Use either `Selections!1` or `SelectionsFile` instead")
  20. public alias Selected = Selections!1;
  21.  
  22. /**
  23. * Top level type for `dub.selections.json`
  24. *
  25. * To support multiple version, we expose a `SumType` which
  26. * contains the "real" version being parsed.
  27. */
  28. public struct SelectionsFile
  29. {
  30. /// Private alias to avoid repetition
  31. private alias DataType = SumType!(Selections!0, Selections!1);
  32.  
  33. /**
  34. * Get the `fileVersion` of this selection file
  35. *
  36. * The `fileVersion` is always present, no matter the version.
  37. * This is a convenience function that matches any version and allows
  38. * one to retrieve it.
  39. *
  40. * Note that the `fileVersion` can be an unsupported version.
  41. */
  42. public uint fileVersion () const @safe pure nothrow @nogc
  43. {
  44. return this.content.match!((s) => s.fileVersion);
  45. }
  46.  
  47. /**
  48. * The content of this selections file
  49. *
  50. * The underlying content can be accessed using
  51. * `dub.internal.yaml.stdsumtype : match`, for example:
  52. * ---
  53. * SelectionsFile file = readSelectionsFile();
  54. * file.content.match!(
  55. * (Selections!0 s) => logWarn("Unsupported version: %s", s.fileVersion),
  56. * (Selections!1 s) => logWarn("Old version (1), please upgrade!"),
  57. * (Selections!2 s) => logInfo("You are up to date"),
  58. * );
  59. * ---
  60. */
  61. public DataType content;
  62.  
  63. /**
  64. * Deserialize the selections file according to its version
  65. *
  66. * This will first deserialize the `fileVersion` only, and then
  67. * the expected version if it is supported. Unsupported versions
  68. * will be returned inside a `Selections!0` struct,
  69. * which only contains a `fileVersion`.
  70. */
  71. public static SelectionsFile fromYAML (scope ConfigParser!SelectionsFile parser)
  72. {
  73. import dub.internal.configy.Read;
  74.  
  75. static struct OnlyVersion { uint fileVersion; }
  76.  
  77. auto vers = parseConfig!OnlyVersion(
  78. CLIArgs.init, parser.node, StrictMode.Ignore);
  79.  
  80. switch (vers.fileVersion) {
  81. case 1:
  82. return SelectionsFile(DataType(parser.parseAs!(Selections!1)));
  83. default:
  84. return SelectionsFile(DataType(Selections!0(vers.fileVersion)));
  85. }
  86. }
  87. }
  88.  
  89. /**
  90. * A specific version of the selections file
  91. *
  92. * Currently, only two instantiations of this struct are possible:
  93. * - `Selections!0` is an invalid/unsupported version;
  94. * - `Selections!1` is the most widespread version;
  95. */
  96. public struct Selections (ushort Version)
  97. {
  98. ///
  99. public uint fileVersion = Version;
  100.  
  101. static if (Version == 0) { /* Invalid version */ }
  102. else static if (Version == 1) {
  103. /// The selected package and their matching versions
  104. public SelectedDependency[string] versions;
  105. }
  106. else
  107. static assert(false, "This version is not supported");
  108. }
  109.  
  110.  
  111. /// Wrapper around `SelectedDependency` to do deserialization but still provide
  112. /// a `Dependency` object to client code.
  113. private struct SelectedDependency
  114. {
  115. public Dependency actual;
  116. alias actual this;
  117.  
  118. /// Constructor, used in `fromYAML`
  119. public this (inout(Dependency) dep) inout @safe pure nothrow @nogc
  120. {
  121. this.actual = dep;
  122. }
  123.  
  124. /// Allow external code to assign to this object as if it was a `Dependency`
  125. public ref SelectedDependency opAssign (Dependency dep) return pure nothrow @nogc
  126. {
  127. this.actual = dep;
  128. return this;
  129. }
  130.  
  131. /// Read a `Dependency` from the config file - Required to support both short and long form
  132. static SelectedDependency fromYAML (scope ConfigParser!SelectedDependency p)
  133. {
  134. import dub.internal.dyaml.node;
  135.  
  136. if (p.node.nodeID == NodeID.scalar)
  137. return SelectedDependency(Dependency(Version(p.node.as!string)));
  138.  
  139. auto d = p.parseAs!YAMLFormat;
  140. if (d.path.length)
  141. return SelectedDependency(Dependency(NativePath(d.path)));
  142. else
  143. {
  144. assert(d.version_.length);
  145. if (d.repository.length)
  146. return SelectedDependency(Dependency(Repository(d.repository, d.version_)));
  147. return SelectedDependency(Dependency(Version(d.version_)));
  148. }
  149. }
  150.  
  151. /// In-file representation of a dependency as permitted in `dub.selections.json`
  152. private struct YAMLFormat
  153. {
  154. @Optional @Name("version") string version_;
  155. @Optional string path;
  156. @Optional string repository;
  157.  
  158. public void validate () const scope @safe pure
  159. {
  160. enforce(this.version_.length || this.path.length || this.repository.length,
  161. "Need to provide a version string, or an object with one of the following fields: `version`, `path`, or `repository`");
  162. enforce(!this.path.length || !this.repository.length,
  163. "Cannot provide a `path` dependency if a repository dependency is used");
  164. enforce(!this.path.length || !this.version_.length,
  165. "Cannot provide a `path` dependency if a `version` dependency is used");
  166. enforce(!this.repository.length || this.version_.length,
  167. "Cannot provide a `repository` dependency without a `version`");
  168. }
  169. }
  170. }
  171.  
  172. // Ensure we can read all type of dependencies
  173. unittest
  174. {
  175. import dub.internal.configy.Read : parseConfigString;
  176.  
  177. immutable string content = `{
  178. "fileVersion": 1,
  179. "versions": {
  180. "simple": "1.5.6",
  181. "branch": "~master",
  182. "branch2": "~main",
  183. "path": { "path": "../some/where" },
  184. "repository": { "repository": "git+https://github.com/dlang/dub", "version": "123456123456123456" }
  185. }
  186. }`;
  187.  
  188. auto file = parseConfigString!SelectionsFile(content, "/dev/null");
  189. assert(file.fileVersion == 1);
  190. auto s = file.content.match!(
  191. (Selections!1 s) => s,
  192. (s) { assert(0); return Selections!(1).init; },
  193. );
  194. assert(s.versions.length == 5);
  195. assert(s.versions["simple"] == Dependency(Version("1.5.6")));
  196. assert(s.versions["branch"] == Dependency(Version("~master")));
  197. assert(s.versions["branch2"] == Dependency(Version("~main")));
  198. assert(s.versions["path"] == Dependency(NativePath("../some/where")));
  199. assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456")));
  200. }
  201.  
  202. // Test reading an unsupported version
  203. unittest
  204. {
  205. import dub.internal.configy.Read : parseConfigString;
  206.  
  207. immutable string content = `{"fileVersion": 9999, "thisis": "notrecognized"}`;
  208. auto s = parseConfigString!SelectionsFile(content, "/dev/null");
  209. assert(s.fileVersion == 9999);
  210. }