Skip to main content

harn_cli/package/lockfile/
materialize.rs

1//! Realizing a lock file on disk: populating `packages/` from the cache,
2//! checking a lock still matches its manifest, and the entry point every
3//! command uses to guarantee dependencies are present.
4
5use crate::package::*;
6
7pub(crate) fn materialize_dependencies_from_lock(
8    workspace: &PackageWorkspace,
9    ctx: &ManifestContext,
10    lock: &LockFile,
11    refetch: Option<&str>,
12    offline: bool,
13) -> Result<usize, PackageError> {
14    publish_package_generation(ctx, lock, refetch.is_some(), |packages_dir| {
15        let mut installed = 0usize;
16        for entry in &lock.packages {
17            let alias = &entry.name;
18            validate_package_alias(alias)?;
19            if entry.source.starts_with("path+") {
20                let source = path_from_source_uri(&entry.source)?;
21                materialize_path_dependency(&source, packages_dir, alias)?;
22                installed += 1;
23                continue;
24            }
25
26            let expected_hash = entry
27                .content_hash
28                .as_deref()
29                .ok_or_else(|| format!("missing content hash for {alias}"))?;
30            let source = entry.source.clone();
31            let refetch_this = refetch == Some("all") || refetch == Some(alias.as_str());
32            let cache_dir = if source.starts_with("git+") {
33                let commit = entry
34                    .commit
35                    .as_deref()
36                    .ok_or_else(|| format!("missing locked commit for {alias}"))?;
37                let url = source.trim_start_matches("git+");
38                ensure_git_cache_populated_in(
39                    workspace,
40                    url,
41                    &source,
42                    commit,
43                    Some(expected_hash),
44                    refetch_this,
45                    offline,
46                )?;
47                git_cache_dir_in(workspace, &source, commit)?
48            } else if source.starts_with("archive+") {
49                let url = archive_url_from_source_uri(&source)?;
50                ensure_archive_cache_populated_in(
51                    workspace,
52                    url,
53                    &source,
54                    expected_hash,
55                    refetch_this,
56                    offline,
57                )?;
58                archive_cache_dir_in(workspace, &source, expected_hash)?
59            } else {
60                return Err(
61                    format!("unsupported locked package source for {alias}: {source}").into(),
62                );
63            };
64            let dest_dir = packages_dir.join(alias);
65            copy_dir_recursive(&cache_dir, &dest_dir)?;
66            write_cached_content_hash(&dest_dir, expected_hash)?;
67            installed += 1;
68        }
69        Ok(installed)
70    })
71}
72
73pub(crate) fn validate_lock_matches_manifest(
74    workspace: &PackageWorkspace,
75    ctx: &ManifestContext,
76    lock: &LockFile,
77) -> Result<(), PackageError> {
78    for (alias, dependency) in &ctx.manifest.dependencies {
79        validate_package_alias(alias)?;
80        let entry = lock.find(alias).ok_or_else(|| {
81            format!(
82                "{} is missing an entry for {alias}",
83                ctx.lock_path().display()
84            )
85        })?;
86        if !compatible_locked_entry(workspace, alias, dependency, entry, &ctx.dir)? {
87            return Err(format!(
88                "{} is out of date for {alias}; run `harn install`",
89                ctx.lock_path().display()
90            )
91            .into());
92        }
93    }
94    Ok(())
95}
96
97pub fn ensure_dependencies_materialized(anchor: &Path) -> Result<(), PackageError> {
98    let Some((manifest, dir)) = load_nearest_manifest(anchor).into_result()? else {
99        return Ok(());
100    };
101    let ctx = ManifestContext { manifest, dir };
102    if ctx.manifest.dependencies.is_empty() {
103        return dependency_package_snapshot(&ctx.manifest, &ctx.dir).map(|_| ());
104    }
105    let lock = LockFile::load(&ctx.lock_path())?.ok_or_else(|| {
106        format!(
107            "{} is missing; run `harn install`",
108            ctx.lock_path().display()
109        )
110    })?;
111    let workspace = PackageWorkspace::from_current_dir()?;
112    validate_lock_matches_manifest(&workspace, &ctx, &lock)?;
113    materialize_dependencies_from_lock(&workspace, &ctx, &lock, None, false)?;
114    Ok(())
115}
116
117pub(super) fn dependency_manifest_item(
118    alias: &str,
119    dependency: &Dependency,
120) -> Result<toml_edit::Item, PackageError> {
121    validate_package_alias(alias)?;
122    let mut fields = toml_edit::InlineTable::new();
123    let table = match dependency {
124        Dependency::Path(path) => {
125            fields.insert("path", path.clone().into());
126            return Ok(toml_edit::Item::Value(fields.into()));
127        }
128        Dependency::Table(table) => table,
129    };
130    for (name, value) in [
131        ("path", table.path.as_deref()),
132        ("git", table.git.as_deref()),
133        ("archive", table.archive.as_deref()),
134    ] {
135        if let Some(value) = value {
136            fields.insert(name, value.into());
137        }
138    }
139    if let Some(branch) = table.branch.as_deref() {
140        fields.insert("branch", branch.into());
141    } else if let Some(tag) = table.tag.as_deref() {
142        fields.insert("tag", tag.into());
143    } else if let Some(rev) = table.rev.as_deref() {
144        fields.insert("rev", rev.into());
145    }
146    for (name, value) in [
147        ("version", table.version.as_deref()),
148        ("package", table.package.as_deref()),
149        ("checksum", table.checksum.as_deref()),
150        ("registry", table.registry.as_deref()),
151        ("registry_name", table.registry_name.as_deref()),
152        ("registry_version", table.registry_version.as_deref()),
153    ] {
154        if let Some(value) = value {
155            fields.insert(name, value.into());
156        }
157    }
158    Ok(toml_edit::Item::Value(fields.into()))
159}