Skip to content

Commit

Permalink
Add lockfile structure
Browse files Browse the repository at this point in the history
commit-id:a905a9c2
  • Loading branch information
MrDenkoV authored and maciektr committed Oct 17, 2023
1 parent f803c6c commit 0ce720f
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
1 change: 1 addition & 0 deletions scarb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
244 changes: 244 additions & 0 deletions scarb/src/core/lockfile.rs
Original file line number Diff line number Diff line change
@@ -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<PackageLock>,
}

#[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<SourceId>,
#[serde(default = "BTreeSet::new")]
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub dependencies: BTreeSet<PackageName>,
}

fn skip_path_source_id(sid: &Option<SourceId>) -> bool {
sid.map(|sid| sid.is_path()).unwrap_or(true)
}

impl Lockfile {
pub fn new(packages: impl IntoIterator<Item = PackageLock>) -> 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<Utf8Path>) -> Result<Self> {
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<Document> {
let doc = toml_edit::ser::to_string_pretty(self)?;
let mut doc = doc.parse::<Document>()?;

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<String> {
Ok(format!("{HEADER}\n{}", self.body()?))
}
}

impl TryFrom<String> for Lockfile {
type Error = anyhow::Error;

fn try_from(value: String) -> Result<Self> {
Ok(toml::from_str(&value)?)
}
}

impl PackageLock {
pub fn new(package: &PackageId, dependencies: impl Iterator<Item = PackageName>) -> Self {
Self {
name: package.name.clone(),
version: package.version.clone(),
source: Some(package.source_id),
dependencies: dependencies.collect(),
}
}
}

impl TryFrom<PackageLock> for PackageId {
type Error = anyhow::Error;

fn try_from(value: PackageLock) -> Result<Self> {
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);
}
}
1 change: 1 addition & 0 deletions scarb/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions scarb/src/core/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = PackageId> + '_ {
self.graph
.neighbors_directed(package_id, petgraph::Direction::Outgoing)
}
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
Expand Down

0 comments on commit 0ce720f

Please sign in to comment.