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 }