From 412ce90e7f925f5979012c2d1ab8f58dcd71ef39 Mon Sep 17 00:00:00 2001 From: daimond113 Date: Sun, 9 Mar 2025 18:57:20 +0100 Subject: [PATCH] feat: add proper versioning to lockfiles Lockfiles now store a format field which is used to migrate the them easily without brute forcing the version it's at. --- src/lib.rs | 23 ++--- src/lockfile.rs | 201 ++++++++++++++++++++++++------------------ src/source/git/mod.rs | 13 +-- 3 files changed, 132 insertions(+), 105 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a8ccc6b..098bd16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -232,18 +232,7 @@ impl Project { #[instrument(skip(self), ret(level = "trace"), level = "debug")] pub async fn deser_lockfile(&self) -> Result { let string = fs::read_to_string(self.package_dir().join(LOCKFILE_FILE_NAME)).await?; - Ok(match toml::from_str(&string) { - Ok(lockfile) => lockfile, - Err(e) => { - #[allow(deprecated)] - let Ok(old_lockfile) = toml::from_str::(&string) else { - return Err(errors::LockfileReadError::Serde(e)); - }; - - #[allow(deprecated)] - old_lockfile.to_new() - } - }) + lockfile::parse_lockfile(&string).map_err(Into::into) } /// Write the lockfile @@ -256,7 +245,9 @@ impl Project { let lockfile = format!( r"# This file is automatically @generated by pesde. # It is not intended for manual editing. -{lockfile}" +format = {} +{lockfile}", + lockfile::CURRENT_FORMAT ); fs::write(self.package_dir().join(LOCKFILE_FILE_NAME), lockfile).await?; @@ -511,9 +502,9 @@ pub mod errors { #[error("io error reading lockfile")] Io(#[from] std::io::Error), - /// An error occurred while deserializing the lockfile - #[error("error deserializing lockfile")] - Serde(#[from] toml::de::Error), + /// An error occurred while parsing the lockfile + #[error("error parsing lockfile")] + Parse(#[from] crate::lockfile::errors::ParseLockfileError), } /// Errors that can occur when writing the lockfile diff --git a/src/lockfile.rs b/src/lockfile.rs index 1f30f20..653f1fa 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -3,12 +3,15 @@ use crate::{ graph::DependencyGraph, manifest::{overrides::OverrideKey, target::TargetKind}, names::PackageName, - source::specifiers::DependencySpecifiers, + source::{ids::PackageId, specifiers::DependencySpecifiers}, }; use relative_path::RelativePathBuf; use semver::Version; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt::Debug}; + +/// The current format of the lockfile +pub const CURRENT_FORMAT: usize = 1; /// A lockfile #[derive(Serialize, Deserialize, Debug, Clone)] @@ -32,91 +35,34 @@ pub struct Lockfile { pub graph: DependencyGraph, } -/// Old lockfile stuff. Will be removed in a future version. -#[deprecated( - note = "Intended to be used to migrate old lockfiles to the new format. Will be removed in a future version." -)] -pub mod old { - use crate::{ - manifest::{ - overrides::OverrideKey, - target::{Target, TargetKind}, - Alias, DependencyType, - }, - names::{PackageName, PackageNames}, - source::{ - ids::{PackageId, VersionId}, - refs::PackageRefs, - specifiers::DependencySpecifiers, - }, - }; - use relative_path::RelativePathBuf; - use semver::Version; - use serde::{Deserialize, Serialize}; - use std::collections::BTreeMap; - - /// An old dependency graph node - #[derive(Serialize, Deserialize, Debug, Clone)] - pub struct DependencyGraphNodeOld { - /// The alias, specifier, and original (as in the manifest) type for the dependency, if it is a direct dependency (i.e. used by the current project) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub direct: Option<(Alias, DependencySpecifiers, DependencyType)>, - /// The dependencies of the package - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, - /// The resolved (transformed, for example Peer -> Standard) type of the dependency - pub resolved_ty: DependencyType, - /// Whether the resolved type should be Peer if this isn't depended on - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub is_peer: bool, - /// The package reference - pub pkg_ref: PackageRefs, +/// Parses the lockfile, updating it to the [`CURRENT_FORMAT`] from the format it's at +pub fn parse_lockfile(lockfile: &str) -> Result { + #[derive(Serialize, Deserialize, Debug)] + pub struct LockfileFormat { + #[serde(default)] + pub format: usize, } - /// A downloaded dependency graph node, i.e. a `DependencyGraphNode` with a `Target` - #[derive(Serialize, Deserialize, Debug, Clone)] - pub struct DownloadedDependencyGraphNodeOld { - /// The target of the package - pub target: Target, - /// The node - #[serde(flatten)] - pub node: DependencyGraphNodeOld, - } + let format: LockfileFormat = toml::de::from_str(lockfile)?; + let format = format.format; - /// An old version of a lockfile - #[derive(Serialize, Deserialize, Debug, Clone)] - pub struct LockfileOld { - /// The name of the package - pub name: PackageName, - /// The version of the package - pub version: Version, - /// The target of the package - pub target: TargetKind, - /// The overrides of the package - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub overrides: BTreeMap, + match format { + 0 => { + let this = match toml::from_str(lockfile) { + Ok(lockfile) => return Ok(lockfile), + Err(e) => match toml::from_str::(lockfile) { + Ok(this) => this, + Err(_) => return Err(errors::ParseLockfileError::De(e)), + }, + }; - /// The workspace members - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub workspace: BTreeMap>, - - /// The graph of dependencies - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub graph: BTreeMap>, - } - - impl LockfileOld { - /// Converts this lockfile to a new lockfile - #[must_use] - #[allow(clippy::wrong_self_convention)] - pub fn to_new(self) -> super::Lockfile { - super::Lockfile { - name: self.name, - version: self.version, - target: self.target, - overrides: self.overrides, - workspace: self.workspace, - graph: self + Ok(Lockfile { + name: this.name, + version: this.version, + target: this.target, + overrides: this.overrides, + workspace: this.workspace, + graph: this .graph .into_iter() .flat_map(|(name, versions)| { @@ -141,7 +87,94 @@ pub mod old { }) }) .collect(), - } + }) } + CURRENT_FORMAT => toml::de::from_str(lockfile).map_err(Into::into), + format => Err(errors::ParseLockfileError::TooNew(format)), + } +} + +/// Lockfile v0 +pub mod v0 { + use crate::{ + manifest::{ + overrides::OverrideKey, + target::{Target, TargetKind}, + Alias, DependencyType, + }, + names::{PackageName, PackageNames}, + source::{ids::VersionId, refs::PackageRefs, specifiers::DependencySpecifiers}, + }; + use relative_path::RelativePathBuf; + use semver::Version; + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + + /// A dependency graph node + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct DependencyGraphNode { + /// The alias, specifier, and original (as in the manifest) type for the dependency, if it is a direct dependency (i.e. used by the current project) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub direct: Option<(Alias, DependencySpecifiers, DependencyType)>, + /// The dependencies of the package + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, + /// The resolved (transformed, for example Peer -> Standard) type of the dependency + pub resolved_ty: DependencyType, + /// Whether the resolved type should be Peer if this isn't depended on + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub is_peer: bool, + /// The package reference + pub pkg_ref: PackageRefs, + } + + /// A downloaded dependency graph node, i.e. a `DependencyGraphNode` with a `Target` + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct DownloadedDependencyGraphNode { + /// The target of the package + pub target: Target, + /// The node + #[serde(flatten)] + pub node: DependencyGraphNode, + } + + /// A lockfile + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct Lockfile { + /// The name of the package + pub name: PackageName, + /// The version of the package + pub version: Version, + /// The target of the package + pub target: TargetKind, + /// The overrides of the package + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub overrides: BTreeMap, + + /// The workspace members + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub workspace: BTreeMap>, + + /// The graph of dependencies + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub graph: BTreeMap>, + } +} + +/// Errors that can occur when working with lockfiles +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when parsing a lockfile + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ParseLockfileError { + /// The lockfile format is too new + #[error("lockfile format {} is too new. newest supported format: {}", .0, super::CURRENT_FORMAT)] + TooNew(usize), + + /// Deserializing the lockfile failed + #[error("deserializing the lockfile failed")] + De(#[from] toml::de::Error), } } diff --git a/src/source/git/mod.rs b/src/source/git/mod.rs index 315a3f7..fd91a1b 100644 --- a/src/source/git/mod.rs +++ b/src/source/git/mod.rs @@ -111,10 +111,10 @@ fn transform_pesde_dependencies( })?; let lockfile = match lockfile { - Some(l) => match toml::from_str::(&l) { + Some(l) => match crate::lockfile::parse_lockfile(&l) { Ok(l) => l, Err(e) => { - return Err(errors::ResolveError::DeserLockfile( + return Err(errors::ResolveError::ParseLockfile( Box::new(repo_url.clone()), e, )) @@ -603,9 +603,12 @@ pub mod errors { #[source] crate::source::git_index::errors::ReadFile, ), - /// An error occurred while deserializing the lockfile - #[error("error deserializing lockfile for repository {0}")] - DeserLockfile(Box, #[source] toml::de::Error), + /// An error occurred while parsing the lockfile + #[error("error parsing lockfile for repository {0}")] + ParseLockfile( + Box, + #[source] crate::lockfile::errors::ParseLockfileError, + ), /// The repository is missing a lockfile #[error("no lockfile found in repository {0}")]