Skip to main content

ephemeris_release/adapters/cargo/
workspace.rs

1//! Cargo's resolved workspace model and the source locations needed to edit it.
2
3mod edit;
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::ffi::OsStr;
7use std::path::{Path, PathBuf};
8use std::process::{Command, ExitStatus};
9use std::{fmt, fs, io};
10
11use cargo_platform::Platform;
12use ephemeris_files::RepositoryRoot;
13use facet::Facet;
14use toml_edit::{DocumentMut, Item, Value};
15
16use super::version_groups::VersionGroups;
17
18/// A Cargo workspace discovery, planning, or edit failure.
19#[derive(Debug, thiserror::Error)]
20pub(crate) enum WorkspaceError {
21    /// A workspace file cannot be read.
22    #[error("cannot read {path}")]
23    Read {
24        /// The file.
25        path: PathBuf,
26        /// Why reading failed.
27        source: io::Error,
28    },
29    /// A manifest is not valid TOML.
30    #[error("invalid Cargo manifest {path}")]
31    Manifest {
32        /// The manifest.
33        path: PathBuf,
34        /// The parse failure.
35        source: toml_edit::TomlError,
36    },
37    /// `cargo metadata` lists no workspace members.
38    #[error("workspace has no members")]
39    NoMembers,
40    /// `cargo metadata` could not be started.
41    #[error("cannot run cargo metadata")]
42    MetadataSpawn(#[source] io::Error),
43    /// `cargo metadata` failed.
44    #[error("cargo metadata exited with {status}: {stderr}")]
45    MetadataFailed {
46        /// The exit status.
47        status: ExitStatus,
48        /// The trimmed standard error output.
49        stderr: String,
50    },
51    /// `cargo metadata` printed output that does not decode.
52    #[error("cannot decode cargo metadata")]
53    MetadataDecode(#[source] Box<facet_json::DeserializeError>),
54    /// Two workspace members share a package name.
55    #[error("duplicate release unit `{0}`")]
56    Duplicate(String),
57    /// A proposal names a package outside the workspace.
58    #[error("unknown release unit `{0}`")]
59    UnknownUnit(String),
60    /// A member manifest path has no parent directory.
61    #[error("manifest {} has no parent directory", .0.display())]
62    NoManifestDirectory(PathBuf),
63    /// A member manifest was not parsed along with the workspace.
64    #[error("manifest {} was not loaded", .0.display())]
65    ManifestNotLoaded(PathBuf),
66    /// A member manifest has no `[package]` table.
67    #[error("manifest of release unit `{0}` has no package table")]
68    NoPackageTable(String),
69    /// A member's version is neither literal nor inherited from the workspace.
70    #[error("release unit `{0}` has no supported package version source")]
71    MissingVersionSource(String),
72    /// A version is not strict semver.
73    #[error("invalid version `{version}`")]
74    InvalidVersion {
75        /// The rejected text.
76        version: String,
77        /// The parse failure.
78        #[source]
79        source: semver::Error,
80    },
81    /// A proposal covers only part of a shared version group.
82    #[error("shared version source {version_source} has unconfigured members: {missing:?}")]
83    IncompleteVersionGroup {
84        /// Where the group's version is set.
85        version_source: VersionSource,
86        /// Members the proposal leaves out.
87        missing: Vec<String>,
88    },
89    /// A shared version group received different targets.
90    #[error("shared version source {version_source} has conflicting targets for {members:?}")]
91    ConflictingVersionGroup {
92        /// Where the group's version is set.
93        version_source: VersionSource,
94        /// The members with conflicting targets.
95        members: Vec<String>,
96    },
97    /// A shared dependency entry would change for packages outside the
98    /// proposal.
99    #[error("cannot update {dependency}: proposed release omits dependent packages {missing:?}")]
100    UnselectedDependencyConsumers {
101        /// The proposed package.
102        dependency: String,
103        /// Consumers of the shared entry that are not proposed.
104        missing: Vec<String>,
105    },
106    /// A dependency requirement is not a valid semver requirement.
107    #[error("invalid requirement {requirement:?} on {dependency} in {consumer}")]
108    InvalidRequirement {
109        /// The package holding the requirement.
110        consumer: String,
111        /// The required package.
112        dependency: String,
113        /// The requirement as written.
114        requirement: String,
115        /// The parse failure.
116        #[source]
117        source: semver::Error,
118    },
119    /// A requirement is too complex to rewrite safely.
120    #[error(
121        "cannot safely update {dependency} for {consumer}: requirement {requirement:?} does not \
122         accept {version}"
123    )]
124    UnsupportedDependencyRequirement {
125        /// The package holding the requirement.
126        consumer: String,
127        /// The proposed package.
128        dependency: String,
129        /// The requirement as written.
130        requirement: String,
131        /// The proposed version.
132        version: String,
133    },
134    /// No manifest entry matches a dependency that `cargo metadata` reports.
135    #[error("cannot locate local dependency source for {dependency} used by {consumer}")]
136    MissingDependencySource {
137        /// The package holding the dependency.
138        consumer: String,
139        /// The dependency.
140        dependency: String,
141    },
142    /// A file changed between planning and applying.
143    #[error("release plan is stale for {path}; refresh before applying")]
144    Stale {
145        /// The changed file.
146        path: PathBuf,
147    },
148}
149
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub(crate) struct Member {
152    pub(crate) name: String,
153    pub(crate) path: PathBuf,
154    pub(crate) version: String,
155    pub(crate) publishable: bool,
156    pub(crate) dependencies: Vec<String>,
157}
158
159/// The manifest entry that sets a package version.
160#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
161pub(crate) enum VersionSource {
162    /// `[package].version` in this member manifest.
163    Member(PathBuf),
164    /// `[workspace.package].version` in this root manifest.
165    Workspace(PathBuf),
166}
167
168#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
169struct DependencySlot {
170    manifest: PathBuf,
171    table_path: Vec<String>,
172    alias: String,
173}
174
175#[derive(Clone, Debug)]
176struct DependencyUse {
177    consumer: String,
178    dependency: String,
179    slot: DependencySlot,
180    requirement: Option<String>,
181    kind: Option<String>,
182}
183
184/// One member dependency as `cargo metadata` resolved it, to be located in
185/// the manifests.
186struct Declaration<'a> {
187    root_manifest: &'a Path,
188    root: &'a DocumentMut,
189    manifest: &'a Path,
190    consumer: &'a str,
191    dependency: &'a str,
192    alias: &'a str,
193    path: &'a Path,
194    kind: Option<&'a str>,
195}
196
197#[derive(Clone, Debug)]
198struct Package {
199    unit: Member,
200    source: VersionSource,
201    dependencies: Vec<DependencyUse>,
202}
203
204struct WorkspaceSources {
205    root_source: String,
206    members: Vec<MetadataPackage>,
207    documents: BTreeMap<PathBuf, DocumentMut>,
208    snapshots: BTreeMap<PathBuf, String>,
209}
210
211#[derive(Debug)]
212pub(crate) struct Workspace {
213    root: RepositoryRoot,
214    packages: Vec<Package>,
215    version_groups: VersionGroups,
216    snapshots: BTreeMap<PathBuf, String>,
217}
218
219#[derive(Clone, Debug, Eq, Facet, PartialEq)]
220struct Metadata {
221    packages: Vec<MetadataPackage>,
222    workspace_members: Vec<String>,
223}
224
225#[derive(Clone, Debug, Eq, Facet, PartialEq)]
226struct MetadataPackage {
227    id: String,
228    name: String,
229    version: String,
230    manifest_path: String,
231    publish: Option<Vec<String>>,
232    dependencies: Vec<MetadataDependency>,
233}
234
235#[derive(Clone, Debug, Eq, Facet, PartialEq)]
236struct MetadataDependency {
237    name: String,
238    req: String,
239    path: Option<String>,
240    rename: Option<String>,
241    kind: Option<String>,
242    target: Option<String>,
243}
244
245impl DependencyUse {
246    fn kind_for_ordering(&self) -> bool {
247        self.kind.as_deref() != Some("dev")
248    }
249}
250
251impl Declaration<'_> {
252    fn missing_source(&self) -> WorkspaceError {
253        WorkspaceError::MissingDependencySource {
254            consumer: self.consumer.to_owned(),
255            dependency: self.dependency.to_owned(),
256        }
257    }
258
259    /// The use declared by `item` at `table_path`, following workspace
260    /// inheritance to the entry that holds the requirement.
261    fn dependency_use(
262        &self,
263        table_path: Vec<String>,
264        item: &Item,
265    ) -> Result<DependencyUse, WorkspaceError> {
266        let inherited = item
267            .as_table_like()
268            .and_then(|table| table.get("workspace"))
269            .and_then(Item::as_bool)
270            == Some(true);
271        let (source_manifest, source_table_path, effective) = if inherited {
272            (
273                self.root_manifest,
274                vec!["workspace".to_owned(), "dependencies".to_owned()],
275                inherited_dependency(self.root, self.alias).ok_or_else(|| self.missing_source())?,
276            )
277        } else {
278            (self.manifest, table_path, item)
279        };
280        let effective_table = effective.as_table_like();
281        let effective_name = effective_table
282            .and_then(|table| table.get("package"))
283            .and_then(Item::as_str)
284            .unwrap_or(self.alias);
285        let effective_path = effective_table
286            .and_then(|table| table.get("path"))
287            .and_then(Item::as_str)
288            .map(|relative| {
289                let path = source_manifest
290                    .parent()
291                    .unwrap_or_else(|| Path::new("."))
292                    .join(relative);
293                fs::canonicalize(&path).map_err(|source| WorkspaceError::Read { path, source })
294            })
295            .transpose()?;
296        if effective_name != self.dependency || effective_path.as_deref() != Some(self.path) {
297            return Err(WorkspaceError::Stale {
298                path: self.manifest.to_path_buf(),
299            });
300        }
301        Ok(DependencyUse {
302            consumer: self.consumer.to_owned(),
303            dependency: self.dependency.to_owned(),
304            slot: DependencySlot {
305                manifest: source_manifest.to_path_buf(),
306                table_path: source_table_path,
307                alias: self.alias.to_owned(),
308            },
309            requirement: effective_table
310                .and_then(|table| table.get("version"))
311                .and_then(Item::as_str)
312                .map(str::to_owned),
313            kind: self.kind.map(str::to_owned),
314        })
315    }
316}
317
318impl Workspace {
319    pub(crate) fn load(root: &RepositoryRoot) -> Result<Self, WorkspaceError> {
320        let root_manifest = root.as_path().join("Cargo.toml");
321        let sources = snapshot_workspace(&root_manifest, root.as_path())?;
322        let root_document = parse_manifest(&root_manifest, &sources.root_source)?;
323        let mut packages = assemble_packages(&root_manifest, &root_document, &sources)?;
324        packages.sort_by(|left, right| left.unit.name.cmp(&right.unit.name));
325        let version_groups = VersionGroups::new(
326            packages
327                .iter()
328                .map(|package| (package.source.clone(), package.unit.name.clone())),
329        );
330        Ok(Self {
331            root: root.clone(),
332            packages,
333            version_groups,
334            snapshots: sources.snapshots,
335        })
336    }
337
338    pub(crate) fn units(&self) -> Vec<Member> {
339        self.packages
340            .iter()
341            .map(|package| package.unit.clone())
342            .collect()
343    }
344
345    /// This workspace with the packages `names` maps renamed to the release
346    /// unit IDs they map to, so that every query uses unit IDs.
347    pub(crate) fn renamed(&self, names: &BTreeMap<String, String>) -> Self {
348        let rename = |name: &String| names.get(name).unwrap_or(name).clone();
349        let packages: Vec<_> = self
350            .packages
351            .iter()
352            .map(|package| Package {
353                unit: Member {
354                    name: rename(&package.unit.name),
355                    dependencies: package.unit.dependencies.iter().map(rename).collect(),
356                    ..package.unit.clone()
357                },
358                source: package.source.clone(),
359                dependencies: package
360                    .dependencies
361                    .iter()
362                    .map(|usage| DependencyUse {
363                        consumer: rename(&usage.consumer),
364                        dependency: rename(&usage.dependency),
365                        ..usage.clone()
366                    })
367                    .collect(),
368            })
369            .collect();
370        let version_groups = VersionGroups::new(
371            packages
372                .iter()
373                .map(|package| (package.source.clone(), package.unit.name.clone())),
374        );
375        Self {
376            root: self.root.clone(),
377            packages,
378            version_groups,
379            snapshots: self.snapshots.clone(),
380        }
381    }
382
383    pub(crate) const fn version_groups(&self) -> &VersionGroups {
384        &self.version_groups
385    }
386
387    pub(crate) const fn snapshots(&self) -> &BTreeMap<PathBuf, String> {
388        &self.snapshots
389    }
390
391    pub(crate) const fn root(&self) -> &RepositoryRoot {
392        &self.root
393    }
394}
395
396impl fmt::Display for VersionSource {
397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398        match self {
399            Self::Member(path) => write!(f, "{} [package].version", path.display()),
400            Self::Workspace(path) => write!(f, "{} [workspace.package].version", path.display()),
401        }
402    }
403}
404
405fn snapshot_workspace(
406    root_manifest: &Path,
407    root: &Path,
408) -> Result<WorkspaceSources, WorkspaceError> {
409    let root_source = read(root_manifest)?;
410    let metadata = read_metadata(root_manifest, root)?;
411    let members = metadata_members(&metadata);
412    if members.is_empty() {
413        return Err(WorkspaceError::NoMembers);
414    }
415    let mut snapshots = BTreeMap::from([(root_manifest.to_path_buf(), root_source.clone())]);
416    let mut documents = BTreeMap::new();
417    for package in &members {
418        let manifest = PathBuf::from(&package.manifest_path);
419        let contents = if let Some(contents) = snapshots.get(&manifest) {
420            contents.clone()
421        } else {
422            let contents = read(&manifest)?;
423            snapshots.insert(manifest.clone(), contents.clone());
424            contents
425        };
426        documents.insert(manifest.clone(), parse_manifest(&manifest, &contents)?);
427    }
428    let stable_metadata = read_metadata(root_manifest, root)?;
429    for (path, snapshot) in &snapshots {
430        if read(path)? != *snapshot {
431            return Err(WorkspaceError::Stale { path: path.clone() });
432        }
433    }
434    if let Some(path) = metadata_difference(&metadata, &stable_metadata, root_manifest) {
435        return Err(WorkspaceError::Stale { path });
436    }
437    Ok(WorkspaceSources {
438        root_source,
439        members,
440        documents,
441        snapshots,
442    })
443}
444
445fn assemble_packages(
446    root_manifest: &Path,
447    root_document: &DocumentMut,
448    sources: &WorkspaceSources,
449) -> Result<Vec<Package>, WorkspaceError> {
450    let member_paths: BTreeMap<_, _> = sources
451        .members
452        .iter()
453        .map(|package| {
454            let manifest = PathBuf::from(&package.manifest_path);
455            let directory = manifest
456                .parent()
457                .ok_or_else(|| WorkspaceError::NoManifestDirectory(manifest.clone()))?;
458            let directory = fs::canonicalize(directory).map_err(|source| WorkspaceError::Read {
459                path: directory.to_path_buf(),
460                source,
461            })?;
462            Ok((directory, (package.id.clone(), package.name.clone())))
463        })
464        .collect::<Result<_, WorkspaceError>>()?;
465    let mut packages = Vec::with_capacity(sources.members.len());
466    let mut names = BTreeSet::new();
467    for metadata_package in &sources.members {
468        if !names.insert(metadata_package.name.clone()) {
469            return Err(WorkspaceError::Duplicate(metadata_package.name.clone()));
470        }
471        let manifest = PathBuf::from(&metadata_package.manifest_path);
472        let document = sources
473            .documents
474            .get(&manifest)
475            .ok_or_else(|| WorkspaceError::ManifestNotLoaded(manifest.clone()))?;
476        let source = version_source(
477            root_manifest,
478            root_document,
479            &manifest,
480            document,
481            &metadata_package.name,
482            &metadata_package.version,
483        )?;
484        let dependencies = dependency_uses(
485            root_manifest,
486            root_document,
487            &manifest,
488            document,
489            metadata_package,
490            &member_paths,
491        )?;
492        let mut ordering_dependencies: Vec<_> = dependencies
493            .iter()
494            .filter(|dependency| dependency.kind_for_ordering())
495            .map(|dependency| dependency.dependency.clone())
496            .collect();
497        ordering_dependencies.sort();
498        ordering_dependencies.dedup();
499        packages.push(Package {
500            unit: Member {
501                name: metadata_package.name.clone(),
502                path: manifest
503                    .parent()
504                    .ok_or_else(|| WorkspaceError::NoManifestDirectory(manifest.clone()))?
505                    .to_path_buf(),
506                version: metadata_package.version.clone(),
507                publishable: metadata_package
508                    .publish
509                    .as_ref()
510                    .is_none_or(|registries| !registries.is_empty()),
511                dependencies: ordering_dependencies,
512            },
513            source,
514            dependencies,
515        });
516    }
517    Ok(packages)
518}
519
520fn version_source(
521    root_manifest: &Path,
522    root: &DocumentMut,
523    manifest: &Path,
524    package_document: &DocumentMut,
525    name: &str,
526    metadata_version: &str,
527) -> Result<VersionSource, WorkspaceError> {
528    let package = package_document
529        .get("package")
530        .and_then(Item::as_table_like)
531        .ok_or_else(|| WorkspaceError::NoPackageTable(name.to_owned()))?;
532    match package.get("version") {
533        Some(Item::Value(Value::String(version))) => {
534            if version.value() != metadata_version {
535                return Err(WorkspaceError::Stale {
536                    path: manifest.to_path_buf(),
537                });
538            }
539            Ok(VersionSource::Member(manifest.to_path_buf()))
540        }
541        Some(item)
542            if item
543                .as_table_like()
544                .and_then(|table| table.get("workspace"))
545                .and_then(Item::as_bool)
546                == Some(true) =>
547        {
548            let version = root
549                .get("workspace")
550                .and_then(Item::as_table_like)
551                .and_then(|table| table.get("package"))
552                .and_then(Item::as_table_like)
553                .and_then(|table| table.get("version"))
554                .and_then(Item::as_str)
555                .ok_or_else(|| WorkspaceError::MissingVersionSource(name.to_owned()))?;
556            if version != metadata_version {
557                return Err(WorkspaceError::Stale {
558                    path: root_manifest.to_path_buf(),
559                });
560            }
561            Ok(VersionSource::Workspace(root_manifest.to_path_buf()))
562        }
563        _ => Err(WorkspaceError::MissingVersionSource(name.to_owned())),
564    }
565}
566
567fn dependency_uses(
568    root_manifest: &Path,
569    root: &DocumentMut,
570    manifest: &Path,
571    package_document: &DocumentMut,
572    package: &MetadataPackage,
573    members: &BTreeMap<PathBuf, (String, String)>,
574) -> Result<Vec<DependencyUse>, WorkspaceError> {
575    let mut result = Vec::new();
576    let mut seen = BTreeSet::new();
577    for dependency in &package.dependencies {
578        for usage in dependency_uses_for_dependency(
579            root_manifest,
580            root,
581            manifest,
582            package_document,
583            package,
584            members,
585            dependency,
586        )? {
587            if seen.insert((
588                usage.slot.clone(),
589                usage.kind.clone(),
590                dependency.target.clone(),
591            )) {
592                result.push(usage);
593            }
594        }
595    }
596    Ok(result)
597}
598
599fn dependency_uses_for_dependency(
600    root_manifest: &Path,
601    root: &DocumentMut,
602    manifest: &Path,
603    package_document: &DocumentMut,
604    package: &MetadataPackage,
605    members: &BTreeMap<PathBuf, (String, String)>,
606    dependency: &MetadataDependency,
607) -> Result<Vec<DependencyUse>, WorkspaceError> {
608    let Some(path) = &dependency.path else {
609        return Ok(Vec::new());
610    };
611    let path = fs::canonicalize(path).map_err(|source| WorkspaceError::Read {
612        path: PathBuf::from(path),
613        source,
614    })?;
615    let Some((_, target_name)) = members.get(&path) else {
616        return Ok(Vec::new());
617    };
618    let Some(section) = dependency_section(dependency.kind.as_deref()) else {
619        return Ok(Vec::new());
620    };
621    let declaration = Declaration {
622        root_manifest,
623        root,
624        manifest,
625        consumer: &package.name,
626        dependency: target_name,
627        alias: dependency.rename.as_deref().unwrap_or(&dependency.name),
628        path: &path,
629        kind: dependency.kind.as_deref(),
630    };
631    let matching: Vec<_> = dependency_items(package_document, declaration.alias)
632        .into_iter()
633        .filter(|(table_path, _)| {
634            declares_section(table_path, section, dependency.target.as_deref())
635        })
636        .collect();
637    if matching.is_empty() {
638        return Err(declaration.missing_source());
639    }
640    matching
641        .into_iter()
642        .map(|(table_path, item)| declaration.dependency_use(table_path, item))
643        .collect()
644}
645
646/// The manifest section of a `cargo metadata` dependency kind, or `None` for
647/// a kind releases do not edit.
648fn dependency_section(kind: Option<&str>) -> Option<&'static str> {
649    match kind {
650        None | Some("normal") => Some("dependencies"),
651        Some("dev") => Some("dev-dependencies"),
652        Some("build") => Some("build-dependencies"),
653        Some(_) => None,
654    }
655}
656
657/// Whether `table_path` is `section`, either top-level or under the
658/// `target` table for `platform`.
659fn declares_section(table_path: &[String], section: &str, platform: Option<&str>) -> bool {
660    if table_path.last().is_none_or(|value| value != section) {
661        return false;
662    }
663    platform.map_or(table_path.len() == 1, |target| {
664        table_path.len() == 3
665            && table_path
666                .get(1)
667                .is_some_and(|candidate| same_platform(candidate, target))
668    })
669}
670
671fn inherited_dependency<'a>(root: &'a DocumentMut, alias: &str) -> Option<&'a Item> {
672    root.get("workspace")
673        .and_then(Item::as_table_like)
674        .and_then(|table| table.get("dependencies"))
675        .and_then(Item::as_table_like)
676        .and_then(|table| table.get(alias))
677}
678
679fn same_platform(source: &str, metadata: &str) -> bool {
680    match (source.parse::<Platform>(), metadata.parse::<Platform>()) {
681        (Ok(source), Ok(metadata)) => source == metadata,
682        _ => false,
683    }
684}
685
686fn dependency_items<'a>(document: &'a DocumentMut, alias: &str) -> Vec<(Vec<String>, &'a Item)> {
687    let sections = ["dependencies", "dev-dependencies", "build-dependencies"];
688    let mut result = Vec::new();
689    for section in sections {
690        if let Some(item) = document
691            .get(section)
692            .and_then(Item::as_table_like)
693            .and_then(|table| table.get(alias))
694        {
695            result.push((vec![section.to_owned()], item));
696        }
697    }
698    if let Some(targets) = document.get("target").and_then(Item::as_table_like) {
699        for (target, target_item) in targets.iter() {
700            let Some(target_table) = target_item.as_table_like() else {
701                continue;
702            };
703            for section in sections {
704                if let Some(item) = target_table
705                    .get(section)
706                    .and_then(Item::as_table_like)
707                    .and_then(|table| table.get(alias))
708                {
709                    result.push((
710                        vec!["target".to_owned(), target.to_owned(), section.to_owned()],
711                        item,
712                    ));
713                }
714            }
715        }
716    }
717    result
718}
719
720fn parse_manifest(path: &Path, contents: &str) -> Result<DocumentMut, WorkspaceError> {
721    contents
722        .parse::<DocumentMut>()
723        .map_err(|source| WorkspaceError::Manifest {
724            path: path.to_path_buf(),
725            source,
726        })
727}
728
729fn read(path: &Path) -> Result<String, WorkspaceError> {
730    fs::read_to_string(path).map_err(|source| WorkspaceError::Read {
731        path: path.to_path_buf(),
732        source,
733    })
734}
735
736/// Run `cargo metadata` in `directory` and return its standard output.
737pub(super) fn run_metadata(
738    directory: &Path,
739    args: impl IntoIterator<Item = impl AsRef<OsStr>>,
740) -> Result<Vec<u8>, WorkspaceError> {
741    let output = Command::new("cargo")
742        .arg("metadata")
743        .args(args)
744        .current_dir(directory)
745        .output()
746        .map_err(WorkspaceError::MetadataSpawn)?;
747    if output.status.success() {
748        Ok(output.stdout)
749    } else {
750        Err(WorkspaceError::MetadataFailed {
751            status: output.status,
752            stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
753        })
754    }
755}
756
757fn read_metadata(root_manifest: &Path, root: &Path) -> Result<Metadata, WorkspaceError> {
758    let stdout = run_metadata(
759        root,
760        [
761            OsStr::new("--no-deps"),
762            OsStr::new("--locked"),
763            OsStr::new("--offline"),
764            OsStr::new("--format-version=1"),
765            OsStr::new("--manifest-path"),
766            root_manifest.as_os_str(),
767        ],
768    )?;
769    facet_json::from_str(&String::from_utf8_lossy(&stdout))
770        .map_err(|error| WorkspaceError::MetadataDecode(Box::new(error)))
771}
772
773fn metadata_members(metadata: &Metadata) -> Vec<MetadataPackage> {
774    let member_ids: BTreeSet<_> = metadata.workspace_members.iter().collect();
775    let mut packages: Vec<_> = metadata
776        .packages
777        .iter()
778        .filter(|package| member_ids.contains(&package.id))
779        .cloned()
780        .collect();
781    packages.sort_by(|left, right| left.manifest_path.cmp(&right.manifest_path));
782    for package in &mut packages {
783        package.dependencies.sort_by(|left, right| {
784            (
785                &left.name,
786                &left.req,
787                &left.path,
788                &left.rename,
789                &left.kind,
790                &left.target,
791            )
792                .cmp(&(
793                    &right.name,
794                    &right.req,
795                    &right.path,
796                    &right.rename,
797                    &right.kind,
798                    &right.target,
799                ))
800        });
801    }
802    packages
803}
804
805fn metadata_difference(
806    before: &Metadata,
807    after: &Metadata,
808    root_manifest: &Path,
809) -> Option<PathBuf> {
810    let left_members: BTreeSet<_> = before.workspace_members.iter().collect();
811    let right_members: BTreeSet<_> = after.workspace_members.iter().collect();
812    if left_members != right_members {
813        return Some(root_manifest.to_path_buf());
814    }
815    let left = metadata_members(before);
816    let right = metadata_members(after);
817    if left == right {
818        return None;
819    }
820    let right_by_manifest: BTreeMap<_, _> = right
821        .iter()
822        .map(|package| (&package.manifest_path, package))
823        .collect();
824    left.iter()
825        .find(|package| right_by_manifest.get(&package.manifest_path) != Some(package))
826        .map(|package| PathBuf::from(&package.manifest_path))
827        .or_else(|| Some(root_manifest.to_path_buf()))
828}