omh 0.2.1

Launch any coding harness, in a sandbox, with your setup already there.
//! Embeds the definitions omh ships into the binary.
//!
//! `adapters/`, `editors/` and `base/` are data, and omh copies them into
//! `~/.omh` on `init`. They used to be *read* from the source tree at runtime
//! via `env!("CARGO_MANIFEST_DIR")`, which meant a binary only worked on the
//! machine that compiled it. This turns them into `include_str!`, so what
//! `init` copies from travels inside the executable.
//!
//! Emits `$OUT_DIR/bundled.rs`: a `Shipped` enum with one variant per
//! directory, and the file table behind it. An enum rather than a string key
//! so that asking for a directory omh does not ship is a compile error instead
//! of a runtime one — there is no bad value to handle, report, or test for.

use std::path::Path;

/// The directories omh ships. A directory that is missing or empty is a build
/// failure rather than an empty entry: shipping an omh with no adapters is the
/// silent failure this whole change exists to remove, and it should not be
/// possible to produce one.
const SHIPPED: [&str; 3] = ["adapters", "base", "editors"];

/// `adapters` -> `Adapters`. ASCII is enough; these are directory names in
/// this repository, not user input.
fn variant(dir: &str) -> String {
    let mut c = dir.chars();
    match c.next() {
        Some(first) => first.to_ascii_uppercase().to_string() + c.as_str(),
        None => unreachable!("SHIPPED holds no empty names"),
    }
}

fn main() {
    let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets this");
    let out = std::env::var("OUT_DIR").expect("cargo sets this");

    let mut variants = String::new();
    let mut all = String::new();
    let mut dir_arms = String::new();
    let mut file_arms = String::new();

    for kind in SHIPPED {
        let dir = Path::new(&manifest).join(kind);
        let name = variant(kind);

        // Re-run when a file is added or removed, not only when one is edited.
        println!("cargo:rerun-if-changed={}", dir.display());

        let listing = std::fs::read_dir(&dir)
            .unwrap_or_else(|e| panic!("omh ships {kind}/, and it is unreadable: {e}"));

        // Not `.flatten()`. A dropped entry here ships a binary missing an
        // adapter, and nothing downstream would notice — which is the bug this
        // file exists to prevent, so it must not be reintroduced writing it.
        let mut files: Vec<_> = listing
            .map(|e| {
                e.unwrap_or_else(|err| {
                    panic!("omh ships {kind}/, and an entry is unreadable: {err}")
                })
                .path()
            })
            .filter(|p| p.extension().is_some_and(|x| x == "toml"))
            .collect();
        files.sort();

        assert!(
            !files.is_empty(),
            "{kind}/ holds no .toml — that would build an omh that installs nothing"
        );

        variants.push_str(&format!("    {name},\n"));
        all.push_str(&format!("Shipped::{name}, "));
        dir_arms.push_str(&format!("            Shipped::{name} => {kind:?},\n"));
        file_arms.push_str(&format!("            Shipped::{name} => &[\n"));

        for file in files {
            let file_name = file
                .file_name()
                .expect("a path from read_dir has a final component")
                .to_str()
                .unwrap_or_else(|| panic!("{}: file names omh ships must be UTF-8", file.display()))
                .to_owned();
            let path = file.to_str().unwrap_or_else(|| {
                panic!(
                    "{}: the path to a shipped file must be UTF-8",
                    file.display()
                )
            });
            println!("cargo:rerun-if-changed={path}");
            file_arms.push_str(&format!(
                "                File {{ name: {file_name:?}, contents: include_str!({path:?}) }},\n"
            ));
        }
        file_arms.push_str("            ],\n");
    }

    let count = SHIPPED.len();
    let generated = format!(
        "// Generated by build.rs. Edit the .toml files, not this.\n\
         #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n\
         pub enum Shipped {{\n{variants}}}\n\
         \n\
         /// Every directory omh ships, so nothing can iterate a stale subset.\n\
         /// Only the tests walk all three; production names the one it wants.\n\
         #[cfg_attr(not(test), allow(dead_code))]\n\
         pub const ALL: [Shipped; {count}] = [{all}];\n\
         \n\
         impl Shipped {{\n\
         \x20   /// The directory name, in this repo and under `~/.omh`.\n\
         \x20   pub fn dir(self) -> &'static str {{\n\
         \x20       match self {{\n{dir_arms}\x20       }}\n\
         \x20   }}\n\
         \n\
         \x20   /// What omh installs for this directory. Never empty: build.rs\n\
         \x20   /// refuses to emit an empty one.\n\
         \x20   pub fn files(self) -> &'static [File] {{\n\
         \x20       match self {{\n{file_arms}\x20       }}\n\
         \x20   }}\n\
         }}\n"
    );

    std::fs::write(Path::new(&out).join("bundled.rs"), generated)
        .expect("writing the generated table");
}