From 2121742c0cf4df01755204b5cbd391ddad6e5edd Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Thu, 14 Nov 2024 22:09:00 +0700 Subject: [PATCH] Move to our own version type --- Cargo.lock | 100 +++++++++++++++++ Cargo.toml | 1 + cli/src/package.rs | 7 +- package/Cargo.toml | 2 + package/src/error.rs | 4 +- package/src/index/mod.rs | 30 +++--- package/src/index/scrape.rs | 4 +- package/src/lib.rs | 119 +++++--------------- package/src/manifest.rs | 20 ++-- package/src/resolve.rs | 112 +++++++++---------- package/src/version.rs | 210 ++++++++++++++++++++++++++++++++++++ 11 files changed, 424 insertions(+), 185 deletions(-) create mode 100644 package/src/version.rs diff --git a/Cargo.lock b/Cargo.lock index 48b05c64a..799facc08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -363,6 +378,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -884,6 +912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -2276,6 +2305,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "home" version = "0.5.9" @@ -2394,6 +2429,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2448,6 +2506,7 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown 0.15.0", + "serde", ] [[package]] @@ -3136,8 +3195,10 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_with", "tempfile", "test-generator", + "thiserror", ] [[package]] @@ -4148,6 +4209,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -5136,6 +5227,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index f2b36fac5..a1bc42e0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ serde = "1.0.164" serde_json = "1.0.96" serde_repr = "0.1" serde-wasm-bindgen = "0.5.0" +serde_with = "3.11.0" serde_yaml = "0.9.19" sha-1 = "0.10.0" sha2 = "0.10.6" diff --git a/cli/src/package.rs b/cli/src/package.rs index 91182eaf5..1518e9c25 100644 --- a/cli/src/package.rs +++ b/cli/src/package.rs @@ -11,7 +11,8 @@ use nickel_lang_core::{ use nickel_lang_package::{ config::Config, index::{self, PackageIndex}, - ManifestFile, SemanticVersion, + version::SemVer, + ManifestFile, }; use crate::{ @@ -46,7 +47,7 @@ pub enum Command { index: PathBuf, #[arg(long)] - version: SemanticVersion, + version: SemVer, #[arg(long)] commit_id: ObjectId, @@ -110,7 +111,7 @@ impl PackageCommand { commit_id, } => { let package = - nickel_lang_package::index::fetch_git(package_id, *version, commit_id)?; + nickel_lang_package::index::fetch_git(package_id, version.clone(), commit_id)?; let config = Config::default().with_index_dir(index.clone()); let mut package_index = PackageIndex::new(config); package_index.save(package)?; diff --git a/package/Cargo.toml b/package/Cargo.toml index f134c3f03..a7a6cb439 100644 --- a/package/Cargo.toml +++ b/package/Cargo.toml @@ -24,7 +24,9 @@ regex.workspace = true semver = { version = "1.0.23", features = ["serde"] } serde.workspace = true serde_json.workspace = true +serde_with.workspace = true tempfile = { workspace = true } +thiserror.workspace = true [dev-dependencies] insta = { workspace = true, features = ["filters"] } diff --git a/package/src/error.rs b/package/src/error.rs index aba95442b..3d5fe2930 100644 --- a/package/src/error.rs +++ b/package/src/error.rs @@ -3,10 +3,10 @@ use std::path::{Path, PathBuf}; use nickel_lang_core::{ eval::cache::CacheImpl, identifier::Ident, package::ObjectId, program::Program, }; -use pubgrub::version::SemanticVersion; use crate::{ index::{self}, + version::SemVer, UnversionedPackage, }; @@ -69,7 +69,7 @@ pub enum Error { /// package and version was already present. DuplicateIndexPackageVersion { id: index::Id, - version: SemanticVersion, + version: SemVer, }, } diff --git a/package/src/index/mod.rs b/package/src/index/mod.rs index b570962ff..1c5c327a7 100644 --- a/package/src/index/mod.rs +++ b/package/src/index/mod.rs @@ -17,7 +17,6 @@ use std::{ use nickel_lang_core::{identifier::Ident, package::ObjectId}; use nickel_lang_git::Spec; -use pubgrub::version::SemanticVersion; use regex::Regex; use serde::{Deserialize, Serialize}; use tempfile::{tempdir_in, NamedTempFile}; @@ -25,6 +24,7 @@ use tempfile::{tempdir_in, NamedTempFile}; use crate::{ config::Config, error::{Error, IoResultExt as _}, + version::SemVer, Precise, VersionReq, }; @@ -79,7 +79,11 @@ impl PackageCache { let data = std::fs::read_to_string(&path).with_path(&path)?; for line in data.lines() { let package: Package = serde_json::from_str(line).unwrap(); - if file.packages.insert(package.vers, package).is_some() { + if file + .packages + .insert(package.vers.clone(), package) + .is_some() + { panic!("duplicate version, index is corrupt"); } } @@ -97,12 +101,12 @@ impl PackageCache { /// (Also retains a cached copy in memory.) pub fn save(&mut self, pkg: Package) -> Result<(), Error> { let id: Id = pkg.id.clone().into(); - let version = pkg.vers; + let version = pkg.vers.clone(); let mut existing = self .load(&id)? .cloned() .unwrap_or(CachedPackageFile::default()); - if existing.packages.insert(pkg.vers, pkg).is_some() { + if existing.packages.insert(pkg.vers.clone(), pkg).is_some() { return Err(Error::DuplicateIndexPackageVersion { id, version }); } let mut tmp = self.tmp_file(&id); @@ -172,7 +176,7 @@ impl PackageIndex { pub fn available_versions<'a>( &'a self, id: &Id, - ) -> Result + 'a, Error> { + ) -> Result + 'a, Error> { let mut cache = self.cache.borrow_mut(); let pkg_file = cache.load(id)?; let versions: Vec<_> = pkg_file @@ -181,7 +185,7 @@ impl PackageIndex { Ok(versions.into_iter()) } - pub fn all_versions(&self, id: &Id) -> Result, Error> { + pub fn all_versions(&self, id: &Id) -> Result, Error> { let mut cache = self.cache.borrow_mut(); let pkg_file = cache.load(id)?; Ok(pkg_file @@ -189,13 +193,13 @@ impl PackageIndex { pkg_file .packages .iter() - .map(|(v, package)| (*v, package.clone())) + .map(|(v, package)| (v.clone(), package.clone())) .collect() }) .unwrap_or_default()) } - pub fn package(&self, id: &Id, v: SemanticVersion) -> Result, Error> { + pub fn package(&self, id: &Id, v: SemVer) -> Result, Error> { Ok(self.all_versions(id)?.get(&v).cloned()) } @@ -203,9 +207,9 @@ impl PackageIndex { self.cache.borrow_mut().save(pkg) } - pub fn ensure_downloaded(&self, id: &Id, v: SemanticVersion) -> Result<(), Error> { + pub fn ensure_downloaded(&self, id: &Id, v: SemVer) -> Result<(), Error> { let package = self - .package(id, v)? + .package(id, v.clone())? .ok_or_else(|| Error::UnknownIndexPackage { id: id.clone() })?; let precise = Precise::Index { id: id.clone(), @@ -371,15 +375,15 @@ impl From for Id { #[derive(Clone, Debug, Default)] pub struct CachedPackageFile { - pub packages: BTreeMap, + pub packages: BTreeMap, } /// A package record in the index. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Package { pub id: PreciseId, - pub vers: SemanticVersion, - pub nickel_vers: SemanticVersion, + pub vers: SemVer, + pub nickel_vers: SemVer, pub deps: BTreeMap, /// Version of the index schema. Currently always zero. diff --git a/package/src/index/scrape.rs b/package/src/index/scrape.rs index c60abe873..de936a491 100644 --- a/package/src/index/scrape.rs +++ b/package/src/index/scrape.rs @@ -6,11 +6,11 @@ use nickel_lang_core::package::ObjectId; use nickel_lang_git::Spec; -use pubgrub::version::SemanticVersion; use tempfile::tempdir; use crate::{ error::{Error, IoResultExt}, + version::SemVer, ManifestFile, }; @@ -19,7 +19,7 @@ use super::{Id, Package, PreciseId}; /// Fetch a package from the specified place, and figure out what its index /// entry should look like. /// TODO: allow a subdirectory? -pub fn fetch_git(id: &Id, version: SemanticVersion, commit: &ObjectId) -> Result { +pub fn fetch_git(id: &Id, version: SemVer, commit: &ObjectId) -> Result { // We need to fetch the manifest file to get some metadata out. We're currently shallow-cloning // the whole repo, but we could use a github API (or maybe some fancier git features) to be more // efficient. diff --git a/package/src/lib.rs b/package/src/lib.rs index 13f742d20..090b1288c 100644 --- a/package/src/lib.rs +++ b/package/src/lib.rs @@ -9,6 +9,7 @@ pub mod index; pub mod lock; pub mod manifest; pub mod resolve; +pub mod version; use config::Config; use error::Error; @@ -16,7 +17,8 @@ pub use manifest::ManifestFile; use nickel_lang_core::{cache::normalize_abs_path, package::ObjectId}; use serde::{Deserialize, Serialize}; -pub use pubgrub::version::SemanticVersion; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use version::{PartialSemVer, PartialSemVerParseError, SemVer, SemVerParseError}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] pub struct GitDependency { @@ -31,60 +33,12 @@ pub struct GitDependency { pub path: PathBuf, } -/// Some places where we ask for semantic versions also allow for "underspecified" -/// versions, like "1" or "1.0" in place of "1.0.0". -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct PartialSemanticVersion { - pub major: u32, - pub minor: Option, - pub patch: Option, -} - -impl From for SemanticVersion { - fn from(v: PartialSemanticVersion) -> Self { - SemanticVersion::new(v.major, v.minor.unwrap_or(0), v.patch.unwrap_or(0)) - } -} - -impl FromStr for PartialSemanticVersion { - // TODO: better error - type Err = pubgrub::version::VersionParseError; - - fn from_str(s: &str) -> Result { - let parse_u32 = |part: &str| { - part.parse::().map_err(|e| Self::Err::ParseIntError { - full_version: s.to_string(), - version_part: part.to_string(), - parse_error: e.to_string(), - }) - }; - - let mut parts = s.split('.'); - let major = parse_u32(parts.next().ok_or_else(|| Self::Err::NotThreeParts { - full_version: s.to_string(), - })?)?; - let minor = parts.next().map(parse_u32).transpose()?; - let patch = parts.next().map(parse_u32).transpose()?; - if parts.next().is_some() { - return Err(Self::Err::NotThreeParts { - full_version: s.to_string(), - }); - } - - Ok(PartialSemanticVersion { - major, - minor, - patch, - }) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)] pub enum VersionReq { - // TODO: could make this a PartialSemanticVersion - Compatible(SemanticVersion), + // TODO: could make this a PartialSemVer + Compatible(SemVer), // TODO: This one could allow pre-releases - Exact(SemanticVersion), + Exact(SemVer), } impl std::fmt::Display for VersionReq { @@ -96,53 +50,31 @@ impl std::fmt::Display for VersionReq { } } +#[derive(Debug, thiserror::Error)] +pub enum VersionReqParseError { + #[error(transparent)] + Exact(#[from] SemVerParseError), + #[error(transparent)] + Compatible(#[from] PartialSemVerParseError), +} + impl FromStr for VersionReq { - type Err = pubgrub::version::VersionParseError; + type Err = VersionReqParseError; fn from_str(s: &str) -> Result { if let Some(v) = s.strip_prefix('=') { Ok(VersionReq::Exact(v.parse()?)) } else { - Ok(VersionReq::Compatible( - PartialSemanticVersion::from_str(s)?.into(), - )) + Ok(VersionReq::Compatible(PartialSemVer::from_str(s)?.into())) } } } -impl serde::Serialize for VersionReq { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.to_string().serialize(serializer) - } -} - -impl<'de> serde::Deserialize<'de> for VersionReq { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(serde::de::Error::custom) - } -} - -fn next_incompatible(v: &SemanticVersion) -> SemanticVersion { - let (major, _minor, _patch) = (*v).into(); - if major == 0 { - v.bump_minor() - } else { - v.bump_major() - } -} - impl VersionReq { - pub fn matches(&self, v: &SemanticVersion) -> bool { + pub fn matches(&self, v: &SemVer) -> bool { match self { VersionReq::Compatible(lower_bound) => { - lower_bound <= v && *v < next_incompatible(lower_bound) + lower_bound <= v && *v < lower_bound.next_incompatible() } VersionReq::Exact(w) => v == w, } @@ -226,7 +158,7 @@ mod serde_url { #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub struct IndexPrecise { id: index::Id, - version: SemanticVersion, + version: SemVer, } /// A precise package version, in a format suitable for putting into a lockfile. @@ -248,11 +180,12 @@ pub enum Precise { /// /// Note that when normalizing we only look at the path and not at the actual filesystem. /// TODO: maybe just leave out the path altogether? cargo does... - Path { path: PathBuf }, + Path { + path: PathBuf, + }, Index { - // TODO: IndexPrecise id: index::Id, - version: SemanticVersion, + version: SemVer, }, } @@ -303,9 +236,9 @@ impl Precise { } } - pub fn version(&self) -> Option { + pub fn version(&self) -> Option { match self { - Precise::Index { version, .. } => Some(*version), + Precise::Index { version, .. } => Some(version.clone()), _ => None, } } diff --git a/package/src/manifest.rs b/package/src/manifest.rs index 08ed91068..dbacf8a4b 100644 --- a/package/src/manifest.rs +++ b/package/src/manifest.rs @@ -13,7 +13,6 @@ use nickel_lang_core::{ term::{make, RichTerm, RuntimeContract, Term}, }; use nickel_lang_git::Spec; -use pubgrub::version::SemanticVersion; use serde::Deserialize; use crate::{ @@ -23,6 +22,7 @@ use crate::{ lock::LockFile, repo_root, resolve::{Resolution, UnversionedPrecise}, + version::{FullSemVer, PartialSemVer, SemVer}, Dependency, GitDependency, Precise, VersionReq, }; @@ -40,8 +40,8 @@ use crate::{ #[derive(Clone, Debug, Deserialize)] struct ManifestFileFormat { name: Ident, - version: SemanticVersion, - nickel_version: SemanticVersion, + version: FullSemVer, + nickel_version: PartialSemVer, dependencies: HashMap, } @@ -89,9 +89,9 @@ pub struct ManifestFile { /// The name of the package. pub name: Ident, /// The version of the package. - pub version: SemanticVersion, + pub version: SemVer, /// The minimum nickel version supported by the package. - pub nickel_version: SemanticVersion, + pub nickel_version: SemVer, /// All the package's dependencies, and the local names that this package will use to refer to them. pub dependencies: HashMap, } @@ -220,8 +220,8 @@ impl ManifestFile { Ok(Self { parent_dir: None, name, - version, - nickel_version, + version: version.into(), + nickel_version: nickel_version.into(), dependencies: dependencies .into_iter() .map(|(k, v)| (k, v.into())) @@ -378,7 +378,7 @@ mod tests { #[test] fn manifest() { let manifest = ManifestFile::from_contents( - r#"{name = "foo", version = "1.0.0", nickel_version = "1.8.0", authors = [], description = "hi"}"#.as_bytes(), + r#"{name = "foo", version = "1.0.0", nickel_version = "1.9.0", authors = [], description = "hi"}"#.as_bytes(), ) .unwrap(); assert_eq!( @@ -386,8 +386,8 @@ mod tests { ManifestFile { parent_dir: None, name: "foo".into(), - version: SemanticVersion::new(1, 0, 0), - nickel_version: SemanticVersion::new(1, 8, 0), + version: SemVer::new(1, 0, 0), + nickel_version: SemVer::new(1, 9, 0), dependencies: HashMap::default() } ) diff --git a/package/src/resolve.rs b/package/src/resolve.rs index a76a322aa..7237eb067 100644 --- a/package/src/resolve.rs +++ b/package/src/resolve.rs @@ -16,7 +16,6 @@ use nickel_lang_core::{cache::normalize_path, identifier::Ident, package::Packag use pubgrub::{ report::{DefaultStringReporter, Reporter as _}, solver::DependencyProvider, - version::SemanticVersion, }; use crate::{ @@ -25,17 +24,18 @@ use crate::{ index::{self, Id, IndexDependency, PackageIndex}, lock::LockFile, manifest::Realization, + version::SemVer, Dependency, IndexPrecise, ManifestFile, ObjectId, Precise, VersionReq, }; -type VersionRange = pubgrub::range::Range; +type VersionRange = pubgrub::range::Range; pub struct PackageRegistry { // The packages whose versions were locked in a lockfile; we'll try to prefer using // those same versions. We won't absolutely insist on it, because if the manifest // changed (or some path-dependency changed) then the old locked versions might not // resolve anymore. - previously_locked: HashMap, + previously_locked: HashMap, index: PackageIndex, realized_unversioned: Realization, } @@ -44,34 +44,30 @@ impl PackageRegistry { pub fn list_versions<'a>( &'a self, package: &Package, - ) -> Result + 'a, Error> { + ) -> Result + 'a, Error> { let locked_version = self.previously_locked.get(package).cloned(); let rest = match package { Package::Unversioned(_) => { - Box::new(std::iter::once(SemanticVersion::zero())) as Box> + Box::new(std::iter::once(SemVer::new(0, 0, 0))) as Box> } Package::Bucket(b) => { let bucket_version = b.version; let iter = self .index .available_versions(&b.id)? - .filter(move |v| bucket_version.contains(*v)); + .filter(move |v| bucket_version.contains(v.clone())); Box::new(iter) } }; // Put the locked version first, and then the other versions in any order (filtering to ensure that the locked version isn't repeated). Ok(locked_version + .clone() .into_iter() .chain(rest.filter(move |v| Some(v) != locked_version.as_ref()))) } - pub fn dep( - &self, - pkg: &Package, - version: &SemanticVersion, - dep_id: &Id, - ) -> Result { + pub fn dep(&self, pkg: &Package, version: &SemVer, dep_id: &Id) -> Result { let deps = match pkg { Package::Unversioned(pkg) => self.unversioned_deps(pkg), Package::Bucket(b) => self.index_deps(&b.id, version)?, @@ -91,7 +87,7 @@ impl PackageRegistry { manifest.dependencies.values().cloned().collect() } - pub fn index_deps(&self, id: &Id, version: &SemanticVersion) -> Result, Error> { + pub fn index_deps(&self, id: &Id, version: &SemVer) -> Result, Error> { let all_versions = self.index.all_versions(id)?; let pkg = all_versions.get(version).unwrap(); Ok(pkg @@ -110,10 +106,10 @@ impl PackageRegistry { pub enum BucketVersion { /// A collection of versions all having the same major version number. /// (For example, 1.x.y) - Major(u32), + Major(u64), /// A collection of versions all having major version zero, and the same minor version number. /// (For example, 0.2.x) - Minor(u32), + Minor(u64), } impl std::fmt::Display for BucketVersion { @@ -126,11 +122,10 @@ impl std::fmt::Display for BucketVersion { } impl BucketVersion { - pub fn contains(&self, semver: SemanticVersion) -> bool { - let (major, minor, _) = semver.into(); + pub fn contains(&self, semver: SemVer) -> bool { match *self { - BucketVersion::Major(v) => v == major, - BucketVersion::Minor(v) => major == 0 && minor == v, + BucketVersion::Major(v) => v == semver.major, + BucketVersion::Minor(v) => semver.major == 0 && semver.minor == v, } } @@ -142,29 +137,25 @@ impl BucketVersion { } pub fn compatible_range(&self) -> VersionRange { - VersionRange::between( - SemanticVersion::from(*self), - SemanticVersion::from(self.next()), - ) + VersionRange::between(SemVer::from(*self), SemVer::from(self.next())) } } -impl From for BucketVersion { - fn from(v: SemanticVersion) -> Self { - let (major, minor, _) = v.into(); - if major == 0 { - BucketVersion::Minor(minor) +impl From for BucketVersion { + fn from(v: SemVer) -> Self { + if v.major == 0 { + BucketVersion::Minor(v.minor) } else { - BucketVersion::Major(major) + BucketVersion::Major(v.major) } } } -impl From for SemanticVersion { +impl From for SemVer { fn from(bv: BucketVersion) -> Self { match bv { - BucketVersion::Major(v) => SemanticVersion::new(v, 0, 0), - BucketVersion::Minor(v) => SemanticVersion::new(0, v, 0), + BucketVersion::Major(v) => SemVer::new(v, 0, 0), + BucketVersion::Minor(v) => SemVer::new(0, v, 0), } } } @@ -261,14 +252,11 @@ impl From for Package { } } -impl DependencyProvider for PackageRegistry { - fn choose_package_version< - T: Borrow, - U: Borrow>, - >( +impl DependencyProvider for PackageRegistry { + fn choose_package_version, U: Borrow>>( &self, potential_packages: impl Iterator, - ) -> Result<(T, Option), Box> { + ) -> Result<(T, Option), Box> { // We try to choose the package with the fewest available versions, as the pubgrub // docs recommend this as a reasonably-performant heuristic. We count a previously locked package // as having one version (even if we'd theoretically be willing to ignore the lock). @@ -299,9 +287,8 @@ impl DependencyProvider for PackageRegistry { fn get_dependencies( &self, package: &Package, - version: &SemanticVersion, - ) -> Result, Box> - { + version: &SemVer, + ) -> Result, Box> { match package { Package::Unversioned(p) => { let precise = Precise::from(p.clone()); @@ -338,7 +325,7 @@ impl DependencyProvider for PackageRegistry { #[derive(Debug)] pub struct Resolution { pub realization: Realization, - pub index_packages: HashMap>, + pub index_packages: HashMap>, pub index: PackageIndex, } @@ -346,18 +333,18 @@ pub fn resolve(manifest: &ManifestFile, config: Config) -> Result HashMap { +fn previously_locked(_top_level: &Package, lock: &LockFile) -> HashMap { fn precise_to_index(p: &Precise) -> Option { match p { Precise::Index { id, version } => Some(IndexPrecise { id: id.clone(), - version: *version, + version: version.clone(), }), _ => None, } } - // A list of (package: Package, version of the package: SemanticVersion, dependency: IndexPrecise) + // A list of (package: Package, version of the package: SemVer, dependency: IndexPrecise) let pkg_deps = lock .dependencies .values() @@ -370,7 +357,7 @@ fn previously_locked(_top_level: &Package, lock: &LockFile) -> HashMap r, - Err(pubgrub::error::PubGrubError::NoSolution(derivation_tree)) => { - //derivation_tree.collapse_no_versions(); - let msg = DefaultStringReporter::report(&derivation_tree); - return Err(Error::Resolution { msg }); - } - Err(e) => return Err(Error::Resolution { msg: e.to_string() }), - }; - let mut selected = HashMap::>::new(); + let resolution = match pubgrub::solver::resolve(®istry, top_level_pkg, SemVer::new(0, 0, 0)) + { + Ok(r) => r, + Err(pubgrub::error::PubGrubError::NoSolution(derivation_tree)) => { + //derivation_tree.collapse_no_versions(); + let msg = DefaultStringReporter::report(&derivation_tree); + return Err(Error::Resolution { msg }); + } + Err(e) => return Err(Error::Resolution { msg: e.to_string() }), + }; + let mut selected = HashMap::>::new(); for (pkg, vers) in resolution.iter() { if let Package::Bucket(Bucket { id, .. }) = pkg { - selected.entry(id.clone()).or_default().push(*vers); + selected.entry(id.clone()).or_default().push(vers.clone()); } } Ok(Resolution { @@ -456,11 +443,12 @@ impl Resolution { }, Dependency::Index { id, version } => Precise::Index { id: id.clone(), - version: *self.index_packages[id] + version: self.index_packages[id] .iter() .filter(|v| version.matches(v)) .max() - .unwrap(), + .unwrap() + .clone(), }, } } @@ -486,7 +474,7 @@ impl Resolution { .collect() } Precise::Index { id, version } => { - let index_pkg = self.index.package(id, *version)?.unwrap(); + let index_pkg = self.index.package(id, version.clone())?.unwrap(); index_pkg .deps .into_iter() @@ -517,7 +505,7 @@ impl Resolution { let index_precises = self.index_packages.iter().flat_map(|(id, vs)| { vs.iter().map(|v| Precise::Index { id: id.clone(), - version: *v, + version: v.clone(), }) }); ret.extend(index_precises); diff --git a/package/src/version.rs b/package/src/version.rs new file mode 100644 index 000000000..2550a5f1a --- /dev/null +++ b/package/src/version.rs @@ -0,0 +1,210 @@ +//! This module contains everything to do with version numbers. + +use std::{num::ParseIntError, str::FromStr}; + +use semver::{BuildMetadata, Prerelease}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +/// A full semantic version, including prerelease and build metadata. +pub type FullSemVer = semver::Version; + +/// Our most-widely-used version type. +/// +/// This drops the build metadata part (which we allow during parsing but +/// ignore for all version-resolution purposes). +/// +/// Possible optimizations: +/// - shrink the numbers to `u32` +/// - intern the prerelease tag. This needs to be done in a way that preserves +/// the ordering rules, which are rather more complicated than a string comparison. +#[derive( + Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, DeserializeFromStr, SerializeDisplay, +)] +pub struct SemVer { + pub major: u64, + pub minor: u64, + pub patch: u64, + pub pre: Prerelease, +} + +impl SemVer { + pub fn new(major: u64, minor: u64, patch: u64) -> Self { + Self { + major, + minor, + patch, + pre: Prerelease::EMPTY, + } + } + + pub fn bump_major(&self) -> SemVer { + SemVer { + major: self.major + 1, + minor: 0, + patch: 0, + pre: Prerelease::EMPTY, + } + } + + pub fn bump_minor(&self) -> SemVer { + SemVer { + major: self.major, + minor: self.minor + 1, + patch: 0, + pre: Prerelease::EMPTY, + } + } + + pub fn next_incompatible(&self) -> SemVer { + // TODO: should we panic or something if pre is non-empty? + if self.major == 0 { + self.bump_minor() + } else { + self.bump_major() + } + } +} + +impl From for SemVer { + fn from(fsv: FullSemVer) -> Self { + Self { + major: fsv.major, + minor: fsv.minor, + patch: fsv.patch, + pre: fsv.pre, + } + } +} + +impl From for FullSemVer { + fn from(sv: SemVer) -> Self { + Self { + major: sv.major, + minor: sv.minor, + patch: sv.patch, + pre: sv.pre, + build: BuildMetadata::EMPTY, + } + } +} + +// This conversion loses information on which of the fields were present. This +// information is sometimes relevant for comparing version requirements (e.g., +// "1.3.0" matches the requirement "1.2" but it doesn't match the requirement +// "1.2.0"). +impl From for SemVer { + fn from(psv: PartialSemVer) -> Self { + Self { + major: psv.major, + minor: psv.minor.unwrap_or(0), + patch: psv.patch.unwrap_or(0), + pre: Prerelease::EMPTY, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SemVerParseError { + #[error("build metadata is not allowed in this semver")] + Metadata, + #[error(transparent)] + Inner(#[from] semver::Error), +} + +impl FromStr for SemVer { + type Err = SemVerParseError; + + fn from_str(s: &str) -> Result { + let full = FullSemVer::from_str(s)?; + if !full.build.is_empty() { + Err(SemVerParseError::Metadata) + } else { + Ok(full.into()) + } + } +} + +impl std::fmt::Display for SemVer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + FullSemVer::from(self.clone()).fmt(f) + } +} + +/// A partial semantic version, with no pre-release part, and optional minor and patch versions. +#[derive( + Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, DeserializeFromStr, SerializeDisplay, +)] +pub struct PartialSemVer { + pub major: u64, + pub minor: Option, + pub patch: Option, +} + +impl PartialSemVer { + pub fn major_minor(major: u64, minor: u64) -> Self { + Self { + major, + minor: Some(minor), + patch: None, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PartialSemVerParseError { + #[error("empty string")] + Empty, + #[error("a semantic version can contain at most 2 dots")] + TooManyDots, + #[error("invalid number: `{0}`")] + Num(#[from] ParseIntError), +} + +impl FromStr for PartialSemVer { + type Err = PartialSemVerParseError; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('.'); + let major = parts + .next() + .ok_or(PartialSemVerParseError::Empty)? + .parse()?; + let minor = parts.next().map(u64::from_str).transpose()?; + let patch = parts.next().map(u64::from_str).transpose()?; + if parts.next().is_some() { + return Err(PartialSemVerParseError::TooManyDots); + } + + Ok(Self { + major, + minor, + patch, + }) + } +} + +impl std::fmt::Display for PartialSemVer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (self.minor, self.patch) { + (None, _) => { + write!(f, "{}", self.major) + } + (Some(minor), None) => { + write!(f, "{}.{}", self.major, minor) + } + (Some(minor), Some(patch)) => { + write!(f, "{}.{}.{}", self.major, minor, patch) + } + } + } +} + +impl pubgrub::version::Version for SemVer { + fn lowest() -> Self { + Self::new(0, 0, 0) + } + + fn bump(&self) -> Self { + Self::new(self.major, self.minor, self.patch + 1) + } +}