harn_cli/package/lockfile/
materialize.rs1use 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 workspace = PackageWorkspace::from_current_dir()?;
99 ensure_dependencies_materialized_in(&workspace, anchor)
100}
101
102pub(crate) fn ensure_dependencies_materialized_in(
103 workspace: &PackageWorkspace,
104 anchor: &Path,
105) -> Result<(), PackageError> {
106 let Some((manifest, dir)) = load_nearest_manifest(anchor).into_result()? else {
107 return Ok(());
108 };
109 let ctx = ManifestContext { manifest, dir };
110 if ctx.manifest.dependencies.is_empty() {
111 return dependency_package_snapshot(&ctx.manifest, &ctx.dir).map(|_| ());
112 }
113 let lock = LockFile::load(&ctx.lock_path())?.ok_or_else(|| {
114 format!(
115 "{} is missing; run `harn install`",
116 ctx.lock_path().display()
117 )
118 })?;
119 validate_lock_matches_manifest(workspace, &ctx, &lock)?;
120 let runtime_lock = lock_for_materialization(workspace, &ctx, lock)?;
121 materialize_dependencies_from_lock(workspace, &ctx, &runtime_lock, None, false)?;
122 Ok(())
123}
124
125fn lock_for_materialization(
126 workspace: &PackageWorkspace,
127 ctx: &ManifestContext,
128 lock: LockFile,
129) -> Result<LockFile, PackageError> {
130 if !lock.requires_git_hash_migration() {
131 return Ok(lock);
132 }
133
134 build_lockfile(workspace, ctx, Some(&lock), None, false, true, false)
138}
139
140pub(super) fn dependency_manifest_item(
141 alias: &str,
142 dependency: &Dependency,
143) -> Result<toml_edit::Item, PackageError> {
144 validate_package_alias(alias)?;
145 let mut fields = toml_edit::InlineTable::new();
146 let table = match dependency {
147 Dependency::Path(path) => {
148 fields.insert("path", path.clone().into());
149 return Ok(toml_edit::Item::Value(fields.into()));
150 }
151 Dependency::Table(table) => table,
152 };
153 for (name, value) in [
154 ("path", table.path.as_deref()),
155 ("git", table.git.as_deref()),
156 ("archive", table.archive.as_deref()),
157 ] {
158 if let Some(value) = value {
159 fields.insert(name, value.into());
160 }
161 }
162 if let Some(branch) = table.branch.as_deref() {
163 fields.insert("branch", branch.into());
164 } else if let Some(tag) = table.tag.as_deref() {
165 fields.insert("tag", tag.into());
166 } else if let Some(rev) = table.rev.as_deref() {
167 fields.insert("rev", rev.into());
168 }
169 for (name, value) in [
170 ("version", table.version.as_deref()),
171 ("package", table.package.as_deref()),
172 ("checksum", table.checksum.as_deref()),
173 ("registry", table.registry.as_deref()),
174 ("registry_name", table.registry_name.as_deref()),
175 ("registry_version", table.registry_version.as_deref()),
176 ("registry_commit", table.registry_commit.as_deref()),
177 ("registry_provenance", table.registry_provenance.as_deref()),
178 ] {
179 if let Some(value) = value {
180 fields.insert(name, value.into());
181 }
182 }
183 Ok(toml_edit::Item::Value(fields.into()))
184}