use crate::code_utils::{
self, build_loop, create_temp_source_file, extract_manifest, get_source_path,
read_file_contents, remove_inner_attributes, strip_curly_braces, wrap_snippet, write_source,
};
use crate::code_utils::{extract_ast_expr, to_ast};
use crate::colors::init_styles;
use crate::config::{self, RealContext};
#[cfg(debug_assertions)]
use crate::debug_timings;
use crate::logging::is_debug_logging_enabled;
use crate::repl::run_repl;
use crate::shared::{find_crates, find_metadata};
use crate::stdin::{edit, read};
#[cfg(debug_assertions)]
use crate::VERSION;
use crate::{
coloring, cvprtln, debug_log, display_timings, get_proc_flags, manifest, maybe_config, regex,
repeat_dash, validate_args, vlog, Ast, BuildState, Cli, CrosstermEventReader, Dependencies,
Lvl, ProcFlags, ScriptState, ThagResult, DYNAMIC_SUBDIR, FLOWER_BOX_LEN, PACKAGE_NAME,
REPL_SCRIPT_NAME, REPL_SUBDIR, RS_SUFFIX, TEMP_SCRIPT_NAME, TMPDIR, V,
};
use cargo_toml::Manifest;
use firestorm::{profile_fn, profile_section};
#[cfg(debug_assertions)]
use log::{log_enabled, Level::Debug};
use nu_ansi_term::Style;
use regex::Regex;
use side_by_side_diff::create_side_by_side_diff;
use std::{
env::current_dir,
fs::{self, OpenOptions},
io::Write,
path::{Path, PathBuf},
process::Command,
string::ToString,
time::Instant,
};
pub fn execute(args: &mut Cli) -> ThagResult<()> {
let start = Instant::now();
let (maybe_color_support, term_theme) = coloring();
let proc_flags = get_proc_flags(args)?;
#[cfg(debug_assertions)]
if log_enabled!(Debug) {
log_init_setup(start, args, &proc_flags);
}
init_styles(term_theme, maybe_color_support);
if args.config {
config::edit(&RealContext::new())?;
return Ok(());
}
let is_repl = args.repl;
let working_dir_path = if is_repl {
TMPDIR.join(REPL_SUBDIR)
} else {
current_dir()?.canonicalize()?
};
validate_args(args, &proc_flags)?;
let repl_source_path = if is_repl && args.script.is_none() {
let gen_repl_temp_dir_path = TMPDIR.join(REPL_SUBDIR);
debug_log!("repl_temp_dir = std::env::temp_dir() = {gen_repl_temp_dir_path:?}");
fs::create_dir_all(&gen_repl_temp_dir_path)?;
let path = gen_repl_temp_dir_path.join(REPL_SCRIPT_NAME);
let _ = fs::File::create(&path)?;
Some(path)
} else {
None
};
let is_expr = proc_flags.contains(ProcFlags::EXPR);
let is_stdin = proc_flags.contains(ProcFlags::STDIN);
let is_edit = proc_flags.contains(ProcFlags::EDIT);
let is_loop = proc_flags.contains(ProcFlags::LOOP);
let is_dynamic = is_expr | is_stdin | is_edit | is_loop;
if is_dynamic {
let _ = create_temp_source_file()?;
}
let script_dir_path = resolve_script_dir_path(
is_repl,
args,
&working_dir_path,
repl_source_path.as_ref(),
is_dynamic,
)?;
let script_state =
set_script_state(args, script_dir_path, is_repl, repl_source_path, is_dynamic)?;
process(&proc_flags, args, &script_state, start)
}
#[inline]
fn resolve_script_dir_path(
is_repl: bool,
args: &Cli,
working_dir_path: &Path,
repl_source_path: Option<&PathBuf>,
is_dynamic: bool,
) -> ThagResult<PathBuf> {
profile_fn!(resolve_script_dir_path);
let script_dir_path = if is_repl {
if let Some(ref script) = args.script {
let source_stem = script
.strip_suffix(RS_SUFFIX)
.ok_or("Failed to strip extension off the script name")?;
working_dir_path.join(source_stem)
} else {
repl_source_path
.as_ref()
.ok_or("Missing path of newly created REPL source file")?
.parent()
.ok_or("Could not find parent directory of repl source file")?
.to_path_buf()
}
} else if is_dynamic {
debug_log!("is_dynamic={is_dynamic}");
TMPDIR.join(DYNAMIC_SUBDIR)
} else {
let script = args
.script
.as_ref()
.ok_or("Problem resolving script path")?;
let script_path = PathBuf::from(script);
let script_dir_path = script_path
.parent()
.ok_or("Problem resolving script parent path")?;
script_dir_path.canonicalize()?
};
Ok(script_dir_path)
}
#[inline]
fn set_script_state(
args: &Cli,
script_dir_path: PathBuf,
is_repl: bool,
repl_source_path: Option<PathBuf>,
is_dynamic: bool,
) -> ThagResult<ScriptState> {
profile_fn!(set_script_state);
let script_state: ScriptState = if let Some(ref script) = args.script {
let script = script.to_owned();
ScriptState::Named {
script,
script_dir_path,
}
} else if is_repl {
let script = repl_source_path
.ok_or("Missing newly created REPL source path")?
.display()
.to_string();
ScriptState::NamedEmpty {
script,
script_dir_path,
}
} else {
assert!(is_dynamic);
ScriptState::NamedEmpty {
script: String::from(TEMP_SCRIPT_NAME),
script_dir_path,
}
};
Ok(script_state)
}
#[inline]
fn process(
proc_flags: &ProcFlags,
args: &mut Cli,
script_state: &ScriptState,
start: Instant,
) -> ThagResult<()> {
let is_repl = args.repl;
let is_expr = proc_flags.contains(ProcFlags::EXPR);
let is_stdin = proc_flags.contains(ProcFlags::STDIN);
let is_edit = proc_flags.contains(ProcFlags::EDIT);
let is_loop = proc_flags.contains(ProcFlags::LOOP);
let is_dynamic = is_expr | is_stdin | is_edit | is_loop;
let mut build_state = BuildState::pre_configure(proc_flags, args, script_state)?;
if is_repl {
debug_log!("build_state.source_path={:?}", build_state.source_path);
run_repl(args, proc_flags, &mut build_state, start)
} else if is_dynamic {
let rs_source = if is_expr {
let Some(rs_source) = args.expression.take() else {
return Err("Missing expression for --expr option".into());
};
rs_source
} else if is_loop {
let Some(filter) = args.filter.take() else {
return Err("Missing expression for --loop option".into());
};
build_loop(args, filter)
} else if is_edit {
debug_log!("About to call stdin::edit()");
let event_reader = CrosstermEventReader;
let vec = edit(&event_reader)?;
debug_log!("vec={vec:#?}");
if vec.is_empty() {
return Ok(());
}
vec.join("\n")
} else {
assert!(is_stdin);
debug_log!("About to call stdin::read())");
let str = read()? + "\n";
debug_log!("str={str}");
str
};
vlog!(V::V, "rs_source={rs_source}");
let rs_manifest = extract_manifest(&rs_source, Instant::now())
?;
build_state.rs_manifest = Some(rs_manifest);
debug_log!(
r"About to try to parse following source to syn::Expr:
{rs_source}"
);
let expr_ast = extract_ast_expr(&rs_source)?;
debug_log!("expr_ast={expr_ast:#?}");
process_expr(&mut build_state, &rs_source, args, proc_flags, &start)
} else {
gen_build_run(args, proc_flags, &mut build_state, &start)
}
}
pub fn process_expr(
build_state: &mut BuildState,
rs_source: &str,
args: &Cli,
proc_flags: &ProcFlags,
start: &Instant,
) -> ThagResult<()> {
profile_fn!(process_expr);
write_source(&build_state.source_path, rs_source)?;
let result = gen_build_run(args, proc_flags, build_state, start);
vlog!(V::N, "{result:?}");
Ok(())
}
#[cfg(debug_assertions)]
fn log_init_setup(start: Instant, args: &Cli, proc_flags: &ProcFlags) {
profile_fn!(log_init_setup);
debug_log_config();
debug_timings(&start, "Set up processing flags");
debug_log!("proc_flags={proc_flags:#?}");
if !&args.args.is_empty() {
debug_log!("... args:");
for arg in &args.args {
debug_log!("{}", arg);
}
}
}
#[cfg(debug_assertions)]
fn debug_log_config() {
profile_fn!(debug_log_config);
debug_log!("PACKAGE_NAME={PACKAGE_NAME}");
debug_log!("VERSION={VERSION}");
debug_log!("REPL_SUBDIR={REPL_SUBDIR}");
}
#[allow(clippy::too_many_lines)]
pub fn gen_build_run(
args: &Cli,
proc_flags: &ProcFlags,
build_state: &mut BuildState,
start: &Instant,
) -> ThagResult<()> {
if build_state.must_gen {
let source_path: &Path = &build_state.source_path;
let start_parsing_rs = Instant::now();
let mut rs_source = read_file_contents(source_path)?;
rs_source = if rs_source.starts_with("#!") && !rs_source.starts_with("#![") {
let split_once = rs_source.split_once('\n');
#[allow(unused_variables)]
let (shebang, rust_code) = split_once.ok_or("Failed to strip shebang")?;
debug_log!("Successfully stripped shebang {shebang}");
rust_code.to_string()
} else {
rs_source
};
let sourch_path_string = source_path.to_string_lossy();
if build_state.ast.is_none() {
build_state.ast = to_ast(&sourch_path_string, &rs_source);
}
if let Some(ref ast) = build_state.ast {
build_state.crates_finder = Some(find_crates(ast));
build_state.metadata_finder = Some(find_metadata(ast));
}
let metadata_finder = build_state.metadata_finder.as_ref();
let main_methods = metadata_finder.map_or_else(
|| {
let re: &Regex = regex!(r"(?m)^\s*(async\s+)?fn\s+main\s*\(\s*\)");
re.find_iter(&rs_source).count()
},
|metadata_finder| metadata_finder.main_count,
);
let has_main = match main_methods {
0 => false,
1 => true,
_ => {
if args.multimain {
true
} else {
writeln!(
&mut std::io::stderr(),
"{main_methods} main methods found, only one allowed by default. Specify --multimain (-m) option to allow more"
)?;
std::process::exit(1);
}
}
};
let is_file = build_state.ast.as_ref().map_or(false, Ast::is_file);
build_state.build_from_orig_source = has_main && args.script.is_some() && is_file;
debug_log!(
"has_main={has_main}; build_state.build_from_orig_source={}",
build_state.build_from_orig_source
);
let rs_manifest: Manifest = { extract_manifest(&rs_source, start_parsing_rs) }?;
debug_log!("rs_source={rs_source}");
if build_state.rs_manifest.is_none() {
build_state.rs_manifest = Some(rs_manifest);
}
if build_state.rs_manifest.is_some() {
manifest::merge(build_state, &rs_source)?;
}
rs_source = if has_main {
if rs_source.starts_with('{') {
strip_curly_braces(&rs_source).unwrap_or(rs_source)
} else {
rs_source
}
} else {
let found =
if let Some(Ast::Expr(syn::Expr::Block(ref mut expr_block))) = build_state.ast {
remove_inner_attributes(expr_block)
} else {
false
};
let (inner_attribs, body) = if found {
code_utils::extract_inner_attribs(&rs_source)
} else {
(String::new(), rs_source)
};
let rust_code = build_state.ast.as_ref().map_or(body, |syntax_tree_ref| {
let returns_unit = match syntax_tree_ref {
Ast::Expr(expr) => code_utils::is_unit_return_type(expr),
Ast::File(_) => true, };
if returns_unit {
debug_log!("Option B: returns unit type");
quote::quote!(
#syntax_tree_ref
)
.to_string()
} else {
debug_log!("Option A: returns a substantive type");
debug_log!(
"args.unquote={:?}, MAYBE_CONFIG={:?}",
args.unquote,
maybe_config()
);
if proc_flags.contains(ProcFlags::UNQUOTE) {
debug_log!("\nIn unquote leg\n");
quote::quote!(
println!("{}", format!("{:?}", #syntax_tree_ref).trim_matches('"'));
)
.to_string()
} else {
debug_log!("\nIn quote leg\n");
quote::quote!(
println!("{}", format!("{:?}", #syntax_tree_ref));
)
.to_string()
}
}
});
wrap_snippet(&inner_attribs, &rust_code)
};
let maybe_rs_source = if has_main && build_state.build_from_orig_source {
None
} else {
Some(rs_source.as_str())
};
generate(build_state, maybe_rs_source, proc_flags)?;
} else {
cvprtln!(
Lvl::EMPH,
V::N,
"Skipping unnecessary generation step. Use --force (-f) to override."
);
build_state.cargo_manifest = None; }
if build_state.must_build {
build(proc_flags, build_state)?;
} else {
let build_qualifier =
if proc_flags.contains(ProcFlags::NORUN) && !proc_flags.contains(ProcFlags::BUILD) {
"Skipping cargo build step because --gen specified without --build."
} else {
"Skipping unnecessary cargo build step. Use --force (-f) to override."
};
cvprtln!(Lvl::EMPH, V::N, "{build_qualifier}");
}
if proc_flags.contains(ProcFlags::RUN) {
run(proc_flags, &args.args, build_state)?;
}
let process = &format!(
"{} completed processing script {}",
PACKAGE_NAME,
Style::from(&Lvl::EMPH).paint(&build_state.source_name)
);
display_timings(start, process, proc_flags);
Ok(())
}
pub fn generate(
build_state: &BuildState,
rs_source: Option<&str>,
proc_flags: &ProcFlags,
) -> ThagResult<()> {
profile_fn!(generate);
let start_gen = Instant::now();
if is_debug_logging_enabled() {
debug_log!("In generate, proc_flags={proc_flags}");
debug_log!(
"build_state.target_dir_path={:#?}",
build_state.target_dir_path
);
}
if !build_state.target_dir_path.exists() {
fs::create_dir_all(&build_state.target_dir_path)?;
}
let target_rs_path = build_state.target_dir_path.join(&build_state.source_name);
vlog!(V::V, "GGGGGGGG Creating source file: {target_rs_path:?}");
if !build_state.build_from_orig_source {
profile_section!(transform_snippet);
let rs_source: &str = {
#[cfg(feature = "format_snippet")]
{
let syntax_tree = syn_parse_file(rs_source)?;
prettyplease_unparse(&syntax_tree)
}
#[cfg(not(feature = "format_snippet"))]
rs_source.expect("Logic error retrieving rs_source")
};
write_source(&target_rs_path, rs_source)?;
}
let lock_path = &build_state.target_dir_path.join("Cargo.lock");
if lock_path.exists() {
fs::remove_file(lock_path)?;
}
let manifest = &build_state
.cargo_manifest
.as_ref()
.ok_or("Could not unwrap BuildState.cargo_manifest")?;
let cargo_manifest_str: &str = &toml::to_string(manifest)?;
debug_log!(
"cargo_manifest_str: {}",
code_utils::disentangle(cargo_manifest_str)
);
let mut toml_file = OpenOptions::new()
.write(true)
.create(true) .truncate(true) .open(&build_state.cargo_toml_path)?;
toml_file.write_all(cargo_manifest_str.as_bytes())?;
display_timings(&start_gen, "Completed generation", proc_flags);
Ok(())
}
#[inline]
#[cfg(feature = "format_snippet")]
fn syn_parse_file(rs_source: Option<&str>) -> ThagResult<syn::File> {
profile_fn!(syn_parse_file);
let syntax_tree = syn::parse_file(rs_source.ok_or("Logic error retrieving rs_source")?)?;
Ok(syntax_tree)
}
#[inline]
#[cfg(feature = "format_snippet")]
fn prettyplease_unparse(syntax_tree: &syn::File) -> String {
profile_fn!(prettyplease_unparse);
prettyplease::unparse(syntax_tree)
}
pub fn build(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
let start_build = Instant::now();
profile_fn!(build);
vlog!(V::V, "BBBBBBBB In build");
if proc_flags.contains(ProcFlags::EXPAND) {
handle_expand(proc_flags, build_state)
} else {
handle_build_or_check(proc_flags, build_state)
}?;
display_timings(&start_build, "Completed build", proc_flags);
Ok(())
}
fn create_cargo_command(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<Command> {
profile_fn!(create_cargo_command);
let cargo_toml_path_str = code_utils::path_to_str(&build_state.cargo_toml_path)?;
let mut cargo_command = Command::new("cargo");
let args = build_command_args(proc_flags, build_state, &cargo_toml_path_str);
cargo_command.args(&args);
configure_command_output(&mut cargo_command, proc_flags);
Ok(cargo_command)
}
fn build_command_args(
proc_flags: &ProcFlags,
build_state: &BuildState,
cargo_toml_path: &str,
) -> Vec<String> {
profile_fn!(build_command_args);
let mut args = vec![
get_cargo_subcommand(proc_flags, build_state).to_string(),
"--manifest-path".to_string(),
cargo_toml_path.to_string(),
];
if proc_flags.contains(ProcFlags::QUIET) || proc_flags.contains(ProcFlags::QUIETER) {
args.push("--quiet".to_string());
}
if proc_flags.contains(ProcFlags::EXECUTABLE) {
args.push("--release".to_string());
} else if proc_flags.contains(ProcFlags::EXPAND) {
args.extend_from_slice(&[
"--bin".to_string(),
build_state.source_stem.clone(),
"--theme=gruvbox-dark".to_string(),
]);
} else if proc_flags.contains(ProcFlags::CARGO) {
args.extend_from_slice(&build_state.args[1..]);
}
args
}
fn get_cargo_subcommand(proc_flags: &ProcFlags, build_state: &BuildState) -> &'static str {
profile_fn!(get_cargo_subcommand);
if proc_flags.contains(ProcFlags::CHECK) {
"check"
} else if proc_flags.contains(ProcFlags::EXPAND) {
"expand"
} else if proc_flags.contains(ProcFlags::CARGO) {
Box::leak(build_state.args[0].clone().into_boxed_str())
} else {
"build"
}
}
fn configure_command_output(command: &mut Command, proc_flags: &ProcFlags) {
profile_fn!(configure_command_output);
if proc_flags.contains(ProcFlags::QUIETER) || proc_flags.contains(ProcFlags::EXPAND) {
command
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
} else {
command
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
}
}
fn handle_expand(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
profile_fn!(handle_expand);
let mut cargo_command = create_cargo_command(proc_flags, build_state)?;
let output = cargo_command.output()?;
if !output.status.success() {
eprintln!(
"Error running `cargo expand`: {}",
String::from_utf8_lossy(&output.stderr)
);
return Err("Expansion failed".into());
}
display_expansion_diff(output.stdout, build_state)?;
Ok(())
}
fn handle_build_or_check(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
profile_fn!(handle_build_or_check);
let mut cargo_command = create_cargo_command(proc_flags, build_state)?;
let status = cargo_command.spawn()?.wait()?;
if !status.success() {
display_build_failure();
return Err("Build failed".into());
}
if proc_flags.contains(ProcFlags::EXECUTABLE) {
deploy_executable(build_state)?;
}
Ok(())
}
fn display_expansion_diff(stdout: Vec<u8>, build_state: &BuildState) -> ThagResult<()> {
profile_fn!(display_expansion_diff);
let expanded_source = String::from_utf8(stdout)?;
let unexpanded_path = get_source_path(build_state);
let unexpanded_source = std::fs::read_to_string(unexpanded_path)?;
let max_width = if let Ok((width, _height)) = crossterm::terminal::size() {
(width - 26) / 2
} else {
80
};
let diff = create_side_by_side_diff(&unexpanded_source, &expanded_source, max_width.into());
println!("{diff}");
Ok(())
}
fn display_build_failure() {
profile_fn!(display_build_failure);
cvprtln!(&Lvl::ERR, V::N, "Build failed");
let config = maybe_config();
let binding = Dependencies::default();
let dep_config = config.as_ref().map_or(&binding, |c| &c.dependencies);
let inference_level = &dep_config.inference_level;
let advice = match inference_level {
config::DependencyInference::None => "You are running without dependency inference.",
config::DependencyInference::Min => "You may be missing features or `thag` may not be picking up dependencies.",
config::DependencyInference::Config => "You may need to tweak your config feature overrides or 'toml` block",
config::DependencyInference::Max => "It may be that maximal dependency inference is specifying conflicting features. Consider trying `config` or failing that, a `toml` block",
};
cvprtln!(
&Lvl::EMPH,
V::N,
r#"Dependency inference_level={inference_level:#?}
If the problem is a dependency error, consider the following advice:
{advice}
{}"#,
if matches!(inference_level, config::DependencyInference::Config) {
""
} else {
"Consider running with dependency inference_level configured as `config` or an embedded `toml` block."
}
);
}
fn deploy_executable(build_state: &BuildState) -> ThagResult<()> {
profile_fn!(deploy_executable);
let mut cargo_bin_path = home::home_dir().ok_or("Could not find home directory")?;
let cargo_bin_subdir = ".cargo/bin";
cargo_bin_path.push(cargo_bin_subdir);
if !cargo_bin_path.exists() {
fs::create_dir_all(&cargo_bin_path)?;
}
let name_option = build_state.cargo_manifest.as_ref().and_then(|manifest| {
let mut iter = manifest
.bin
.iter()
.filter_map(|p: &cargo_toml::Product| p.name.as_ref().map(ToString::to_string));
match (iter.next(), iter.next()) {
(Some(name), None) => Some(name), _ => None, }
});
#[allow(clippy::option_if_let_else)]
let executable_name = if let Some(name) = name_option {
name
} else {
#[cfg(target_os = "windows")]
{
format!("{}.exe", build_state.source_stem)
}
#[cfg(not(target_os = "windows"))]
{
build_state.source_stem.to_string()
}
};
let executable_path = &build_state
.target_dir_path
.join("target/release")
.join(&executable_name);
let output_path = cargo_bin_path.join(&build_state.source_stem);
debug_log!("executable_path={executable_path:#?}, output_path={output_path:#?}");
fs::rename(executable_path, output_path)?;
repeat_dash!(70);
cvprtln!(Lvl::EMPH, V::Q, "{DASH_LINE}");
vlog!(
V::QQ,
"Executable built and moved to ~/{cargo_bin_subdir}/{executable_name}"
);
cvprtln!(Lvl::EMPH, V::Q, "{DASH_LINE}");
Ok(())
}
pub fn run(proc_flags: &ProcFlags, args: &[String], build_state: &BuildState) -> ThagResult<()> {
let start_run = Instant::now();
#[cfg(debug_assertions)]
debug_log!("RRRRRRRR In run");
let target_path: &Path = build_state.target_path.as_ref();
let mut run_command = Command::new(format!("{}", target_path.display()));
run_command.args(args);
debug_log!("Run command is {run_command:?}");
let dash_line = "-".repeat(FLOWER_BOX_LEN);
cvprtln!(Lvl::EMPH, V::Q, "{dash_line}");
let _exit_status = run_command.spawn()?.wait()?;
cvprtln!(Lvl::EMPH, V::Q, "{dash_line}");
display_timings(&start_run, "Completed run", proc_flags);
Ok(())
}