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},
};
#[derive(Debug, Default, Parser)]
pub(crate) struct Compile {
#[clap(short, long)]
pub path: Option<String>,
#[clap(long = "offline")]
pub offline: bool,
#[clap(long)]
pub locked: bool,
#[clap(long)]
pub ipfs_node: Option<IPFSNode>,
#[clap(flatten)]
pub experimental: sway_features::CliFields,
#[clap(long = "silent", short = 's')]
pub silent: bool,
}
impl Compile {
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()
}
}
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}"))?)
}
}
#[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.",
]
))),
}
}
#[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; 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`");
let migration_effort_in_mins = if migration_step.duration == 0 && count > 0 {
10
} else {
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)
);
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}.");
}
}
}
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()
}