release_utils/
package.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::cmd::{
10    format_cmd, get_cmd_stdout_utf8, wait_for_child, RunCommandError,
11};
12use std::env;
13use std::fmt::{self, Display, Formatter};
14use std::path::{Path, PathBuf};
15use std::process::{Command, Stdio};
16
17/// A package in the workspace.
18#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
19pub struct Package {
20    /// Path of the root workspace directory, or just the directory of
21    /// the package in non-workspace projects.
22    workspace: PathBuf,
23
24    /// Name of the package.
25    name: String,
26}
27
28impl Package {
29    /// Create a `Package` with the given name.
30    ///
31    /// This uses the current directory as the the workspace path.
32    pub fn new<S>(name: S) -> Self
33    where
34        S: Into<String>,
35    {
36        let workspace = env::current_dir().unwrap();
37        Self::with_workspace(name, workspace)
38    }
39
40    /// Create a `Package` with the given name and workspace.
41    ///
42    /// The workspace directory should be the root of the workspace, or
43    /// just the directory of the package in non-workspace projects.
44    pub fn with_workspace<S, P>(name: S, workspace: P) -> Self
45    where
46        S: Into<String>,
47        P: Into<PathBuf>,
48    {
49        Self {
50            workspace: workspace.into(),
51            name: name.into(),
52        }
53    }
54
55    /// Get the package's name.
56    pub fn name(&self) -> &str {
57        &self.name
58    }
59
60    /// Get the package's root workspace directory.
61    pub fn workspace(&self) -> &Path {
62        &self.workspace
63    }
64
65    /// Format a package version as a git tag.
66    pub fn get_git_tag_name(&self, local_version: &str) -> String {
67        format!("{}-v{}", self.name, local_version)
68    }
69
70    /// Use `cargo metadata` to get the local version of a package
71    /// in the workspace.
72    pub fn get_local_version(&self) -> Result<String, GetLocalVersionError> {
73        // Spawn `cargo metadata`. The output goes to a new pipe, which
74        // will be passed as the input to `jq`.
75        let mut metadata_cmd = self.get_cargo_metadata_cmd();
76        let metadata_cmd_str = format_cmd(&metadata_cmd);
77        println!("Running: {}", metadata_cmd_str);
78        let mut metadata_proc =
79            metadata_cmd.stdout(Stdio::piped()).spawn().map_err(|err| {
80                GetLocalVersionError::Process(RunCommandError::Launch {
81                    cmd: metadata_cmd_str.clone(),
82                    err,
83                })
84            })?;
85
86        // OK to unwrap, we know stdout is set.
87        let pipe = metadata_proc.stdout.take().unwrap();
88
89        let mut jq_cmd = Command::new("jq");
90        jq_cmd.arg("--raw-output");
91        jq_cmd.arg(format!(
92            ".packages[] | select(.name == \"{}\") | .version",
93            self.name
94        ));
95        jq_cmd.stdin(pipe);
96
97        let mut output = get_cmd_stdout_utf8(jq_cmd)
98            .map_err(GetLocalVersionError::Process)?;
99
100        wait_for_child(metadata_proc, metadata_cmd_str)
101            .map_err(GetLocalVersionError::Process)?;
102
103        if output.is_empty() {
104            Err(GetLocalVersionError::PackageNotFound(
105                self.name().to_string(),
106            ))
107        } else {
108            // Remove trailing newline.
109            output.pop();
110            Ok(output)
111        }
112    }
113
114    fn get_cargo_metadata_cmd(&self) -> Command {
115        let mut cmd = Command::new("cargo");
116        cmd.arg("metadata");
117        cmd.args(["--format-version", "1"]);
118        cmd.arg("--manifest-path");
119        cmd.arg(self.workspace.join("Cargo.toml"));
120        // Ignore deps, we only need local packages.
121        cmd.arg("--no-deps");
122        cmd
123    }
124}
125
126/// Error returned by [`Package::get_local_version`].
127#[derive(Debug)]
128pub enum GetLocalVersionError {
129    /// A child process failed.
130    Process(RunCommandError),
131
132    /// Requested package not found in the metadata.
133    PackageNotFound(String),
134}
135
136impl Display for GetLocalVersionError {
137    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
138        match self {
139            Self::Process(err) => {
140                write!(f, "failed to get cargo metadata: {err}")
141            }
142            Self::PackageNotFound(pkg) => {
143                write!(f, "package {pkg} not found in cargo metadata")
144            }
145        }
146    }
147}
148
149impl std::error::Error for GetLocalVersionError {}