| use std::io::prelude::*; | |
| use crate::compiler::layout::outer_first; | |
| use crate::resolver::encode::into_resolve; | |
| use crate::resolver::{Resolve, ResolveVersion}; | |
| use crate::util::Filesystem; | |
| use crate::util::errors::CargoResult; | |
| use crate::workspace::Workspace; | |
| use anyhow::Context as _; | |
| use cargo_util_schemas::lockfile::TomlLockfile; | |
| pub const LOCKFILE_NAME: &str = "Cargo.lock"; | |
| #[tracing::instrument(skip_all)] | |
| pub fn load_pkg_lockfile(ws: &Workspace<'_>) -> CargoResult<Option<Resolve>> { | |
| let lock_root = ws.lock_root(); | |
| if !lock_root.as_path_unlocked().join(LOCKFILE_NAME).exists() { | |
| return Ok(None); | |
| } | |
| let mut f = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file")?; | |
| let mut s = String::new(); | |
| f.read_to_string(&mut s) | |
| .with_context(|| format!("failed to read file: {}", f.path().display()))?; | |
| let resolve = (|| -> CargoResult<Option<Resolve>> { | |
| let v: TomlLockfile = toml::from_str(&s)?; | |
| Ok(Some(into_resolve(v, &s, ws)?)) | |
| })() | |
| .with_context(|| format!("failed to parse lock file at: {}", f.path().display()))?; | |
| Ok(resolve) | |
| } | |
| /// Generate a toml String of Cargo.lock from a Resolve. | |
| pub fn resolve_to_string(ws: &Workspace<'_>, resolve: &Resolve) -> CargoResult<String> { | |
| let (_orig, out, _lock_root) = resolve_to_string_orig(ws, resolve); | |
| Ok(out) | |
| } | |
| /// Ensure the resolve result is written to fisk | |
| /// | |
| /// Returns `true` if the lockfile changed | |
| #[tracing::instrument(skip_all)] | |
| pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &mut Resolve) -> CargoResult<bool> { | |
| let (orig, mut out, lock_root) = resolve_to_string_orig(ws, resolve); | |
| // If the lock file contents haven't changed so don't rewrite it. This is | |
| // helpful on read-only filesystems. | |
| if let Some(orig) = &orig { | |
| if are_equal_lockfiles(orig, &out, ws) { | |
| return Ok(false); | |
| } | |
| } | |
| if let Some(locked_flag) = ws.gctx().locked_flag() { | |
| let lockfile_path = lock_root.as_path_unlocked().join(LOCKFILE_NAME); | |
| let action = if lockfile_path.exists() { | |
| "update" | |
| } else { | |
| "create" | |
| }; | |
| let lockfile_path = lockfile_path.display(); | |
| anyhow::bail!( | |
| "cannot {action} the lock file {lockfile_path} because {locked_flag} was passed to prevent this\n\ | |
| help: to generate the lock file without accessing the network, \ | |
| remove the {locked_flag} flag and use --offline instead." | |
| ); | |
| } | |
| // While we're updating the lock file anyway go ahead and update its | |
| // encoding to whatever the latest default is. That way we can slowly roll | |
| // out lock file updates as they're otherwise already updated, and changes | |
| // which don't touch dependencies won't seemingly spuriously update the lock | |
| // file. | |
| let default_version = ResolveVersion::with_rust_version(ws.lowest_rust_version()); | |
| let current_version = resolve.version(); | |
| let next_lockfile_bump = ws.gctx().cli_unstable().next_lockfile_bump; | |
| tracing::debug!("lockfile - current: {current_version:?}, default: {default_version:?}"); | |
| if current_version < default_version { | |
| resolve.set_version(default_version); | |
| out = serialize_resolve(resolve, orig.as_deref()); | |
| } else if current_version > ResolveVersion::max_stable() && !next_lockfile_bump { | |
| // The next version hasn't yet stabilized. | |
| anyhow::bail!("lock file version `{current_version:?}` requires `-Znext-lockfile-bump`") | |
| } | |
| if !lock_root.as_path_unlocked().exists() { | |
| // A lock root in the target or build directory, as for a cargo script, | |
| // would create that directory without excluding it from backups, and | |
| // `Layout` skips a directory that exists already. | |
| let target_dir = ws.target_dir().into_path_unlocked(); | |
| let build_dir = ws.build_dir().into_path_unlocked(); | |
| for dir in outer_first(&target_dir, &build_dir) { | |
| if lock_root.as_path_unlocked().starts_with(dir) { | |
| cargo_util::paths::create_dir_all_excluded_from_backups_atomic(dir)?; | |
| } | |
| } | |
| lock_root.create_dir()?; | |
| } | |
| // Ok, if that didn't work just write it out | |
| lock_root | |
| .open_rw_exclusive_create(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file") | |
| .and_then(|mut f| { | |
| f.file().set_len(0)?; | |
| f.write_all(out.as_bytes())?; | |
| Ok(()) | |
| }) | |
| .with_context(|| { | |
| format!( | |
| "failed to write {}", | |
| lock_root.as_path_unlocked().join(LOCKFILE_NAME).display() | |
| ) | |
| })?; | |
| Ok(true) | |
| } | |
| fn resolve_to_string_orig( | |
| ws: &Workspace<'_>, | |
| resolve: &Resolve, | |
| ) -> (Option<String>, String, Filesystem) { | |
| // Load the original lock file if it exists. | |
| let lock_root = ws.lock_root(); | |
| let orig = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file"); | |
| let orig = orig.and_then(|mut f| { | |
| let mut s = String::new(); | |
| f.read_to_string(&mut s)?; | |
| Ok(s) | |
| }); | |
| let out = serialize_resolve(resolve, orig.as_deref().ok()); | |
| (orig.ok(), out, lock_root) | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String { | |
| let toml = toml::Table::try_from(resolve).unwrap(); | |
| let mut out = String::new(); | |
| // At the start of the file we notify the reader that the file is generated. | |
| // Specifically Phabricator ignores files containing "@generated", so we use that. | |
| let marker_line = "# This file is automatically @generated by Cargo."; | |
| let extra_line = "# It is not intended for manual editing."; | |
| out.push_str(marker_line); | |
| out.push('\n'); | |
| out.push_str(extra_line); | |
| out.push('\n'); | |
| // and preserve any other top comments | |
| if let Some(orig) = orig { | |
| let mut comments = orig.lines().take_while(|line| line.starts_with('#')); | |
| if let Some(first) = comments.next() { | |
| if first != marker_line { | |
| out.push_str(first); | |
| out.push('\n'); | |
| } | |
| if let Some(second) = comments.next() { | |
| if second != extra_line { | |
| out.push_str(second); | |
| out.push('\n'); | |
| } | |
| for line in comments { | |
| out.push_str(line); | |
| out.push('\n'); | |
| } | |
| } | |
| } | |
| } | |
| if let Some(version) = toml.get("version") { | |
| out.push_str(&format!("version = {}\n\n", version)); | |
| } | |
| let deps = toml["package"].as_array().unwrap(); | |
| for dep in deps { | |
| let dep = dep.as_table().unwrap(); | |
| out.push_str("[[package]]\n"); | |
| emit_package(dep, &mut out); | |
| } | |
| if let Some(patch) = toml.get("patch") { | |
| let list = patch["unused"].as_array().unwrap(); | |
| for entry in list { | |
| out.push_str("[[patch.unused]]\n"); | |
| emit_package(entry.as_table().unwrap(), &mut out); | |
| out.push('\n'); | |
| } | |
| } | |
| if let Some(meta) = toml.get("metadata") { | |
| // 1. We need to ensure we print the entire tree, not just the direct members of `metadata` | |
| // (which `toml_edit::Table::to_string` only shows) | |
| // 2. We need to ensure all children tables have `metadata.` prefix | |
| let meta_table = meta | |
| .as_table() | |
| .expect("validation ensures this is a table") | |
| .clone(); | |
| let mut meta_doc = toml::Table::new(); | |
| meta_doc.insert("metadata".to_owned(), toml::Value::Table(meta_table)); | |
| out.push_str(&meta_doc.to_string()); | |
| } | |
| // Historical versions of Cargo in the old format accidentally left trailing | |
| // blank newlines at the end of files, so we just leave that as-is. For all | |
| // encodings going forward, though, we want to be sure that our encoded lock | |
| // file doesn't contain any trailing newlines so trim out the extra if | |
| // necessary. | |
| if resolve.version() >= ResolveVersion::V2 { | |
| while out.ends_with("\n\n") { | |
| out.pop(); | |
| } | |
| } | |
| out | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool { | |
| // Avoid deserializing lockfiles when their contents already match. | |
| if orig.lines().eq(current.lines()) { | |
| return true; | |
| } | |
| // If we want to try and avoid updating the lock file, parse both and | |
| // compare them; since this is somewhat expensive, don't do it in the | |
| // common case where we can update lock files. | |
| if !ws.gctx().lock_update_allowed() { | |
| let res: CargoResult<bool> = (|| { | |
| let old: TomlLockfile = toml::from_str(orig)?; | |
| let new: TomlLockfile = toml::from_str(current)?; | |
| Ok(into_resolve(old, orig, ws)? == into_resolve(new, current, ws)?) | |
| })(); | |
| if let Ok(true) = res { | |
| return true; | |
| } | |
| } | |
| false | |
| } | |
| fn emit_package(dep: &toml::Table, out: &mut String) { | |
| out.push_str(&format!("name = {}\n", &dep["name"])); | |
| out.push_str(&format!("version = {}\n", &dep["version"])); | |
| if dep.contains_key("source") { | |
| out.push_str(&format!("source = {}\n", &dep["source"])); | |
| } | |
| if dep.contains_key("checksum") { | |
| out.push_str(&format!("checksum = {}\n", &dep["checksum"])); | |
| } | |
| if let Some(s) = dep.get("dependencies") { | |
| let slice = s.as_array().unwrap(); | |
| if !slice.is_empty() { | |
| out.push_str("dependencies = [\n"); | |
| for child in slice.iter() { | |
| out.push_str(&format!(" {},\n", child)); | |
| } | |
| out.push_str("]\n"); | |
| } | |
| out.push('\n'); | |
| } else if dep.contains_key("replace") { | |
| out.push_str(&format!("replace = {}\n\n", &dep["replace"])); | |
| } | |
| } |