/
githubmirror
/
sway
Обзор
Документация
Войти
/
githubmirror
/
sway
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
forc-plugins/forc-migrate/src/cli/shared.rs
333 строки
12 KB
Igor Rončević
Add migration for `new_hashing` (#7287)
18 июл 2025, 07:40
Не верифицирован
18 июл 2025, 07:40
e303b0f
Код
Авторство
О чём код?
use std::path::PathBuf; use anyhow::{bail, Ok, Result}; use clap::Parser; use forc_pkg::{self as pkg, PackageManifestFile}; use forc_pkg::{ manifest::{GenericManifestFile, ManifestFile}, source::IPFSNode, }; use forc_tracing::println_action_green; use sway_core::{BuildTarget, Engines}; use sway_error::diagnostic::*; use sway_features::{ExperimentalFeatures, Feature}; use sway_types::SourceEngine; use crate::migrations::{MigrationStepKind, MigrationStepsWithOccurrences, Occurrence}; use crate::{ instructive_error, migrations::{MigrationStep, MigrationStepExecution, ProgramInfo}, }; /// Args that can be shared between all commands that `compile` a package. E.g. `check`, `run`. #[derive(Debug, Default, Parser)] pub(crate) struct Compile { /// Path to the project. /// /// If not specified, current working directory will be used. #[clap(short, long)] pub path: Option<String>, /// Offline mode, prevents Forc from using the network when managing dependencies. /// Meaning it will only try to use previously downloaded dependencies. #[clap(long = "offline")] pub offline: bool, /// Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it /// needs to be updated, Forc will exit with an error. #[clap(long)] pub locked: bool, /// The IPFS Node to use for fetching IPFS sources. /// /// Possible values: FUEL, PUBLIC, LOCAL, <GATEWAY_URL> #[clap(long)] pub ipfs_node: Option<IPFSNode>, #[clap(flatten)] pub experimental: sway_features::CliFields, /// Silent mode. Don't output any warnings or errors to the command line. #[clap(long = "silent", short = 's')] pub silent: bool, } impl Compile { /// Returns the [Compile::path] if provided, otherwise the current directory. pub(crate) fn manifest_dir(&self) -> std::io::Result<PathBuf> { if let Some(path) = &self.path { std::result::Result::Ok(PathBuf::from(path)) } else { std::env::current_dir() } } /// Returns the cumulative [ExperimentalFeatures], from the package manifest /// file and the CLI experimental flag. pub(crate) fn experimental_features(&self) -> Result<ExperimentalFeatures> { let manifest = ManifestFile::from_dir(self.manifest_dir()?)?; let pkg_manifest = get_pkg_manifest_file(&manifest)?; Ok(ExperimentalFeatures::new( &pkg_manifest.project.experimental, &self.experimental.experimental, &self.experimental.no_experimental, ) .map_err(|err| anyhow::anyhow!("{err}"))?) } } // Clippy issue. It erroneously assumes that `vec!`s in `instructive_error` calls are not needed. #[allow(clippy::useless_vec)] fn get_pkg_manifest_file(manifest: &ManifestFile) -> Result<&PackageManifestFile> { match manifest { ManifestFile::Package(pkg_manifest) => Ok(pkg_manifest), ManifestFile::Workspace(_) => Err(anyhow::anyhow!(instructive_error( "`forc migrate` does not support migrating workspaces.", &vec![ &format!("\"{}\" is a workspace.", manifest.dir().to_string_lossy()), "Please migrate each workspace member individually.", ] ))), } } // Clippy issue. It erroneously assumes that `vec!`s in `instructive_error` calls are not needed. #[allow(clippy::useless_vec)] pub(crate) fn compile_package<'a>( engines: &'a Engines, build_instructions: &Compile, ) -> Result<ProgramInfo<'a>> { let manifest_dir = build_instructions.manifest_dir()?; let manifest = ManifestFile::from_dir(manifest_dir.clone())?; let pkg_manifest = get_pkg_manifest_file(&manifest)?; let pkg_name = pkg_manifest.project_name(); println_action_green( "Compiling", &format!("{} ({})", pkg_name, manifest.dir().to_string_lossy()), ); let member_manifests = manifest.member_manifests()?; let lock_path = manifest.lock_path()?; let ipfs_node = build_instructions.ipfs_node.clone().unwrap_or_default(); let plan = pkg::BuildPlan::from_lock_and_manifests( &lock_path, &member_manifests, build_instructions.locked, build_instructions.offline, &ipfs_node, )?; let include_tests = true; // We want to migrate the tests as well. let mut compile_results = pkg::check( &plan, BuildTarget::default(), build_instructions.silent, None, include_tests, engines, None, &build_instructions.experimental.experimental, &build_instructions.experimental.no_experimental, sway_core::DbgGeneration::Full, )?; let Some(programs) = compile_results .pop() .and_then(|(programs, handler)| if handler.has_errors() { None } else { programs }) else { bail!(instructive_compilation_error( &pkg_manifest.path().to_string_lossy() )); }; let core::result::Result::Ok(ty_program) = programs.typed else { bail!(instructive_compilation_error( &pkg_manifest.path().to_string_lossy() )); }; return Ok(ProgramInfo { pkg_name: pkg_name.to_string(), lexed_program: programs.lexed, ty_program, engines, }); fn instructive_compilation_error(manifest_dir: &str) -> String { instructive_error("The Sway project cannot be compiled.", &vec![ &format!("`forc migrate` could not compile the Sway project located at \"{manifest_dir}\"."), "To see the compilation errors, run `forc build` on the project.", "Did you maybe forget to specify experimental features?", "If the project uses experimental features, they need to be specified when running `forc migrate`.", "E.g.:", " forc migrate run --experimental <feature_1>,<feature_2>", ]) } } pub(crate) const PROJECT_IS_COMPATIBLE: &str = "Project is compatible with the next breaking change version of Sway"; pub(crate) fn print_features_and_migration_steps( features_and_migration_steps: MigrationStepsWithOccurrences, ) { let show_migration_effort = features_and_migration_steps .iter() .flat_map(|(_, steps)| steps.iter().map(|step| step.1)) .all(|occurrences| occurrences.is_some()); let mut total_migration_effort = 0; for (feature, migration_steps) in features_and_migration_steps { println!("{}", feature.name()); for (migration_step, occurrence) in migration_steps.iter() { println!( " {} {}", match migration_step.execution() { MigrationStepExecution::Manual => "[M]", MigrationStepExecution::Semiautomatic => "[S]", MigrationStepExecution::Automatic => "[A]", }, migration_step.title ); if show_migration_effort { let count = occurrence .expect("if the `show_migration_effort` is true, all occurrences are `Some`"); // For automatic steps **that have occurrences**, plan ~10 minutes // for the review of the automatically changed code. let migration_effort_in_mins = if migration_step.duration == 0 && count > 0 { 10 } else { // Otherwise, a very simple linear calculation will give // a decent and useful rough estimate. count * migration_step.duration }; println!( " Occurrences: {count:>5} Migration effort (hh::mm): ~{}\n", duration_to_str(migration_effort_in_mins) ); total_migration_effort += migration_effort_in_mins; } } if !show_migration_effort { println!(); } } if show_migration_effort { println!( "Total migration effort (hh::mm): ~{}", duration_to_str(total_migration_effort) ); // If there are no occurrences in code that require migration, // inform that the project is compatible with the next breaking change version of Sway. let num_of_occurrences = features_and_migration_steps .iter() .flat_map(|(_, steps)| steps.iter().map(|step| step.1.unwrap_or(0))) .sum::<usize>(); if num_of_occurrences == 0 { println!(); println!("{PROJECT_IS_COMPATIBLE}."); } } } /// Creates a single migration [Diagnostic] that shows **all the occurrences** in code /// that require migration effort expected by the `migration_step`. /// /// Returns `None` if the migration step is not necessary, in other words, if there /// are no occurrences in code that require this particular migration. pub(crate) fn create_migration_diagnostic( source_engine: &SourceEngine, feature: &Feature, migration_step: &MigrationStep, occurrences: &[Occurrence], ) -> Option<Diagnostic> { if occurrences.is_empty() { return None; } let description = format!("[{}] {}", feature.name(), migration_step.title); Some(Diagnostic { reason: Some(Reason::new(Code::migrations(1), description)), issue: Issue::info( source_engine, occurrences[0].span.clone(), occurrences[0].msg_or_empty(), ), hints: occurrences .iter() .skip(1) .map(|occurrence| { Hint::info( source_engine, occurrence.span.clone(), occurrence.msg_or_empty(), ) }) .collect(), help: migration_step .help .iter() .map(|help| help.to_string()) .chain(if migration_step.help.is_empty() { vec![] } else { vec![Diagnostic::help_empty_line()] }) .chain(match migration_step.kind { MigrationStepKind::Instruction(_) => vec![], MigrationStepKind::CodeModification(_, [], _) => vec![], MigrationStepKind::CodeModification(_, manual_migration_actions, _) => { get_manual_migration_actions_help(manual_migration_actions) } MigrationStepKind::Interaction(_, _, [], _) => vec![ "This migration step will interactively modify the code, based on your input." .to_string(), Diagnostic::help_empty_line(), ], MigrationStepKind::Interaction(_, _, manual_migration_actions, _) => vec![ "This migration step will interactively modify the code, based on your input." .to_string(), Diagnostic::help_empty_line(), ] .into_iter() .chain(get_manual_migration_actions_help(manual_migration_actions)) .collect(), }) .chain(vec![detailed_migration_guide_msg(feature)]) .collect(), }) } fn get_manual_migration_actions_help(manual_migration_actions: &[&str]) -> Vec<String> { ["After the migration, you will still need to:".to_string()] .into_iter() .chain( manual_migration_actions .iter() .map(|help| format!("- {help}")) .chain(vec![Diagnostic::help_empty_line()]), ) .collect() } pub(crate) fn detailed_migration_guide_msg(feature: &Feature) -> String { format!("For a detailed migration guide see: {}", feature.url()) } fn duration_to_str(duration_in_mins: usize) -> String { let hours = duration_in_mins / 60; let minutes = duration_in_mins % 60; format!("{hours:#02}:{minutes:#02}") } pub(crate) fn max_feature_name_len<T>(features: &[(Feature, T)]) -> usize { features .iter() .map(|(feature, _)| feature.name().len()) .max() .unwrap_or_default() }