| //! High-level overview of how `fix` works: | |
| //! | |
| //! The main goal is to run `cargo check` to get rustc to emit JSON | |
| //! diagnostics with suggested fixes that can be applied to the files on the | |
| //! filesystem, and validate that those changes didn't break anything. | |
| //! | |
| //! Cargo begins by launching a [`LockServer`] thread in the background to | |
| //! listen for network connections to coordinate locking when multiple targets | |
| //! are built simultaneously. It ensures each package has only one fix running | |
| //! at once. | |
| //! | |
| //! The [`RustfixDiagnosticServer`] is launched in a background thread (in | |
| //! `JobQueue`) to listen for network connections to coordinate displaying | |
| //! messages to the user on the console (so that multiple processes don't try | |
| //! to print at the same time). | |
| //! | |
| //! Cargo begins a normal `cargo check` operation with itself set as a proxy | |
| //! for rustc by setting `BuildConfig::primary_unit_rustc` in the build config. When | |
| //! cargo launches rustc to check a crate, it is actually launching itself. | |
| //! The `FIX_ENV_INTERNAL` environment variable is set to the value of the [`LockServer`]'s | |
| //! address so that cargo knows it is in fix-proxy-mode. | |
| //! | |
| //! Each proxied cargo-as-rustc detects it is in fix-proxy-mode (via `FIX_ENV_INTERNAL` | |
| //! environment variable in `main`) and does the following: | |
| //! | |
| //! - Acquire a lock from the [`LockServer`] from the master cargo process. | |
| //! - Launches the real rustc ([`rustfix_and_fix`]), looking at the JSON output | |
| //! for suggested fixes. | |
| //! - Uses the `rustfix` crate to apply the suggestions to the files on the | |
| //! file system. | |
| //! - If rustfix fails to apply any suggestions (for example, they are | |
| //! overlapping), but at least some suggestions succeeded, it will try the | |
| //! previous two steps up to 4 times as long as some suggestions succeed. | |
| //! - Assuming there's at least one suggestion applied, and the suggestions | |
| //! applied cleanly, rustc is run again to verify the suggestions didn't | |
| //! break anything. The change will be backed out if it fails (unless | |
| //! `--broken-code` is used). | |
| use cargo_util::collections::{HashMap, HashSet}; | |
| use std::collections::BTreeSet; | |
| use std::ffi::OsString; | |
| use std::io::Write; | |
| use std::path::{Path, PathBuf}; | |
| use std::process::{self, ExitStatus, Output}; | |
| use std::{env, fs, str}; | |
| use anyhow::{Context as _, bail}; | |
| use cargo_util::{ProcessBuilder, exit_status_to_string, is_simple_exit_code, paths}; | |
| use cargo_util_schemas::manifest::TomlManifest; | |
| use rustfix::CodeFix; | |
| use rustfix::diagnostics::Diagnostic; | |
| use semver::Version; | |
| use tracing::{debug, trace, warn}; | |
| pub use self::fix_edition::fix_edition; | |
| use crate::compiler::CompileKind; | |
| use crate::compiler::RustcTargetData; | |
| use crate::ops::resolve::WorkspaceResolve; | |
| use crate::ops::{self, CompileOptions}; | |
| use crate::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor}; | |
| use crate::resolver::{HasDevUnits, Resolve, ResolveBehavior}; | |
| use crate::util::GlobalContext; | |
| use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer}; | |
| use crate::util::errors::CargoResult; | |
| use crate::util::{LockServer, LockServerClient, existing_vcs_repo}; | |
| use crate::workspace::PackageIdSpecQuery as _; | |
| use crate::workspace::editor::manifest::LocalManifest; | |
| use crate::workspace::{Edition, MaybePackage, Package, PackageId, Workspace}; | |
| use crate::{drop_eprint, drop_eprintln}; | |
| mod fix_edition; | |
| /// **Internal only.** | |
| /// Indicates Cargo is in fix-proxy-mode if presents. | |
| /// The value of it is the socket address of the [`LockServer`] being used. | |
| /// See the [module-level documentation](mod@super::cargo_fix) for more. | |
| const FIX_ENV_INTERNAL: &str = "__CARGO_FIX_PLZ"; | |
| /// **Internal only.** | |
| /// For passing [`FixOptions::broken_code`] through to cargo running in proxy mode. | |
| const BROKEN_CODE_ENV_INTERNAL: &str = "__CARGO_FIX_BROKEN_CODE"; | |
| /// **Internal only.** | |
| /// For passing [`FixOptions::edition`] through to cargo running in proxy mode. | |
| const EDITION_ENV_INTERNAL: &str = "__CARGO_FIX_EDITION"; | |
| /// **Internal only.** | |
| /// For passing [`FixOptions::idioms`] through to cargo running in proxy mode. | |
| const IDIOMS_ENV_INTERNAL: &str = "__CARGO_FIX_IDIOMS"; | |
| /// **Internal only.** | |
| /// The sysroot path. | |
| /// | |
| /// This is for preventing `cargo fix` from fixing rust std/core libs. See | |
| /// | |
| /// * <https://github.com/rust-lang/cargo/issues/9857> | |
| /// * <https://github.com/rust-lang/rust/issues/88514#issuecomment-2043469384> | |
| const SYSROOT_INTERNAL: &str = "__CARGO_FIX_RUST_SRC"; | |
| pub struct FixOptions { | |
| pub edition: Option<EditionFixMode>, | |
| pub idioms: bool, | |
| pub compile_opts: CompileOptions, | |
| pub allow_dirty: bool, | |
| pub allow_no_vcs: bool, | |
| pub allow_staged: bool, | |
| pub broken_code: bool, | |
| } | |
| /// The behavior of `--edition` migration. | |
| #[derive(Clone, Copy)] | |
| pub enum EditionFixMode { | |
| /// Migrates the package from the current edition to the next. | |
| /// | |
| /// This is the normal (stable) behavior of `--edition`. | |
| NextRelative, | |
| /// Migrates to a specific edition. | |
| /// | |
| /// This is used by `-Zfix-edition` to force a specific edition like | |
| /// `future`, which does not have a relative value. | |
| OverrideSpecific(Edition), | |
| } | |
| impl EditionFixMode { | |
| /// Returns the edition to use for the given current edition. | |
| pub fn next_edition(&self, current_edition: Edition) -> Edition { | |
| match self { | |
| EditionFixMode::NextRelative => current_edition.saturating_next(), | |
| EditionFixMode::OverrideSpecific(edition) => *edition, | |
| } | |
| } | |
| /// Serializes to a string. | |
| fn to_string(&self) -> String { | |
| match self { | |
| EditionFixMode::NextRelative => "1".to_string(), | |
| EditionFixMode::OverrideSpecific(edition) => edition.to_string(), | |
| } | |
| } | |
| /// Deserializes from the given string. | |
| fn from_str(s: &str) -> EditionFixMode { | |
| match s { | |
| "1" => EditionFixMode::NextRelative, | |
| edition => EditionFixMode::OverrideSpecific(edition.parse().unwrap()), | |
| } | |
| } | |
| } | |
| pub fn fix( | |
| gctx: &GlobalContext, | |
| original_ws: &Workspace<'_>, | |
| opts: &mut FixOptions, | |
| ) -> CargoResult<()> { | |
| check_version_control(gctx, opts)?; | |
| let mut target_data = | |
| RustcTargetData::new(original_ws, &opts.compile_opts.build_config.requested_kinds)?; | |
| let specs = opts.compile_opts.spec.to_package_id_specs(&original_ws)?; | |
| let members: Vec<&Package> = original_ws | |
| .members() | |
| .filter(|m| specs.iter().any(|spec| spec.matches(m.package_id()))) | |
| .collect(); | |
| if let Some(edition_mode) = opts.edition { | |
| migrate_manifests(original_ws, &members, edition_mode)?; | |
| check_resolver_change(&original_ws, &mut target_data, opts)?; | |
| } | |
| fix_manifests(original_ws, &members)?; | |
| let ws = original_ws.reload(gctx)?; | |
| // Spin up our lock server, which our subprocesses will use to synchronize fixes. | |
| let lock_server = LockServer::new()?; | |
| let mut wrapper = ProcessBuilder::new(env::current_exe()?); | |
| wrapper.env(FIX_ENV_INTERNAL, lock_server.addr().to_string()); | |
| let _started = lock_server.start()?; | |
| opts.compile_opts.build_config.force_rebuild = true; | |
| if opts.broken_code { | |
| wrapper.env(BROKEN_CODE_ENV_INTERNAL, "1"); | |
| } | |
| if let Some(mode) = &opts.edition { | |
| wrapper.env(EDITION_ENV_INTERNAL, mode.to_string()); | |
| } | |
| if opts.idioms { | |
| wrapper.env(IDIOMS_ENV_INTERNAL, "1"); | |
| } | |
| let sysroot = &target_data.info(CompileKind::Host).sysroot; | |
| if sysroot.is_dir() { | |
| wrapper.env(SYSROOT_INTERNAL, sysroot); | |
| } | |
| *opts | |
| .compile_opts | |
| .build_config | |
| .rustfix_diagnostic_server | |
| .borrow_mut() = Some(RustfixDiagnosticServer::new()?); | |
| if let Some(server) = opts | |
| .compile_opts | |
| .build_config | |
| .rustfix_diagnostic_server | |
| .borrow() | |
| .as_ref() | |
| { | |
| server.configure(&mut wrapper); | |
| } | |
| let rustc = ws.gctx().load_global_rustc(Some(&ws))?; | |
| wrapper.arg(&rustc.path); | |
| // This is calling rustc in cargo fix-proxy-mode, so it also need to retry. | |
| // The argfile handling are located at `FixArgs::from_args`. | |
| wrapper.retry_with_argfile(true); | |
| // primary crates are compiled using a cargo subprocess to do extra work of applying fixes and | |
| // repeating build until there are no more changes to be applied | |
| opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper); | |
| ops::compile(&ws, &opts.compile_opts)?; | |
| Ok(()) | |
| } | |
| fn check_version_control(gctx: &GlobalContext, opts: &FixOptions) -> CargoResult<()> { | |
| if opts.allow_no_vcs { | |
| return Ok(()); | |
| } | |
| if !existing_vcs_repo(gctx.cwd(), gctx.cwd()) { | |
| bail!( | |
| "no VCS found for this package and `cargo fix` can potentially \ | |
| perform destructive changes; if you'd like to suppress this \ | |
| error pass `--allow-no-vcs`" | |
| ) | |
| } | |
| if opts.allow_dirty && opts.allow_staged { | |
| return Ok(()); | |
| } | |
| let mut dirty_files = Vec::new(); | |
| let mut staged_files = Vec::new(); | |
| if let Ok(repo) = git2::Repository::discover(gctx.cwd()) { | |
| let mut repo_opts = git2::StatusOptions::new(); | |
| repo_opts.include_ignored(false); | |
| repo_opts.include_untracked(true); | |
| for status in repo.statuses(Some(&mut repo_opts))?.iter() { | |
| if let Ok(path) = status.path() { | |
| match status.status() { | |
| git2::Status::CURRENT => (), | |
| git2::Status::INDEX_NEW | |
| | git2::Status::INDEX_MODIFIED | |
| | git2::Status::INDEX_DELETED | |
| | git2::Status::INDEX_RENAMED | |
| | git2::Status::INDEX_TYPECHANGE => { | |
| if !opts.allow_staged { | |
| staged_files.push(path.to_string()) | |
| } | |
| } | |
| _ => { | |
| if !opts.allow_dirty { | |
| dirty_files.push(path.to_string()) | |
| } | |
| } | |
| }; | |
| } | |
| } | |
| } | |
| if dirty_files.is_empty() && staged_files.is_empty() { | |
| return Ok(()); | |
| } | |
| let mut files_list = String::new(); | |
| for file in dirty_files { | |
| files_list.push_str(" * "); | |
| files_list.push_str(&file); | |
| files_list.push_str(" (dirty)\n"); | |
| } | |
| for file in staged_files { | |
| files_list.push_str(" * "); | |
| files_list.push_str(&file); | |
| files_list.push_str(" (staged)\n"); | |
| } | |
| bail!( | |
| "the working directory of this package has uncommitted changes, and \ | |
| `cargo fix` can potentially perform destructive changes; if you'd \ | |
| like to suppress this error pass `--allow-dirty`, \ | |
| or commit the changes to these files:\n\ | |
| \n\ | |
| {}\n\ | |
| ", | |
| files_list | |
| ); | |
| } | |
| fn fix_manifests(ws: &Workspace<'_>, pkgs: &[&Package]) -> CargoResult<()> { | |
| for pkg in pkgs { | |
| let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?; | |
| let mut fixes = 0; | |
| if manifest_mut.ensure_edition() { | |
| fixes += 1; | |
| } | |
| if 0 < fixes { | |
| let file = pkg.manifest_path(); | |
| let file = file.strip_prefix(ws.root()).unwrap_or(file); | |
| let file = file.display(); | |
| let verb = if fixes == 1 { "fix" } else { "fixes" }; | |
| let msg = format!("{file} ({fixes} {verb})"); | |
| ws.gctx().shell().status("Fixed", msg)?; | |
| manifest_mut.write()?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn migrate_manifests( | |
| ws: &Workspace<'_>, | |
| pkgs: &[&Package], | |
| edition_mode: EditionFixMode, | |
| ) -> CargoResult<()> { | |
| // HACK: Duplicate workspace migration logic between virtual manifests and real manifests to | |
| // reduce multiple Migrating messages being reported for the same file to the user | |
| if matches!(ws.root_maybe(), MaybePackage::Virtual(_)) { | |
| // Warning: workspaces do not have an edition so this should only include changes needed by | |
| // packages that preserve the behavior of the workspace on all editions | |
| let highest_edition = pkgs | |
| .iter() | |
| .map(|p| p.manifest().edition()) | |
| .max() | |
| .unwrap_or_default(); | |
| let prepare_for_edition = edition_mode.next_edition(highest_edition); | |
| if highest_edition == prepare_for_edition | |
| || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed) | |
| { | |
| // | |
| } else { | |
| let mut manifest_mut = LocalManifest::try_new(ws.root_manifest())?; | |
| let document = &mut manifest_mut.data; | |
| let mut fixes = 0; | |
| if Edition::Edition2024 <= prepare_for_edition { | |
| let root = document.as_table_mut(); | |
| if let Some(workspace) = root | |
| .get_mut("workspace") | |
| .and_then(|t| t.as_table_like_mut()) | |
| { | |
| // strictly speaking, the edition doesn't apply to this table but it should be safe | |
| // enough | |
| fixes += rename_dep_fields_2024(workspace, "dependencies"); | |
| } | |
| } | |
| if 0 < fixes { | |
| // HACK: As workspace migration is a special case, only report it if something | |
| // happened | |
| let file = ws.root_manifest(); | |
| let file = file.strip_prefix(ws.root()).unwrap_or(file); | |
| let file = file.display(); | |
| ws.gctx().shell().status( | |
| "Migrating", | |
| format!("{file} from {highest_edition} edition to {prepare_for_edition}"), | |
| )?; | |
| let verb = if fixes == 1 { "fix" } else { "fixes" }; | |
| let msg = format!("{file} ({fixes} {verb})"); | |
| ws.gctx().shell().status("Fixed", msg)?; | |
| manifest_mut.write()?; | |
| } | |
| } | |
| } | |
| for pkg in pkgs { | |
| let existing_edition = pkg.manifest().edition(); | |
| let prepare_for_edition = edition_mode.next_edition(existing_edition); | |
| if existing_edition == prepare_for_edition | |
| || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed) | |
| { | |
| continue; | |
| } | |
| let file = pkg.manifest_path(); | |
| let file = file.strip_prefix(ws.root()).unwrap_or(file); | |
| let file = file.display(); | |
| ws.gctx().shell().status( | |
| "Migrating", | |
| format!("{file} from {existing_edition} edition to {prepare_for_edition}"), | |
| )?; | |
| let mut manifest_mut = LocalManifest::try_new(pkg.manifest_path())?; | |
| let document = &mut manifest_mut.data; | |
| let mut fixes = 0; | |
| let ws_original_toml = match ws.root_maybe() { | |
| MaybePackage::Package(package) => package.manifest().original_toml(), | |
| MaybePackage::Virtual(manifest) => manifest.original_toml(), | |
| }; | |
| if Edition::Edition2024 <= prepare_for_edition { | |
| let root = document.as_table_mut(); | |
| if let Some(workspace) = root | |
| .get_mut("workspace") | |
| .and_then(|t| t.as_table_like_mut()) | |
| { | |
| // strictly speaking, the edition doesn't apply to this table but it should be safe | |
| // enough | |
| fixes += rename_dep_fields_2024(workspace, "dependencies"); | |
| } | |
| fixes += rename_table(root, "project", "package"); | |
| if let Some(target) = root.get_mut("lib").and_then(|t| t.as_table_like_mut()) { | |
| fixes += rename_target_fields_2024(target); | |
| } | |
| fixes += rename_array_of_target_fields_2024(root, "bin"); | |
| fixes += rename_array_of_target_fields_2024(root, "example"); | |
| fixes += rename_array_of_target_fields_2024(root, "test"); | |
| fixes += rename_array_of_target_fields_2024(root, "bench"); | |
| fixes += rename_dep_fields_2024(root, "dependencies"); | |
| fixes += remove_ignored_default_features_2024(root, "dependencies", ws_original_toml); | |
| fixes += rename_table(root, "dev_dependencies", "dev-dependencies"); | |
| fixes += rename_dep_fields_2024(root, "dev-dependencies"); | |
| fixes += | |
| remove_ignored_default_features_2024(root, "dev-dependencies", ws_original_toml); | |
| fixes += rename_table(root, "build_dependencies", "build-dependencies"); | |
| fixes += rename_dep_fields_2024(root, "build-dependencies"); | |
| fixes += | |
| remove_ignored_default_features_2024(root, "build-dependencies", ws_original_toml); | |
| for target in root | |
| .get_mut("target") | |
| .and_then(|t| t.as_table_like_mut()) | |
| .iter_mut() | |
| .flat_map(|t| t.iter_mut()) | |
| .filter_map(|(_k, t)| t.as_table_like_mut()) | |
| { | |
| fixes += rename_dep_fields_2024(target, "dependencies"); | |
| fixes += | |
| remove_ignored_default_features_2024(target, "dependencies", ws_original_toml); | |
| fixes += rename_table(target, "dev_dependencies", "dev-dependencies"); | |
| fixes += rename_dep_fields_2024(target, "dev-dependencies"); | |
| fixes += remove_ignored_default_features_2024( | |
| target, | |
| "dev-dependencies", | |
| ws_original_toml, | |
| ); | |
| fixes += rename_table(target, "build_dependencies", "build-dependencies"); | |
| fixes += rename_dep_fields_2024(target, "build-dependencies"); | |
| fixes += remove_ignored_default_features_2024( | |
| target, | |
| "build-dependencies", | |
| ws_original_toml, | |
| ); | |
| } | |
| } | |
| if 0 < fixes { | |
| let verb = if fixes == 1 { "fix" } else { "fixes" }; | |
| let msg = format!("{file} ({fixes} {verb})"); | |
| ws.gctx().shell().status("Fixed", msg)?; | |
| manifest_mut.write()?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn rename_dep_fields_2024(parent: &mut dyn toml_edit::TableLike, dep_kind: &str) -> usize { | |
| let mut fixes = 0; | |
| for target in parent | |
| .get_mut(dep_kind) | |
| .and_then(|t| t.as_table_like_mut()) | |
| .iter_mut() | |
| .flat_map(|t| t.iter_mut()) | |
| .filter_map(|(_k, t)| t.as_table_like_mut()) | |
| { | |
| fixes += rename_table(target, "default_features", "default-features"); | |
| } | |
| fixes | |
| } | |
| fn remove_ignored_default_features_2024( | |
| parent: &mut dyn toml_edit::TableLike, | |
| dep_kind: &str, | |
| ws_original_toml: Option<&TomlManifest>, | |
| ) -> usize { | |
| let Some(ws_original_toml) = ws_original_toml else { | |
| return 0; | |
| }; | |
| let mut fixes = 0; | |
| for (name_in_toml, target) in parent | |
| .get_mut(dep_kind) | |
| .and_then(|t| t.as_table_like_mut()) | |
| .iter_mut() | |
| .flat_map(|t| t.iter_mut()) | |
| .filter_map(|(k, t)| t.as_table_like_mut().map(|t| (k, t))) | |
| { | |
| let name_in_toml: &str = &name_in_toml; | |
| let ws_deps = ws_original_toml | |
| .workspace | |
| .as_ref() | |
| .and_then(|ws| ws.dependencies.as_ref()); | |
| if let Some(ws_dep) = ws_deps.and_then(|ws_deps| ws_deps.get(name_in_toml)) { | |
| if ws_dep.default_features() == Some(false) { | |
| continue; | |
| } | |
| } | |
| if target | |
| .get("workspace") | |
| .and_then(|i| i.as_value()) | |
| .and_then(|i| i.as_bool()) | |
| == Some(true) | |
| && target | |
| .get("default-features") | |
| .and_then(|i| i.as_value()) | |
| .and_then(|i| i.as_bool()) | |
| == Some(false) | |
| { | |
| target.remove("default-features"); | |
| fixes += 1; | |
| } | |
| } | |
| fixes | |
| } | |
| fn rename_array_of_target_fields_2024(root: &mut dyn toml_edit::TableLike, kind: &str) -> usize { | |
| let mut fixes = 0; | |
| for target in root | |
| .get_mut(kind) | |
| .and_then(|t| t.as_array_of_tables_mut()) | |
| .iter_mut() | |
| .flat_map(|t| t.iter_mut()) | |
| { | |
| fixes += rename_target_fields_2024(target); | |
| } | |
| fixes | |
| } | |
| fn rename_target_fields_2024(target: &mut dyn toml_edit::TableLike) -> usize { | |
| let mut fixes = 0; | |
| fixes += rename_table(target, "crate_type", "crate-type"); | |
| fixes += rename_table(target, "proc_macro", "proc-macro"); | |
| fixes | |
| } | |
| fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> usize { | |
| let Some(old_key) = parent.key(old).cloned() else { | |
| return 0; | |
| }; | |
| let project = parent.remove(old).expect("returned early"); | |
| if !parent.contains_key(new) { | |
| parent.insert(new, project); | |
| let mut new_key = parent.key_mut(new).expect("just inserted"); | |
| *new_key.dotted_decor_mut() = old_key.dotted_decor().clone(); | |
| *new_key.leaf_decor_mut() = old_key.leaf_decor().clone(); | |
| } | |
| 1 | |
| } | |
| fn check_resolver_change<'gctx>( | |
| ws: &Workspace<'gctx>, | |
| target_data: &mut RustcTargetData<'gctx>, | |
| opts: &FixOptions, | |
| ) -> CargoResult<()> { | |
| let root = ws.root_maybe(); | |
| match root { | |
| MaybePackage::Package(root_pkg) => { | |
| if root_pkg.manifest().resolve_behavior().is_some() { | |
| // If explicitly specified by the user, no need to check. | |
| return Ok(()); | |
| } | |
| // Only trigger if updating the root package from 2018. | |
| let pkgs = opts.compile_opts.spec.get_packages(ws)?; | |
| if !pkgs.contains(&root_pkg) { | |
| // The root is not being migrated. | |
| return Ok(()); | |
| } | |
| if root_pkg.manifest().edition() != Edition::Edition2018 { | |
| // V1 to V2 only happens on 2018 to 2021. | |
| return Ok(()); | |
| } | |
| } | |
| MaybePackage::Virtual(_vm) => { | |
| // Virtual workspaces don't have a global edition to set (yet). | |
| return Ok(()); | |
| } | |
| } | |
| // 2018 without `resolver` set must be V1 | |
| assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1); | |
| let specs = opts.compile_opts.spec.to_package_id_specs(ws)?; | |
| let mut resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> { | |
| let dry_run = false; | |
| let ws_resolve = ops::resolve_ws_with_opts( | |
| ws, | |
| target_data, | |
| &opts.compile_opts.build_config.requested_kinds, | |
| &opts.compile_opts.cli_features, | |
| &specs, | |
| has_dev_units, | |
| crate::resolver::features::ForceAllTargets::No, | |
| dry_run, | |
| )?; | |
| let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units); | |
| let v2_features = FeatureResolver::resolve( | |
| ws, | |
| target_data, | |
| &ws_resolve.targeted_resolve, | |
| &ws_resolve.pkg_set, | |
| &opts.compile_opts.cli_features, | |
| &specs, | |
| &opts.compile_opts.build_config.requested_kinds, | |
| feature_opts, | |
| )?; | |
| if ws_resolve.specs_and_features.len() != 1 { | |
| bail!(r#"cannot fix edition when using `feature-unification = "package"`."#); | |
| } | |
| let resolved_features = &ws_resolve | |
| .specs_and_features | |
| .first() | |
| .expect("We've already checked that there is exactly one.") | |
| .resolved_features; | |
| let diffs = v2_features.compare_legacy(resolved_features); | |
| Ok((ws_resolve, diffs)) | |
| }; | |
| let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?; | |
| let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?; | |
| if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() { | |
| // Nothing is different, nothing to report. | |
| return Ok(()); | |
| } | |
| // Only display unique changes with dev-dependencies. | |
| with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals)); | |
| let gctx = ws.gctx(); | |
| gctx.shell().note( | |
| "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.", | |
| )?; | |
| drop_eprintln!( | |
| gctx, | |
| "This may cause some dependencies to be built with fewer features enabled than previously." | |
| ); | |
| drop_eprintln!( | |
| gctx, | |
| "More information about the resolver changes may be found \ | |
| at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html" | |
| ); | |
| drop_eprintln!( | |
| gctx, | |
| "When building the following dependencies, \ | |
| the given features will no longer be used:\n" | |
| ); | |
| let show_diffs = |differences: DiffMap| { | |
| for ((pkg_id, features_for), removed) in differences { | |
| drop_eprint!(gctx, " {}", pkg_id); | |
| if let FeaturesFor::HostDep = features_for { | |
| drop_eprint!(gctx, " (as host dependency)"); | |
| } | |
| drop_eprint!(gctx, " removed features: "); | |
| let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect(); | |
| drop_eprintln!(gctx, "{}", joined.join(", ")); | |
| } | |
| drop_eprint!(gctx, "\n"); | |
| }; | |
| if !without_dev_diffs.is_empty() { | |
| show_diffs(without_dev_diffs); | |
| } | |
| if !with_dev_diffs.is_empty() { | |
| drop_eprintln!( | |
| gctx, | |
| "The following differences only apply when building with dev-dependencies:\n" | |
| ); | |
| show_diffs(with_dev_diffs); | |
| } | |
| report_maybe_diesel(gctx, &ws_resolve.targeted_resolve)?; | |
| Ok(()) | |
| } | |
| fn report_maybe_diesel(gctx: &GlobalContext, resolve: &Resolve) -> CargoResult<()> { | |
| fn is_broken_diesel(pid: PackageId) -> bool { | |
| pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8) | |
| } | |
| fn is_broken_diesel_migration(pid: PackageId) -> bool { | |
| pid.name() == "diesel_migrations" && pid.version().major <= 1 | |
| } | |
| if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) { | |
| gctx.shell().note( | |
| "\ | |
| This project appears to use both diesel and diesel_migrations. These packages have | |
| a known issue where the build may fail due to the version 2 resolver preventing | |
| feature unification between those two packages. Please update to at least diesel 1.4.8 | |
| to prevent this issue from happening. | |
| ", | |
| )?; | |
| } | |
| Ok(()) | |
| } | |
| /// Provide the lock address when running in proxy mode | |
| /// | |
| /// Returns `None` if `fix` is not being run (not in proxy mode). Returns | |
| /// `Some(...)` if in `fix` proxy mode | |
| pub fn fix_get_proxy_lock_addr() -> Option<String> { | |
| #[expect( | |
| clippy::disallowed_methods, | |
| reason = "internal only, no reason for config support" | |
| )] | |
| env::var(FIX_ENV_INTERNAL).ok() | |
| } | |
| /// Entry point for `cargo` running as a proxy for `rustc`. | |
| /// | |
| /// This is called every time `cargo` is run to check if it is in proxy mode. | |
| /// | |
| /// If there are warnings or errors, this does not return, | |
| /// and the process exits with the corresponding `rustc` exit code. | |
| /// | |
| /// See [`fix_get_proxy_lock_addr`] | |
| pub fn fix_exec_rustc(gctx: &GlobalContext, lock_addr: &str) -> CargoResult<()> { | |
| let args = FixArgs::get()?; | |
| trace!("cargo-fix as rustc got file {:?}", args.file); | |
| let workspace_rustc = gctx | |
| .get_env("RUSTC_WORKSPACE_WRAPPER") | |
| .map(PathBuf::from) | |
| .ok(); | |
| let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref()); | |
| rustc.retry_with_argfile(true); | |
| rustc.env_remove(FIX_ENV_INTERNAL); | |
| args.apply(&mut rustc); | |
| // Removes `FD_CLOEXEC` set by `jobserver::Client` to ensure that the | |
| // compiler can access the jobserver. | |
| if let Some(client) = gctx.jobserver_from_env() { | |
| rustc.inherit_jobserver(client); | |
| } | |
| trace!("start rustfixing {:?}", args.file); | |
| let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, gctx)?; | |
| if fixes.last_output.status.success() { | |
| for (path, file) in fixes.files.iter() { | |
| Message::Fixed { | |
| file: path.clone(), | |
| fixes: file.fixes_applied, | |
| } | |
| .post(gctx)?; | |
| } | |
| // Display any remaining diagnostics. | |
| emit_output(&fixes.last_output)?; | |
| return Ok(()); | |
| } | |
| let allow_broken_code = gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some(); | |
| // There was an error running rustc during the last run. | |
| // | |
| // Back out all of the changes unless --broken-code was used. | |
| if !allow_broken_code { | |
| for (path, file) in fixes.files.iter() { | |
| debug!("reverting {:?} due to errors", path); | |
| paths::write(path, &file.original_code)?; | |
| } | |
| } | |
| // If there were any fixes, let the user know that there was a failure | |
| // attempting to apply them, and to ask for a bug report. | |
| // | |
| // FIXME: The error message here is not correct with --broken-code. | |
| // https://github.com/rust-lang/cargo/issues/10955 | |
| if fixes.files.is_empty() { | |
| // No fixes were available. Display whatever errors happened. | |
| emit_output(&fixes.last_output)?; | |
| exit_with(fixes.last_output.status); | |
| } else { | |
| let krate = { | |
| let mut iter = rustc.get_args(); | |
| let mut krate = None; | |
| while let Some(arg) = iter.next() { | |
| if arg == "--crate-name" { | |
| krate = iter.next().and_then(|s| s.to_owned().into_string().ok()); | |
| } | |
| } | |
| krate | |
| }; | |
| log_failed_fix( | |
| gctx, | |
| krate, | |
| &fixes.last_output.stderr, | |
| fixes.last_output.status, | |
| )?; | |
| // Display the diagnostics that appeared at the start, before the | |
| // fixes failed. This can help with diagnosing which suggestions | |
| // caused the failure. | |
| emit_output(&fixes.first_output)?; | |
| // Exit with whatever exit code we initially started with. `cargo fix` | |
| // treats this as a warning, and shouldn't return a failure code | |
| // unless the code didn't compile in the first place. | |
| exit_with(fixes.first_output.status); | |
| } | |
| } | |
| fn emit_output(output: &Output) -> CargoResult<()> { | |
| // Unfortunately if there is output on stdout, this does not preserve the | |
| // order of output relative to stderr. In practice, rustc should never | |
| // print to stdout unless some proc-macro does it. | |
| std::io::stderr().write_all(&output.stderr)?; | |
| std::io::stdout().write_all(&output.stdout)?; | |
| Ok(()) | |
| } | |
| struct FixedCrate { | |
| /// Map of file path to some information about modifications made to that file. | |
| files: HashMap<String, FixedFile>, | |
| /// The output from rustc from the first time it was called. | |
| /// | |
| /// This is needed when fixes fail to apply, so that it can display the | |
| /// original diagnostics to the user which can help with diagnosing which | |
| /// suggestions caused the failure. | |
| first_output: Output, | |
| /// The output from rustc from the last time it was called. | |
| /// | |
| /// This will be displayed to the user to show any remaining diagnostics | |
| /// or errors. | |
| last_output: Output, | |
| } | |
| #[derive(Debug)] | |
| struct FixedFile { | |
| errors_applying_fixes: Vec<String>, | |
| fixes_applied: u32, | |
| original_code: String, | |
| } | |
| /// Attempts to apply fixes to a single crate. | |
| /// | |
| /// This runs `rustc` (possibly multiple times) to gather suggestions from the | |
| /// compiler and applies them to the files on disk. | |
| fn rustfix_crate( | |
| lock_addr: &str, | |
| rustc: &ProcessBuilder, | |
| filename: &Path, | |
| args: &FixArgs, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<FixedCrate> { | |
| // First up, we want to make sure that each crate is only checked by one | |
| // process at a time. If two invocations concurrently check a crate then | |
| // it's likely to corrupt it. | |
| // | |
| // Historically this used per-source-file locking, then per-package | |
| // locking. It now uses a single, global lock as some users do things like | |
| // #[path] or include!() of shared files between packages. Serializing | |
| // makes it slower, but is the only safe way to prevent concurrent | |
| // modification. | |
| let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?; | |
| // Map of files that have been modified. | |
| let mut files = HashMap::default(); | |
| if !args.can_run_rustfix(gctx)? { | |
| // This fix should not be run. Skipping... | |
| // We still need to run rustc at least once to make sure any potential | |
| // rmeta gets generated, and diagnostics get displayed. | |
| debug!("can't fix {filename:?}, running rustc: {rustc}"); | |
| let last_output = rustc.output()?; | |
| let fixes = FixedCrate { | |
| files, | |
| first_output: last_output.clone(), | |
| last_output, | |
| }; | |
| return Ok(fixes); | |
| } | |
| // Next up, this is a bit suspicious, but we *iteratively* execute rustc and | |
| // collect suggestions to feed to rustfix. Once we hit our limit of times to | |
| // execute rustc or we appear to be reaching a fixed point we stop running | |
| // rustc. | |
| // | |
| // This is currently done to handle code like: | |
| // | |
| // ::foo::<::Bar>(); | |
| // | |
| // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`. | |
| // The spans for these two suggestions are overlapping and its difficult in | |
| // the compiler to **not** have overlapping spans here. As a result, a naive | |
| // implementation would feed the two compiler suggestions for the above fix | |
| // into `rustfix`, but one would be rejected because it overlaps with the | |
| // other. | |
| // | |
| // In this case though, both suggestions are valid and can be automatically | |
| // applied! To handle this case we execute rustc multiple times, collecting | |
| // fixes each time we do so. Along the way we discard any suggestions that | |
| // failed to apply, assuming that they can be fixed the next time we run | |
| // rustc. | |
| // | |
| // Naturally, we want a few protections in place here though to avoid looping | |
| // forever or otherwise losing data. To that end we have a few termination | |
| // conditions: | |
| // | |
| // * Do this whole process a fixed number of times. In theory we probably | |
| // need an infinite number of times to apply fixes, but we're not gonna | |
| // sit around waiting for that. | |
| // * If it looks like a fix genuinely can't be applied we need to bail out. | |
| // Detect this when a fix fails to get applied *and* no suggestions | |
| // successfully applied to the same file. In that case looks like we | |
| // definitely can't make progress, so bail out. | |
| let max_iterations = gctx | |
| .get_env("CARGO_FIX_MAX_RETRIES") | |
| .ok() | |
| .and_then(|n| n.parse().ok()) | |
| .unwrap_or(4); | |
| let mut last_output; | |
| let mut last_made_changes; | |
| let mut first_output = None; | |
| let mut current_iteration = 0; | |
| loop { | |
| for file in files.values_mut() { | |
| // We'll generate new errors below. | |
| file.errors_applying_fixes.clear(); | |
| } | |
| (last_output, last_made_changes) = | |
| rustfix_and_fix(&mut files, rustc, filename, args, gctx)?; | |
| if current_iteration == 0 { | |
| first_output = Some(last_output.clone()); | |
| } | |
| let mut progress_yet_to_be_made = false; | |
| for (path, file) in files.iter_mut() { | |
| if file.errors_applying_fixes.is_empty() { | |
| continue; | |
| } | |
| debug!("had rustfix apply errors in {path:?} {file:?}"); | |
| // If anything was successfully fixed *and* there's at least one | |
| // error, then assume the error was spurious and we'll try again on | |
| // the next iteration. | |
| if last_made_changes { | |
| progress_yet_to_be_made = true; | |
| } | |
| } | |
| if !progress_yet_to_be_made { | |
| break; | |
| } | |
| current_iteration += 1; | |
| if current_iteration >= max_iterations { | |
| break; | |
| } | |
| } | |
| if last_made_changes { | |
| debug!("calling rustc one last time for final results: {rustc}"); | |
| last_output = rustc.output()?; | |
| } | |
| // Any errors still remaining at this point need to be reported as probably | |
| // bugs in Cargo and/or rustfix. | |
| for (path, file) in files.iter_mut() { | |
| for error in file.errors_applying_fixes.drain(..) { | |
| Message::ReplaceFailed { | |
| file: path.clone(), | |
| message: error, | |
| } | |
| .post(gctx)?; | |
| } | |
| } | |
| Ok(FixedCrate { | |
| files, | |
| first_output: first_output.expect("at least one iteration"), | |
| last_output, | |
| }) | |
| } | |
| /// Executes `rustc` to apply one round of suggestions to the crate in question. | |
| /// | |
| /// This will fill in the `fixes` map with original code, suggestions applied, | |
| /// and any errors encountered while fixing files. | |
| fn rustfix_and_fix( | |
| files: &mut HashMap<String, FixedFile>, | |
| rustc: &ProcessBuilder, | |
| filename: &Path, | |
| args: &FixArgs, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<(Output, bool)> { | |
| // If not empty, filter by these lints. | |
| // TODO: implement a way to specify this. | |
| let only = HashSet::default(); | |
| debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}"); | |
| let output = rustc.output()?; | |
| // If rustc didn't succeed for whatever reasons then we're very likely to be | |
| // looking at otherwise broken code. Let's not make things accidentally | |
| // worse by applying fixes where a bug could cause *more* broken code. | |
| // Instead, punt upwards which will reexec rustc over the original code, | |
| // displaying pretty versions of the diagnostics we just read out. | |
| if !output.status.success() && gctx.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() { | |
| debug!( | |
| "rustfixing `{:?}` failed, rustc exited with {:?}", | |
| filename, | |
| output.status.code() | |
| ); | |
| return Ok((output, false)); | |
| } | |
| let fix_mode = gctx | |
| .get_env_os("__CARGO_FIX_YOLO") | |
| .map(|_| rustfix::Filter::Everything) | |
| .unwrap_or(rustfix::Filter::MachineApplicableOnly); | |
| // Sift through the output of the compiler to look for JSON messages. | |
| // indicating fixes that we can apply. | |
| let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?; | |
| let suggestions = stderr | |
| .lines() | |
| .filter(|x| !x.is_empty()) | |
| .inspect(|y| trace!("line: {}", y)) | |
| // Parse each line of stderr, ignoring errors, as they may not all be JSON. | |
| .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok()) | |
| // From each diagnostic, try to extract suggestions from rustc. | |
| .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode)); | |
| // Collect suggestions by file so we can apply them one at a time later. | |
| let mut file_map = HashMap::default(); | |
| let mut num_suggestion = 0; | |
| // It's safe since we won't read any content under home dir. | |
| let home_path = gctx.home().as_path_unlocked(); | |
| for suggestion in suggestions { | |
| trace!("suggestion"); | |
| // Make sure we've got a file associated with this suggestion and all | |
| // snippets point to the same file. Right now it's not clear what | |
| // we would do with multiple files. | |
| let file_names = suggestion | |
| .solutions | |
| .iter() | |
| .flat_map(|s| s.replacements.iter()) | |
| .map(|r| &r.snippet.file_name); | |
| let file_name = if let Some(file_name) = file_names.clone().next() { | |
| file_name.clone() | |
| } else { | |
| trace!("rejecting as it has no solutions {:?}", suggestion); | |
| continue; | |
| }; | |
| let file_path = Path::new(&file_name); | |
| // Do not write into registry cache. See rust-lang/cargo#9857. | |
| if file_path.starts_with(home_path) { | |
| continue; | |
| } | |
| // Do not write into standard library source. See rust-lang/cargo#9857. | |
| if let Some(sysroot) = args.sysroot.as_deref() { | |
| if file_path.starts_with(sysroot) { | |
| continue; | |
| } | |
| } | |
| if !file_names.clone().all(|f| f == &file_name) { | |
| trace!("rejecting as it changes multiple files: {:?}", suggestion); | |
| continue; | |
| } | |
| trace!("adding suggestion for {:?}: {:?}", file_name, suggestion); | |
| file_map | |
| .entry(file_name) | |
| .or_insert_with(Vec::new) | |
| .push(suggestion); | |
| num_suggestion += 1; | |
| } | |
| debug!( | |
| "collected {} suggestions for `{}`", | |
| num_suggestion, | |
| filename.display(), | |
| ); | |
| let mut made_changes = false; | |
| for (file, suggestions) in file_map { | |
| // Attempt to read the source code for this file. If this fails then | |
| // that'd be pretty surprising, so log a message and otherwise keep | |
| // going. | |
| let code = match paths::read(file.as_ref()) { | |
| Ok(s) => s, | |
| Err(e) => { | |
| warn!("failed to read `{}`: {}", file, e); | |
| continue; | |
| } | |
| }; | |
| let num_suggestions = suggestions.len(); | |
| debug!("applying {} fixes to {}", num_suggestions, file); | |
| // If this file doesn't already exist then we just read the original | |
| // code, so save it. If the file already exists then the original code | |
| // doesn't need to be updated as we've just read an interim state with | |
| // some fixes but perhaps not all. | |
| let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile { | |
| errors_applying_fixes: Vec::new(), | |
| fixes_applied: 0, | |
| original_code: code.clone(), | |
| }); | |
| let mut fixed = CodeFix::new(&code); | |
| for suggestion in suggestions.iter().rev() { | |
| // As mentioned above in `rustfix_crate`, | |
| // we don't immediately warn about suggestions that fail to apply here, | |
| // and instead we save them off for later processing. | |
| // | |
| // However, we don't bother reporting conflicts that exactly match prior replacements. | |
| // This is currently done to reduce noise for things like rust-lang/rust#51211, | |
| // although it may be removed if that's fixed deeper in the compiler. | |
| match fixed.apply(suggestion) { | |
| Ok(()) => fixed_file.fixes_applied += 1, | |
| Err(rustfix::Error::AlreadyReplaced { | |
| is_identical: true, .. | |
| }) => continue, | |
| Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()), | |
| } | |
| } | |
| if fixed.modified() { | |
| made_changes = true; | |
| let new_code = fixed.finish()?; | |
| paths::write(&file, new_code)?; | |
| } | |
| } | |
| Ok((output, made_changes)) | |
| } | |
| fn exit_with(status: ExitStatus) -> ! { | |
| #[cfg(unix)] | |
| { | |
| use std::os::unix::prelude::*; | |
| if let Some(signal) = status.signal() { | |
| drop(writeln!( | |
| std::io::stderr().lock(), | |
| "child failed with signal `{}`", | |
| signal | |
| )); | |
| process::exit(2); | |
| } | |
| } | |
| process::exit(status.code().unwrap_or(3)); | |
| } | |
| fn log_failed_fix( | |
| gctx: &GlobalContext, | |
| krate: Option<String>, | |
| stderr: &[u8], | |
| status: ExitStatus, | |
| ) -> CargoResult<()> { | |
| let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?; | |
| let diagnostics = stderr | |
| .lines() | |
| .filter(|x| !x.is_empty()) | |
| .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok()); | |
| let mut files = BTreeSet::new(); | |
| let mut errors = Vec::new(); | |
| for diagnostic in diagnostics { | |
| errors.push(diagnostic.rendered.unwrap_or(diagnostic.message)); | |
| for span in diagnostic.spans.into_iter() { | |
| files.insert(span.file_name); | |
| } | |
| } | |
| // Include any abnormal messages (like an ICE or whatever). | |
| errors.extend( | |
| stderr | |
| .lines() | |
| .filter(|x| !x.starts_with('{')) | |
| .map(|x| x.to_string()), | |
| ); | |
| let files = files.into_iter().collect(); | |
| let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) { | |
| None | |
| } else { | |
| Some(exit_status_to_string(status)) | |
| }; | |
| Message::FixFailed { | |
| files, | |
| krate, | |
| errors, | |
| abnormal_exit, | |
| } | |
| .post(gctx)?; | |
| Ok(()) | |
| } | |
| /// Various command-line options and settings used when `cargo` is running as | |
| /// a proxy for `rustc` during the fix operation. | |
| struct FixArgs { | |
| /// This is the `.rs` file that is being fixed. | |
| file: PathBuf, | |
| /// If `--edition` is used to migrate to the next edition, this is the | |
| /// edition we are migrating towards. | |
| prepare_for_edition: Option<Edition>, | |
| /// `true` if `--edition-idioms` is enabled. | |
| idioms: bool, | |
| /// The current edition. | |
| /// | |
| /// `None` if on 2015. | |
| enabled_edition: Option<Edition>, | |
| /// Other command-line arguments not reflected by other fields in | |
| /// `FixArgs`. | |
| other: Vec<OsString>, | |
| /// Path to the `rustc` executable. | |
| rustc: PathBuf, | |
| /// Path to host sysroot. | |
| sysroot: Option<PathBuf>, | |
| } | |
| impl FixArgs { | |
| fn get() -> CargoResult<FixArgs> { | |
| Self::from_args(env::args_os()) | |
| } | |
| // This is a separate function so that we can use it in tests. | |
| fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> { | |
| let mut argv = argv.into_iter(); | |
| let mut rustc = argv | |
| .nth(1) | |
| .map(PathBuf::from) | |
| .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?; | |
| let mut file = None; | |
| let mut enabled_edition = None; | |
| let mut other = Vec::new(); | |
| let mut handle_arg = |arg: OsString| -> CargoResult<()> { | |
| let path = PathBuf::from(arg); | |
| if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() { | |
| file = Some(path); | |
| return Ok(()); | |
| } | |
| if let Some(s) = path.to_str() { | |
| if let Some(edition) = s.strip_prefix("--edition=") { | |
| enabled_edition = Some(edition.parse()?); | |
| return Ok(()); | |
| } | |
| } | |
| other.push(path.into()); | |
| Ok(()) | |
| }; | |
| if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") { | |
| // Because cargo in fix-proxy-mode might hit the command line size limit, | |
| // cargo fix need handle `@path` argfile for this special case. | |
| if argv.next().is_some() { | |
| bail!("argfile `@path` cannot be combined with other arguments"); | |
| } | |
| let contents = fs::read_to_string(argfile_path) | |
| .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?; | |
| let mut iter = contents.lines().map(OsString::from); | |
| rustc = iter | |
| .next() | |
| .map(PathBuf::from) | |
| .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?; | |
| for arg in iter { | |
| handle_arg(arg)?; | |
| } | |
| } else { | |
| for arg in argv { | |
| handle_arg(arg)?; | |
| } | |
| } | |
| let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?; | |
| #[expect( | |
| clippy::disallowed_methods, | |
| reason = "internal only, no reason for config support" | |
| )] | |
| let idioms = env::var(IDIOMS_ENV_INTERNAL).is_ok(); | |
| #[expect( | |
| clippy::disallowed_methods, | |
| reason = "internal only, no reason for config support" | |
| )] | |
| let prepare_for_edition = env::var(EDITION_ENV_INTERNAL).ok().map(|v| { | |
| let enabled_edition = enabled_edition.unwrap_or(Edition::Edition2015); | |
| let mode = EditionFixMode::from_str(&v); | |
| mode.next_edition(enabled_edition) | |
| }); | |
| #[expect( | |
| clippy::disallowed_methods, | |
| reason = "internal only, no reason for config support" | |
| )] | |
| let sysroot = env::var_os(SYSROOT_INTERNAL).map(PathBuf::from); | |
| Ok(FixArgs { | |
| file, | |
| prepare_for_edition, | |
| idioms, | |
| enabled_edition, | |
| other, | |
| rustc, | |
| sysroot, | |
| }) | |
| } | |
| fn apply(&self, cmd: &mut ProcessBuilder) { | |
| cmd.arg(&self.file); | |
| cmd.args(&self.other); | |
| if self.prepare_for_edition.is_some() { | |
| // When migrating an edition, we don't want to fix other lints as | |
| // they can sometimes add suggestions that fail to apply, causing | |
| // the entire migration to fail. But those lints aren't needed to | |
| // migrate. | |
| cmd.arg("--cap-lints=allow"); | |
| } else { | |
| // This allows `cargo fix` to work even if the crate has #[deny(warnings)]. | |
| cmd.arg("--cap-lints=warn"); | |
| } | |
| if let Some(edition) = self.enabled_edition { | |
| cmd.arg("--edition").arg(edition.to_string()); | |
| if self.idioms && edition.supports_idiom_lint() { | |
| cmd.arg(format!("-Wrust-{}-idioms", edition)); | |
| } | |
| } | |
| if let Some(edition) = self.prepare_for_edition { | |
| edition.force_warn_arg(cmd); | |
| } | |
| } | |
| /// Validates the edition, and sends a message indicating what is being | |
| /// done. Returns a flag indicating whether this fix should be run. | |
| fn can_run_rustfix(&self, gctx: &GlobalContext) -> CargoResult<bool> { | |
| let Some(to_edition) = self.prepare_for_edition else { | |
| return Message::Fixing { | |
| file: self.file.display().to_string(), | |
| } | |
| .post(gctx) | |
| .and(Ok(true)); | |
| }; | |
| // Unfortunately determining which cargo targets are being built | |
| // isn't easy, and each target can be a different edition. The | |
| // cargo-as-rustc fix wrapper doesn't know anything about the | |
| // workspace, so it can't check for the `cargo-features` unstable | |
| // opt-in. As a compromise, this just restricts to the nightly | |
| // toolchain. | |
| // | |
| // Unfortunately this results in a pretty poor error message when | |
| // multiple jobs run in parallel (the error appears multiple | |
| // times). Hopefully this doesn't happen often in practice. | |
| if !to_edition.is_stable() && !gctx.nightly_features_allowed { | |
| let message = format!( | |
| "`{file}` is on the latest edition, but trying to \ | |
| migrate to edition {to_edition}.\n\ | |
| Edition {to_edition} is unstable and not allowed in \ | |
| this release, consider trying the nightly release channel.", | |
| file = self.file.display(), | |
| to_edition = to_edition | |
| ); | |
| return Message::EditionAlreadyEnabled { | |
| message, | |
| edition: to_edition.previous().unwrap(), | |
| } | |
| .post(gctx) | |
| .and(Ok(false)); // Do not run rustfix for this the edition. | |
| } | |
| let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015); | |
| if from_edition == to_edition { | |
| let message = format!( | |
| "`{}` is already on the latest edition ({}), \ | |
| unable to migrate further", | |
| self.file.display(), | |
| to_edition | |
| ); | |
| Message::EditionAlreadyEnabled { | |
| message, | |
| edition: to_edition, | |
| } | |
| .post(gctx) | |
| } else { | |
| Message::Migrating { | |
| file: self.file.display().to_string(), | |
| from_edition, | |
| to_edition, | |
| } | |
| .post(gctx) | |
| } | |
| .and(Ok(true)) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::FixArgs; | |
| use std::ffi::OsString; | |
| use std::io::Write as _; | |
| use std::path::PathBuf; | |
| #[test] | |
| fn get_fix_args_from_argfile() { | |
| let mut temp = tempfile::Builder::new().tempfile().unwrap(); | |
| let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap(); | |
| let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display()); | |
| temp.write_all(content.as_bytes()).unwrap(); | |
| let argfile = format!("@{}", temp.path().display()); | |
| let args = ["cargo", &argfile]; | |
| let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap(); | |
| assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc")); | |
| assert_eq!(fix_args.file, main_rs.path()); | |
| assert_eq!(fix_args.other, vec![OsString::from("foobar")]); | |
| } | |
| #[test] | |
| fn get_fix_args_from_argfile_with_extra_arg() { | |
| let mut temp = tempfile::Builder::new().tempfile().unwrap(); | |
| let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap(); | |
| let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display()); | |
| temp.write_all(content.as_bytes()).unwrap(); | |
| let argfile = format!("@{}", temp.path().display()); | |
| let args = ["cargo", &argfile, "boo!"]; | |
| match FixArgs::from_args(args.map(|x| x.into())) { | |
| Err(e) => assert_eq!( | |
| e.to_string(), | |
| "argfile `@path` cannot be combined with other arguments" | |
| ), | |
| Ok(_) => panic!("should fail"), | |
| } | |
| } | |
| } |