use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Stdio};
use bop_compile::{ModuleResolver, Options, transpile};
pub fn compile_file(
input: &str,
output: Option<&str>,
emit_rs: bool,
keep: bool,
) -> ExitCode {
let input_path = PathBuf::from(input);
let source = match std::fs::read_to_string(&input_path) {
Ok(s) => s,
Err(e) => {
eprintln!("error reading `{input}`: {e}");
return ExitCode::from(1);
}
};
let input_dir = input_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let resolver = make_resolver(input_dir);
let opts = Options {
emit_main: true,
use_bop_sys: true,
sandbox: false,
module_name: None,
module_resolver: Some(resolver),
};
let rust_src = match transpile(&source, &opts) {
Ok(s) => s,
Err(e) => {
eprint!("{}", e.render(&source));
return ExitCode::from(1);
}
};
if emit_rs {
let out_path = match output {
Some(p) => PathBuf::from(p),
None => default_rs_path(&input_path),
};
if let Err(e) = std::fs::write(&out_path, &rust_src) {
eprintln!("error writing `{}`: {e}", out_path.display());
return ExitCode::from(1);
}
eprintln!("wrote {}", out_path.display());
return ExitCode::SUCCESS;
}
if !cargo_available() {
eprintln!(
"error: couldn't find `cargo` on your PATH.\n\
`bop compile` needs a Rust toolchain to produce a native binary.\n\
Install it from https://rustup.rs, or re-run with `--emit-rs`\n\
to get the transpiled Rust source without building it."
);
return ExitCode::from(1);
}
let output_path = match output {
Some(p) => PathBuf::from(p),
None => default_binary_path(&input_path),
};
let scratch = match build_native(&rust_src, &input_path, &output_path) {
Ok(s) => s,
Err(code) => return code,
};
if keep {
eprintln!("scratch project kept at {}", scratch.display());
} else {
let _ = std::fs::remove_dir_all(&scratch);
}
eprintln!("built {}", output_path.display());
ExitCode::SUCCESS
}
fn make_resolver(root: PathBuf) -> ModuleResolver {
use std::cell::RefCell;
use std::rc::Rc;
Rc::new(RefCell::new(move |name: &str| {
if let Some(src) = bop::stdlib::resolve(name) {
return Some(Ok(src.to_string()));
}
let mut path = root.clone();
for segment in name.split('.') {
path.push(segment);
}
path.set_extension("bop");
match std::fs::read_to_string(&path) {
Ok(src) => Some(Ok(src)),
Err(_) => None,
}
}))
}
fn build_native(
rust_src: &str,
input_path: &Path,
output_path: &Path,
) -> Result<PathBuf, ExitCode> {
let stem = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("script");
let scratch = scratch_dir(stem);
if let Err(e) = std::fs::create_dir_all(scratch.join("src")) {
eprintln!(
"error creating scratch dir `{}`: {e}",
scratch.display()
);
return Err(ExitCode::from(1));
}
let manifest = manifest_for_output(stem);
if let Err(e) = std::fs::write(scratch.join("Cargo.toml"), manifest) {
eprintln!("error writing scratch Cargo.toml: {e}");
return Err(ExitCode::from(1));
}
if let Err(e) = std::fs::write(scratch.join("src/main.rs"), rust_src) {
eprintln!("error writing scratch main.rs: {e}");
return Err(ExitCode::from(1));
}
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(&scratch)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
let status = match status {
Ok(s) => s,
Err(e) => {
eprintln!("error invoking cargo: {e}");
return Err(ExitCode::from(1));
}
};
if !status.success() {
eprintln!("cargo build failed — generated Rust source is under {}", scratch.display());
return Err(ExitCode::from(1));
}
let mut built = scratch.join("target/release").join(stem);
if cfg!(windows) {
built.set_extension("exe");
}
if let Err(e) = std::fs::copy(&built, output_path) {
eprintln!(
"error copying built binary `{}` → `{}`: {e}",
built.display(),
output_path.display()
);
return Err(ExitCode::from(1));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(output_path) {
let mut perms = meta.permissions();
perms.set_mode(perms.mode() | 0o111);
let _ = std::fs::set_permissions(output_path, perms);
}
}
Ok(scratch)
}
fn cargo_available() -> bool {
Command::new("cargo")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn default_rs_path(input: &Path) -> PathBuf {
let mut p = input.to_path_buf();
p.set_extension("rs");
p
}
fn default_binary_path(input: &Path) -> PathBuf {
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("script");
let mut p = PathBuf::from(stem);
if cfg!(windows) {
p.set_extension("exe");
}
p
}
fn scratch_dir(stem: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("bop-compile-{stem}-{}", std::process::id()));
p
}
fn manifest_for_output(stem: &str) -> String {
let version = env!("CARGO_PKG_VERSION");
let deps = match std::env::var("BOP_DEV_WORKSPACE") {
Ok(root) if !root.is_empty() => format!(
r#"bop = {{ path = "{root}/bop", package = "bop-lang" }}
bop-sys = {{ path = "{root}/bop-sys" }}"#,
),
_ => format!(
r#"bop = {{ version = "{version}", package = "bop-lang" }}
bop-sys = "{version}""#,
),
};
format!(
r#"[package]
name = "{stem}"
version = "0.0.0"
edition = "2024"
publish = false
[[bin]]
name = "{stem}"
path = "src/main.rs"
[dependencies]
{deps}
[workspace]
[profile.release]
# Small + fast enough: matches what a hand-written Rust user
# would reach for when building a CLI. LTO trims the AOT-emitted
# dispatch noise without pushing build time into the stratosphere.
opt-level = 3
lto = "thin"
"#
)
}