Skip to main content

bomper/replacers/
cargo.rs

1use anyhow::anyhow;
2use cargo_metadata::camino::Utf8Path;
3use std::path::Path;
4use std::{io::prelude::*, path::PathBuf, str::FromStr};
5
6use super::file;
7use super::VersionReplacement;
8use crate::config::CargoReplaceMode;
9use crate::error::{Error, Result};
10use crate::replacers::ReplacementBuilder;
11
12/// Replaces all instances of a given value with a new one.
13/// This is a somewhat naive implementation, but it works.
14/// The area surrounding the value will be checked for matches in the supplied regex
15pub struct Replacer {
16    lock_path: PathBuf,
17    versions: VersionReplacement,
18    replace_mode: CargoReplaceMode,
19}
20
21impl Replacer {
22    #[must_use]
23    pub fn new(versions: VersionReplacement, replace_mode: CargoReplaceMode) -> Self {
24        Self {
25            // TODO: This may need to be specified, or detected
26            lock_path: PathBuf::from("Cargo.lock"),
27            versions,
28            replace_mode,
29        }
30    }
31}
32
33impl ReplacementBuilder for Replacer {
34    fn determine_replacements(self) -> Result<Option<Vec<file::Replacer>>> {
35        let mut replacers = Vec::new();
36
37        let metadata = get_workspace_metadata()?;
38        let workspace_root = &metadata.workspace_root;
39
40        // Read in the file
41        let mut lockfile = cargo_lock::Lockfile::load(&self.lock_path)?;
42
43        let packages = match &self.replace_mode {
44            CargoReplaceMode::Autodetect => metadata.packages,
45            CargoReplaceMode::Packages(packages) => list_packages(&metadata, packages),
46        };
47
48        let new_version = cargo_lock::Version::from_str(&self.versions.new_version)?;
49        let old_version = cargo_lock::Version::from_str(&self.versions.old_version)?;
50
51        let package_names = packages
52            .iter()
53            .map(|package| package.name.as_str().to_string())
54            .collect::<Vec<String>>();
55
56        // update cargo.lock with new versions of packages
57        lockfile.packages.iter_mut().for_each(|package| {
58            let package_name = package.name.as_str().to_string();
59
60            if package_names.contains(&package_name) && package.version == old_version {
61                package.version = new_version.clone();
62            }
63        });
64
65        let new_data = lockfile.to_string().into_bytes();
66
67        let temp_file = tempfile::NamedTempFile::new_in(
68            (self.lock_path)
69                .parent()
70                .ok_or_else(|| Error::InvalidPath(self.lock_path.clone()))?,
71        )?;
72        let mut file = temp_file.as_file();
73        file.write_all(&new_data)?;
74
75        replacers.push(file::Replacer {
76            path: self.lock_path.clone(),
77            temp_file,
78        });
79
80        // Update each package's Cargo.toml with the new version
81        for package in packages {
82            let replacer =
83                update_package(&package, workspace_root, &self.lock_path, &self.versions)?;
84            if let Some(replacer) = replacer {
85                replacers.push(replacer);
86            };
87        }
88
89        // Now, we need to update the Cargo.toml in the workspace root
90        // This can be a bit more complicated, because the workspace root may be one of the packages
91        // (specifically if there is a single package in the workspace)
92        // In this case we need to find the package that is the workspace root, and if we're already
93        // updating that package
94
95        // First, check to see if we've updated the `Cargo.toml` in the workspace root
96        let root_toml_path = workspace_root.join("Cargo.toml");
97        let found_workspace_root = replacers
98            .iter()
99            .find(|replacer| replacer.path == root_toml_path);
100
101        if found_workspace_root.is_none() {
102            // We haven't updated the workspace root, so we need to do that now
103            let replacer = update_workspace_root(workspace_root, &self.versions)?;
104            if let Some(replacer) = replacer {
105                replacers.push(replacer);
106            };
107        }
108
109        Ok(Some(replacers))
110    }
111}
112
113/// Returns all packages in the cargo workspace that match the given name
114fn list_packages(
115    metadata: &cargo_metadata::Metadata,
116    names: &[String],
117) -> Vec<cargo_metadata::Package> {
118    metadata
119        .clone()
120        .packages
121        .into_iter()
122        .filter(|package| names.contains(&package.name))
123        .collect()
124}
125
126/// Retrieves the metadata for the current workspace.
127fn get_workspace_metadata() -> Result<cargo_metadata::Metadata> {
128    let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
129    metadata_cmd.features(cargo_metadata::CargoOpt::AllFeatures);
130    metadata_cmd.no_deps();
131
132    let metadata = metadata_cmd.exec()?;
133
134    Ok(metadata)
135}
136
137/// Updates the workspace root's Cargo.toml with the new version
138fn update_workspace_root(
139    workspace_root: &Utf8Path,
140    versions: &VersionReplacement,
141) -> Result<Option<file::Replacer>> {
142    let cargo_toml_path = workspace_root.join("Cargo.toml");
143    let cargo_toml_path = cargo_toml_path.strip_prefix(workspace_root)?;
144    let cargo_toml_content = std::fs::read(cargo_toml_path)?;
145
146    let mut cargo_toml = cargo_toml::Manifest::from_slice(&cargo_toml_content)?;
147
148    let Some(ref mut workspace) = cargo_toml.workspace else {
149        return Ok(None);
150    };
151    let Some(ref mut workspace_package) = workspace.package else {
152        return Ok(None);
153    };
154
155    if workspace_package.version != Some(versions.old_version.clone()) {
156        return Ok(None);
157    }
158    workspace_package.version = Some(versions.new_version.clone());
159
160    let temp_file = tempfile::NamedTempFile::new_in(
161        (workspace_root)
162            .parent()
163            .ok_or_else(|| Error::Other(anyhow!("Invalid path: {:?}", workspace_root)))?,
164    )?;
165    let mut file = temp_file.as_file();
166
167    let data = toml::to_string(&cargo_toml)?;
168    file.write_all(data.as_bytes())?;
169
170    Ok(Some(file::Replacer {
171        path: cargo_toml_path.into(),
172        temp_file,
173    }))
174}
175
176/// Updates a package's Cargo.toml with the new version
177fn update_package(
178    package: &cargo_metadata::Package,
179    workspace_root: &Utf8Path,
180    lock_path: &Path,
181    versions: &VersionReplacement,
182) -> Result<Option<file::Replacer>> {
183    let cargo_toml_path = package.manifest_path.clone();
184    let cargo_toml_path = cargo_toml_path.strip_prefix(workspace_root)?;
185    let cargo_toml_content = std::fs::read(cargo_toml_path)?;
186
187    let mut cargo_toml = cargo_toml::Manifest::from_slice(&cargo_toml_content)?;
188    // let mut cargo_toml = cargo_toml::Manifest::from_path(&cargo_toml_path)?;
189
190    {
191        let Some(ref mut toml_package) = cargo_toml.package else {
192            return Err(Error::InvalidCargoToml(cargo_toml_path.into()));
193        };
194
195        let file_version = match &mut toml_package.version {
196            // If the version is inherited, we don't need to do anything
197            cargo_toml::Inheritable::Inherited { .. } => return Ok(None),
198            cargo_toml::Inheritable::Set(value) => value,
199        };
200
201        if file_version != &versions.old_version {
202            return Ok(None);
203        }
204
205        file_version.clone_from(&versions.new_version);
206    }
207
208    // check if this is a workspace root
209    // if it is, we need to update the workspace root's Cargo.toml
210    let workspace_root_toml_path = workspace_root.join("Cargo.toml");
211    if cargo_toml_path == workspace_root_toml_path {
212        modify_workspace_root(&mut cargo_toml, versions);
213    }
214
215    let temp_file = tempfile::NamedTempFile::new_in(
216        (lock_path)
217            .parent()
218            .ok_or_else(|| Error::InvalidPath((lock_path).to_path_buf()))?,
219    )?;
220    let mut file = temp_file.as_file();
221
222    let data = toml::to_string(&cargo_toml)?;
223    file.write_all(data.as_bytes())?;
224
225    Ok(Some(file::Replacer {
226        path: cargo_toml_path.into(),
227        temp_file,
228    }))
229}
230
231fn modify_workspace_root(cargo_toml: &mut cargo_toml::Manifest, versions: &VersionReplacement) {
232    let Some(ref mut workspace) = cargo_toml.workspace else {
233        return;
234    };
235    let Some(ref mut workspace_package) = workspace.package else {
236        return;
237    };
238
239    if workspace_package.version != Some(versions.old_version.clone()) {
240        return;
241    }
242    workspace_package.version = Some(versions.new_version.clone());
243}