hara-native 0.1.21

HAL-free native host runtime and package launcher for Hara
Documentation
use hara_native::{
    kernel::{halc, parse_forms},
    package,
    vm::{compile_bytecode_bundle, ModuleSource},
    work::plan::WorkPlan,
};
use std::{
    env, fs,
    path::{Path, PathBuf},
    process,
};

const ASSET_DIRECTORY: &str = "assets/host-fixtures";
const HBX_FIXTURE: &str = "portable-module.hbx";
const HALC_FIXTURE: &str = "portable-module.halc";
const HTA_FIXTURE: &str = "portable-work-plan.hta";
const HARP_FIXTURE: &str = "portable-package.harp";

const HBX_SOURCE: &str = "(ns fixture.hbx) (def answer 42)";
const HALC_SOURCE: &str = "(ns fixture.halc) (def answer 42)";
const HARP_SOURCE: &str = "(ns fixture.harp) (def answer 42)";
const HARP_PROJECT: &str = "{:hara/type :project :hara/version \"1.0.0\" :project/id fixture/rust-host-fixtures :project/version \"1.0.0\" :project/source-paths [\"src\"] :project/test-paths [] :project/extension-paths [] :project/capabilities #{}}\n";

struct Fixture {
    name: &'static str,
    bytes: Vec<u8>,
}

fn main() {
    if let Err(error) = run() {
        eprintln!("hara-native-host-fixtures: {error}");
        process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let command = env::args().nth(1).unwrap_or_else(|| "check".into());
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let fixtures = fixtures()?;
    match command.as_str() {
        "generate" => write_fixtures(&root, &fixtures),
        "check" => check_fixtures(&root, &fixtures),
        _ => Err("usage: hara-native-host-fixtures [generate|check]".into()),
    }
}

fn fixtures() -> Result<Vec<Fixture>, String> {
    let hbx = compile_bytecode_bundle(&[ModuleSource {
        resource: "fixture.hbx",
        source: HBX_SOURCE,
    }])?;
    let halc = halc::encode_halc_module(
        "fixture.halc",
        "fixture/halc.hal",
        HALC_SOURCE,
        parse_forms(HALC_SOURCE)?,
    )?;
    let hta = WorkPlan::pure("fixture/answer")?.encode_hta()?;
    let harp = build_harp_fixture()?;
    Ok(vec![
        Fixture {
            name: HBX_FIXTURE,
            bytes: hbx,
        },
        Fixture {
            name: HALC_FIXTURE,
            bytes: halc,
        },
        Fixture {
            name: HTA_FIXTURE,
            bytes: hta,
        },
        Fixture {
            name: HARP_FIXTURE,
            bytes: harp,
        },
    ])
}

fn write_fixtures(root: &Path, fixtures: &[Fixture]) -> Result<(), String> {
    let directory = root.join(ASSET_DIRECTORY);
    fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
    for fixture in fixtures {
        let path = directory.join(fixture.name);
        fs::write(&path, &fixture.bytes).map_err(|error| error.to_string())?;
        println!("wrote {} bytes to {}", fixture.bytes.len(), path.display());
    }
    Ok(())
}

fn check_fixtures(root: &Path, fixtures: &[Fixture]) -> Result<(), String> {
    let directory = root.join(ASSET_DIRECTORY);
    for fixture in fixtures {
        let path = directory.join(fixture.name);
        let tracked = fs::read(&path).map_err(|error| error.to_string())?;
        if tracked != fixture.bytes {
            return Err(format!("{} is stale; run with generate", path.display()));
        }
        println!("{} is current ({} bytes)", path.display(), tracked.len());
    }
    Ok(())
}

fn build_harp_fixture() -> Result<Vec<u8>, String> {
    let root = env::temp_dir().join(format!("hara-native-host-fixtures-{}", process::id()));
    if root.exists() {
        fs::remove_dir_all(&root).map_err(|error| error.to_string())?;
    }
    let result = (|| {
        let project = root.join("project");
        let source = project.join("src/fixture/harp/main.hal");
        let archive = root.join(HARP_FIXTURE);
        fs::create_dir_all(source.parent().expect("fixture source has parent"))
            .map_err(|error| error.to_string())?;
        fs::write(project.join("project.edn"), HARP_PROJECT).map_err(|error| error.to_string())?;
        fs::write(source, HARP_SOURCE).map_err(|error| error.to_string())?;
        package::build_path(&project, Some(&archive))?;
        fs::read(archive).map_err(|error| error.to_string())
    })();
    let cleanup = fs::remove_dir_all(&root).map_err(|error| error.to_string());
    match (result, cleanup) {
        (Ok(bytes), Ok(())) => Ok(bytes),
        (Err(error), _) => Err(error),
        (Ok(_), Err(error)) => Err(error),
    }
}