ic_build/cargo/release_metadata/
mod.rs1use super::lock_projection::{
8 LockProjectionError, WorkspaceVersionProjection, verify_workspace_release_projection,
9};
10use std::{collections::HashSet, ops::Range};
11use toml_edit::{Document, Item, Value};
12
13#[derive(Clone, Copy, Debug)]
15pub struct DependencyVersionProjection<'a> {
16 pub alias: &'a str,
18 pub previous: &'a str,
20 pub next: &'a str,
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum ReleaseMetadataError {
27 #[error("{0}")]
29 InvalidSelection(&'static str),
30 #[error("{0}")]
32 Parse(#[source] toml_edit::TomlError),
33 #[error("{0}")]
35 Projection(#[source] LockProjectionError),
36}
37
38const fn refuse(reason: &'static str) -> ReleaseMetadataError {
39 ReleaseMetadataError::InvalidSelection(reason)
40}
41fn stable(version: &str) -> bool {
42 let mut count = 0;
43 version.split('.').all(|part| {
44 count += 1;
45 !part.is_empty()
46 && part.bytes().all(|byte| byte.is_ascii_digit())
47 && (part == "0" || !part.starts_with('0'))
48 }) && count == 3
49}
50fn edit(
51 value: &Value,
52 previous: &str,
53 next: &str,
54) -> Result<(Range<usize>, String), ReleaseMetadataError> {
55 if value.as_str() != Some(previous) {
56 return Err(refuse("selected version differs from the admitted input"));
57 }
58 let span = value
59 .span()
60 .ok_or_else(|| refuse("selected literal has no source span"))?;
61 Ok((span, Value::from(next).to_string()))
62}
63fn apply(
64 input: &str,
65 mut edits: Vec<(Range<usize>, String)>,
66) -> Result<String, ReleaseMetadataError> {
67 edits.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
68 let mut output = input.to_owned();
69 let mut boundary = input.len();
70 for (span, replacement) in edits {
71 if span.end > boundary || input.get(span.clone()).is_none() {
72 return Err(refuse("overlapping or invalid selected literal spans"));
73 }
74 boundary = span.start;
75 output.replace_range(span, &replacement);
76 }
77 Document::parse(output.as_str()).map_err(ReleaseMetadataError::Parse)?;
78 Ok(output)
79}
80
81pub fn render_workspace_manifest(
91 input: &str,
92 previous: &str,
93 next: &str,
94 dependencies: &[DependencyVersionProjection<'_>],
95) -> Result<String, ReleaseMetadataError> {
96 if !stable(previous) || !stable(next) {
97 return Err(refuse("expected canonical stable workspace versions"));
98 }
99 let document = Document::parse(input).map_err(ReleaseMetadataError::Parse)?;
100 let workspace = document
101 .as_item()
102 .get("workspace")
103 .ok_or_else(|| refuse("missing workspace"))?;
104 let version = workspace
105 .get("package")
106 .and_then(|item| item.get("version"))
107 .and_then(Item::as_value)
108 .ok_or_else(|| refuse("missing workspace.package.version"))?;
109 let mut edits = vec![edit(version, previous, next)?];
110 let mut seen = HashSet::new();
111 for dependency in dependencies {
112 if dependency.alias.is_empty()
113 || !seen.insert(dependency.alias)
114 || dependency.previous.is_empty()
115 || dependency.next.is_empty()
116 {
117 return Err(refuse("invalid or duplicate dependency selection"));
118 }
119 let version = workspace
120 .get("dependencies")
121 .and_then(|item| item.get(dependency.alias))
122 .and_then(|item| item.get("version"))
123 .and_then(Item::as_value)
124 .ok_or_else(|| refuse("missing selected dependency version"))?;
125 edits.push(edit(version, dependency.previous, dependency.next)?);
126 }
127 apply(input, edits)
128}
129
130pub fn render_workspace_lock(
140 input: &str,
141 projections: &[WorkspaceVersionProjection<'_>],
142) -> Result<String, ReleaseMetadataError> {
143 if projections.is_empty()
144 || projections
145 .iter()
146 .any(|item| !stable(item.previous) || !stable(item.next) || item.previous == item.next)
147 {
148 return Err(refuse("expected distinct canonical stable lock versions"));
149 }
150 match verify_workspace_release_projection(input, input, projections) {
153 Err(LockProjectionError::ChangedInputs) => {}
154 Err(error) => return Err(ReleaseMetadataError::Projection(error)),
155 Ok(()) => return Err(refuse("lock selection changes no versions")),
156 }
157 let document = Document::parse(input).map_err(ReleaseMetadataError::Parse)?;
158 if !matches!(
159 document.as_item().get("version").and_then(Item::as_integer),
160 Some(3 | 4)
161 ) {
162 return Err(refuse("expected Cargo lock format 3 or 4"));
163 }
164 let packages = document
165 .as_item()
166 .get("package")
167 .and_then(Item::as_array_of_tables)
168 .ok_or_else(|| refuse("missing lock packages"))?;
169 let mut edits = Vec::new();
170 for package in packages {
171 if package.get("source").is_none()
172 && let Some(projection) = projections
173 .iter()
174 .find(|item| package.get("name").and_then(Item::as_str) == Some(item.package))
175 {
176 let value = package
177 .get("version")
178 .and_then(Item::as_value)
179 .ok_or_else(|| refuse("missing local package version"))?;
180 edits.push(edit(value, projection.previous, projection.next)?);
181 }
182 if let Some(dependencies) = package.get("dependencies").and_then(Item::as_array) {
183 for dependency in dependencies {
184 for projection in projections {
185 let previous = format!("{} {}", projection.package, projection.previous);
186 if dependency.as_str() == Some(previous.as_str()) {
187 edits.push(edit(
188 dependency,
189 &previous,
190 &format!("{} {}", projection.package, projection.next),
191 )?);
192 }
193 }
194 }
195 }
196 }
197 let output = apply(input, edits)?;
198 verify_workspace_release_projection(input, &output, projections)
199 .map_err(ReleaseMetadataError::Projection)?;
200 Ok(output)
201}
202
203#[cfg(test)]
204mod tests;