Skip to content

Commit dc35924

Browse files
committed
Decouple lockfile manifest version mismatch handling, add error for it
1 parent 652b22c commit dc35924

File tree

8 files changed

+369
-275
lines changed

8 files changed

+369
-275
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cargo-gpu-build/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ license.workspace = true
1010
[dependencies]
1111
rustc_codegen_spirv-cache.workspace = true
1212
thiserror.workspace = true
13+
semver.workspace = true
14+
log.workspace = true
1315

1416
[lints]
1517
workspace = true

crates/cargo-gpu-build/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@
2525
2626
pub use rustc_codegen_spirv_cache as cache;
2727

28+
pub mod lockfile;
29+
2830
// TODO build script API without shader crate path repetitions
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
//! Handles lockfile version conflicts and downgrades.
2+
//!
3+
//! Stable uses lockfile v4, but `rust-gpu` v0.9.0 uses an old toolchain requiring v3
4+
//! and will refuse to build shader crate with a v4 lockfile being present.
5+
//! This module takes care of warning the user and potentially downgrading the lockfile.
6+
7+
#![expect(clippy::non_ascii_literal, reason = "'⚠️' character is really needed")]
8+
9+
use std::{
10+
fs,
11+
io::{self, Write as _},
12+
path::{Path, PathBuf},
13+
};
14+
15+
use rustc_codegen_spirv_cache::spirv_builder::query_rustc_version;
16+
use semver::Version;
17+
18+
/// `Cargo.lock` manifest version 4 became the default in Rust 1.83.0. Conflicting manifest
19+
/// versions between the workspace and the shader crate, can cause problems.
20+
const RUST_VERSION_THAT_USES_V4_CARGO_LOCKS: Version = Version::new(1, 83, 0);
21+
22+
/// Cargo dependency for `spirv-builder` and the rust toolchain channel.
23+
#[derive(Debug, Clone)]
24+
#[non_exhaustive]
25+
#[expect(clippy::module_name_repetitions, reason = "it is intended")]
26+
pub struct LockfileMismatchHandler {
27+
/// `Cargo.lock`s that have had their manifest versions changed by us and need changing back.
28+
pub cargo_lock_files_with_changed_manifest_versions: Vec<PathBuf>,
29+
}
30+
31+
impl LockfileMismatchHandler {
32+
/// Creates self from the given parameters.
33+
///
34+
/// # Errors
35+
///
36+
/// Returns an error if there was a problem checking or changing lockfile manifest versions.
37+
/// See [`LockfileMismatchError`] for details.
38+
#[inline]
39+
pub fn new(
40+
shader_crate_path: &Path,
41+
toolchain_channel: &str,
42+
is_force_overwrite_lockfiles_v4_to_v3: bool,
43+
) -> Result<Self, LockfileMismatchError> {
44+
let mut cargo_lock_files_with_changed_manifest_versions = vec![];
45+
46+
let maybe_shader_crate_lock =
47+
Self::ensure_workspace_rust_version_does_not_conflict_with_shader(
48+
shader_crate_path,
49+
is_force_overwrite_lockfiles_v4_to_v3,
50+
)?;
51+
52+
if let Some(shader_crate_lock) = maybe_shader_crate_lock {
53+
cargo_lock_files_with_changed_manifest_versions.push(shader_crate_lock);
54+
}
55+
56+
let maybe_workspace_crate_lock =
57+
Self::ensure_shader_rust_version_does_not_conflict_with_any_cargo_locks(
58+
shader_crate_path,
59+
toolchain_channel,
60+
is_force_overwrite_lockfiles_v4_to_v3,
61+
)?;
62+
63+
if let Some(workspace_crate_lock) = maybe_workspace_crate_lock {
64+
cargo_lock_files_with_changed_manifest_versions.push(workspace_crate_lock);
65+
}
66+
67+
Ok(Self {
68+
cargo_lock_files_with_changed_manifest_versions,
69+
})
70+
}
71+
72+
/// See docs for [`force_overwrite_lockfiles_v4_to_v3`](crate::cache::install::InstallParams::force_overwrite_lockfiles_v4_to_v3)
73+
/// flag for why we do this.
74+
fn ensure_workspace_rust_version_does_not_conflict_with_shader(
75+
shader_crate_path: &Path,
76+
is_force_overwrite_lockfiles_v4_to_v3: bool,
77+
) -> Result<Option<PathBuf>, LockfileMismatchError> {
78+
log::debug!("Ensuring no v3/v4 `Cargo.lock` conflicts from workspace Rust...");
79+
let workspace_rust_version =
80+
query_rustc_version(None).map_err(LockfileMismatchError::QueryRustcVersion)?;
81+
if workspace_rust_version >= RUST_VERSION_THAT_USES_V4_CARGO_LOCKS {
82+
log::debug!(
83+
"user's Rust is v{workspace_rust_version}, so no v3/v4 conflicts possible."
84+
);
85+
return Ok(None);
86+
}
87+
88+
Self::handle_conflicting_cargo_lock_v4(
89+
shader_crate_path,
90+
is_force_overwrite_lockfiles_v4_to_v3,
91+
)?;
92+
93+
if is_force_overwrite_lockfiles_v4_to_v3 {
94+
Ok(Some(shader_crate_path.join("Cargo.lock")))
95+
} else {
96+
Ok(None)
97+
}
98+
}
99+
100+
/// See docs for [`force_overwrite_lockfiles_v4_to_v3`](crate::cache::install::InstallParams::force_overwrite_lockfiles_v4_to_v3)
101+
/// flag for why we do this.
102+
fn ensure_shader_rust_version_does_not_conflict_with_any_cargo_locks(
103+
shader_crate_path: &Path,
104+
channel: &str,
105+
is_force_overwrite_lockfiles_v4_to_v3: bool,
106+
) -> Result<Option<PathBuf>, LockfileMismatchError> {
107+
log::debug!("Ensuring no v3/v4 `Cargo.lock` conflicts from shader's Rust...");
108+
let shader_rust_version =
109+
query_rustc_version(Some(channel)).map_err(LockfileMismatchError::QueryRustcVersion)?;
110+
if shader_rust_version >= RUST_VERSION_THAT_USES_V4_CARGO_LOCKS {
111+
log::debug!("shader's Rust is v{shader_rust_version}, so no v3/v4 conflicts possible.");
112+
return Ok(None);
113+
}
114+
115+
log::debug!(
116+
"shader's Rust is v{shader_rust_version}, so checking both shader and workspace `Cargo.lock` manifest versions..."
117+
);
118+
119+
if shader_crate_path.join("Cargo.lock").exists() {
120+
// Note that we don't return the `Cargo.lock` here (so that it's marked for reversion
121+
// after the build), because we can be sure that updating it now is actually updating it
122+
// to the state it should have been all along. Therefore it doesn't need reverting once
123+
// fixed.
124+
Self::handle_conflicting_cargo_lock_v4(
125+
shader_crate_path,
126+
is_force_overwrite_lockfiles_v4_to_v3,
127+
)?;
128+
}
129+
130+
if let Some(workspace_root) = Self::get_workspace_root(shader_crate_path)? {
131+
Self::handle_conflicting_cargo_lock_v4(
132+
workspace_root,
133+
is_force_overwrite_lockfiles_v4_to_v3,
134+
)?;
135+
return Ok(Some(workspace_root.join("Cargo.lock")));
136+
}
137+
138+
Ok(None)
139+
}
140+
141+
/// Get the path to the shader crate's workspace, if it has one. We can't use the traditional
142+
/// `cargo metadata` because if the workspace has a conflicting `Cargo.lock` manifest version
143+
/// then that command won't work. Instead we do an old school recursive file tree walk.
144+
fn get_workspace_root(
145+
shader_crate_path: &Path,
146+
) -> Result<Option<&Path>, LockfileMismatchError> {
147+
let shader_cargo_toml_path = shader_crate_path.join("Cargo.toml");
148+
let shader_cargo_toml = match fs::read_to_string(shader_cargo_toml_path) {
149+
Ok(contents) => contents,
150+
Err(source) => {
151+
let file = shader_crate_path.join("Cargo.toml");
152+
return Err(LockfileMismatchError::ReadFile { file, source });
153+
}
154+
};
155+
if !shader_cargo_toml.contains("workspace = true") {
156+
return Ok(None);
157+
}
158+
159+
let mut current_path = shader_crate_path;
160+
#[expect(clippy::default_numeric_fallback, reason = "It's just a loop")]
161+
for _ in 0..15 {
162+
if let Some(parent_path) = current_path.parent() {
163+
if parent_path.join("Cargo.lock").exists() {
164+
return Ok(Some(parent_path));
165+
}
166+
current_path = parent_path;
167+
} else {
168+
break;
169+
}
170+
}
171+
172+
Ok(None)
173+
}
174+
175+
/// When Rust < 1.83.0 is being used an error will occur if it tries to parse `Cargo.lock`
176+
/// files that use lockfile manifest version 4. Here we check and handle that.
177+
fn handle_conflicting_cargo_lock_v4(
178+
folder: &Path,
179+
is_force_overwrite_lockfiles_v4_to_v3: bool,
180+
) -> Result<(), LockfileMismatchError> {
181+
let shader_cargo_lock_path = folder.join("Cargo.lock");
182+
let shader_cargo_lock = match fs::read_to_string(&shader_cargo_lock_path) {
183+
Ok(contents) => contents,
184+
Err(source) => {
185+
let file = shader_cargo_lock_path;
186+
return Err(LockfileMismatchError::ReadFile { file, source });
187+
}
188+
};
189+
190+
let Some(third_line) = shader_cargo_lock.lines().nth(2) else {
191+
let file = shader_cargo_lock_path;
192+
return Err(LockfileMismatchError::TooFewLinesInLockfile { file });
193+
};
194+
if third_line.contains("version = 4") {
195+
Self::handle_v3v4_conflict(
196+
&shader_cargo_lock_path,
197+
is_force_overwrite_lockfiles_v4_to_v3,
198+
)?;
199+
return Ok(());
200+
}
201+
if third_line.contains("version = 3") {
202+
return Ok(());
203+
}
204+
205+
let file = shader_cargo_lock_path;
206+
let version_line = third_line.to_owned();
207+
Err(LockfileMismatchError::UnrecognizedLockfileVersion { file, version_line })
208+
}
209+
210+
/// Handle conflicting `Cargo.lock` manifest versions by either overwriting the manifest
211+
/// version or exiting with advice on how to handle the conflict.
212+
fn handle_v3v4_conflict(
213+
offending_cargo_lock: &Path,
214+
is_force_overwrite_lockfiles_v4_to_v3: bool,
215+
) -> Result<(), LockfileMismatchError> {
216+
if !is_force_overwrite_lockfiles_v4_to_v3 {
217+
return Err(LockfileMismatchError::ConflictingVersions);
218+
}
219+
220+
Self::replace_cargo_lock_manifest_version(offending_cargo_lock, "4", "3")
221+
}
222+
223+
/// Once all install and builds have completed put their manifest versions
224+
/// back to how they were.
225+
///
226+
/// # Errors
227+
///
228+
/// Returns an error if there was a problem reverting any of the lockfiles.
229+
/// See [`LockfileMismatchError`] for details.
230+
#[inline]
231+
pub fn revert_cargo_lock_manifest_versions(&self) -> Result<(), LockfileMismatchError> {
232+
for offending_cargo_lock in &self.cargo_lock_files_with_changed_manifest_versions {
233+
log::debug!("Reverting: {}", offending_cargo_lock.display());
234+
Self::replace_cargo_lock_manifest_version(offending_cargo_lock, "3", "4")?;
235+
}
236+
Ok(())
237+
}
238+
239+
/// Replace the manifest version, eg `version = 4`, in a `Cargo.lock` file.
240+
fn replace_cargo_lock_manifest_version(
241+
offending_cargo_lock: &Path,
242+
from_version: &str,
243+
to_version: &str,
244+
) -> Result<(), LockfileMismatchError> {
245+
log::warn!(
246+
"Replacing manifest version 'version = {from_version}' with 'version = {to_version}' in: {}",
247+
offending_cargo_lock.display()
248+
);
249+
let old_contents = match fs::read_to_string(offending_cargo_lock) {
250+
Ok(contents) => contents,
251+
Err(source) => {
252+
let file = offending_cargo_lock.to_path_buf();
253+
return Err(LockfileMismatchError::ReadFile { file, source });
254+
}
255+
};
256+
let new_contents = old_contents.replace(
257+
&format!("\nversion = {from_version}\n"),
258+
&format!("\nversion = {to_version}\n"),
259+
);
260+
261+
if let Err(source) = fs::OpenOptions::new()
262+
.write(true)
263+
.truncate(true)
264+
.open(offending_cargo_lock)
265+
.and_then(|mut file| file.write_all(new_contents.as_bytes()))
266+
{
267+
let err = LockfileMismatchError::RewriteLockfile {
268+
file: offending_cargo_lock.to_path_buf(),
269+
from_version: from_version.to_owned(),
270+
to_version: to_version.to_owned(),
271+
source,
272+
};
273+
return Err(err);
274+
}
275+
276+
Ok(())
277+
}
278+
}
279+
280+
impl Drop for LockfileMismatchHandler {
281+
#[inline]
282+
fn drop(&mut self) {
283+
let result = self.revert_cargo_lock_manifest_versions();
284+
if let Err(error) = result {
285+
log::error!("Couldn't revert some or all of the shader `Cargo.lock` files ({error})");
286+
}
287+
}
288+
}
289+
290+
/// An error indicating a problem occurred
291+
/// while handling lockfile manifest version mismatches.
292+
#[derive(Debug, thiserror::Error)]
293+
#[non_exhaustive]
294+
#[expect(clippy::module_name_repetitions, reason = "it is intended")]
295+
pub enum LockfileMismatchError {
296+
/// Could not query current rustc version.
297+
#[error("could not query rustc version: {0}")]
298+
QueryRustcVersion(#[source] io::Error),
299+
/// Could not read contents of the file.
300+
#[error("could not read file {file}: {source}")]
301+
ReadFile {
302+
/// Path to the file that couldn't be read.
303+
file: PathBuf,
304+
/// Source of the error.
305+
source: io::Error,
306+
},
307+
/// Could not rewrite the lockfile with new manifest version.
308+
#[error(
309+
"could not rewrite lockfile {file} from version {from_version} to {to_version}: {source}"
310+
)]
311+
RewriteLockfile {
312+
/// Path to the file that couldn't be rewritten.
313+
file: PathBuf,
314+
/// Old manifest version we were changing from.
315+
from_version: String,
316+
/// New manifest version we were changing to.
317+
to_version: String,
318+
/// Source of the error.
319+
source: io::Error,
320+
},
321+
/// Lockfile has too few lines to determine manifest version.
322+
#[error("lockfile at {file} has too few lines to determine manifest version")]
323+
TooFewLinesInLockfile {
324+
/// Path to the lockfile that contains too few lines.
325+
file: PathBuf,
326+
},
327+
/// Lockfile manifest version could not be recognized.
328+
#[error("unrecognized lockfile {file} manifest version at \"{version_line}\"")]
329+
UnrecognizedLockfileVersion {
330+
/// Path to the lockfile that contains the unrecognized version line.
331+
file: PathBuf,
332+
/// The unrecognized version line.
333+
version_line: String,
334+
},
335+
/// Conflicting lockfile manifest versions detected, with advice on how to resolve them
336+
/// by setting the [`force_overwrite_lockfiles_v4_to_v3`] flag.
337+
///
338+
/// [`force_overwrite_lockfiles_v4_to_v3`]: crate::cache::install::InstallParams::force_overwrite_lockfiles_v4_to_v3
339+
#[error(
340+
r#"conflicting `Cargo.lock` versions detected ⚠️
341+
342+
Because a dedicated Rust toolchain for compiling shaders is being used,
343+
it's possible that the `Cargo.lock` manifest version of the shader crate
344+
does not match the `Cargo.lock` manifest version of the workspace.
345+
This is due to a change in the defaults introduced in Rust 1.83.0.
346+
347+
One way to resolve this is to force the workspace to use the same version
348+
of Rust as required by the shader. However, that is not often ideal or even
349+
possible. Another way is to exclude the shader from the workspace. This is
350+
also not ideal if you have many shaders sharing config from the workspace.
351+
352+
Therefore, `cargo gpu build/install` offers a workaround with the argument:
353+
--force-overwrite-lockfiles-v4-to-v3
354+
355+
which corresponds to the `force_overwrite_lockfiles_v4_to_v3` flag of `InstallParams`.
356+
357+
See `cargo gpu build --help` or flag docs for more information."#
358+
)]
359+
ConflictingVersions,
360+
}

0 commit comments

Comments
 (0)