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 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 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}