use lapp;
use shlex;
use anyhow::{bail, Context, Result};
use std::collections::HashSet;
use std::env;
use std::env::consts::EXE_SUFFIX;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
mod cache;
mod cargo_lock;
mod compile;
mod crate_utils;
mod meta;
mod platform;
mod state;
mod strutil;
use crate::cache::{compare_file_times, lookup_file_path};
use crate::compile::extract_externs;
use compile::{compile_crate, massage_snippet};
use crate_utils::rustup_lib;
use platform::{edit, open};
use state::State;
const VERSION: &str = "0.7.0";
const USAGE: &str = "
Compile and run small Rust snippets
-s, --static build statically (default is dynamic)
-d, --display use Display instead of Debug in -e, -i and -n
-e, --expression evaluate an expression
-i, --iterator iterate over an expression
-n, --lines evaluate expression over stdin; the var 'line' is defined
-x, --extern... (string) add an extern crate to the snippet
-X, --wild... (string) like -x but implies wildcard import
-M, --macro... (string) like -x but implies macro import
-p, --prepend (default '') put this statement in body (useful for -i etc)
-N, --no-prelude do not include runner prelude
-c, --compile-only compiles program and copies to output dir
-o, --output (path default cargo) change the default output dir for compilation
-r, --run don't compile, only re-run
-R, --rerun only recompile if snippet has fresher timestamp
-S, --no-simplify by default, attempt to simplify rustc error messages
-E, --edition (default '2024') Rust edition
Cache Management:
--add (string...) add new crates to the cache
--update update all, or a specific package given as argument
--edit edit the static cache Cargo.toml
--build rebuild the static cache
--cleanup clean out stale rlibs from cache
--crates current crates and their versions in cache
--doc display documentation (any argument will be specific crate name)
--edit-prelude edit the default prelude for snippets
--alias (string...) crate aliases in form alias=crate_name (used with -x)
Dynamic compilation:
-P, --crate-path show path of crate source in Cargo cache
-C, --compile compile crate dynamically (limited)
-L, --link (string) path for extra libraries
--cfg... (string) pass configuration variables to rustc
--features (string...) enable features in compilation
--libc link dynamically against libc (special case)
(--extern is used to explicitly link in a crate by name)
-v, --verbose describe what's happening
-V, --version version of runner
<program> (string) Rust program, snippet or expression
<args> (string...) arguments to pass to program
";
fn read_file_with_arg_comment(args: &mut lapp::Args, file: &Path) -> Result<(String, bool)> {
let contents = fs::read_to_string(file).context("cannot read file")?;
let first_line = contents.lines().next().context("empty file")?;
let arg_comment = "//: ";
let has_arg_comment = first_line.starts_with(arg_comment);
if has_arg_comment {
let default_args = &first_line[arg_comment.len()..];
let default_args = shlex::split(default_args).context("bad comment args")?;
args.parse_command_line(default_args)
.context("cannot parse comment args")?;
args.clear_used();
}
Ok((contents, has_arg_comment))
}
fn main() -> Result<()> {
let mut args = lapp::Args::new(USAGE);
args.parse_spec().context("bad spec")?;
args.parse_env_args().context("bad command line")?;
let (program_contents, file) = if let Ok(program) = args.get_string_result("program") {
if program.ends_with(".rs") {
let prog = lookup_file_path(&program, None).context("source file does not exist")?;
args.clear_used();
let (contents, has_arg_comment) = read_file_with_arg_comment(&mut args, &prog)?;
if has_arg_comment {
args.parse_env_args().context("bad command line")?;
}
(Some(contents), prog)
} else {
(None, PathBuf::from(&program))
}
} else {
(None, PathBuf::default())
};
let env_prelude = if let Some(env) = lookup_file_path("env.rs", Some(&file)) {
let (contents, _) = read_file_with_arg_comment(&mut args, &env)?;
Some(contents)
} else {
None
};
let mut prelude = cache::get_prelude()?;
if let Some(env_prelude) = env_prelude {
prelude.push_str(&env_prelude);
}
let b = |p| args.get_bool(p);
let exe_suffix = if EXE_SUFFIX.len() > 0 {
&EXE_SUFFIX[1..]
} else {
""
};
if b("version") {
println!("runner {}", VERSION);
return Ok(());
}
let verbose = b("verbose");
if b("run") && b("compile-only") {
bail!("--run and compile-only make no sense together");
}
let aliases = args.get_strings("alias");
if aliases.len() > 0 {
cache::add_aliases(aliases)?;
return Ok(());
}
if b("edit-prelude") {
let rdir = cache::runner_directory()?.join("prelude");
edit(&rdir)?;
return Ok(());
}
let mut crates = args.get_strings("add");
if crates.len() > 0 {
if crates.len() == 1 && crates[0] == "." {
crates = cache::read_missing_crates()?;
cache::delete_missing_crates()?;
}
cache::create_static_cache(&crates)?;
return Ok(());
}
let (edit_toml, build, doc, update, cleanup, crates) = (
b("edit"),
b("build"),
b("doc"),
b("update"),
b("cleanup"),
b("crates"),
);
if edit_toml || build || doc || update || cleanup || crates {
let maybe_argument = args.get_string_result("program");
let static_cache = cache::static_cache_dir_check()?;
if build || update {
env::set_current_dir(&static_cache).context("static cache wasn't a directory?")?;
if build {
cache::build_static_cache()?;
} else {
if let Ok(package) = maybe_argument {
cache::cargo(&["update", "--package", &package])?;
} else {
cache::cargo(&["update"])?;
}
return Ok(());
}
} else if doc {
let the_crate = crate_utils::proper_crate_name(&if let Ok(file) = maybe_argument {
file
} else {
"static_cache".to_string()
});
let docs = static_cache.join(&format!("target/doc/{}/index.html", the_crate));
open(&docs)?;
} else if cleanup {
cache::cargo(&["clean"])?;
} else if crates {
let mut m = cache::get_metadata()?;
let mut crates = Vec::new();
if let Some(name) = maybe_argument.ok() {
crates.push(name);
crates.extend(args.get_strings("args"));
}
m.dump_crates(crates, verbose)?;
} else {
let toml = static_cache.join("Cargo.toml");
edit(&toml)?;
}
return Ok(());
}
let first_arg = args.get_string("program");
let optimized = true;
let (print_path, compile) = (b("crate-path"), b("compile"));
if print_path || compile {
let mut state = State::dll(optimized, &args);
if crate_utils::plain_name(&first_arg) {
let m = cache::get_metadata()?;
if let Some(e) = m.get_meta_entry(&first_arg) {
if e.path == Path::new("") {
bail!("please run 'runner --build' to update metadata");
}
let path = e.path.parent().unwrap().parent().unwrap();
if print_path {
println!("{}", path.display());
} else {
let ci = crate_utils::crate_info(&path.join("Cargo.toml"));
let ci = ci?;
state.edition = ci.edition;
let build_features = &e.features;
println!(
"building crate '{}' {} at {}",
e.crate_name,
build_features,
e.path.display()
);
compile_crate(
&state,
&e.crate_name,
&e.path,
None,
Vec::new(),
build_features
.split_whitespace()
.map(|s| s.to_string())
.collect(),
)?;
}
return Ok(());
} else {
bail!("cannot find crate '{}'", first_arg);
}
} else if compile {
if !file.exists() {
bail!("no such file or directory");
}
let (crate_name, crate_path) = if file.is_dir() {
match crate_utils::cargo_dir(&file) {
Ok((path, cargo_toml)) => {
let source = path.join("src").join("lib.rs");
let ci = crate_utils::crate_info(&cargo_toml)?;
state.edition = ci.edition;
(ci.name, source)
}
Err(msg) => bail!(msg),
}
} else {
if file.extension().context("expecting extension")? != "rs" {
bail!("expecting known crate, dir containing Cargo.toml or Rust source file");
}
let name = crate_utils::path_file_name(&file.with_extension(""));
(name, file.clone())
};
println!(
"building crate '{}' at {}",
crate_name,
crate_path.display()
);
compile_crate(
&state,
&crate_name,
&crate_path,
None,
Vec::new(),
Vec::new(),
)?;
return Ok(());
} else {
bail!("not found in the static cache");
}
}
let static_state = b("static");
let mut state = State::exe(static_state, optimized, &args);
let program_args = args.get_strings("args");
let mut just_run = b("run");
let mut expression = true;
let display = b("display");
use cache::quote;
let mut code = if b("expression") {
if display {
format!("println!(\"{{}}\",{});", quote(first_arg))
} else {
format!("println!(\"{{:?}}\",{});", quote(first_arg))
}
} else if b("iterator") {
if display {
format!(
"for val in {} {{\n println!(\"{{}}\",val);\n}}",
quote(first_arg)
)
} else {
format!(
"for val in {} {{\n println!(\"{{:?}}\",val);\n}}",
quote(first_arg)
)
}
} else if b("lines") {
let first_arg = quote(first_arg);
let stmt = first_arg.trim_end().ends_with('}');
let mut s = String::from(
"
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
",
);
s += &if !stmt {
if display {
format!("let val = {};\nprintln!(\"{{}}\",val);", first_arg)
} else {
format!("let val = {};\nprintln!(\"{{:?}}\",val);", first_arg)
}
} else {
format!(" {};", first_arg)
};
s += "\n}";
s
} else {
expression = false;
program_contents.context("no .rs file")?
};
let mut bin = cache::runner_directory()?.join("bin");
let mut externs = Vec::new();
let proper = code.find("fn main").is_some();
let (rust_file, program) = if !proper {
let mut extern_crates = args.get_strings("extern");
let wild_crates = args.get_strings("wild");
let macro_crates = args.get_strings("macro");
if wild_crates.len() > 0 {
extern_crates.extend(wild_crates.iter().cloned());
}
if macro_crates.len() > 0 {
extern_crates.extend(macro_crates.iter().cloned());
}
let macro_crates: HashSet<_> = macro_crates.into_iter().collect();
let mut extra = args.get_string("prepend");
if !extra.is_empty() {
extra.push(';');
}
let maybe_prelude = if b("no-prelude") { "".into() } else { prelude };
let (massaged_code, deduced_externs) = massage_snippet(
code,
maybe_prelude,
extern_crates,
wild_crates,
macro_crates,
extra,
)?;
code = massaged_code;
externs = deduced_externs;
if !expression {
bin.push(file.file_name().unwrap());
bin.set_extension("rs");
} else {
bin.push("tmp.rs");
}
fs::write(&bin, &code).context("cannot write code")?;
let program = bin.with_extension(exe_suffix);
if !expression && b("rerun") {
just_run = !compare_file_times(&file, &program)?;
}
(bin, program)
} else {
let mut static_externs = 0;
for line in code.lines() {
if extract_externs(line, &mut externs) {
static_externs += 1;
}
}
if static_externs > 0 {
state.build_static = true;
}
bin.push(file.file_name().unwrap());
let program = bin.with_extension(exe_suffix);
(file, program)
};
if just_run {
if !program.exists() {
bail!("program {:?} does not exist", program);
}
} else {
if !compile_crate(&state, "", &rust_file, Some(&program), externs, Vec::new())? {
process::exit(1);
}
if verbose {
println!("compiled {:?} successfully", rust_file);
}
}
if b("compile-only") {
let file_name = rust_file.file_name().context("no file name?")?;
let out_dir = args.get_path("output");
let home = if out_dir == Path::new("cargo") {
let home = crate_utils::cargo_home()?.join("bin");
if !home.is_dir() {
fs::create_dir(&home).context("could not create Cargo bin directory")?;
println!(
"creating Cargo bin directory {}\nEnsure it is on your PATH",
home.display()
);
}
home
} else {
out_dir
};
let here = home.join(file_name).with_extension(exe_suffix);
println!("Copying {} to {}", program.display(), here.display());
fs::copy(&program, &here).context("cannot copy program")?;
return Ok(());
}
let ch = cache::get_cache(&state)?;
let mut builder = process::Command::new(&program);
if !state.build_static {
if cfg!(windows) {
let path = env::var("PATH")?;
let new_path = format!("{};{}", path, ch.display());
builder.env("PATH", new_path);
} else {
builder.env(
"LD_LIBRARY_PATH",
format!("{}:{}", rustup_lib()?, ch.display()),
);
}
}
let status = builder
.args(&program_args)
.status()
.with_context(|| format!("can't run program {:?}", program))?;
if !status.success() {
process::exit(status.code().unwrap_or(-1));
}
Ok(())
}