use std::path::{Path, PathBuf};
use std::process::Command;
use crate::cache::{self, CachePaths};
const RUST_MISSING: &str = "\
very rust. much missing.
doge compiles through Rust's toolchain, but cargo wasn't found.
such fix: install Rust from https://rustup.rs";
const BUILD_LOCK_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(300);
const BUILD_LOCK_POLL: std::time::Duration = std::time::Duration::from_millis(50);
pub fn ensure_binary(source: &str, generated: &str) -> Result<PathBuf, String> {
let paths = cache::resolve(source)?;
if cache::cache_hit(&paths.entry_dir, &paths.binary, source) {
return Ok(paths.binary);
}
detect_toolchain()?;
let _lock = BuildLock::acquire(&paths.entry_dir)?;
if cache::cache_hit(&paths.entry_dir, &paths.binary, source) {
return Ok(paths.binary);
}
write_entry(&paths, source, generated)?;
compile(&paths)?;
Ok(paths.binary)
}
struct BuildLock {
path: PathBuf,
}
impl BuildLock {
fn acquire(entry_dir: &Path) -> Result<BuildLock, String> {
std::fs::create_dir_all(entry_dir).map_err(disk_err)?;
let path = entry_dir.join("build.lock");
loop {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(_) => return Ok(BuildLock { path }),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
if lock_is_stale(&path) {
let _ = std::fs::remove_file(&path);
continue;
}
std::thread::sleep(BUILD_LOCK_POLL);
}
Err(err) => return Err(disk_err(err)),
}
}
}
}
impl Drop for BuildLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn lock_is_stale(path: &Path) -> bool {
match std::fs::metadata(path).and_then(|meta| meta.modified()) {
Ok(created) => created
.elapsed()
.map(|age| age > BUILD_LOCK_STALE_AFTER)
.unwrap_or(false),
Err(_) => true,
}
}
pub fn spawn(binary: &Path, args: &[String]) -> Result<i32, String> {
match Command::new(binary).args(args).status() {
Ok(status) => Ok(status.code().unwrap_or(1)),
Err(err) => Err(format!(
"very run. much fail.\n\n doge built your script but could not run it: {err}"
)),
}
}
pub fn copy_to_cwd(binary: &Path, stem: &str) -> Result<(), String> {
let name = format!("{stem}{}", std::env::consts::EXE_SUFFIX);
let dest = PathBuf::from(format!("./{name}"));
std::fs::copy(binary, &dest)
.map(|_| ())
.map_err(|err| format!("very disk. much sad.\n\n doge could not write ./{name}: {err}"))
}
fn detect_toolchain() -> Result<(), String> {
match Command::new("cargo").arg("--version").output() {
Ok(output) if output.status.success() => Ok(()),
_ => Err(RUST_MISSING.to_string()),
}
}
fn runtime_path() -> Option<PathBuf> {
let raw = concat!(env!("CARGO_MANIFEST_DIR"), "/../doge-runtime");
std::fs::canonicalize(raw).ok()
}
fn runtime_dependency(runtime: Option<&Path>) -> String {
match runtime {
Some(path) => format!("doge-runtime = {{ path = {:?} }}", path.to_string_lossy()),
None => format!("doge-runtime = \"={}\"", env!("CARGO_PKG_VERSION")),
}
}
fn cargo_manifest(package: &str, runtime: Option<&Path>) -> String {
format!(
"[package]\n\
name = \"{package}\"\n\
version = \"0.0.0\"\n\
edition = \"2021\"\n\
\n\
[workspace]\n\
\n\
[dependencies]\n\
{dependency}\n\
\n\
[[bin]]\n\
name = \"{package}\"\n\
path = \"src/main.rs\"\n",
dependency = runtime_dependency(runtime),
)
}
fn write_entry(paths: &CachePaths, source: &str, generated: &str) -> Result<(), String> {
let src_dir = paths.entry_dir.join("src");
std::fs::create_dir_all(&src_dir).map_err(disk_err)?;
let cargo_toml = cargo_manifest(&paths.package, runtime_path().as_deref());
std::fs::write(paths.entry_dir.join("Cargo.toml"), cargo_toml).map_err(disk_err)?;
std::fs::write(src_dir.join("main.rs"), generated).map_err(disk_err)?;
std::fs::write(paths.entry_dir.join("source.doge"), source).map_err(disk_err)?;
Ok(())
}
fn compile(paths: &CachePaths) -> Result<(), String> {
let output = Command::new("cargo")
.arg("build")
.arg("--release")
.arg("--quiet")
.current_dir(&paths.entry_dir)
.env("CARGO_TARGET_DIR", &paths.target_dir)
.output()
.map_err(|_| RUST_MISSING.to_string())?;
if output.status.success() {
return Ok(());
}
let log = paths.entry_dir.join("build.log");
let mut captured = output.stdout;
captured.extend_from_slice(&output.stderr);
let _ = std::fs::write(&log, &captured);
Err(format!(
"very bug. much sorry.\n\n\
the Rust that doge generated failed to build — this is a doge bug, not your script.\n\
pls report it and attach: {}",
log.display()
))
}
fn disk_err(err: std::io::Error) -> String {
format!("very disk. much sad.\n\n doge could not write its build cache: {err}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn installed_build_uses_the_versioned_runtime() {
let manifest = cargo_manifest("doge_script_abc", None);
assert!(
manifest.contains(&format!(
"doge-runtime = \"={}\"",
env!("CARGO_PKG_VERSION")
)),
"expected a version-pinned runtime dep, got:\n{manifest}"
);
assert!(
!manifest.contains("doge-runtime = { path"),
"no path runtime dep when installed, got:\n{manifest}"
);
}
#[test]
fn dev_build_uses_the_path_runtime() {
let runtime = PathBuf::from("/checkout/doge-runtime");
let manifest = cargo_manifest("doge_script_abc", Some(&runtime));
assert!(
manifest.contains("doge-runtime = { path = "),
"expected a path runtime dep, got:\n{manifest}"
);
}
}