cargo_release/steps/
plan.rs

1use std::path::Path;
2use std::path::PathBuf;
3
4use crate::config;
5use crate::error::CargoResult;
6use crate::ops::cargo;
7use crate::ops::git;
8use crate::ops::replace::Template;
9use crate::ops::version::VersionExt as _;
10
11pub fn load(
12    args: &config::ConfigArgs,
13    ws_meta: &cargo_metadata::Metadata,
14) -> CargoResult<indexmap::IndexMap<cargo_metadata::PackageId, PackageRelease>> {
15    let root = git::top_level(ws_meta.workspace_root.as_std_path())?;
16
17    let member_ids = cargo::sort_workspace(ws_meta);
18    member_ids
19        .iter()
20        .map(|p| PackageRelease::load(args, &root, ws_meta, &ws_meta[p]))
21        .map(|p| p.map(|p| (p.meta.id.clone(), p)))
22        .collect()
23}
24
25pub fn plan(
26    mut pkgs: indexmap::IndexMap<cargo_metadata::PackageId, PackageRelease>,
27) -> CargoResult<indexmap::IndexMap<cargo_metadata::PackageId, PackageRelease>> {
28    let mut shared_versions: std::collections::HashMap<String, Version> = Default::default();
29    for pkg in pkgs.values() {
30        if !pkg.config.release() {
31            continue;
32        }
33        let group_name = if let Some(group_name) = pkg.config.shared_version() {
34            group_name.to_owned()
35        } else {
36            continue;
37        };
38        let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
39        match shared_versions.entry(group_name) {
40            std::collections::hash_map::Entry::Occupied(mut existing) => {
41                if existing.get().full_version < version.full_version {
42                    existing.insert(version.clone());
43                }
44            }
45            std::collections::hash_map::Entry::Vacant(vacant) => {
46                vacant.insert(version.clone());
47            }
48        }
49    }
50    if !shared_versions.is_empty() {
51        for pkg in pkgs.values_mut() {
52            if !pkg.config.release() {
53                continue;
54            }
55            let group_name = if let Some(group_name) = pkg.config.shared_version() {
56                group_name
57            } else {
58                continue;
59            };
60            let shared_max = shared_versions.get(group_name).unwrap();
61            if pkg.initial_version.bare_version != shared_max.bare_version {
62                pkg.planned_version = Some(shared_max.clone());
63            } else {
64                pkg.planned_version = None;
65            }
66        }
67    }
68
69    for pkg in pkgs.values_mut() {
70        pkg.plan()?;
71    }
72
73    Ok(pkgs)
74}
75
76#[derive(Debug)]
77pub struct PackageRelease {
78    pub meta: cargo_metadata::Package,
79    pub manifest_path: PathBuf,
80    pub package_root: PathBuf,
81    pub is_root: bool,
82    pub config: config::Config,
83
84    pub package_content: Vec<PathBuf>,
85    pub bin: bool,
86    pub dependents: Vec<Dependency>,
87    pub features: cargo::Features,
88
89    pub initial_version: Version,
90    pub prior_tag: Option<String>,
91
92    pub planned_version: Option<Version>,
93    pub planned_tag: Option<String>,
94
95    pub ensure_owners: bool,
96}
97
98impl PackageRelease {
99    pub fn load(
100        args: &config::ConfigArgs,
101        git_root: &Path,
102        ws_meta: &cargo_metadata::Metadata,
103        pkg_meta: &cargo_metadata::Package,
104    ) -> CargoResult<Self> {
105        let meta = pkg_meta.clone();
106        let manifest_path = pkg_meta.manifest_path.as_std_path().to_owned();
107        let package_root = manifest_path
108            .parent()
109            .unwrap_or_else(|| Path::new("."))
110            .to_owned();
111        let config = config::load_package_config(args, ws_meta, pkg_meta)?;
112        if !config.release() {
113            log::trace!("disabled in config, skipping {}", manifest_path.display());
114        }
115
116        let bin = pkg_meta
117            .targets
118            .iter()
119            .flat_map(|t| t.kind.iter())
120            .any(|k| *k == cargo_metadata::TargetKind::Bin);
121        let mut package_content = cargo::package_content(&manifest_path)?;
122        if bin {
123            // When publishing bins, the lock file is listed as relative to the package root, so
124            // let's remap it to the workspace root
125            let lock_file = ws_meta.workspace_root.as_std_path().join("Cargo.lock");
126            if !package_content.contains(&lock_file) {
127                package_content.push(lock_file);
128            }
129        } else {
130            // Lock files are not relevant when publishing non-bins
131            package_content.retain(|p| !p.ends_with("Cargo.lock"));
132        }
133        package_content.retain(|p| {
134            !p.strip_prefix(&package_root)
135                .map(|p| p.starts_with("tests"))
136                .unwrap_or(false)
137        });
138        let features = config.features();
139        let dependents = find_dependents(ws_meta, pkg_meta)
140            .map(|(pkg, dep)| Dependency {
141                pkg: pkg.clone(),
142                req: dep.req.clone(),
143            })
144            .collect();
145
146        let is_root = git_root == package_root;
147        let initial_version = Version::from(pkg_meta.version.clone());
148        let tag_name = config.tag_name();
149        let tag_prefix = config.tag_prefix(is_root);
150        let name = pkg_meta.name.as_str();
151
152        let initial_tag = render_tag(
153            tag_name,
154            tag_prefix,
155            name,
156            &initial_version,
157            &initial_version,
158        );
159        let prior_tag = if git::tag_exists(&package_root, &initial_tag)? {
160            Some(initial_tag)
161        } else {
162            let tag_name = config.tag_name();
163            let tag_prefix = config.tag_prefix(is_root);
164            let name = meta.name.as_str();
165            let tag_glob = render_tag_glob(tag_name, tag_prefix, name);
166            match globset::Glob::new(&tag_glob) {
167                Ok(tag_glob) => {
168                    let tag_glob = tag_glob.compile_matcher();
169                    git::find_last_tag(&package_root, &tag_glob)
170                }
171                Err(err) => {
172                    log::debug!("failed to find tag with glob `{tag_glob}`: {err}");
173                    None
174                }
175            }
176        };
177
178        let planned_version = None;
179        let planned_tag = None;
180        let ensure_owners = config.publish() && !config.owners().is_empty();
181
182        let pkg = PackageRelease {
183            meta,
184            manifest_path,
185            package_root,
186            is_root,
187            config,
188
189            package_content,
190            bin,
191            dependents,
192            features,
193
194            initial_version,
195            prior_tag,
196
197            planned_version,
198            planned_tag,
199            ensure_owners,
200        };
201        Ok(pkg)
202    }
203
204    pub fn set_prior_tag(&mut self, prior_tag: String) {
205        self.prior_tag = Some(prior_tag);
206    }
207
208    pub fn bump<'s>(
209        &'s mut self,
210        level_or_version: &super::TargetVersion,
211        mut metadata: Option<&'s str>,
212    ) -> CargoResult<()> {
213        match self.config.metadata() {
214            config::MetadataPolicy::Optional => {}
215            config::MetadataPolicy::Required => {
216                if metadata.is_none() {
217                    anyhow::bail!(
218                        "`{}` requires the metadata to be overridden",
219                        self.meta.name
220                    )
221                }
222            }
223            config::MetadataPolicy::Ignore => {
224                if let Some(metadata) = metadata {
225                    log::debug!("ignoring metadata `{}` for `{}`", metadata, self.meta.name);
226                }
227                metadata = None;
228            }
229            config::MetadataPolicy::Persistent => {
230                let initial_metadata = &self.initial_version.full_version.build;
231                if !initial_metadata.is_empty() {
232                    metadata.get_or_insert(initial_metadata.as_str());
233                }
234            }
235        }
236        self.planned_version =
237            level_or_version.bump(&self.initial_version.full_version, metadata)?;
238        Ok(())
239    }
240
241    pub fn plan(&mut self) -> CargoResult<()> {
242        if !self.config.release() {
243            return Ok(());
244        }
245
246        let base = self
247            .planned_version
248            .as_ref()
249            .unwrap_or(&self.initial_version);
250        let tag = if self.config.tag() {
251            let tag_name = self.config.tag_name();
252            let tag_prefix = self.config.tag_prefix(self.is_root);
253            let name = self.meta.name.as_str();
254            Some(render_tag(
255                tag_name,
256                tag_prefix,
257                name,
258                &self.initial_version,
259                base,
260            ))
261        } else {
262            None
263        };
264
265        self.planned_tag = tag;
266
267        Ok(())
268    }
269}
270
271fn render_tag(
272    tag_name: &str,
273    tag_prefix: &str,
274    name: &str,
275    prev: &Version,
276    base: &Version,
277) -> String {
278    let initial_version_var = prev.bare_version_string.as_str();
279    let existing_metadata_var = prev.full_version.build.as_str();
280    let version_var = base.bare_version_string.as_str();
281    let metadata_var = base.full_version.build.as_str();
282    let mut template = Template {
283        prev_version: Some(initial_version_var),
284        prev_metadata: Some(existing_metadata_var),
285        version: Some(version_var),
286        metadata: Some(metadata_var),
287        crate_name: Some(name),
288        ..Default::default()
289    };
290
291    let tag_prefix = template.render(tag_prefix);
292    template.prefix = Some(&tag_prefix);
293    template.render(tag_name)
294}
295
296fn render_tag_glob(tag_name: &str, tag_prefix: &str, name: &str) -> String {
297    let initial_version_var = "*";
298    let existing_metadata_var = "*";
299    let version_var = "*";
300    let metadata_var = "*";
301    let mut template = Template {
302        prev_version: Some(initial_version_var),
303        prev_metadata: Some(existing_metadata_var),
304        version: Some(version_var),
305        metadata: Some(metadata_var),
306        crate_name: Some(name),
307        ..Default::default()
308    };
309
310    let tag_prefix = template.render(tag_prefix);
311    template.prefix = Some(&tag_prefix);
312    template.render(tag_name)
313}
314
315fn find_dependents<'w>(
316    ws_meta: &'w cargo_metadata::Metadata,
317    pkg_meta: &'w cargo_metadata::Package,
318) -> impl Iterator<Item = (&'w cargo_metadata::Package, &'w cargo_metadata::Dependency)> {
319    ws_meta.packages.iter().filter_map(move |p| {
320        if ws_meta.workspace_members.contains(&p.id) {
321            p.dependencies
322                .iter()
323                .find(|d| d.name == pkg_meta.name.as_str())
324                .map(|d| (p, d))
325        } else {
326            None
327        }
328    })
329}
330
331#[derive(Debug)]
332pub struct Dependency {
333    pub pkg: cargo_metadata::Package,
334    pub req: semver::VersionReq,
335}
336
337#[derive(Debug, Clone)]
338pub struct Version {
339    pub full_version: semver::Version,
340    pub full_version_string: String,
341    pub bare_version: semver::Version,
342    pub bare_version_string: String,
343}
344
345impl Version {
346    pub fn is_prerelease(&self) -> bool {
347        self.full_version.is_prerelease()
348    }
349}
350
351impl From<semver::Version> for Version {
352    fn from(full_version: semver::Version) -> Self {
353        let full_version_string = full_version.to_string();
354        let mut bare_version = full_version.clone();
355        bare_version.build = semver::BuildMetadata::EMPTY;
356        let bare_version_string = bare_version.to_string();
357        Self {
358            full_version,
359            full_version_string,
360            bare_version,
361            bare_version_string,
362        }
363    }
364}