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 * Whether this dub.selections.json can be inherited by nested projects 49 * without local dub.selections.json 50 */ 51 public bool inheritable () const @safe pure nothrow @nogc 52 { 53 return this.content.match!( 54 (const Selections!0 _) => false, 55 (const Selections!1 s) => s.inheritable, 56 ); 57 } 58 59 /** 60 * The content of this selections file 61 * 62 * The underlying content can be accessed using 63 * `dub.internal.yaml.stdsumtype : match`, for example: 64 * --- 65 * SelectionsFile file = readSelectionsFile(); 66 * file.content.match!( 67 * (Selections!0 s) => logWarn("Unsupported version: %s", s.fileVersion), 68 * (Selections!1 s) => logWarn("Old version (1), please upgrade!"), 69 * (Selections!2 s) => logInfo("You are up to date"), 70 * ); 71 * --- 72 */ 73 public DataType content; 74 75 /** 76 * Deserialize the selections file according to its version 77 * 78 * This will first deserialize the `fileVersion` only, and then 79 * the expected version if it is supported. Unsupported versions 80 * will be returned inside a `Selections!0` struct, 81 * which only contains a `fileVersion`. 82 */ 83 public static SelectionsFile fromYAML (scope ConfigParser!SelectionsFile parser) 84 { 85 import dub.internal.configy.Read; 86 87 static struct OnlyVersion { uint fileVersion; } 88 89 auto vers = parseConfig!OnlyVersion( 90 CLIArgs.init, parser.node, StrictMode.Ignore); 91 92 switch (vers.fileVersion) { 93 case 1: 94 return SelectionsFile(DataType(parser.parseAs!(Selections!1))); 95 default: 96 return SelectionsFile(DataType(Selections!0(vers.fileVersion))); 97 } 98 } 99 } 100 101 /** 102 * A specific version of the selections file 103 * 104 * Currently, only two instantiations of this struct are possible: 105 * - `Selections!0` is an invalid/unsupported version; 106 * - `Selections!1` is the most widespread version; 107 */ 108 public struct Selections (ushort Version) 109 { 110 /// 111 public uint fileVersion = Version; 112 113 static if (Version == 0) { /* Invalid version */ } 114 else static if (Version == 1) { 115 /// The selected package and their matching versions 116 public SelectedDependency[string] versions; 117 118 /// Whether this dub.selections.json can be inherited by nested projects 119 /// without local dub.selections.json 120 @Optional public bool inheritable; 121 } 122 else 123 static assert(false, "This version is not supported"); 124 } 125 126 127 /// Wrapper around `SelectedDependency` to do deserialization but still provide 128 /// a `Dependency` object to client code. 129 private struct SelectedDependency 130 { 131 public Dependency actual; 132 alias actual this; 133 134 /// Constructor, used in `fromYAML` 135 public this (inout(Dependency) dep) inout @safe pure nothrow @nogc 136 { 137 this.actual = dep; 138 } 139 140 /// Allow external code to assign to this object as if it was a `Dependency` 141 public ref SelectedDependency opAssign (Dependency dep) return pure nothrow @nogc 142 { 143 this.actual = dep; 144 return this; 145 } 146 147 /// Read a `Dependency` from the config file - Required to support both short and long form 148 static SelectedDependency fromYAML (scope ConfigParser!SelectedDependency p) 149 { 150 import dub.internal.dyaml.node; 151 152 if (p.node.nodeID == NodeID.scalar) 153 return SelectedDependency(Dependency(Version(p.node.as!string))); 154 155 auto d = p.parseAs!YAMLFormat; 156 if (d.path.length) 157 return SelectedDependency(Dependency(NativePath(d.path))); 158 else 159 { 160 assert(d.version_.length); 161 if (d.repository.length) 162 return SelectedDependency(Dependency(Repository(d.repository, d.version_))); 163 return SelectedDependency(Dependency(Version(d.version_))); 164 } 165 } 166 167 /// In-file representation of a dependency as permitted in `dub.selections.json` 168 private struct YAMLFormat 169 { 170 @Optional @Name("version") string version_; 171 @Optional string path; 172 @Optional string repository; 173 174 public void validate () const scope @safe pure 175 { 176 enforce(this.version_.length || this.path.length || this.repository.length, 177 "Need to provide a version string, or an object with one of the following fields: `version`, `path`, or `repository`"); 178 enforce(!this.path.length || !this.repository.length, 179 "Cannot provide a `path` dependency if a repository dependency is used"); 180 enforce(!this.path.length || !this.version_.length, 181 "Cannot provide a `path` dependency if a `version` dependency is used"); 182 enforce(!this.repository.length || this.version_.length, 183 "Cannot provide a `repository` dependency without a `version`"); 184 } 185 } 186 } 187 188 // Ensure we can read all type of dependencies 189 unittest 190 { 191 import dub.internal.configy.Read : parseConfigString; 192 193 immutable string content = `{ 194 "fileVersion": 1, 195 "versions": { 196 "simple": "1.5.6", 197 "branch": "~master", 198 "branch2": "~main", 199 "path": { "path": "../some/where" }, 200 "repository": { "repository": "git+https://github.com/dlang/dub", "version": "123456123456123456" } 201 } 202 }`; 203 204 auto file = parseConfigString!SelectionsFile(content, "/dev/null"); 205 assert(file.fileVersion == 1); 206 auto s = file.content.match!( 207 (Selections!1 s) => s, 208 (s) { assert(0); return Selections!(1).init; }, 209 ); 210 assert(!s.inheritable); 211 assert(s.versions.length == 5); 212 assert(s.versions["simple"] == Dependency(Version("1.5.6"))); 213 assert(s.versions["branch"] == Dependency(Version("~master"))); 214 assert(s.versions["branch2"] == Dependency(Version("~main"))); 215 assert(s.versions["path"] == Dependency(NativePath("../some/where"))); 216 assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456"))); 217 } 218 219 // with optional `inheritable` Boolean 220 unittest 221 { 222 import dub.internal.configy.Read : parseConfigString; 223 224 immutable string content = `{ 225 "fileVersion": 1, 226 "inheritable": true, 227 "versions": { 228 "simple": "1.5.6", 229 } 230 }`; 231 232 auto s = parseConfigString!SelectionsFile(content, "/dev/null"); 233 assert(s.inheritable); 234 } 235 236 // Test reading an unsupported version 237 unittest 238 { 239 import dub.internal.configy.Read : parseConfigString; 240 241 immutable string content = `{"fileVersion": 9999, "thisis": "notrecognized"}`; 242 auto s = parseConfigString!SelectionsFile(content, "/dev/null"); 243 assert(s.fileVersion == 9999); 244 }