From c2651903c9625dd070ffed3edcafe8aa49e03336 Mon Sep 17 00:00:00 2001 From: maciektr Date: Tue, 17 Oct 2023 16:41:47 +0200 Subject: [PATCH] Add lockfile structure (#777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit-id:a905a9c2 --- **Stack**: - #781 - #800 - #780 - #779 - #778 - #777 ⬅ ⚠️ *Part of a stack created by [spr](https://github.com/ejoffe/spr). Do not merge manually using the UI - doing so may have unexpected results.* Co-authored-by: MrDenkoV --- Cargo.lock | 1 + Cargo.toml | 1 + scarb/Cargo.toml | 1 + scarb/src/core/lockfile.rs | 244 +++++++++++++++++++++++++++++++++++++ scarb/src/core/mod.rs | 1 + scarb/src/core/resolver.rs | 9 ++ 6 files changed, 257 insertions(+) create mode 100644 scarb/src/core/lockfile.rs diff --git a/Cargo.lock b/Cargo.lock index da039ab8a..41356ca74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3980,6 +3980,7 @@ dependencies = [ "serde-untagged", "serde-value", "serde_json", + "serde_repr", "serde_test", "sha2", "similar-asserts", diff --git a/Cargo.toml b/Cargo.toml index 5458012b4..e4c2d71ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ serde = { version = "1", features = ["serde_derive"] } serde-untagged = "0.1" serde-value = "0.7" serde_json = "1" +serde_repr = "0.1" serde_test = "1" sha2 = "0.10" similar-asserts = { version = "1", features = ["serde"] } diff --git a/scarb/Cargo.toml b/scarb/Cargo.toml index 127bb4dc7..03413fbc7 100644 --- a/scarb/Cargo.toml +++ b/scarb/Cargo.toml @@ -55,6 +55,7 @@ serde-value.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true +serde_repr.workspace = true smallvec.workspace = true smol_str.workspace = true tar.workspace = true diff --git a/scarb/src/core/lockfile.rs b/scarb/src/core/lockfile.rs new file mode 100644 index 000000000..fcf94d524 --- /dev/null +++ b/scarb/src/core/lockfile.rs @@ -0,0 +1,244 @@ +#![allow(dead_code)] + +use anyhow::{anyhow, Context, Result}; +use camino::Utf8Path; +use semver::Version; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::collections::BTreeSet; +use toml_edit::Document; + +use crate::core::{PackageId, PackageName, Resolve, SourceId}; +use crate::internal::fsx; + +const HEADER: &str = "# Code generated by scarb DO NOT EDIT."; + +#[derive( + Default, PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord, Serialize_repr, Deserialize_repr, +)] +#[repr(u8)] +pub enum LockVersion { + #[default] + V1 = 1, +} + +#[derive(Debug, Eq, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Lockfile { + pub version: LockVersion, + #[serde(rename = "package")] + #[serde(default = "BTreeSet::new")] + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + pub packages: BTreeSet, +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PackageLock { + pub name: PackageName, + pub version: Version, + #[serde(skip_serializing_if = "skip_path_source_id")] + pub source: Option, + #[serde(default = "BTreeSet::new")] + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + pub dependencies: BTreeSet, +} + +fn skip_path_source_id(sid: &Option) -> bool { + sid.map(|sid| sid.is_path()).unwrap_or(true) +} + +impl Lockfile { + pub fn new(packages: impl IntoIterator) -> Self { + Self { + version: Default::default(), + packages: packages.into_iter().collect(), + } + } + + pub fn from_resolve(resolve: &Resolve) -> Self { + let packages = resolve.package_ids().map(|package| { + let deps = resolve + .package_dependencies(package) + .map(|dep| dep.name.clone()); + PackageLock::new(&package, deps) + }); + Self::new(packages) + } + + pub fn from_path(path: impl AsRef) -> Result { + if path.as_ref().is_file() { + let content = fsx::read_to_string(path.as_ref()) + .with_context(|| format!("Failed to read lockfile at {}", path.as_ref()))?; + if content.is_empty() { + Ok(Self::default()) + } else { + content + .try_into() + .with_context(|| format!("Failed to parse lockfile at {}", path.as_ref())) + } + } else { + Ok(Self::default()) + } + } + + fn body(&self) -> Result { + let doc = toml_edit::ser::to_string_pretty(self)?; + let mut doc = doc.parse::()?; + + for packages in doc["package"].as_array_of_tables_mut().iter_mut() { + for pkg in packages.iter_mut() { + if let Some(deps) = pkg.get_mut("dependencies") { + if let Some(deps) = deps.as_array_mut() { + deps.iter_mut().for_each(|dep| { + dep.decor_mut().set_prefix("\n "); + }); + if deps.len() > 1 { + deps.set_trailing("\n"); + } else { + deps.set_trailing(",\n"); + } + } + } + } + } + + Ok(doc) + } + + pub fn render(&self) -> Result { + Ok(format!("{HEADER}\n{}", self.body()?)) + } +} + +impl TryFrom for Lockfile { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + Ok(toml::from_str(&value)?) + } +} + +impl PackageLock { + pub fn new(package: &PackageId, dependencies: impl Iterator) -> Self { + Self { + name: package.name.clone(), + version: package.version.clone(), + source: Some(package.source_id), + dependencies: dependencies.collect(), + } + } +} + +impl TryFrom for PackageId { + type Error = anyhow::Error; + + fn try_from(value: PackageLock) -> Result { + let source_id = value.source.ok_or_else(|| { + anyhow!( + "missing source id in package lock for package {}", + value.name + ) + })?; + Ok(Self::new(value.name, value.version, source_id)) + } +} + +#[cfg(test)] +mod tests { + use crate::core::lockfile::{Lockfile, PackageLock}; + use crate::core::{PackageId, PackageName, SourceId}; + + use core::default::Default; + use indoc::indoc; + use semver::Version; + use snapbox::assert_eq; + + #[test] + fn simple() { + let pkg1 = PackageLock::new( + &PackageId::new( + PackageName::CORE, + Version::parse("1.0.0").unwrap(), + Default::default(), + ), + vec![PackageName::STARKNET, PackageName::new("locker")].into_iter(), + ); + + let pkg2 = PackageLock { + name: PackageName::STARKNET, + version: Version::parse("1.0.0").unwrap(), + source: None, + dependencies: vec![PackageName::CORE].into_iter().collect(), + }; + + let pkg3 = PackageLock::new( + &PackageId::new( + PackageName::new("third"), + Version::parse("2.1.0").unwrap(), + SourceId::mock_git(), + ), + vec![].into_iter(), + ); + + let pkg4 = PackageLock::new( + &PackageId::new( + PackageName::new("fourth"), + Version::parse("80.0.85").unwrap(), + SourceId::for_std(), + ), + vec![].into_iter(), + ); + + let lock = Lockfile::new(vec![pkg1, pkg2, pkg3, pkg4]); + + let serialized = indoc! {r#" + # Code generated by scarb DO NOT EDIT. + version = 1 + + [[package]] + name = "core" + version = "1.0.0" + source = "registry+https://there-is-no-default-registry-yet.com/" + dependencies = [ + "locker", + "starknet", + ] + + [[package]] + name = "fourth" + version = "80.0.85" + source = "std" + + [[package]] + name = "starknet" + version = "1.0.0" + dependencies = [ + "core", + ] + + [[package]] + name = "third" + version = "2.1.0" + source = "git+https://github.com/starkware-libs/cairo.git?tag=test" + "#}; + + assert_eq(serialized, lock.render().unwrap()); + let deserialized: Lockfile = serialized.to_string().try_into().unwrap(); + assert_eq!(lock, deserialized); + } + + #[test] + fn empty() { + let lock = Lockfile { + version: Default::default(), + packages: Default::default(), + }; + + let serialized = "# Code generated by scarb DO NOT EDIT.\nversion = 1\n"; + assert_eq!(serialized, lock.render().unwrap()); + + let deserialized: Lockfile = serialized.to_string().try_into().unwrap(); + assert_eq!(lock, deserialized); + } +} diff --git a/scarb/src/core/mod.rs b/scarb/src/core/mod.rs index fdd8da220..8a30d3eb2 100644 --- a/scarb/src/core/mod.rs +++ b/scarb/src/core/mod.rs @@ -15,6 +15,7 @@ mod checksum; pub(crate) mod config; mod dirs; pub mod errors; +pub(crate) mod lockfile; pub(crate) mod manifest; pub(crate) mod package; pub(crate) mod publishing; diff --git a/scarb/src/core/resolver.rs b/scarb/src/core/resolver.rs index 3e16bd03e..508880702 100644 --- a/scarb/src/core/resolver.rs +++ b/scarb/src/core/resolver.rs @@ -43,6 +43,15 @@ impl Resolve { .unique() .collect_vec() } + + /// Collect [`PackageId`]s of all directed dependencies of the package. + pub fn package_dependencies( + &self, + package_id: PackageId, + ) -> impl Iterator + '_ { + self.graph + .neighbors_directed(package_id, petgraph::Direction::Outgoing) + } } #[derive(Debug, Default, Clone, PartialEq, Eq)]