diff --git a/source/dub/project.d b/source/dub/project.d index 0a50673..482cbaa 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -104,6 +104,9 @@ */ static package SelectedVersions loadSelections(in Package pack) { + import dub.version_; + import dub.internal.dyaml.stdsumtype; + auto selverfile = (pack.path ~ SelectedVersions.defaultFile).toNativeString(); // No file exists @@ -113,13 +116,23 @@ // TODO: Remove `StrictMode.Warn` after v1.40 release // The default is to error, but as the previous parser wasn't // complaining, we should first warn the user. - auto selected = parseConfigFileSimple!Selected(selverfile, StrictMode.Warn); + auto selected = parseConfigFileSimple!SelectionsFile(selverfile, StrictMode.Warn); // Parsing error, it will be displayed to the user if (selected.isNull()) return new SelectedVersions(); - return new SelectedVersions(selected.get()); + return selected.get().content.match!( + (Selections!0 s) { + logWarnTag("Unsupported version", + "File %s has fileVersion %s, which is not yet supported by DUB %s.", + selverfile, s.fileVersion, dubVersion); + logWarn("Ignoring selections file. Use a newer DUB version " ~ + "and set the appropriate toolchainRequirements in your recipe file"); + return new SelectedVersions(); + }, + (Selections!1 s) => new SelectedVersions(s), + ); } /** List of all resolved dependencies. @@ -1757,7 +1770,7 @@ public class SelectedVersions { protected { enum FileVersion = 1; - Selected m_selections; + Selections!1 m_selections; bool m_dirty = false; // has changes since last save bool m_bare = true; } @@ -1766,13 +1779,14 @@ enum defaultFile = "dub.selections.json"; /// Constructs a new empty version selection. - public this(uint version_ = FileVersion) @safe pure nothrow @nogc + public this(uint version_ = FileVersion) @safe pure { - this.m_selections = Selected(version_); + enforce(version_ == 1, "Unsupported file version"); + this.m_selections = Selections!1(version_); } /// Constructs a new non-empty version selection. - public this(Selected data) @safe pure nothrow @nogc + public this(Selections!1 data) @safe pure nothrow @nogc { this.m_selections = data; this.m_bare = false; diff --git a/source/dub/recipe/selection.d b/source/dub/recipe/selection.d index 1e127e3..11758e8 100644 --- a/source/dub/recipe/selection.d +++ b/source/dub/recipe/selection.d @@ -1,5 +1,10 @@ /** - * Contains type definition for `dub.selections.json` + * Contains type definition for the selections file + * + * The selections file, commonly known by its file name + * `dub.selections.json`, is used by Dub to store resolved + * dependencies. Its purpose is identical to other package + * managers' lock file. */ module dub.recipe.selection; @@ -7,16 +12,99 @@ import dub.internal.vibecompat.inet.path : NativePath; import dub.internal.configy.Attributes; +import dub.internal.dyaml.stdsumtype; import std.exception; -public struct Selected -{ - /// The current version of the file format - public uint fileVersion; +deprecated("Use either `Selections!1` or `SelectionsFile` instead") +public alias Selected = Selections!1; - /// The selected package and their matching versions - public SelectedDependency[string] versions; +/** + * Top level type for `dub.selections.json` + * + * To support multiple version, we expose a `SumType` which + * contains the "real" version being parsed. + */ +public struct SelectionsFile +{ + /// Private alias to avoid repetition + private alias DataType = SumType!(Selections!0, Selections!1); + + /** + * Get the `fileVersion` of this selection file + * + * The `fileVersion` is always present, no matter the version. + * This is a convenience function that matches any version and allows + * one to retrieve it. + * + * Note that the `fileVersion` can be an unsupported version. + */ + public uint fileVersion () const @safe pure nothrow @nogc + { + return this.content.match!((s) => s.fileVersion); + } + + /** + * The content of this selections file + * + * The underlying content can be accessed using + * `dub.internal.yaml.stdsumtype : match`, for example: + * --- + * SelectionsFile file = readSelectionsFile(); + * file.content.match!( + * (Selections!0 s) => logWarn("Unsupported version: %s", s.fileVersion), + * (Selections!1 s) => logWarn("Old version (1), please upgrade!"), + * (Selections!2 s) => logInfo("You are up to date"), + * ); + * --- + */ + public DataType content; + + /** + * Deserialize the selections file according to its version + * + * This will first deserialize the `fileVersion` only, and then + * the expected version if it is supported. Unsupported versions + * will be returned inside a `Selections!0` struct, + * which only contains a `fileVersion`. + */ + public static SelectionsFile fromYAML (scope ConfigParser!SelectionsFile parser) + { + import dub.internal.configy.Read; + + static struct OnlyVersion { uint fileVersion; } + + auto vers = parseConfig!OnlyVersion( + CLIArgs.init, parser.node, StrictMode.Ignore); + + switch (vers.fileVersion) { + case 1: + return SelectionsFile(DataType(parser.parseAs!(Selections!1))); + default: + return SelectionsFile(DataType(Selections!0(vers.fileVersion))); + } + } +} + +/** + * A specific version of the selections file + * + * Currently, only two instantiations of this struct are possible: + * - `Selections!0` is an invalid/unsupported version; + * - `Selections!1` is the most widespread version; + */ +public struct Selections (ushort Version) +{ + /// + public uint fileVersion = Version; + + static if (Version == 0) { /* Invalid version */ } + else static if (Version == 1) { + /// The selected package and their matching versions + public SelectedDependency[string] versions; + } + else + static assert(false, "This version is not supported"); } @@ -97,8 +185,12 @@ } }`; - auto s = parseConfigString!Selected(content, "/dev/null"); - assert(s.fileVersion == 1); + auto file = parseConfigString!SelectionsFile(content, "/dev/null"); + assert(file.fileVersion == 1); + auto s = file.content.match!( + (Selections!1 s) => s, + (s) { assert(0); return Selections!(1).init; }, + ); assert(s.versions.length == 5); assert(s.versions["simple"] == Dependency(Version("1.5.6"))); assert(s.versions["branch"] == Dependency(Version("~master"))); @@ -106,3 +198,13 @@ assert(s.versions["path"] == Dependency(NativePath("../some/where"))); assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456"))); } + +// Test reading an unsupported version +unittest +{ + import dub.internal.configy.Read : parseConfigString; + + immutable string content = `{"fileVersion": 9999, "thisis": "notrecognized"}`; + auto s = parseConfigString!SelectionsFile(content, "/dev/null"); + assert(s.fileVersion == 9999); +} diff --git a/source/dub/test/base.d b/source/dub/test/base.d index 15e1d06..7c06b01 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -242,13 +242,13 @@ import dub.recipe.selection; /// Forward to parent's constructor - public this(uint version_ = FileVersion) @safe pure nothrow @nogc + public this(uint version_ = FileVersion) @safe pure { super(version_); } /// Ditto - public this(Selected data) @safe pure nothrow @nogc + public this(Selections!1 data) @safe pure nothrow @nogc { super(data); }