use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use clap::ValueEnum;
use xshell::Shell;
use crate::environment::{get_workspace_root, CmdExt, ProgressGuard, WorkspaceManifest};
use crate::toolchain::{prepare_toolchain, Toolchain};
const CARGO_LOCK: &str = "Cargo.lock";
const CARGO_LOCK_BACKUP: &str = "Cargo.lock.backup";
const NUL: char = '\0';
pub struct LockFileGuard {
backup_path: PathBuf,
restore_path: PathBuf,
}
impl LockFileGuard {
pub fn new(sh: &Shell) -> Result<Self, Box<dyn std::error::Error>> {
let workspace_root = get_workspace_root(sh)?;
let source = workspace_root.join(CARGO_LOCK);
let backup = workspace_root.join(CARGO_LOCK_BACKUP);
if source.exists() {
fs::copy(&source, &backup)?;
}
Ok(Self { backup_path: backup, restore_path: source })
}
}
impl Drop for LockFileGuard {
fn drop(&mut self) {
if self.backup_path.exists() {
if let Err(e) = fs::copy(&self.backup_path, &self.restore_path) {
eprintln!("Warning: Failed to restore Cargo.lock from backup: {}", e);
return;
}
if let Err(e) = fs::remove_file(&self.backup_path) {
eprintln!("Warning: Failed to remove Cargo.lock backup: {}", e);
}
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum LockFile {
Minimal,
Maximum,
#[default]
Recent,
Existing,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum GeneratableLockFile {
Minimal,
Maximum,
#[default]
Recent,
}
impl From<GeneratableLockFile> for LockFile {
fn from(lockfile: GeneratableLockFile) -> Self {
match lockfile {
GeneratableLockFile::Minimal => Self::Minimal,
GeneratableLockFile::Maximum => Self::Maximum,
GeneratableLockFile::Recent => Self::Recent,
}
}
}
impl LockFile {
pub fn filename(self) -> &'static str {
match self {
Self::Minimal => "Cargo-minimal.lock",
Self::Maximum => "Cargo-maximum.lock",
Self::Recent => "Cargo-recent.lock",
Self::Existing => CARGO_LOCK,
}
}
pub fn derive(self, sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::Minimal => derive_minimal_lockfile(sh),
Self::Maximum => derive_maximum_lockfile(sh),
Self::Recent => update_recent_lockfile(sh),
Self::Existing => {
Ok(())
}
}
}
fn restore(self, sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::Minimal | Self::Maximum | Self::Recent => {
let workspace_root = get_workspace_root(sh)?;
let source = workspace_root.join(self.filename());
let dest = workspace_root.join(CARGO_LOCK);
fs::copy(&source, &dest).map_err(|e| -> Box<dyn std::error::Error> {
format!(
"Failed to restore {} lockfile (workspace: {:?}, from: {:?}, to: {:?}): {}",
self.filename(),
workspace_root,
source,
dest,
e
)
.into()
})?;
Ok(())
}
Self::Existing => {
Ok(())
}
}
}
pub fn activate(self, sh: &Shell) -> Result<LockFileGuard, Box<dyn std::error::Error>> {
let guard = LockFileGuard::new(sh)?;
self.restore(sh)?;
Ok(guard)
}
}
#[derive(serde::Deserialize, Default)]
struct RbmtLockConfig {
lock: Option<LockConfig>,
}
#[derive(serde::Deserialize)]
struct LockConfig {
enabled: Option<bool>,
}
impl RbmtLockConfig {
fn enabled(&self) -> Option<bool> { self.lock.as_ref().and_then(|lock| lock.enabled) }
}
pub fn run(
sh: &Shell,
lockfiles: &[GeneratableLockFile],
) -> Result<(), Box<dyn std::error::Error>> {
let _progress = ProgressGuard::new();
prepare_toolchain(sh, Toolchain::Nightly)?;
let workspace_root = get_workspace_root(sh)?;
rbmt_eprintln!("Updating lockfiles in: {}", workspace_root.display());
let _lockfile_guard = LockFileGuard::new(sh)?;
for &lockfile in lockfiles {
LockFile::from(lockfile).derive(sh)?;
}
sync_nested_lockfiles(sh)?;
rbmt_eprintln!("Lockfiles updated successfully");
Ok(())
}
fn sync_nested_lockfiles(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
let workspace_root = get_workspace_root(sh)?;
let lockfiles = find_nested_lockfiles(sh, &workspace_root);
if lockfiles.is_empty() {
return Ok(());
}
rbmt_eprintln!("Syncing nested workspace lockfiles...");
for lockfile in lockfiles {
let dir = lockfile.parent().ok_or("Cargo.lock has no parent directory")?;
let relative_path = lockfile.strip_prefix(&workspace_root).unwrap_or(&lockfile);
let manifest = dir.join("Cargo.toml");
if !manifest.exists() {
rbmt_eprintln!("Skipping {}: no adjacent Cargo.toml", relative_path.display());
continue;
}
let _dir = sh.push_dir(dir);
match get_workspace_root(sh) {
Ok(root) if root == dir => {}
Ok(_) => {
rbmt_eprintln!(
"Skipping {}: directory is not a workspace root (stray lockfile?)",
relative_path.display()
);
continue;
}
Err(e) => {
rbmt_eprintln!(
"Skipping {}: failed to read cargo metadata: {}",
relative_path.display(),
e
);
continue;
}
}
let contents = fs::read_to_string(&manifest)?;
let toml: WorkspaceManifest<RbmtLockConfig> = toml::from_str(&contents)?;
let enabled = toml
.workspace
.metadata
.rbmt
.enabled()
.or(toml.package.metadata.rbmt.enabled())
.unwrap_or(true);
if !enabled {
rbmt_eprintln!("Skipping {}: disabled by rbmt.lock metadata", relative_path.display());
continue;
}
rbmt_eprintln!("Syncing {}...", relative_path.display());
rbmt_cmd!(sh, "cargo fetch").run_with_capture()?;
}
Ok(())
}
fn find_nested_lockfiles(sh: &Shell, workspace_root: &Path) -> Vec<PathBuf> {
let _dir = sh.push_dir(workspace_root);
let output = match rbmt_cmd!(sh, "git ls-files --cached -z -- .").read() {
Ok(output) => output,
Err(e) => {
rbmt_eprintln!("Skipping nested lockfile sync: git ls-files failed: {}", e);
return Vec::new();
}
};
let mut lockfiles: Vec<PathBuf> = output
.split_terminator(NUL)
.map(PathBuf::from)
.filter(|path| path.file_name() == Some(OsStr::new(CARGO_LOCK)))
.map(|path| workspace_root.join(path))
.filter(|path| path.parent() != Some(workspace_root))
.filter(|path| path.exists())
.collect();
lockfiles.sort();
lockfiles
}
fn derive_minimal_lockfile(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
rbmt_eprintln!("Checking direct minimal versions...");
remove_lockfile(sh)?;
rbmt_cmd!(sh, "cargo check --all-features -Z direct-minimal-versions").run_with_capture()?;
rbmt_eprintln!("Generating minimal versions lockfile...");
remove_lockfile(sh)?;
rbmt_cmd!(sh, "cargo check --all-features -Z minimal-versions").run_with_capture()?;
copy_lockfile(sh, LockFile::Minimal)?;
Ok(())
}
fn derive_maximum_lockfile(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
rbmt_eprintln!("Generating maximum versions lockfile...");
remove_lockfile(sh)?;
rbmt_cmd!(sh, "cargo generate-lockfile").run_with_capture()?;
copy_lockfile(sh, LockFile::Maximum)?;
Ok(())
}
fn update_recent_lockfile(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
rbmt_eprintln!("Generating recent versions lockfile...");
remove_lockfile(sh)?;
let _ = LockFile::Recent.restore(sh);
rbmt_cmd!(sh, "cargo check --all-features").run_with_capture()?;
copy_lockfile(sh, LockFile::Recent)?;
Ok(())
}
fn remove_lockfile(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
let lock_path = get_workspace_root(sh)?.join(CARGO_LOCK);
if lock_path.exists() {
fs::remove_file(&lock_path)?;
}
Ok(())
}
fn copy_lockfile(sh: &Shell, target: LockFile) -> Result<(), Box<dyn std::error::Error>> {
let workspace_root = get_workspace_root(sh)?;
fs::copy(workspace_root.join(CARGO_LOCK), workspace_root.join(target.filename()))?;
Ok(())
}