harn_cli/package/lockfile/
install.rs1use crate::package::*;
5
6pub(crate) fn install_packages_impl(
7 frozen: bool,
8 refetch: Option<&str>,
9 offline: bool,
10) -> Result<usize, PackageError> {
11 install_packages_in(
12 &PackageWorkspace::from_current_dir()?,
13 frozen,
14 refetch,
15 offline,
16 )
17}
18
19pub(crate) fn install_packages_in_locked(
20 workspace: &PackageWorkspace,
21 frozen: bool,
22 refetch: Option<&str>,
23 offline: bool,
24) -> Result<usize, PackageError> {
25 let ctx = workspace.load_manifest_context()?;
26 let existing = LockFile::load(&ctx.lock_path())?;
27 if ctx.manifest.dependencies.is_empty() {
28 let empty = LockFile::default();
29 if frozen || offline {
30 if existing
34 .as_ref()
35 .is_some_and(|lock| !lock.packages.is_empty())
36 {
37 return Err(format!("{} would need to change", ctx.lock_path().display()).into());
38 }
39 } else {
40 empty.save(&ctx.lock_path())?;
41 }
42 return materialize_dependencies_from_lock(workspace, &ctx, &empty, refetch, offline);
43 }
44
45 if (frozen || offline) && existing.is_none() {
46 return Err(format!("{} is missing", ctx.lock_path().display()).into());
47 }
48 if (frozen || offline)
49 && existing
50 .as_ref()
51 .is_some_and(LockFile::requires_git_hash_migration)
52 {
53 return Err(format!(
54 "{} contains pre-v5 Git content hashes; run `harn install` and commit the migrated lockfile before using --locked or --offline",
55 ctx.lock_path().display()
56 )
57 .into());
58 }
59
60 let desired = build_lockfile(
61 workspace,
62 &ctx,
63 existing.as_ref(),
64 None,
65 false,
66 !frozen && !offline,
67 offline,
68 )?;
69 if frozen || offline {
70 if !existing
71 .as_ref()
72 .is_some_and(|lock| lock.same_resolution(&desired))
73 {
74 return Err(format!("{} would need to change", ctx.lock_path().display()).into());
75 }
76 } else {
77 desired.save(&ctx.lock_path())?;
78 }
79 materialize_dependencies_from_lock(workspace, &ctx, &desired, refetch, offline)
80}
81
82pub fn install_packages(frozen: bool, refetch: Option<&str>, offline: bool, json: bool) {
83 match install_packages_impl(frozen, refetch, offline) {
84 Ok(installed) if json => {
85 print_install_summary_json("install", installed, frozen, offline);
86 }
87 Ok(0) => println!("No dependencies to install."),
88 Ok(installed) => {
89 println!("Installed {installed} package(s) in a new immutable generation.");
90 }
91 Err(error) if json => {
92 print_install_error_json("install", &error);
93 process::exit(1);
94 }
95 Err(error) => {
96 eprintln!("error: {error}");
97 process::exit(1);
98 }
99 }
100}
101
102fn print_install_summary_json(action: &str, installed: usize, frozen: bool, offline: bool) {
103 let body = serde_json::json!({
104 "action": action,
105 "ok": true,
106 "installed": installed,
107 "frozen": frozen,
108 "offline": offline,
109 "lock_file": LOCK_FILE,
110 "package_pointer": ".harn/package-current.toml",
111 });
112 println!(
113 "{}",
114 serde_json::to_string_pretty(&body).unwrap_or_default()
115 );
116}
117
118fn print_install_error_json(action: &str, error: &PackageError) {
119 let body = serde_json::json!({
120 "action": action,
121 "ok": false,
122 "error": error.to_string(),
123 });
124 println!(
125 "{}",
126 serde_json::to_string_pretty(&body).unwrap_or_default()
127 );
128}
129pub fn lock_packages() {
130 let result = (|| -> Result<usize, PackageError> {
131 let workspace = PackageWorkspace::from_current_dir()?;
132 let _mutation_lock = acquire_package_mutation_lock(&workspace)?;
133 let ctx = workspace.load_manifest_context()?;
134 let existing = LockFile::load(&ctx.lock_path())?;
135 let lock = build_lockfile(&workspace, &ctx, existing.as_ref(), None, true, true, false)?;
136 lock.save(&ctx.lock_path())?;
137 Ok(lock.packages.len())
138 })();
139
140 match result {
141 Ok(count) => println!("Wrote {LOCK_FILE} with {count} package(s)."),
142 Err(error) => {
143 eprintln!("error: {error}");
144 process::exit(1);
145 }
146 }
147}
148pub fn update_packages(alias: Option<&str>, all: bool, json: bool) {
149 let result = PackageWorkspace::from_current_dir()
150 .and_then(|workspace| update_packages_in(&workspace, alias, all));
151 print_update_packages_result(result, json);
152}
153
154pub(crate) fn update_packages_in(
155 workspace: &PackageWorkspace,
156 alias: Option<&str>,
157 all: bool,
158) -> Result<usize, PackageError> {
159 let _mutation_lock = acquire_package_mutation_lock(workspace)?;
160 if !all && alias.is_none() {
161 return Err("specify a dependency alias or pass --all"
162 .to_string()
163 .into());
164 }
165
166 let ctx = workspace.load_manifest_context()?;
167 if let Some(alias) = alias {
168 validate_package_alias(alias)?;
169 if !ctx.manifest.dependencies.contains_key(alias) {
170 return Err(format!("{alias} is not present in [dependencies]").into());
171 }
172 }
173 let existing = LockFile::load(&ctx.lock_path())?;
174 let lock = build_lockfile(workspace, &ctx, existing.as_ref(), alias, all, true, false)?;
175 lock.save(&ctx.lock_path())?;
176 materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)
177}
178
179fn print_update_packages_result(result: Result<usize, PackageError>, json: bool) {
180 match result {
181 Ok(installed) if json => print_install_summary_json("update", installed, false, false),
182 Ok(installed) => println!("Updated {installed} package(s)."),
183 Err(error) if json => {
184 print_install_error_json("update", &error);
185 process::exit(1);
186 }
187 Err(error) => {
188 eprintln!("error: {error}");
189 process::exit(1);
190 }
191 }
192}
193pub fn remove_package(alias: &str) {
194 let result = PackageWorkspace::from_current_dir()
195 .and_then(|workspace| remove_package_in(&workspace, alias));
196 print_remove_package_result(alias, result);
197}
198
199pub(crate) fn remove_package_in(
200 workspace: &PackageWorkspace,
201 alias: &str,
202) -> Result<bool, PackageError> {
203 let _mutation_lock = acquire_package_mutation_lock(workspace)?;
204 validate_package_alias(alias)?;
205 let ctx = workspace.load_manifest_context()?;
206 let removed = remove_dependency_from_manifest(&ctx.manifest_path(), alias)?;
207 if !removed {
208 return Ok(false);
209 }
210 let mut lock = LockFile::load(&ctx.lock_path())?.unwrap_or_default();
211 lock.remove(alias);
212 lock.save(&ctx.lock_path())?;
213 materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)?;
214 Ok(true)
215}
216
217fn print_remove_package_result(alias: &str, result: Result<bool, PackageError>) {
218 match result {
219 Ok(true) => println!("Removed {alias} from {MANIFEST} and {LOCK_FILE}."),
220 Ok(false) => {
221 eprintln!("error: {alias} is not present in [dependencies]");
222 process::exit(1);
223 }
224 Err(error) => {
225 eprintln!("error: {error}");
226 process::exit(1);
227 }
228 }
229}