diff --git a/source/dub/project.d b/source/dub/project.d index b3613e3..a3d6ea4 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -20,6 +20,8 @@ import dub.packagemanager; import dub.recipe.selection; +import configy.Read; + import std.algorithm; import std.array; import std.conv : to; @@ -75,14 +77,11 @@ m_packageManager = package_manager; m_rootPackage = pack; - auto selverfile = m_rootPackage.path ~ SelectedVersions.defaultFile; + auto selverfile = (m_rootPackage.path ~ SelectedVersions.defaultFile).toNativeString(); if (existsFile(selverfile)) { - try m_selections = new SelectedVersions(selverfile); - catch(Exception e) { - logWarn("Failed to load %s: %s", SelectedVersions.defaultFile, e.msg); - logDiagnostic("Full error: %s", e.toString().sanitize); - m_selections = new SelectedVersions; - } + auto selected = parseConfigFileSimple!Selected(selverfile); + enforce(!selected.isNull(), "Could not read '" ~ selverfile ~ "'"); + m_selections = new SelectedVersions(selected.get()); } else m_selections = new SelectedVersions; reinit(); @@ -1681,9 +1680,16 @@ enum defaultFile = "dub.selections.json"; /// Constructs a new empty version selection. - public this(Selected data = Selected(FileVersion)) @safe pure nothrow @nogc + public this(uint version_ = FileVersion) @safe pure nothrow @nogc + { + this.m_selections = Selected(version_); + } + + /// Constructs a new non-empty version selection. + public this(Selected data) @safe pure nothrow @nogc { this.m_selections = data; + this.m_bare = false; } /** Constructs a new version selection from JSON data. @@ -1700,6 +1706,7 @@ /** Constructs a new version selections from an existing JSON file. */ + deprecated("JSON deserialization is deprecated") this(NativePath path) { auto json = jsonFromFile(path); @@ -1838,6 +1845,7 @@ return d.toJson(true); } + deprecated("JSON deserialization is deprecated") static Dependency dependencyFromJson(Json j) { if (j.type == Json.Type.string) @@ -1861,6 +1869,7 @@ return serialized; } + deprecated("JSON deserialization is deprecated") private void deserialize(Json json) { const fileVersion = cast(int)json["fileVersion"]; diff --git a/source/dub/recipe/selection.d b/source/dub/recipe/selection.d index a634a12..097f735 100644 --- a/source/dub/recipe/selection.d +++ b/source/dub/recipe/selection.d @@ -4,6 +4,11 @@ module dub.recipe.selection; import dub.dependency; +import dub.internal.vibecompat.core.file : NativePath; + +import configy.Attributes; + +import std.exception; public struct Selected { @@ -11,5 +16,96 @@ public uint fileVersion; /// The selected package and their matching versions - public Dependency[string] versions; + public YAMLSelectedDependency[string] versions; +} + + +/// Actual representation of a dependency as permitted in `dub.selections.json` +private struct SelectedDependency +{ + @Optional @Name("version") string version_; + @Optional string path; + @Optional string repository; + + public void validate () const scope @safe pure + { + enforce(this.version_.length || this.path.length || this.repository.length, + "Need to provide a version string, or an object with one of the following fields: `version`, `path`, or `repository`"); + enforce(!this.path.length || !this.repository.length, + "Cannot provide a `path` dependency if a repository dependency is used"); + enforce(!this.path.length || !this.version_.length, + "Cannot provide a `path` dependency if a `version` dependency is used"); + enforce(!this.repository.length || this.version_.length, + "Cannot provide a `repository` dependency without a `version`"); + } +} + +/// Wrapper around `SelectedDependency` to do deserialization but still provide +/// a `Dependency` object to client code. +private struct YAMLSelectedDependency +{ + private SelectedDependency representation; + + public Dependency actual; + alias actual this; + + /// Constructor, used in `fromYAML` + public this (inout(Dependency) dep) inout @safe pure nothrow @nogc + { + this.actual = dep; + } + + /// Allow external code to assign to this object as if it was a `Dependency` + public ref YAMLSelectedDependency opAssign (Dependency dep) return pure nothrow @nogc + { + this.actual = dep; + return this; + } + + /// Read a `Dependency` from the config file - Required to support both short and long form + static YAMLSelectedDependency fromYAML (scope ConfigParser!YAMLSelectedDependency p) + { + import dyaml.node; + + if (p.node.nodeID == NodeID.scalar) + return YAMLSelectedDependency(Dependency(Version(p.node.as!string))); + + auto d = p.parseField!"representation"; + if (d.path.length) + return YAMLSelectedDependency(Dependency(NativePath(d.path))); + else + { + assert(d.version_.length); + if (d.repository.length) + return YAMLSelectedDependency(Dependency(Repository(d.repository, d.version_))); + return YAMLSelectedDependency(Dependency(Version(d.version_))); + } + } +} + +// Ensure we can read all type of dependencies +unittest +{ + import configy.Read : parseConfigString; + import dub.internal.vibecompat.core.file : NativePath; + + immutable string content = `{ + "fileVersion": 1, + "versions": { + "simple": "1.5.6", + "branch": "~master", + "branch2": "~main", + "path": { "path": "../some/where" }, + "repository": { "repository": "git+https://github.com/dlang/dub", "version": "123456123456123456" } + } +}`; + + auto s = parseConfigString!Selected(content, "/dev/null"); + assert(s.fileVersion == 1); + assert(s.versions.length == 5); + assert(s.versions["simple"] == Dependency(Version("1.5.6"))); + assert(s.versions["branch"] == Dependency(Version("~master"))); + assert(s.versions["branch2"] == Dependency(Version("~main"))); + assert(s.versions["path"] == Dependency(NativePath("../some/where"))); + assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456"))); }