use crate::ast::{self, is_unit_return_type};
use crate::code_utils::{
self, build_loop, create_temp_source_file, extract_ast_expr, get_source_path,
read_file_contents, remove_inner_attributes, strip_curly_braces, to_ast, wrap_snippet,
write_source,
};
use crate::config::{self, DependencyInference, RealContext};
use crate::crossterm::terminal;
use crate::manifest::extract;
use crate::Verbosity::{Debug as Dbug, Verbose};
use crate::{
get_home_dir, get_proc_flags, get_verbosity, manifest, maybe_config, modified_since_compiled,
repeat_dash, validate_args, Ast, Cli, ColorSupport, Dependencies, ProcFlags, Role, ThagError,
ThagResult, DYNAMIC_SUBDIR, EXECUTABLE_CACHE_SUBDIR, FLOWER_BOX_LEN, PACKAGE_NAME,
REPL_SCRIPT_NAME, REPL_SUBDIR, RS_SUFFIX, SHARED_TARGET_SUBDIR, TEMP_DIR_NAME,
TEMP_SCRIPT_NAME, TMPDIR, TOML_NAME,
};
use cargo_toml::Manifest;
use regex::Regex;
use side_by_side_diff::create_side_by_side_diff;
use std::env;
use std::{
fs::{self, OpenOptions},
io::Write,
path::{Path, PathBuf},
process::Command,
string::ToString,
time::Instant,
};
use thag_common::{self, debug_log, re, vprtln, V};
use thag_profiler::profiled;
use thag_styling::{paint_for_role, svprtln, TermAttributes};
#[cfg(feature = "tui")]
use crate::{
stdin::{edit, read},
CrosstermEventReader,
};
#[cfg(debug_assertions)]
use {
crate::{debug_timings, logging::is_debug_logging_enabled, VERSION},
log::{log_enabled, Level::Debug},
};
#[cfg(feature = "repl")]
use crate::repl::run_repl;
#[cfg(feature = "build")]
struct ExecutionFlags {
is_repl: bool,
is_dynamic: bool,
}
#[cfg(feature = "build")]
impl ExecutionFlags {
const fn new(proc_flags: &ProcFlags, cli: &Cli) -> Self {
let is_repl = proc_flags.contains(ProcFlags::REPL);
let is_expr = cli.expression.is_some();
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;
Self {
is_repl,
is_dynamic,
}
}
}
#[cfg(feature = "build")]
struct BuildPaths {
working_dir_path: PathBuf,
source_path: PathBuf,
source_dir_path: PathBuf,
cargo_home: PathBuf,
target_dir_path: PathBuf,
target_path: PathBuf,
cargo_toml_path: PathBuf,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, Default)]
#[cfg(feature = "build")]
pub struct BuildState {
#[allow(dead_code)]
pub working_dir_path: PathBuf,
pub source_stem: String,
pub source_name: String,
#[allow(dead_code)]
pub source_dir_path: PathBuf,
pub source_path: PathBuf,
pub cargo_home: PathBuf,
pub target_dir_path: PathBuf,
pub target_path: PathBuf,
pub cargo_toml_path: PathBuf,
pub rs_manifest: Option<Manifest>,
pub cargo_manifest: Option<Manifest>,
pub must_gen: bool,
pub must_build: bool,
pub build_from_orig_source: bool,
pub thag_auto_processed: bool,
pub ast: Option<Ast>,
pub crates_finder: Option<ast::CratesFinder>,
pub metadata_finder: Option<ast::MetadataFinder>,
pub infer: DependencyInference,
pub features: Option<String>,
pub args: Vec<String>,
}
#[cfg(feature = "build")]
impl BuildState {
#[profiled]
pub fn pre_configure(
proc_flags: &ProcFlags,
cli: &Cli,
script_state: &ScriptState,
) -> ThagResult<Self> {
let (source_name, source_stem) = Self::extract_script_info(script_state)?;
let execution_flags = ExecutionFlags::new(proc_flags, cli);
let paths = Self::set_up_paths(&execution_flags, script_state, &source_name, &source_stem)?;
let mut build_state = Self::create_initial_state(paths, source_name, source_stem, cli);
build_state.determine_build_requirements(proc_flags, script_state, &execution_flags)?;
#[cfg(debug_assertions)]
build_state.validate_state(proc_flags);
Ok(build_state)
}
#[profiled]
fn extract_script_info(script_state: &ScriptState) -> ThagResult<(String, String)> {
let script = script_state
.get_script()
.ok_or(ThagError::NoneOption("No script specified".to_string()))?;
let path = Path::new(&script);
let filename = path
.file_name()
.ok_or(ThagError::NoneOption("No filename specified".to_string()))?;
let source_name = filename
.to_str()
.ok_or(ThagError::NoneOption(
"Error converting filename to a string".to_string(),
))?
.to_string();
let source_stem = source_name
.strip_suffix(RS_SUFFIX)
.ok_or_else(|| -> ThagError {
format!("Error stripping suffix from {source_name}").into()
})?
.to_string();
Ok((source_name, source_stem))
}
#[profiled]
fn set_up_paths(
flags: &ExecutionFlags,
script_state: &ScriptState,
source_name: &str,
source_stem: &str,
) -> ThagResult<BuildPaths> {
let working_dir_path = if flags.is_repl {
TMPDIR.join(REPL_SUBDIR)
} else {
env::current_dir()?.canonicalize()?
};
let script_path = if flags.is_repl {
script_state
.get_script_dir_path()
.ok_or("Missing script path")?
.join(source_name)
} else if flags.is_dynamic {
script_state
.get_script_dir_path()
.ok_or("Missing script path")?
.join(TEMP_SCRIPT_NAME)
} else {
working_dir_path.join(script_state.get_script().unwrap()) };
let source_path = script_path.canonicalize()?;
if !source_path.exists() {
return Err(format!(
"No script named {source_stem} or {source_name} in path {}",
source_path.display()
)
.into());
}
let source_dir_path = source_path
.parent()
.ok_or("Problem resolving to parent directory")?
.to_path_buf();
let cargo_home = PathBuf::from(match std::env::var("CARGO_HOME") {
Ok(string) if string != String::new() => string,
_ => {
let home_dir = get_home_dir()?;
home_dir.join(".cargo").display().to_string()
}
});
let target_dir_path = if flags.is_repl {
script_state
.get_script_dir_path()
.ok_or("Missing ScriptState::NamedEmpty.repl_path")?
.join(TEMP_DIR_NAME)
} else if flags.is_dynamic {
TMPDIR.join(DYNAMIC_SUBDIR)
} else {
TMPDIR.join(PACKAGE_NAME).join(source_stem)
};
let mut target_path = TMPDIR.join(EXECUTABLE_CACHE_SUBDIR);
#[cfg(target_os = "windows")]
{
target_path = target_path.join(format!("{source_stem}.exe"));
}
#[cfg(not(target_os = "windows"))]
{
target_path = target_path.join(source_stem);
}
let cargo_toml_path = target_dir_path.join(TOML_NAME);
Ok(BuildPaths {
working_dir_path,
source_path,
source_dir_path,
cargo_home,
target_dir_path,
target_path,
cargo_toml_path,
})
}
#[profiled]
fn create_initial_state(
paths: BuildPaths,
source_name: String,
source_stem: String,
cli: &Cli,
) -> Self {
Self {
working_dir_path: paths.working_dir_path,
source_stem,
source_name,
source_dir_path: paths.source_dir_path,
source_path: paths.source_path,
cargo_home: paths.cargo_home,
target_dir_path: paths.target_dir_path,
target_path: paths.target_path,
cargo_toml_path: paths.cargo_toml_path,
ast: None,
crates_finder: None,
metadata_finder: None,
thag_auto_processed: false,
infer: cli.infer.as_ref().map_or_else(
|| {
let config = maybe_config();
let binding = Dependencies::default();
let dep_config = config.as_ref().map_or(&binding, |c| &c.dependencies);
let infer = &dep_config.inference_level;
infer.clone()
},
Clone::clone,
),
args: cli.args.clone(),
features: cli.features.clone(),
..Default::default()
}
}
#[profiled]
fn determine_build_requirements(
&mut self,
proc_flags: &ProcFlags,
script_state: &ScriptState,
flags: &ExecutionFlags,
) -> ThagResult<()> {
if flags.is_dynamic
|| flags.is_repl
|| proc_flags.contains(ProcFlags::FORCE)
|| proc_flags.contains(ProcFlags::CHECK)
{
self.must_gen = true;
self.must_build = true;
return Ok(());
}
if proc_flags.contains(ProcFlags::NORUN) {
self.must_build = proc_flags.contains(ProcFlags::BUILD)
|| proc_flags.contains(ProcFlags::EXECUTABLE)
|| proc_flags.contains(ProcFlags::EXPAND)
|| proc_flags.contains(ProcFlags::CARGO)|| proc_flags.contains(ProcFlags::TEST_ONLY);
self.must_gen = self.must_build
|| proc_flags.contains(ProcFlags::GENERATE)
|| !self.cargo_toml_path.exists();
return Ok(());
}
if matches!(script_state, ScriptState::NamedEmpty { .. })
|| !self.target_path.exists()
|| modified_since_compiled(self)?.is_some()
{
self.must_gen = true;
self.must_build = true;
return Ok(());
}
self.must_gen = false;
self.must_build = false;
Ok(())
}
#[cfg(debug_assertions)]
#[profiled]
fn validate_state(&self, proc_flags: &ProcFlags) {
if proc_flags.contains(ProcFlags::BUILD)
|| proc_flags.contains(ProcFlags::CHECK)
|| proc_flags.contains(ProcFlags::EXECUTABLE)
|| proc_flags.contains(ProcFlags::EXPAND)
|| proc_flags.contains(ProcFlags::CARGO)
|| proc_flags.contains(ProcFlags::TEST_ONLY)
{
assert!(self.must_gen & self.must_build & proc_flags.contains(ProcFlags::NORUN));
}
if proc_flags.contains(ProcFlags::FORCE) {
assert!(self.must_gen & self.must_build);
}
if self.must_build {
assert!(self.must_gen);
}
debug_log!("build_state={self:#?}");
}
}
#[derive(Debug)]
pub enum ScriptState {
#[allow(dead_code)]
Anonymous,
NamedEmpty {
script: String,
script_dir_path: PathBuf,
},
Named {
script: String,
script_dir_path: PathBuf,
},
}
impl ScriptState {
#[must_use]
#[profiled]
pub fn get_script(&self) -> Option<String> {
match self {
Self::Anonymous => None,
Self::NamedEmpty { script, .. } | Self::Named { script, .. } => {
Some(script.to_string())
}
}
}
#[must_use]
#[profiled]
pub fn get_script_dir_path(&self) -> Option<PathBuf> {
match self {
Self::Anonymous => None,
Self::Named {
script_dir_path, ..
} => Some(script_dir_path.clone()),
Self::NamedEmpty {
script_dir_path: script_path,
..
} => Some(script_path.clone()),
}
}
}
#[profiled]
fn clean_cache(what: &str) -> ThagResult<()> {
let bins_dir = TMPDIR.join(EXECUTABLE_CACHE_SUBDIR);
let target_dir = TMPDIR.join(SHARED_TARGET_SUBDIR);
match what {
"bins" => {
if bins_dir.exists() {
vprtln!(V::N, "Cleaning executable cache: {}", bins_dir.display());
fs::remove_dir_all(&bins_dir)?;
vprtln!(V::N, "✓ Executable cache cleaned");
} else {
vprtln!(V::N, "Executable cache does not exist");
}
}
"target" => {
if target_dir.exists() {
vprtln!(
V::N,
"Cleaning shared build cache: {}",
target_dir.display()
);
fs::remove_dir_all(&target_dir)?;
vprtln!(V::N, "✓ Shared build cache cleaned");
} else {
vprtln!(V::N, "Shared build cache does not exist");
}
}
"all" => {
let cleaned = if bins_dir.exists() {
vprtln!(V::N, "Cleaning executable cache: {}", bins_dir.display());
fs::remove_dir_all(&bins_dir)?;
true
} else if target_dir.exists() {
vprtln!(
V::N,
"Cleaning shared build cache: {}",
target_dir.display()
);
fs::remove_dir_all(&target_dir)?;
true
} else {
false
};
if cleaned {
vprtln!(V::N, "✓ All caches cleaned");
} else {
vprtln!(V::N, "No caches to clean");
}
}
_ => {
return Err(
format!("Invalid clean option: '{what}'. Use 'bins', 'target', or 'all'").into(),
);
}
}
Ok(())
}
pub fn execute(args: &mut Cli) -> ThagResult<()> {
let start = Instant::now();
TermAttributes::get_or_init();
let proc_flags = get_proc_flags(args)?;
#[cfg(debug_assertions)]
if log_enabled!(Debug) {
log_init_setup(start, args, &proc_flags);
}
if args.config {
config::open(&RealContext::new())?;
return Ok(());
}
if let Some(ref what) = args.clean {
return clean_cache(what);
}
let is_repl = args.repl;
validate_args(args, &proc_flags)?;
let repl_source_path = if is_repl {
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);
if is_edit && TermAttributes::get_or_init().color_support == ColorSupport::None {
return Err(ThagError::UnsupportedTerm(
r" for `--edit (-d)` option.
Unfortunately, TUI features require terminal color support.
As an alternative, consider using the `edit` + `run` functions of `--repl (-r)`."
.into(),
));
}
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, 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,
repl_source_path: Option<&PathBuf>,
is_dynamic: bool,
) -> ThagResult<PathBuf> {
let script_dir_path = if is_repl {
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);
if !script_path.exists() {
return Err(ThagError::FromStr(
format!("Script path `{}` does not exist.", script_path.display()).into(),
));
}
script_path
.canonicalize()?
.parent()
.ok_or("Problem resolving script parent path")?
.to_path_buf()
};
Ok(script_dir_path)
}
#[inline]
#[profiled]
fn set_script_state(
args: &Cli,
script_dir_path: PathBuf,
is_repl: bool,
repl_source_path: Option<PathBuf>,
is_dynamic: bool,
) -> ThagResult<ScriptState> {
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 {
#[cfg(not(feature = "repl"))]
return Err("repl requires `repl` feature".into());
#[cfg(feature = "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 {
#[cfg(not(feature = "tui"))]
return Err("`stdin` and `edit` options require `tui` feature".into());
#[cfg(feature = "tui")]
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 {
debug_log!("About to call stdin::read())");
let str = read()? + "\n";
debug_log!("str={str}");
str
}
};
vprtln!(V::V, "rs_source={rs_source}");
let rs_manifest = extract(&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)
}
}
#[profiled]
pub fn process_expr(
build_state: &mut BuildState,
rs_source: &str,
args: &Cli,
proc_flags: &ProcFlags,
start: &Instant,
) -> ThagResult<()> {
write_source(&build_state.source_path, rs_source)?;
let result = gen_build_run(args, proc_flags, build_state, start);
vprtln!(V::V, "{result:?}");
Ok(())
}
#[cfg(debug_assertions)]
#[profiled]
fn log_init_setup(start: Instant, args: &Cli, proc_flags: &ProcFlags) {
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)]
#[profiled]
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)]
#[allow(clippy::cognitive_complexity)]
#[profiled]
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(ast::find_crates(ast));
build_state.metadata_finder = Some(ast::find_metadata(ast));
}
let test_only = proc_flags.contains(ProcFlags::TEST_ONLY);
let metadata_finder = build_state.metadata_finder.as_ref();
let main_methods = if test_only {
None
} else {
Some(metadata_finder.map_or_else(
|| {
let re: &Regex = re!(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: Option<bool> = if test_only {
None
} else {
match main_methods {
Some(0) => Some(false),
Some(1) => Some(true),
Some(count) => {
if args.multimain {
Some(true)
} else {
writeln!(
&mut std::io::stderr(),
"{count} main methods found, only one allowed by default. Specify --multimain (-m) option to allow more"
)?;
std::process::exit(1);
}
}
None => None,
}
};
let is_file = build_state.ast.as_ref().is_some_and(Ast::is_file);
build_state.build_from_orig_source =
(test_only || has_main == Some(true)) && args.script.is_some() && is_file;
debug_log!(
"has_main={has_main:#?}; build_state.build_from_orig_source={}",
build_state.build_from_orig_source
);
debug_log!("rs_source={rs_source}");
if build_state.rs_manifest.is_none() {
let rs_manifest: Manifest = { extract(&rs_source, start_parsing_rs) }?;
build_state.rs_manifest = Some(rs_manifest);
}
if build_state.rs_manifest.is_some() {
manifest::process_thag_auto_dependencies(build_state)?;
manifest::merge(build_state, &rs_source)?;
}
rs_source = if test_only || has_main == Some(true) {
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) => 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 (test_only || has_main == Some(true)) && build_state.build_from_orig_source {
None
} else {
Some(rs_source.as_str())
};
generate(build_state, maybe_rs_source, proc_flags)?;
} else {
svprtln!(
Role::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."
};
svprtln!(Role::EMPH, V::N, "{build_qualifier}");
}
if proc_flags.contains(ProcFlags::RUN) {
run(proc_flags, &args.args, build_state)?;
}
let process = &format!(
"{PACKAGE_NAME} completed processing script {}",
paint_for_role(Role::EMPH, &build_state.source_name)
);
display_timings(start, process, proc_flags);
Ok(())
}
#[profiled]
pub fn generate(
build_state: &BuildState,
rs_source: Option<&str>,
proc_flags: &ProcFlags,
) -> ThagResult<()> {
let start_gen = Instant::now();
#[cfg(debug_assertions)]
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);
vprtln!(V::V, "GGGGGGGG Creating source file: {target_rs_path:?}");
if !build_state.build_from_orig_source {
let rs_source: &str = {
#[cfg(not(feature = "no_format_snippet"))]
{
let syntax_tree = syn_parse_file(rs_source)?;
&prettyplease_unparse(&syntax_tree)
}
#[cfg(feature = "no_format_snippet")]
{
if proc_flags.contains(ProcFlags::CARGO)
|| proc_flags.contains(ProcFlags::TEST_ONLY)
{
let syntax_tree = syn_parse_file(rs_source)?;
&prettyplease_unparse(&syntax_tree)
} else {
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: {}",
thag_common::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]
#[profiled]
fn syn_parse_file(rs_source: Option<&str>) -> ThagResult<syn::File> {
let syntax_tree = syn::parse_file(rs_source.ok_or("Logic error retrieving rs_source")?)?;
Ok(syntax_tree)
}
#[inline]
#[profiled]
fn prettyplease_unparse(syntax_tree: &syn::File) -> String {
prettyplease::unparse(syntax_tree)
}
#[profiled]
pub fn build(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
let start_build = Instant::now();
vprtln!(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(())
}
#[profiled]
fn create_cargo_command(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<Command> {
let cargo_toml_path_str = code_utils::path_to_str(&build_state.cargo_toml_path)?;
let mut cargo_command = Command::new("cargo");
let shared_target_dir = TMPDIR.join(SHARED_TARGET_SUBDIR);
cargo_command.env("CARGO_TARGET_DIR", &shared_target_dir);
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)
}
#[profiled]
fn build_command_args(
proc_flags: &ProcFlags,
build_state: &BuildState,
cargo_toml_path: &str,
) -> Vec<String> {
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 let Some(features) = &build_state.features {
args.push("--features".to_string());
args.push(features.clone());
}
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..]);
} else if proc_flags.contains(ProcFlags::TEST_ONLY) && !build_state.args.is_empty() {
svprtln!(Role::INFO, V::V, "build_state.args={:#?}", build_state.args);
args.push("--".to_string());
args.extend_from_slice(&build_state.args[..]);
}
args
}
#[profiled]
fn get_cargo_subcommand(proc_flags: &ProcFlags, build_state: &BuildState) -> &'static str {
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 if proc_flags.contains(ProcFlags::TEST_ONLY) {
"test"
} else {
"build"
}
}
#[profiled]
fn configure_command_output(command: &mut Command, proc_flags: &ProcFlags) {
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());
}
}
#[profiled]
fn handle_expand(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
let mut cargo_command = create_cargo_command(proc_flags, build_state)?;
svprtln!(Role::INFO, V::V, "cargo_command={cargo_command:#?}");
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(())
}
#[profiled]
fn handle_build_or_check(proc_flags: &ProcFlags, build_state: &BuildState) -> ThagResult<()> {
let mut cargo_command = create_cargo_command(proc_flags, build_state)?;
svprtln!(Role::INFO, V::VV, "cargo_command={cargo_command:#?}");
let status = cargo_command.spawn()?.wait()?;
if !status.success() {
let verbosity = get_verbosity();
let err_msg = match verbosity {
Verbose | Dbug => {
display_build_failure(&build_state.infer);
"Build failed"
}
_ => "Build failed: run with -v for diagnostics",
};
return Err(err_msg.into());
}
if proc_flags.contains(ProcFlags::EXECUTABLE) {
deploy_executable(build_state)?;
} else if !proc_flags.contains(ProcFlags::CHECK) {
cache_executable(build_state)?;
}
Ok(())
}
#[profiled]
fn cache_executable(build_state: &BuildState) -> ThagResult<()> {
let shared_target_dir = TMPDIR.join(SHARED_TARGET_SUBDIR);
let cache_dir = TMPDIR.join(EXECUTABLE_CACHE_SUBDIR);
fs::create_dir_all(&cache_dir)?;
#[cfg(target_os = "windows")]
let exe_name = format!("{}.exe", build_state.source_stem);
#[cfg(not(target_os = "windows"))]
let exe_name = build_state.source_stem.clone();
let source_exe = shared_target_dir.join("debug").join(&exe_name);
let dest_exe = cache_dir.join(&exe_name);
if source_exe.exists() {
fs::copy(&source_exe, &dest_exe)?;
svprtln!(
Role::INFO,
V::VV,
"Cached executable: {}",
dest_exe.display()
);
} else {
return Err(format!(
"Built executable not found at expected location: {}",
source_exe.display()
)
.into());
}
Ok(())
}
#[profiled]
fn display_expansion_diff(stdout: Vec<u8>, build_state: &BuildState) -> ThagResult<()> {
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)) = terminal::size() {
(width - 26) / 2
} else {
80
};
let diff = create_side_by_side_diff(&unexpanded_source, &expanded_source, max_width.into());
println!("{diff}");
Ok(())
}
#[profiled]
fn display_build_failure(inference_level: &DependencyInference) {
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",
};
svprtln!(
Role::HD3,
V::V,
r"Dependency inference_level={inference_level:#?}
If the problem is a dependency error, consider the following advice:"
);
svprtln!(
Role::EMPH,
V::V,
r"{advice}
{}",
if matches!(inference_level, config::DependencyInference::Config) {
""
} else {
"Consider running with dependency inference_level configured as `config` or else an embedded `toml` block."
}
);
}
#[profiled]
fn deploy_executable(build_state: &BuildState) -> ThagResult<()> {
let mut cargo_bin_path = PathBuf::from(crate::get_home_dir_string()?);
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_stem = if let Some(name) = name_option {
name
} else {
build_state.source_stem.to_string()
};
let shared_target_dir = TMPDIR.join(SHARED_TARGET_SUBDIR);
let release_path = shared_target_dir.join("release");
let output_path = cargo_bin_path.join(&build_state.source_stem);
#[cfg(not(target_os = "windows"))]
{
let executable_path = release_path.join(&executable_stem);
debug_log!("executable_path={executable_path:#?}, output_path={output_path:#?}");
fs::rename(executable_path, output_path)?;
}
#[cfg(target_os = "windows")]
{
let executable_name = format!("{executable_stem}.exe");
let executable_path = release_path.join(&executable_name);
let pdb_name = format!("{executable_stem}.pdb");
let pdb_path = release_path.join(pdb_name);
let mut output_path_exe = output_path.clone();
output_path_exe.set_extension("exe");
let mut output_path_pdb = output_path.clone();
output_path_pdb.set_extension("pdb");
debug_log!("executable_path={executable_path:#?}, pdb_path={pdb_path:#?}, output_path_exe={output_path_exe:#?}, output_path_pdb={output_path_pdb:#?}");
eprintln!("executable_path={executable_path:#?}, pdb_path={pdb_path:#?}, output_path_exe={output_path_exe:#?}, output_path_pdb={output_path_pdb:#?}");
fs::copy(executable_path, &output_path_exe)?;
fs::copy(pdb_path, &output_path_pdb)?;
}
repeat_dash!(70);
svprtln!(Role::EMPH, V::Q, "{DASH_LINE}");
vprtln!(
V::QQ,
"Executable built and moved to ~/{cargo_bin_subdir}/{executable_stem}"
);
svprtln!(Role::EMPH, V::Q, "{DASH_LINE}");
Ok(())
}
#[profiled]
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);
svprtln!(Role::EMPH, V::Q, "{dash_line}");
let exit_status = run_command.status()?;
svprtln!(Role::EMPH, V::Q, "{dash_line}");
display_timings(&start_run, "Completed run", proc_flags);
if !exit_status.success() {
return Err(ThagError::Command("Script execution was unsuccessful"));
}
Ok(())
}
#[inline]
#[profiled]
pub fn display_timings(start: &Instant, process: &str, proc_flags: &ProcFlags) {
#[cfg(not(debug_assertions))]
if !proc_flags.intersects(ProcFlags::DEBUG | ProcFlags::VERBOSE | ProcFlags::TIMINGS) {
return;
}
let dur = start.elapsed();
let msg = format!("{process} in {}.{}s", dur.as_secs(), dur.subsec_millis());
#[cfg(debug_assertions)]
debug_log!("{msg}");
if proc_flags.intersects(ProcFlags::DEBUG | ProcFlags::VERBOSE | ProcFlags::TIMINGS) {
vprtln!(V::QQ, "{msg}");
}
}