knave 0.11.2

A small Lisp for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use serde_json::Value;

use super::BuildError;
use super::build_plan::BuildPlan;
use super::cargo_manifest::CargoManifest;
use super::interrupt_signal::InterruptSignal;
use super::rustc_errors::RustcDiagnosticMapper;
use crate::emit::EmittedCrate;
use crate::source::Span;

pub struct CargoCompiler {
    directory: PathBuf,
    plan: BuildPlan,
}

pub struct CargoTarget<'t> {
    pub manifest: CargoManifest<'t>,
    pub lock_file: PathBuf,
    pub output: &'t Path,
}

struct CargoReport {
    bin_name: String,
    program_messages: Vec<String>,
    dependency_errors: Vec<String>,
    executable: Option<PathBuf>,
}

impl CargoCompiler {
    const CARGO: &str = "cargo";
    const MANIFEST: &str = "Cargo.toml";
    const LOCK_FILE: &str = "Cargo.lock";
    const SOURCE: &str = "src/main.rs";
    const COMPILER_MESSAGE: &str = "compiler-message";
    const COMPILER_ARTIFACT: &str = "compiler-artifact";
    const ERROR_LEVEL: &str = "error";

    pub fn new(directory: PathBuf, plan: BuildPlan) -> Self {
        Self { directory, plan }
    }

    pub fn compile(
        &self,
        emitted: &EmittedCrate,
        target: &CargoTarget<'_>,
        entry: Span,
    ) -> Result<(), BuildError> {
        self.write_crate(emitted, target)
            .map_err(|error| Self::environment("write the generated crate", &error))?;
        let output = self.run_cargo(&target.manifest.bin_name)?;
        if InterruptSignal::stopped(output.status) {
            return Err(BuildError::Interrupted);
        }
        let report = CargoReport::read(
            &String::from_utf8_lossy(&output.stdout),
            &target.manifest.bin_name,
        );
        match (&report.executable, output.status.success()) {
            (Some(executable), true) => self.finish(executable, target),
            _ => Err(report.failure(emitted, entry, &output.stderr)),
        }
    }

    fn write_crate(&self, emitted: &EmittedCrate, target: &CargoTarget<'_>) -> io::Result<()> {
        let source = self.directory.join(Self::SOURCE);
        fs::create_dir_all(source.parent().unwrap_or(&self.directory))?;
        Self::write_if_changed(
            &self.directory.join(Self::MANIFEST),
            &target.manifest.render(&self.plan),
        )?;
        Self::write_if_changed(&source, &emitted.code)?;
        if target.lock_file.is_file() {
            fs::copy(&target.lock_file, self.directory.join(Self::LOCK_FILE))?;
        }
        Ok(())
    }

    fn write_if_changed(path: &Path, text: &str) -> io::Result<()> {
        if fs::read_to_string(path).is_ok_and(|written| written == text) {
            return Ok(());
        }
        fs::write(path, text)
    }

    fn run_cargo(&self, bin_name: &str) -> Result<Output, BuildError> {
        Command::new(Self::CARGO)
            .args(["rustc", "--bin", bin_name, "--message-format=json"])
            .args(self.plan.cargo_flags())
            .arg("--")
            .args(self.plan.lint_flags())
            .current_dir(&self.directory)
            .output()
            .map_err(|error| {
                Self::environment(
                    "run cargo, which builds a project with crate dependencies",
                    &error,
                )
            })
    }

    fn finish(&self, executable: &Path, target: &CargoTarget<'_>) -> Result<(), BuildError> {
        if let Some(directory) = target.output.parent() {
            fs::create_dir_all(directory)
                .map_err(|error| Self::environment("create the output directory", &error))?;
        }
        fs::copy(executable, target.output)
            .map_err(|error| Self::environment("copy the built program", &error))?;
        fs::copy(self.directory.join(Self::LOCK_FILE), &target.lock_file)
            .map_err(|error| Self::environment("copy `Cargo.lock` back", &error))?;
        Ok(())
    }

    fn environment(action: &str, error: &io::Error) -> BuildError {
        BuildError::Environment(format!("cannot {action}: {error}"))
    }
}

impl CargoReport {
    fn read(stdout: &str, bin_name: &str) -> Self {
        let mut report = Self {
            bin_name: bin_name.to_string(),
            program_messages: Vec::new(),
            dependency_errors: Vec::new(),
            executable: None,
        };
        stdout
            .lines()
            .filter_map(|line| serde_json::from_str::<Value>(line).ok())
            .for_each(|record| report.add(&record));
        report
    }

    fn add(&mut self, record: &Value) {
        match record["reason"].as_str() {
            Some(CargoCompiler::COMPILER_MESSAGE) => self.add_message(record),
            Some(CargoCompiler::COMPILER_ARTIFACT) => self.add_artifact(record),
            _ => {}
        }
    }

    fn add_message(&mut self, record: &Value) {
        let message = &record["message"];
        if self.is_program(record) {
            return self.program_messages.push(message.to_string());
        }
        if message["level"].as_str() == Some(CargoCompiler::ERROR_LEVEL) {
            let rendered = message["rendered"].as_str().unwrap_or_default();
            let crate_name = record["target"]["name"].as_str().unwrap_or_default();
            self.dependency_errors
                .push(format!("crate `{crate_name}` failed to build:\n{rendered}"));
        }
    }

    fn add_artifact(&mut self, record: &Value) {
        if let Some(executable) = record["executable"].as_str() {
            self.executable = Some(PathBuf::from(executable));
        }
    }

    fn is_program(&self, record: &Value) -> bool {
        record["target"]["name"].as_str() == Some(self.bin_name.as_str())
    }

    fn failure(&self, emitted: &EmittedCrate, entry: Span, stderr: &[u8]) -> BuildError {
        let program_errors =
            RustcDiagnosticMapper::new(emitted, entry).errors_of(&self.program_messages);
        if program_errors.has_errors() {
            return BuildError::Program(program_errors);
        }
        if !self.dependency_errors.is_empty() {
            return BuildError::Environment(self.dependency_errors.join("\n"));
        }
        BuildError::Environment(String::from_utf8_lossy(stderr).trim_end().to_string())
    }
}