1use crate::lock::{LockCollector, write_lockfile};
2use crate::report::Report;
3use flate2::read::GzDecoder;
4use home::home_dir;
5use nodejs_semver::{Range, Version};
6use reqwest::blocking::Client;
7use serde::Deserialize;
8use serde_json::Value;
9use ssri::Integrity;
10use std::collections::{HashMap, HashSet};
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14use tar::Archive;
15use tempfile::tempdir;
16
17#[derive(Debug, Clone, Deserialize)]
18pub struct Manifest {
19 pub name: String,
20 pub version: String,
21 #[serde(default)]
22 pub dependencies: Option<HashMap<String, String>>,
23 #[serde(default, rename = "devDependencies")]
24 pub dev_dependencies: Option<HashMap<String, String>>,
25 #[serde(default, rename = "optionalDependencies")]
26 pub optional_dependencies: Option<HashMap<String, String>>,
27 #[serde(default, rename = "peerDependencies")]
28 pub peer_dependencies: Option<HashMap<String, String>>,
29}
30
31pub fn read_manifest(path: &str) -> io::Result<Manifest> {
32 let manifest_path = find_manifest_path(Path::new(path))?;
33 read_manifest_from_path(&manifest_path)
34}
35
36fn read_manifest_from_path(path: &Path) -> io::Result<Manifest> {
37 let source = fs::read_to_string(path)?;
38 serde_json::from_str(&source).map_err(|source| {
39 io::Error::new(
40 io::ErrorKind::InvalidData,
41 format!("failed to parse {}: {source}", path.display()),
42 )
43 })
44}
45
46fn find_manifest_path(start: &Path) -> io::Result<PathBuf> {
47 let mut current = if start.is_dir() {
48 start.to_path_buf()
49 } else {
50 start
51 .parent()
52 .map(Path::to_path_buf)
53 .unwrap_or_else(|| PathBuf::from("."))
54 };
55
56 loop {
57 let candidate = current.join("package.json");
58 if candidate.is_file() {
59 return Ok(candidate);
60 }
61
62 if !current.pop() {
63 return Err(io::Error::new(
64 io::ErrorKind::NotFound,
65 format!("failed to find package.json from {}", start.display()),
66 ));
67 }
68 }
69}
70
71#[derive(Debug, Deserialize)]
72struct Packument {
73 versions: HashMap<String, RegistryVersion>,
74}
75
76#[derive(Debug, Deserialize)]
77struct RegistryVersion {
78 #[serde(default)]
79 dependencies: HashMap<String, String>,
80 #[serde(default)]
81 optional_dependencies: HashMap<String, String>,
82 #[serde(default)]
83 peer_dependencies: HashMap<String, String>,
84 dist: RegistryDist,
85}
86
87#[derive(Debug, Deserialize)]
88struct RegistryDist {
89 tarball: String,
90 integrity: Option<String>,
91}
92
93#[derive(Debug)]
94struct ResolvedPackage {
95 name: String,
96 version: String,
97 dependencies: HashMap<String, String>,
98 optional_dependencies: HashMap<String, String>,
99 peer_dependencies: HashMap<String, String>,
100 tarball_url: String,
101 integrity: Option<String>,
102}
103
104#[derive(Debug, Clone, Copy)]
105enum DependencyKind {
106 Prod,
107 Dev,
108 Optional,
109 Peer,
110}
111
112#[derive(Debug, Default)]
113struct InstallSummary {
114 prod_installed: usize,
115 dev_installed: usize,
116 optional_installed: usize,
117 peer_installed: usize,
118 warnings: Vec<String>,
119}
120
121impl InstallSummary {
122 fn record_install(&mut self, kind: DependencyKind) {
123 match kind {
124 DependencyKind::Prod => self.prod_installed += 1,
125 DependencyKind::Dev => self.dev_installed += 1,
126 DependencyKind::Optional => self.optional_installed += 1,
127 DependencyKind::Peer => self.peer_installed += 1,
128 }
129 }
130
131 fn warn(&mut self, warning: impl Into<String>) {
132 self.warnings.push(warning.into());
133 }
134}
135
136#[derive(Debug)]
137pub enum PmError {
138 HomeDirUnavailable,
139 MissingGlobalPackageSpec,
140 InvalidPackageSpec {
141 spec: String,
142 },
143 PackageNotInstalled {
144 name: String,
145 path: PathBuf,
146 },
147 FindManifest {
148 start: PathBuf,
149 source: io::Error,
150 },
151 ReadManifest {
152 path: PathBuf,
153 source: io::Error,
154 },
155 ParseManifest {
156 path: PathBuf,
157 source: serde_json::Error,
158 },
159 ProjectRootMissing {
160 path: PathBuf,
161 },
162 CreateDir {
163 path: PathBuf,
164 source: io::Error,
165 },
166 FetchMetadata {
167 package: String,
168 source: reqwest::Error,
169 },
170 MetadataStatus {
171 package: String,
172 source: reqwest::Error,
173 },
174 ReadMetadataBody {
175 package: String,
176 source: reqwest::Error,
177 },
178 ParseMetadata {
179 package: String,
180 source: serde_json::Error,
181 },
182 InvalidRange {
183 package: String,
184 range: String,
185 source: String,
186 },
187 VersionNotFound {
188 package: String,
189 range: String,
190 },
191 MissingResolvedVersion {
192 package: String,
193 version: String,
194 },
195 DownloadTarball {
196 package: String,
197 source: reqwest::Error,
198 },
199 TarballStatus {
200 package: String,
201 source: reqwest::Error,
202 },
203 ReadTarballBody {
204 package: String,
205 source: reqwest::Error,
206 },
207 InvalidIntegrity {
208 package: String,
209 version: String,
210 source: String,
211 },
212 IntegrityMismatch {
213 package: String,
214 version: String,
215 source: String,
216 },
217 ExtractTarball {
218 package: String,
219 source: io::Error,
220 },
221 MissingPackageDir {
222 package: String,
223 path: PathBuf,
224 },
225 RemoveExistingInstall {
226 path: PathBuf,
227 source: io::Error,
228 },
229 CopyInstall {
230 from: PathBuf,
231 to: PathBuf,
232 source: io::Error,
233 },
234 ReadInstalledManifest {
235 path: PathBuf,
236 source: io::Error,
237 },
238 MissingInstalledName {
239 path: PathBuf,
240 },
241 InvalidBinField {
242 path: PathBuf,
243 },
244 InvalidBinEntry {
245 path: PathBuf,
246 entry: String,
247 },
248 AmbiguousBinEntry {
249 package: String,
250 path: PathBuf,
251 available: Vec<String>,
252 },
253 MissingBinTarget {
254 package_dir: PathBuf,
255 target: PathBuf,
256 },
257 CreateTempDir {
258 source: io::Error,
259 },
260 CurrentDir {
261 source: io::Error,
262 },
263 WriteGeneratedManifest {
264 path: PathBuf,
265 source: io::Error,
266 },
267 WriteProcessOutput {
268 source: io::Error,
269 },
270 MissingPackageBinary {
271 package: String,
272 command: String,
273 path: PathBuf,
274 },
275 SpawnPackageBinary {
276 package: String,
277 command: PathBuf,
278 source: io::Error,
279 },
280 PackageBinaryFailed {
281 package: String,
282 command: PathBuf,
283 status: String,
284 stderr: Option<String>,
285 },
286 CreateBinLink {
287 command: String,
288 path: PathBuf,
289 source: io::Error,
290 },
291 RemoveBinLink {
292 command: String,
293 path: PathBuf,
294 source: io::Error,
295 },
296 RemoveInstalledPackage {
297 path: PathBuf,
298 source: io::Error,
299 },
300 ReadLockfile {
301 path: PathBuf,
302 source: io::Error,
303 },
304 ParseLockfile {
305 path: PathBuf,
306 source: serde_json::Error,
307 },
308 WriteLockfile {
309 path: PathBuf,
310 source: io::Error,
311 },
312 InvalidTempPath {
313 path: PathBuf,
314 },
315}
316
317impl PmError {
318 pub fn report(&self) -> Report {
319 match self {
320 Self::HomeDirUnavailable => Report::new("could not resolve home directory")
321 .detail("`$HOME` is unavailable in the current environment"),
322 Self::MissingGlobalPackageSpec => Report::new("global install requires a package name")
323 .detail("example: `o- install --global cowsay`"),
324 Self::InvalidPackageSpec { spec } => Report::new("failed to parse package spec")
325 .detail(format!("spec: {spec}"))
326 .detail("expected `name`, `name@version`, `@scope/name`, or `@scope/name@version`"),
327 Self::PackageNotInstalled { name, path } => {
328 Report::new(format!("package `{name}` is not installed"))
329 .detail(format!("path: {}", path.display()))
330 }
331 Self::FindManifest { start, source } => Report::new("failed to find package.json")
332 .detail(format!("start: {}", start.display()))
333 .detail(format!("cause: {source}")),
334 Self::ReadManifest { path, source } => Report::new("failed to read package.json")
335 .detail(format!("path: {}", path.display()))
336 .detail(format!("cause: {source}")),
337 Self::ParseManifest { path, source } => Report::new("failed to parse package.json")
338 .detail(format!("path: {}", path.display()))
339 .detail(format!("cause: {source}")),
340 Self::ProjectRootMissing { path } => Report::new("failed to resolve project root")
341 .detail(format!("path: {}", path.display())),
342 Self::CreateDir { path, source } => Report::new("failed to create directory")
343 .detail(format!("path: {}", path.display()))
344 .detail(format!("cause: {source}")),
345 Self::FetchMetadata { package, source } => {
346 Report::new(format!("failed to fetch package metadata for `{package}`"))
347 .detail(format!("cause: {source}"))
348 }
349 Self::MetadataStatus { package, source } => {
350 Report::new(format!("registry returned an error for `{package}`"))
351 .detail(format!("cause: {source}"))
352 }
353 Self::ReadMetadataBody { package, source } => Report::new(format!(
354 "failed to read package metadata body for `{package}`"
355 ))
356 .detail(format!("cause: {source}")),
357 Self::ParseMetadata { package, source } => {
358 Report::new(format!("failed to decode package metadata for `{package}`"))
359 .detail(format!("cause: {source}"))
360 }
361 Self::InvalidRange {
362 package,
363 range,
364 source,
365 } => Report::new(format!("invalid semver range for `{package}`"))
366 .detail(format!("range: {range}"))
367 .detail(format!("cause: {source}")),
368 Self::VersionNotFound { package, range } => Report::new(format!(
369 "no version of `{package}` satisfies the requested range"
370 ))
371 .detail(format!("range: {range}")),
372 Self::MissingResolvedVersion { package, version } => {
373 Report::new(format!("registry metadata is incomplete for `{package}`"))
374 .detail(format!("version: {version}"))
375 }
376 Self::DownloadTarball { package, source } => {
377 Report::new(format!("failed to download tarball for `{package}`"))
378 .detail(format!("cause: {source}"))
379 }
380 Self::TarballStatus { package, source } => {
381 Report::new(format!("tarball request failed for `{package}`"))
382 .detail(format!("cause: {source}"))
383 }
384 Self::ReadTarballBody { package, source } => {
385 Report::new(format!("failed to read tarball body for `{package}`"))
386 .detail(format!("cause: {source}"))
387 }
388 Self::InvalidIntegrity {
389 package,
390 version,
391 source,
392 } => Report::new(format!("registry integrity is invalid for `{package}`"))
393 .detail(format!("version: {version}"))
394 .detail(format!("cause: {source}")),
395 Self::IntegrityMismatch {
396 package,
397 version,
398 source,
399 } => Report::new(format!("integrity check failed for `{package}`"))
400 .detail(format!("version: {version}"))
401 .detail(format!("cause: {source}")),
402 Self::ExtractTarball { package, source } => {
403 Report::new(format!("failed to extract tarball for `{package}`"))
404 .detail(format!("cause: {source}"))
405 }
406 Self::MissingPackageDir { package, path } => {
407 Report::new(format!("downloaded tarball for `{package}` is malformed"))
408 .detail(format!("missing: {}", path.display()))
409 }
410 Self::RemoveExistingInstall { path, source } => {
411 Report::new("failed to remove existing package installation")
412 .detail(format!("path: {}", path.display()))
413 .detail(format!("cause: {source}"))
414 }
415 Self::CopyInstall { from, to, source } => Report::new("failed to copy package files")
416 .detail(format!("from: {}", from.display()))
417 .detail(format!("to: {}", to.display()))
418 .detail(format!("cause: {source}")),
419 Self::ReadInstalledManifest { path, source } => {
420 Report::new("failed to read installed package manifest")
421 .detail(format!("path: {}", path.display()))
422 .detail(format!("cause: {source}"))
423 }
424 Self::MissingInstalledName { path } => {
425 Report::new("installed package manifest is missing `name`")
426 .detail(format!("path: {}", path.display()))
427 }
428 Self::InvalidBinField { path } => {
429 Report::new("installed package has an invalid `bin` field")
430 .detail(format!("path: {}", path.display()))
431 }
432 Self::InvalidBinEntry { path, entry } => {
433 Report::new("installed package has an invalid `bin` entry")
434 .detail(format!("path: {}", path.display()))
435 .detail(format!("entry: {entry}"))
436 }
437 Self::AmbiguousBinEntry {
438 package,
439 path,
440 available,
441 } => Report::new(format!("package `{package}` exposes multiple bin commands"))
442 .detail(format!("path: {}", path.display()))
443 .detail(format!("available: {}", available.join(", "))),
444 Self::MissingBinTarget {
445 package_dir,
446 target,
447 } => Report::new("installed package bin target does not exist")
448 .detail(format!("package: {}", package_dir.display()))
449 .detail(format!("target: {}", target.display())),
450 Self::CreateTempDir { source } => Report::new("failed to create temporary directory")
451 .detail(format!("cause: {source}")),
452 Self::CurrentDir { source } => Report::new("failed to resolve current directory")
453 .detail(format!("cause: {source}")),
454 Self::WriteGeneratedManifest { path, source } => {
455 Report::new("failed to write generated package.json")
456 .detail(format!("path: {}", path.display()))
457 .detail(format!("cause: {source}"))
458 }
459 Self::WriteProcessOutput { source } => {
460 Report::new("failed to write package output").detail(format!("cause: {source}"))
461 }
462 Self::MissingPackageBinary {
463 package,
464 command,
465 path,
466 } => Report::new(format!(
467 "package `{package}` does not expose a runnable binary"
468 ))
469 .detail(format!("command: {command}"))
470 .detail(format!("expected shim: {}", path.display())),
471 Self::SpawnPackageBinary {
472 package,
473 command,
474 source,
475 } => Report::new(format!("failed to execute package binary for `{package}`"))
476 .detail(format!("command: {}", command.display()))
477 .detail(format!("cause: {source}")),
478 Self::PackageBinaryFailed {
479 package,
480 command,
481 status,
482 stderr,
483 } => {
484 let report = Report::new(format!("package binary `{package}` failed"))
485 .detail(format!("command: {}", command.display()))
486 .detail(format!("status: {status}"));
487 if let Some(stderr) = stderr {
488 report.detail(format!("stderr: {stderr}"))
489 } else {
490 report
491 }
492 }
493 Self::CreateBinLink {
494 command,
495 path,
496 source,
497 } => Report::new(format!("failed to create bin link `{command}`"))
498 .detail(format!("path: {}", path.display()))
499 .detail(format!("cause: {source}")),
500 Self::RemoveBinLink {
501 command,
502 path,
503 source,
504 } => Report::new(format!("failed to remove bin link `{command}`"))
505 .detail(format!("path: {}", path.display()))
506 .detail(format!("cause: {source}")),
507 Self::RemoveInstalledPackage { path, source } => {
508 Report::new("failed to remove installed package")
509 .detail(format!("path: {}", path.display()))
510 .detail(format!("cause: {source}"))
511 }
512 Self::ReadLockfile { path, source } => Report::new("failed to read package-lock.json")
513 .detail(format!("path: {}", path.display()))
514 .detail(format!("cause: {source}")),
515 Self::ParseLockfile { path, source } => {
516 Report::new("failed to parse package-lock.json")
517 .detail(format!("path: {}", path.display()))
518 .detail(format!("cause: {source}"))
519 }
520 Self::WriteLockfile { path, source } => {
521 Report::new("failed to write package-lock.json")
522 .detail(format!("path: {}", path.display()))
523 .detail(format!("cause: {source}"))
524 }
525 Self::InvalidTempPath { path } => Report::new("failed to run x")
526 .detail("note: tempdir failed".to_string())
527 .detail(format!("path: {}", path.display())),
528 }
529 }
530
531 fn warning_summary(&self) -> String {
532 self.report().summary().to_string()
533 }
534}
535
536pub fn install() -> Result<Report, PmError> {
537 install_from(".")
538}
539
540pub fn global_install(package_spec: Option<&str>) -> Result<Report, PmError> {
541 let package_spec = package_spec.ok_or(PmError::MissingGlobalPackageSpec)?;
542 if package_spec.trim().is_empty() {
543 return Err(PmError::MissingGlobalPackageSpec);
544 }
545 let (package_name, package_range) = parse_package_spec(package_spec);
546 let global_root = global_packages_root()?;
547 let node_modules = global_node_modules_dir(&global_root);
548 fs::create_dir_all(&node_modules).map_err(|source| PmError::CreateDir {
549 path: node_modules.clone(),
550 source,
551 })?;
552
553 let mut installed = HashSet::new();
554 let mut lock = LockCollector::new();
555 let mut root_dependencies = HashMap::new();
556 root_dependencies.insert(package_name.clone(), package_range.clone());
557 let empty_dependencies = HashMap::new();
558 lock.insert_root_fields(
559 "o--global",
560 "0.0.0",
561 &root_dependencies,
562 &empty_dependencies,
563 &empty_dependencies,
564 &empty_dependencies,
565 );
566
567 let mut summary = InstallSummary::default();
568 let client = Client::new();
569 install_dependency(
570 &client,
571 &global_root,
572 &package_name,
573 &package_range,
574 &node_modules,
575 &mut installed,
576 &mut lock,
577 &mut summary,
578 DependencyKind::Prod,
579 )?;
580
581 let installed_manifest_path = install_dir(&node_modules, &package_name).join("package.json");
582 let installed_manifest = read_manifest_from_path(&installed_manifest_path)
583 .map_err(|source| map_manifest_error(&installed_manifest_path, source))?;
584 let lockfile = lock.into_lockfile_fields("o--global", "0.0.0");
585 let lockfile_path =
586 write_lockfile(&global_root, &lockfile).map_err(|source| PmError::WriteLockfile {
587 path: global_root.join("package-lock.json"),
588 source,
589 })?;
590
591 Ok(Report::new(format!(
592 "installed global package `{}`",
593 installed_manifest.name
594 ))
595 .detail(format!("requested: {package_spec}"))
596 .detail(format!("resolved version: {}", installed_manifest.version))
597 .detail(format!("root: {}", global_root.display()))
598 .detail(format!(
599 "bin dir: {}",
600 global_bin_dir(&node_modules).display()
601 ))
602 .detail(format!("lockfile: {}", lockfile_path.display()))
603 .detail(format!("dependencies: {}", summary.prod_installed))
604 .detail(format!(
605 "optionalDependencies: {}",
606 summary.optional_installed
607 ))
608 .detail(format!("peerDependencies: {}", summary.peer_installed))
609 .detail(format!("peer warnings: {}", summary.warnings.len()))
610 .detail(if summary.warnings.is_empty() {
611 "peer/optional warnings: none".to_string()
612 } else {
613 format!("peer/optional warnings: {}", summary.warnings.join(" | "))
614 }))
615}
616
617pub fn install_from(path: &str) -> Result<Report, PmError> {
618 let manifest_path =
619 find_manifest_path(Path::new(path)).map_err(|source| PmError::FindManifest {
620 start: PathBuf::from(path),
621 source,
622 })?;
623 let project_root = manifest_path
624 .parent()
625 .ok_or_else(|| PmError::ProjectRootMissing {
626 path: manifest_path.clone(),
627 })?;
628 let manifest = read_manifest_from_path(&manifest_path)
629 .map_err(|source| map_manifest_error(&manifest_path, source))?;
630 let node_modules = project_root.join("node_modules");
631 fs::create_dir_all(&node_modules).map_err(|source| PmError::CreateDir {
632 path: node_modules.clone(),
633 source,
634 })?;
635
636 let mut installed = HashSet::new();
637 let mut lock = LockCollector::new();
638 lock.insert_root_fields(
639 &manifest.name,
640 &manifest.version,
641 &manifest.dependencies.clone().unwrap_or_default(),
642 &manifest.dev_dependencies.clone().unwrap_or_default(),
643 &manifest.optional_dependencies.clone().unwrap_or_default(),
644 &manifest.peer_dependencies.clone().unwrap_or_default(),
645 );
646 let mut summary = InstallSummary::default();
647 let client = Client::new();
648
649 let root_dependencies = manifest.dependencies.clone().unwrap_or_default();
650 let root_dev_dependencies = manifest.dev_dependencies.clone().unwrap_or_default();
651 let root_optional_dependencies = manifest.optional_dependencies.clone().unwrap_or_default();
652 let root_peer_dependencies = manifest.peer_dependencies.clone().unwrap_or_default();
653
654 install_dependency_set(
655 &client,
656 project_root,
657 &root_dependencies,
658 &node_modules,
659 &mut installed,
660 &mut lock,
661 &mut summary,
662 DependencyKind::Prod,
663 )?;
664 install_dependency_set(
665 &client,
666 project_root,
667 &root_dev_dependencies,
668 &node_modules,
669 &mut installed,
670 &mut lock,
671 &mut summary,
672 DependencyKind::Dev,
673 )?;
674 install_optional_dependency_set(
675 &client,
676 project_root,
677 &root_optional_dependencies,
678 &node_modules,
679 &mut installed,
680 &mut lock,
681 &mut summary,
682 "root package",
683 );
684 reconcile_peer_dependencies(
685 "root package",
686 &root_peer_dependencies,
687 &client,
688 project_root,
689 &node_modules,
690 &mut installed,
691 &mut lock,
692 &mut summary,
693 );
694
695 let lockfile = lock.into_lockfile_fields(&manifest.name, &manifest.version);
696 let lockfile_path =
697 write_lockfile(project_root, &lockfile).map_err(|source| PmError::WriteLockfile {
698 path: project_root.join("package-lock.json"),
699 source,
700 })?;
701
702 Ok(Report::new("installed project dependencies")
703 .detail(format!("root: {}", project_root.display()))
704 .detail(format!("dependencies: {}", summary.prod_installed))
705 .detail(format!("devDependencies: {}", summary.dev_installed))
706 .detail(format!(
707 "optionalDependencies: {}",
708 summary.optional_installed
709 ))
710 .detail(format!("peerDependencies: {}", summary.peer_installed))
711 .detail(format!("peer warnings: {}", summary.warnings.len()))
712 .detail(format!("lockfile: {}", lockfile_path.display()))
713 .detail(format!(
714 "declared dependencies: {}",
715 root_dependencies.len()
716 ))
717 .detail(format!(
718 "declared devDependencies: {}",
719 root_dev_dependencies.len()
720 ))
721 .detail(format!(
722 "declared optionalDependencies: {}",
723 root_optional_dependencies.len()
724 ))
725 .detail(format!(
726 "declared peerDependencies: {}",
727 root_peer_dependencies.len()
728 ))
729 .detail(if summary.warnings.is_empty() {
730 "peer/optional warnings: none".to_string()
731 } else {
732 format!("peer/optional warnings: {}", summary.warnings.join(" | "))
733 }))
734}
735
736fn install_dependency_set(
737 client: &Client,
738 project_root: &Path,
739 dependencies: &HashMap<String, String>,
740 node_modules_dir: &Path,
741 installed: &mut HashSet<String>,
742 lock: &mut LockCollector,
743 summary: &mut InstallSummary,
744 kind: DependencyKind,
745) -> Result<(), PmError> {
746 for (name, range) in dependencies {
747 install_dependency(
748 client,
749 project_root,
750 name,
751 range,
752 node_modules_dir,
753 installed,
754 lock,
755 summary,
756 kind,
757 )?;
758 }
759
760 Ok(())
761}
762
763fn install_optional_dependency_set(
764 client: &Client,
765 project_root: &Path,
766 dependencies: &HashMap<String, String>,
767 node_modules_dir: &Path,
768 installed: &mut HashSet<String>,
769 lock: &mut LockCollector,
770 summary: &mut InstallSummary,
771 owner: &str,
772) {
773 for (name, range) in dependencies {
774 if let Err(error) = install_dependency(
775 client,
776 project_root,
777 name,
778 range,
779 node_modules_dir,
780 installed,
781 lock,
782 summary,
783 DependencyKind::Optional,
784 ) {
785 summary.warn(format!(
786 "optional dependency `{name}` for `{owner}` was skipped: {}",
787 error.warning_summary()
788 ));
789 }
790 }
791}
792
793fn install_dependency(
794 client: &Client,
795 project_root: &Path,
796 name: &str,
797 range: &str,
798 node_modules_dir: &Path,
799 installed: &mut HashSet<String>,
800 lock: &mut LockCollector,
801 summary: &mut InstallSummary,
802 kind: DependencyKind,
803) -> Result<(), PmError> {
804 let resolved = resolve_package(client, name, range)?;
805 let install_key = format!(
806 "{}@{}::{}",
807 resolved.name,
808 resolved.version,
809 node_modules_dir.display()
810 );
811
812 if !installed.insert(install_key) {
813 return Ok(());
814 }
815
816 let target_dir = install_dir(node_modules_dir, &resolved.name);
817 if is_matching_install(&target_dir, &resolved.version)? {
818 lock.insert_package(
819 project_root,
820 &target_dir,
821 &resolved.name,
822 &resolved.version,
823 &resolved.tarball_url,
824 resolved.integrity.as_deref(),
825 &resolved.dependencies,
826 &resolved.optional_dependencies,
827 &resolved.peer_dependencies,
828 )
829 .map_err(|source| PmError::WriteLockfile {
830 path: project_root.join("package-lock.json"),
831 source,
832 })?;
833 install_dependency_set(
834 client,
835 project_root,
836 &resolved.dependencies,
837 &target_dir.join("node_modules"),
838 installed,
839 lock,
840 summary,
841 DependencyKind::Prod,
842 )?;
843 install_optional_dependency_set(
844 client,
845 project_root,
846 &resolved.optional_dependencies,
847 &target_dir.join("node_modules"),
848 installed,
849 lock,
850 summary,
851 &resolved.name,
852 );
853 reconcile_peer_dependencies(
854 &resolved.name,
855 &resolved.peer_dependencies,
856 client,
857 project_root,
858 node_modules_dir,
859 installed,
860 lock,
861 summary,
862 );
863 return Ok(());
864 }
865
866 if let Some(parent) = target_dir.parent() {
867 fs::create_dir_all(parent).map_err(|source| PmError::CreateDir {
868 path: parent.to_path_buf(),
869 source,
870 })?;
871 }
872
873 let package_root = download_and_extract_package(client, &resolved)?;
874
875 if target_dir.exists() {
876 fs::remove_dir_all(&target_dir).map_err(|source| PmError::RemoveExistingInstall {
877 path: target_dir.clone(),
878 source,
879 })?;
880 }
881 copy_dir_all(&package_root, &target_dir).map_err(|source| PmError::CopyInstall {
882 from: package_root.clone(),
883 to: target_dir.clone(),
884 source,
885 })?;
886 create_bin_links(node_modules_dir, &target_dir)?;
887 summary.record_install(kind);
888 lock.insert_package(
889 project_root,
890 &target_dir,
891 &resolved.name,
892 &resolved.version,
893 &resolved.tarball_url,
894 resolved.integrity.as_deref(),
895 &resolved.dependencies,
896 &resolved.optional_dependencies,
897 &resolved.peer_dependencies,
898 )
899 .map_err(|source| PmError::WriteLockfile {
900 path: project_root.join("package-lock.json"),
901 source,
902 })?;
903
904 let nested_node_modules = target_dir.join("node_modules");
905 fs::create_dir_all(&nested_node_modules).map_err(|source| PmError::CreateDir {
906 path: nested_node_modules.clone(),
907 source,
908 })?;
909 install_dependency_set(
910 client,
911 project_root,
912 &resolved.dependencies,
913 &nested_node_modules,
914 installed,
915 lock,
916 summary,
917 DependencyKind::Prod,
918 )?;
919 install_optional_dependency_set(
920 client,
921 project_root,
922 &resolved.optional_dependencies,
923 &nested_node_modules,
924 installed,
925 lock,
926 summary,
927 &resolved.name,
928 );
929 reconcile_peer_dependencies(
930 &resolved.name,
931 &resolved.peer_dependencies,
932 client,
933 project_root,
934 node_modules_dir,
935 installed,
936 lock,
937 summary,
938 );
939
940 Ok(())
941}
942
943fn resolve_package(client: &Client, name: &str, range: &str) -> Result<ResolvedPackage, PmError> {
944 let url = resolve_npm_url(name);
945 let response = client
946 .get(url)
947 .send()
948 .map_err(|source| PmError::FetchMetadata {
949 package: name.to_string(),
950 source,
951 })?;
952
953 let response = response
954 .error_for_status()
955 .map_err(|source| PmError::MetadataStatus {
956 package: name.to_string(),
957 source,
958 })?;
959
960 let body = response
961 .text()
962 .map_err(|source| PmError::ReadMetadataBody {
963 package: name.to_string(),
964 source,
965 })?;
966
967 let packument: Packument =
968 serde_json::from_str(&body).map_err(|source| PmError::ParseMetadata {
969 package: name.to_string(),
970 source,
971 })?;
972
973 let range: Range = range
974 .parse()
975 .map_err(|source: nodejs_semver::SemverError| PmError::InvalidRange {
976 package: name.to_string(),
977 range: range.to_string(),
978 source: source.to_string(),
979 })?;
980
981 let version = packument
982 .versions
983 .keys()
984 .filter_map(|raw_version| {
985 Version::parse(raw_version)
986 .ok()
987 .map(|parsed| (raw_version, parsed))
988 })
989 .filter(|(_, parsed)| parsed.satisfies(&range))
990 .map(|(_, parsed)| parsed)
991 .max()
992 .ok_or_else(|| PmError::VersionNotFound {
993 package: name.to_string(),
994 range: range.to_string(),
995 })?;
996
997 let version_string = version.to_string();
998 let metadata =
999 packument
1000 .versions
1001 .get(&version_string)
1002 .ok_or_else(|| PmError::MissingResolvedVersion {
1003 package: name.to_string(),
1004 version: version_string.clone(),
1005 })?;
1006
1007 Ok(ResolvedPackage {
1008 name: name.to_string(),
1009 version: version_string,
1010 dependencies: metadata.dependencies.clone(),
1011 optional_dependencies: metadata.optional_dependencies.clone(),
1012 peer_dependencies: metadata.peer_dependencies.clone(),
1013 tarball_url: metadata.dist.tarball.clone(),
1014 integrity: metadata.dist.integrity.clone(),
1015 })
1016}
1017
1018fn download_and_extract_package(
1019 client: &Client,
1020 package: &ResolvedPackage,
1021) -> Result<PathBuf, PmError> {
1022 let response =
1023 client
1024 .get(&package.tarball_url)
1025 .send()
1026 .map_err(|source| PmError::DownloadTarball {
1027 package: package.name.clone(),
1028 source,
1029 })?;
1030 let response = response
1031 .error_for_status()
1032 .map_err(|source| PmError::TarballStatus {
1033 package: package.name.clone(),
1034 source,
1035 })?;
1036
1037 let bytes = response
1038 .bytes()
1039 .map_err(|source| PmError::ReadTarballBody {
1040 package: package.name.clone(),
1041 source,
1042 })?;
1043 verify_integrity(package, bytes.as_ref())?;
1044
1045 let temp = tempdir().map_err(|source| PmError::ExtractTarball {
1046 package: package.name.clone(),
1047 source,
1048 })?;
1049 let temp_path = temp.keep();
1050
1051 let tar = GzDecoder::new(bytes.as_ref());
1052 let mut archive = Archive::new(tar);
1053 archive
1054 .unpack(&temp_path)
1055 .map_err(|source| PmError::ExtractTarball {
1056 package: package.name.clone(),
1057 source,
1058 })?;
1059
1060 let package_root = temp_path.join("package");
1061 if !package_root.is_dir() {
1062 return Err(PmError::MissingPackageDir {
1063 package: package.name.clone(),
1064 path: package_root,
1065 });
1066 }
1067
1068 Ok(package_root)
1069}
1070
1071fn verify_integrity(package: &ResolvedPackage, bytes: &[u8]) -> Result<(), PmError> {
1072 let Some(integrity) = &package.integrity else {
1073 return Ok(());
1074 };
1075
1076 let parsed: Integrity =
1077 integrity
1078 .parse()
1079 .map_err(|source: ssri::Error| PmError::InvalidIntegrity {
1080 package: package.name.clone(),
1081 version: package.version.clone(),
1082 source: source.to_string(),
1083 })?;
1084
1085 parsed
1086 .check(bytes)
1087 .map_err(|source: ssri::Error| PmError::IntegrityMismatch {
1088 package: package.name.clone(),
1089 version: package.version.clone(),
1090 source: source.to_string(),
1091 })?;
1092
1093 Ok(())
1094}
1095
1096fn install_dir(node_modules: &Path, package_name: &str) -> PathBuf {
1097 if let Some((scope, name)) = package_name.split_once('/') {
1098 node_modules.join(scope).join(name)
1099 } else {
1100 node_modules.join(package_name)
1101 }
1102}
1103
1104fn is_matching_install(path: &Path, version: &str) -> Result<bool, PmError> {
1105 let manifest_path = path.join("package.json");
1106 if !manifest_path.is_file() {
1107 return Ok(false);
1108 }
1109
1110 let manifest = read_manifest_from_path(&manifest_path)
1111 .map_err(|source| map_manifest_error(&manifest_path, source))?;
1112 Ok(manifest.version == version)
1113}
1114
1115fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
1116 fs::create_dir_all(dst)?;
1117
1118 for entry in fs::read_dir(src)? {
1119 let entry = entry?;
1120 let file_type = entry.file_type()?;
1121 let from = entry.path();
1122 let to = dst.join(entry.file_name());
1123
1124 if file_type.is_dir() {
1125 copy_dir_all(&from, &to)?;
1126 } else {
1127 fs::copy(&from, &to)?;
1128 }
1129 }
1130
1131 Ok(())
1132}
1133
1134fn create_bin_links(node_modules_dir: &Path, package_dir: &Path) -> Result<(), PmError> {
1135 let bin_entries = read_bin_entries(package_dir)?;
1136 if bin_entries.is_empty() {
1137 return Ok(());
1138 }
1139
1140 let bin_dir = node_modules_dir.join(".bin");
1141 fs::create_dir_all(&bin_dir).map_err(|source| PmError::CreateDir {
1142 path: bin_dir.clone(),
1143 source,
1144 })?;
1145
1146 for (command_name, relative_target) in bin_entries {
1147 let target = package_dir.join(normalize_package_relative_path(&relative_target));
1148 if !target.is_file() {
1149 return Err(PmError::MissingBinTarget {
1150 package_dir: package_dir.to_path_buf(),
1151 target,
1152 });
1153 }
1154
1155 create_bin_link(&bin_dir, &command_name, &target)?;
1156 }
1157
1158 Ok(())
1159}
1160
1161fn read_bin_entries(package_dir: &Path) -> Result<Vec<(String, String)>, PmError> {
1162 let package_json_path = package_dir.join("package.json");
1163 let source = fs::read_to_string(&package_json_path).map_err(|source| {
1164 PmError::ReadInstalledManifest {
1165 path: package_json_path.clone(),
1166 source,
1167 }
1168 })?;
1169 let value: Value = serde_json::from_str(&source).map_err(|source| PmError::ParseManifest {
1170 path: package_json_path.clone(),
1171 source,
1172 })?;
1173
1174 let package_name = value
1175 .get("name")
1176 .and_then(Value::as_str)
1177 .map(default_bin_name)
1178 .ok_or_else(|| PmError::MissingInstalledName {
1179 path: package_json_path.clone(),
1180 })?;
1181
1182 let Some(bin_value) = value.get("bin") else {
1183 return Ok(Vec::new());
1184 };
1185
1186 match bin_value {
1187 Value::String(path) => Ok(vec![(package_name, path.clone())]),
1188 Value::Object(entries) => {
1189 let mut bins = Vec::with_capacity(entries.len());
1190 for (command_name, target) in entries {
1191 let target = target.as_str().ok_or_else(|| PmError::InvalidBinEntry {
1192 path: package_json_path.clone(),
1193 entry: command_name.clone(),
1194 })?;
1195 bins.push((command_name.clone(), target.to_string()));
1196 }
1197 Ok(bins)
1198 }
1199 Value::Null => Ok(Vec::new()),
1200 _ => Err(PmError::InvalidBinField {
1201 path: package_json_path,
1202 }),
1203 }
1204}
1205
1206fn default_bin_name(package_name: &str) -> String {
1207 package_name
1208 .rsplit_once('/')
1209 .map(|(_, name)| name.to_string())
1210 .unwrap_or_else(|| package_name.to_string())
1211}
1212
1213fn normalize_package_relative_path(path: &str) -> PathBuf {
1214 let trimmed = path.strip_prefix("./").unwrap_or(path);
1215 PathBuf::from(trimmed)
1216}
1217
1218fn reconcile_peer_dependencies(
1219 owner: &str,
1220 peer_dependencies: &HashMap<String, String>,
1221 client: &Client,
1222 project_root: &Path,
1223 node_modules_dir: &Path,
1224 installed: &mut HashSet<String>,
1225 lock: &mut LockCollector,
1226 summary: &mut InstallSummary,
1227) {
1228 for (name, range) in peer_dependencies {
1229 if peer_dependency_warning(name, range, node_modules_dir).is_some() {
1230 if let Err(error) = install_dependency(
1231 client,
1232 project_root,
1233 name,
1234 range,
1235 node_modules_dir,
1236 installed,
1237 lock,
1238 summary,
1239 DependencyKind::Peer,
1240 ) {
1241 summary.warn(format!(
1242 "peer dependency `{name}` for `{owner}` could not be installed: {}",
1243 error.warning_summary()
1244 ));
1245 }
1246 }
1247 }
1248
1249 validate_peer_dependencies(owner, peer_dependencies, node_modules_dir, summary);
1250}
1251
1252fn validate_peer_dependencies(
1253 owner: &str,
1254 peer_dependencies: &HashMap<String, String>,
1255 node_modules_dir: &Path,
1256 summary: &mut InstallSummary,
1257) {
1258 for (name, range) in peer_dependencies {
1259 if let Some(warning) = peer_dependency_warning(name, range, node_modules_dir) {
1260 summary.warn(format!("peer dependency for `{owner}`: {warning}"));
1261 }
1262 }
1263}
1264
1265fn peer_dependency_warning(name: &str, range: &str, node_modules_dir: &Path) -> Option<String> {
1266 let package_dir = install_dir(node_modules_dir, name);
1267 let manifest_path = package_dir.join("package.json");
1268 if !manifest_path.is_file() {
1269 return Some(format!("missing `{name}` required by range `{range}`"));
1270 }
1271
1272 let manifest = match read_manifest_from_path(&manifest_path) {
1273 Ok(manifest) => manifest,
1274 Err(error) => {
1275 return Some(format!(
1276 "failed to read installed `{name}` manifest: {error}"
1277 ));
1278 }
1279 };
1280 let installed_version = match Version::parse(&manifest.version) {
1281 Ok(version) => version,
1282 Err(error) => {
1283 return Some(format!(
1284 "`{name}` is installed with invalid version `{}`: {error}",
1285 manifest.version
1286 ));
1287 }
1288 };
1289 let expected_range: Range = match range.parse() {
1290 Ok(parsed) => parsed,
1291 Err(error) => {
1292 return Some(format!(
1293 "`{name}` requires invalid peer range `{range}`: {error}"
1294 ));
1295 }
1296 };
1297 if installed_version.satisfies(&expected_range) {
1298 None
1299 } else {
1300 Some(format!(
1301 "`{name}` is installed as `{}` but `{range}` is required",
1302 manifest.version
1303 ))
1304 }
1305}
1306
1307#[cfg(unix)]
1308fn create_bin_link(bin_dir: &Path, command_name: &str, target: &Path) -> Result<(), PmError> {
1309 use std::os::unix::fs::symlink;
1310
1311 let link_path = bin_dir.join(command_name);
1312 remove_existing_link_path(&link_path).map_err(|source| PmError::CreateBinLink {
1313 command: command_name.to_string(),
1314 path: link_path.clone(),
1315 source,
1316 })?;
1317 symlink(target, &link_path).map_err(|source| PmError::CreateBinLink {
1318 command: command_name.to_string(),
1319 path: link_path,
1320 source,
1321 })
1322}
1323
1324#[cfg(windows)]
1325fn create_bin_link(bin_dir: &Path, command_name: &str, target: &Path) -> Result<(), PmError> {
1326 let link_path = bin_dir.join(format!("{command_name}.cmd"));
1327 remove_existing_link_path(&link_path).map_err(|source| PmError::CreateBinLink {
1328 command: command_name.to_string(),
1329 path: link_path.clone(),
1330 source,
1331 })?;
1332 let script = format!("@ECHO off\r\nnode \"{}\" %*\r\n", target.display());
1333 fs::write(&link_path, script).map_err(|source| PmError::CreateBinLink {
1334 command: command_name.to_string(),
1335 path: link_path,
1336 source,
1337 })
1338}
1339
1340fn remove_existing_link_path(path: &Path) -> io::Result<()> {
1341 if !path.exists() {
1342 return Ok(());
1343 }
1344
1345 let metadata = fs::symlink_metadata(path)?;
1346 if metadata.file_type().is_dir() && !metadata.file_type().is_symlink() {
1347 fs::remove_dir_all(path)
1348 } else {
1349 fs::remove_file(path)
1350 }
1351}
1352
1353fn resolve_npm_url(package: &str) -> String {
1354 let encoded = package.replace('@', "%40").replace('/', "%2F");
1355 format!("https://registry.npmjs.org/{encoded}")
1356}
1357
1358fn global_packages_root() -> Result<PathBuf, PmError> {
1359 let mut path = home_dir().ok_or(PmError::HomeDirUnavailable)?;
1360 path.push(".config");
1361 path.push("o-");
1362 path.push("packages");
1363 Ok(path)
1364}
1365
1366fn global_node_modules_dir(global_root: &Path) -> PathBuf {
1367 global_root.join("node_modules")
1368}
1369
1370fn global_bin_dir(node_modules_dir: &Path) -> PathBuf {
1371 node_modules_dir.join(".bin")
1372}
1373
1374fn parse_package_spec(spec: &str) -> (String, String) {
1375 let trimmed = spec.trim();
1376 if trimmed.is_empty() {
1377 return (String::new(), "*".to_string());
1378 }
1379
1380 if let Some((name, range)) = split_package_spec(trimmed) {
1381 return (name.to_string(), normalize_package_range(range).to_string());
1382 }
1383
1384 (trimmed.to_string(), "*".to_string())
1385}
1386
1387fn split_package_spec(spec: &str) -> Option<(&str, &str)> {
1388 if spec.starts_with('@') {
1389 let slash = spec.find('/')?;
1390 let tail = &spec[slash + 1..];
1391 let at = tail.rfind('@')?;
1392 let split_index = slash + 1 + at;
1393 let name = &spec[..split_index];
1394 let range = &spec[split_index + 1..];
1395 if range.is_empty() {
1396 None
1397 } else {
1398 Some((name, range))
1399 }
1400 } else if let Some((name, range)) = spec.rsplit_once('@') {
1401 if name.is_empty() || range.is_empty() {
1402 None
1403 } else {
1404 Some((name, range))
1405 }
1406 } else {
1407 None
1408 }
1409}
1410
1411fn normalize_package_range(range: &str) -> &str {
1412 if range == "latest" { "*" } else { range }
1413}
1414
1415pub fn remove_shim(node_modules_dir: &Path, command: &str) -> Result<bool, PmError> {
1416 let shim_path = shim_path_for_command(node_modules_dir, command);
1417 if !shim_path.exists() {
1418 return Ok(false);
1419 }
1420
1421 remove_existing_link_path(&shim_path).map_err(|source| PmError::RemoveBinLink {
1422 command: command.to_string(),
1423 path: shim_path,
1424 source,
1425 })?;
1426 Ok(true)
1427}
1428
1429pub fn uninstall(name: &str) -> Result<Report, PmError> {
1430 let global_root = global_packages_root()?;
1431 let node_modules_dir = global_node_modules_dir(&global_root);
1432 let package_dir = install_dir(&node_modules_dir, name);
1433 if !package_dir.is_dir() {
1434 return Err(PmError::PackageNotInstalled {
1435 name: name.to_string(),
1436 path: package_dir,
1437 });
1438 }
1439
1440 let manifest_path = package_dir.join("package.json");
1441 let manifest = read_manifest_from_path(&manifest_path)
1442 .map_err(|source| map_manifest_error(&manifest_path, source))?;
1443 let bin_entries = read_bin_entries(&package_dir)?;
1444
1445 fs::remove_dir_all(&package_dir).map_err(|source| PmError::RemoveInstalledPackage {
1446 path: package_dir.clone(),
1447 source,
1448 })?;
1449 remove_empty_scope_dir(&package_dir)?;
1450
1451 let mut removed_shims = Vec::new();
1452 for (command, _) in bin_entries {
1453 if remove_shim(&node_modules_dir, &command)? {
1454 removed_shims.push(command);
1455 }
1456 }
1457
1458 let lockfile_path = remove_global_lockfile_entry(&global_root, &package_dir, &manifest.name)?;
1459
1460 Ok(
1461 Report::new(format!("uninstalled package `{}`", manifest.name))
1462 .detail(format!("version: {}", manifest.version))
1463 .detail(format!("package: {}", package_dir.display()))
1464 .detail(format!(
1465 "bin dir: {}",
1466 global_bin_dir(&node_modules_dir).display()
1467 ))
1468 .detail(match lockfile_path {
1469 Some(path) => format!("lockfile: {}", path.display()),
1470 None => "lockfile: none".to_string(),
1471 })
1472 .detail(if removed_shims.is_empty() {
1473 "removed shims: none".to_string()
1474 } else {
1475 format!("removed shims: {}", removed_shims.join(", "))
1476 }),
1477 )
1478}
1479
1480#[cfg(unix)]
1481fn shim_path_for_command(node_modules_dir: &Path, command: &str) -> PathBuf {
1482 global_bin_dir(node_modules_dir).join(command)
1483}
1484
1485#[cfg(windows)]
1486fn shim_path_for_command(node_modules_dir: &Path, command: &str) -> PathBuf {
1487 global_bin_dir(node_modules_dir).join(format!("{command}.cmd"))
1488}
1489
1490fn remove_empty_scope_dir(package_dir: &Path) -> Result<(), PmError> {
1491 let Some(parent) = package_dir.parent() else {
1492 return Ok(());
1493 };
1494
1495 let Some(scope_name) = parent.file_name().and_then(|name| name.to_str()) else {
1496 return Ok(());
1497 };
1498
1499 if !scope_name.starts_with('@') {
1500 return Ok(());
1501 }
1502
1503 if parent
1504 .read_dir()
1505 .map_err(|source| PmError::RemoveInstalledPackage {
1506 path: parent.to_path_buf(),
1507 source,
1508 })?
1509 .next()
1510 .is_none()
1511 {
1512 fs::remove_dir(parent).map_err(|source| PmError::RemoveInstalledPackage {
1513 path: parent.to_path_buf(),
1514 source,
1515 })?;
1516 }
1517
1518 Ok(())
1519}
1520
1521fn remove_global_lockfile_entry(
1522 global_root: &Path,
1523 package_dir: &Path,
1524 package_name: &str,
1525) -> Result<Option<PathBuf>, PmError> {
1526 let lockfile_path = global_root.join("package-lock.json");
1527 if !lockfile_path.is_file() {
1528 return Ok(None);
1529 }
1530
1531 let source = fs::read_to_string(&lockfile_path).map_err(|source| PmError::ReadLockfile {
1532 path: lockfile_path.clone(),
1533 source,
1534 })?;
1535 let mut lockfile: crate::lock::LockFile =
1536 serde_json::from_str(&source).map_err(|source| PmError::ParseLockfile {
1537 path: lockfile_path.clone(),
1538 source,
1539 })?;
1540
1541 let key = package_dir
1542 .strip_prefix(global_root)
1543 .map(|path| path.to_string_lossy().replace('\\', "/"))
1544 .unwrap_or_else(|_| package_dir.to_string_lossy().replace('\\', "/"));
1545 lockfile.packages.remove(&key);
1546
1547 if let Some(root) = lockfile.packages.get_mut("") {
1548 remove_dependency_entry(&mut root.dependencies, package_name);
1549 remove_dependency_entry(&mut root.dev_dependencies, package_name);
1550 remove_dependency_entry(&mut root.optional_dependencies, package_name);
1551 remove_dependency_entry(&mut root.peer_dependencies, package_name);
1552 }
1553
1554 let rewritten =
1555 write_lockfile(global_root, &lockfile).map_err(|source| PmError::WriteLockfile {
1556 path: lockfile_path,
1557 source,
1558 })?;
1559 Ok(Some(rewritten))
1560}
1561
1562fn remove_dependency_entry(
1563 dependencies: &mut Option<std::collections::BTreeMap<String, String>>,
1564 name: &str,
1565) {
1566 let should_clear = if let Some(entries) = dependencies.as_mut() {
1567 entries.remove(name);
1568 entries.is_empty()
1569 } else {
1570 false
1571 };
1572
1573 if should_clear {
1574 *dependencies = None;
1575 }
1576}
1577
1578fn map_manifest_error(path: &Path, source: io::Error) -> PmError {
1579 match source.kind() {
1580 io::ErrorKind::InvalidData => {
1581 let parse_source =
1582 serde_json::Error::io(io::Error::new(source.kind(), source.to_string()));
1583 PmError::ParseManifest {
1584 path: path.to_path_buf(),
1585 source: parse_source,
1586 }
1587 }
1588 _ => PmError::ReadManifest {
1589 path: path.to_path_buf(),
1590 source,
1591 },
1592 }
1593}