use std::env::current_dir; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use clap::{arg, command, ColorChoice, Parser}; use nockapp::driver::Operation; use nockapp::kernel::boot::{self, default_boot_cli, Cli as BootCli}; use nockapp::noun::slab::NounSlab; use nockapp::{system_data_dir, AtomExt, Noun, NounExt}; use nockvm::interpreter::{self, Context}; use nockvm::noun::{Atom, D, T}; use nockvm_macros::tas; use tokio::fs::{self, File}; use tokio::io::AsyncReadExt; use tracing::{debug, info, instrument, trace}; use walkdir::{DirEntry, WalkDir}; pub const OUT_JAM_NAME: &str = "out.jam"; pub type Error = Box; static KERNEL_JAM: &[u8] = include_bytes!("../bootstrap/hoonc.jam"); static HOON_TXT: &[u8] = include_bytes!("../hoon/hoon-138.hoon"); #[derive(Parser, Debug)] #[command(about = "Tests various poke types for the kernel", author = "zorp", version, color = ColorChoice::Auto)] pub struct ChooCli { #[command(flatten)] pub boot: BootCli, // TODO: REPRODUCIBILITY: // make entry path relative to the dependency directory // we may have to go back to requiring that the entry exists in the dependency directory #[arg(help = "Path to file to compile")] pub entry: std::path::PathBuf, #[arg(help = "Path to root of dependency directory", default_value = "hoon")] pub directory: std::path::PathBuf, #[arg( long, help = "Build raw, without file hash injection", default_value = "false" )] pub arbitrary: bool, #[arg(long, help = "Output file path", default_value = None)] pub output: Option, } pub async fn hoonc_data_dir() -> PathBuf { let hoonc_data_dir = system_data_dir().join("hoonc"); if !hoonc_data_dir.exists() { fs::create_dir_all(&hoonc_data_dir) .await .unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }); } hoonc_data_dir } /// Builds and interprets a Hoon generator. /// /// This function: /// 1. Builds the specified Hoon generator into a jam /// 2. Decodes the jam into a Nock noun /// 3. Interprets the noun with a kick operation to run the generator /// /// # Parameters /// - `context`: The Nock interpreter context /// - `path`: Path to the Hoon generator file /// /// # Returns /// - A noun pub async fn build_and_kick_jam( context: &mut Context, path: &str, deps_dir: PathBuf, out_dir: Option, ) -> Noun { let jam = build_jam(path, deps_dir, out_dir, true, false) .await .expect("failed to build page"); debug!("Built jam"); let generator_trap = Noun::cue_bytes_slice(&mut context.stack, &jam).expect("invalid generator jam"); let kick = T(&mut context.stack, &[D(9), D(2), D(0), D(1)]); debug!("Kicking trap"); interpreter::interpret(context, generator_trap, kick).unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) } pub async fn save_generator( context: &mut Context, path: &str, deps_dir: PathBuf, out_dir: Option, ) -> Result<(), Error> { let cli = default_boot_cli(true); boot::init_default_tracing(&cli); let kicked = build_and_kick_jam(context, path, deps_dir, out_dir.clone()).await; let jammed = kicked.jam_self(&mut context.stack); let file_name = Path::new(path) .file_stem() .unwrap_or_else(|| OsStr::new("generator")) .to_string_lossy() .to_string(); let output_file = out_dir .clone() .unwrap_or_else(|| current_dir().expect("Failed to get current directory")) .join(format!("{}.jam", file_name)); if let Some(parent) = output_file.parent() { fs::create_dir_all(parent).await?; } fs::write(&output_file, jammed).await?; println!("Generator saved to: {}", output_file.display()); Ok(()) } /// Builds a jam (serialized Nock noun) from a Hoon source file /// /// This function: /// 1. Locates the source file relative to the hoon directory /// 2. Creates a temporary directory for build artifacts /// 3. Initializes a Nock app with the hoonc build system /// 4. Builds the source file and returns the resulting jam as bytes /// /// # Parameters /// - `entry`: Path to the Hoon source file, relative to the hoon directory /// - `arbitrary`: Whether to build with arbitrary mode enabled /// - `new`: Whether to force a clean build /// /// # Returns /// - A Result containing either the jam bytes or a hoonc error pub async fn build_jam( entry: &str, deps_dir: PathBuf, out_dir: Option, arbitrary: bool, new: bool, ) -> Result, Error> { info!("Dependencies directory: {:?}", deps_dir); info!("Entry file: {:?}", entry); let (nockapp, out_path) = initialize_with_default_cli(entry.into(), deps_dir, out_dir, arbitrary, new).await?; info!("Output path: {:?}", out_path); run_build(nockapp, Some(out_path.clone())).await } pub async fn initialize_hoonc(cli: ChooCli) -> Result<(nockapp::NockApp, PathBuf), Error> { initialize_hoonc_( cli.entry, cli.directory, cli.arbitrary, cli.output, cli.boot.clone(), ) .await } pub async fn initialize_with_default_cli( entry: std::path::PathBuf, deps_dir: std::path::PathBuf, out: Option, arbitrary: bool, new: bool, ) -> Result<(nockapp::NockApp, PathBuf), Error> { let cli = default_boot_cli(new); initialize_hoonc_(entry, deps_dir, arbitrary, out, cli).await } pub async fn initialize_hoonc_( entry: std::path::PathBuf, deps_dir: std::path::PathBuf, arbitrary: bool, out: Option, boot_cli: BootCli, ) -> Result<(nockapp::NockApp, PathBuf), Error> { debug!("Dependencies directory: {:?}", deps_dir); debug!("Entry file: {:?}", entry); let data_dir = system_data_dir(); let mut nockapp = boot::setup( KERNEL_JAM, Some(boot_cli.clone()), &[], "hoonc", Some(data_dir), ) .await?; let mut slab = NounSlab::new(); let hoon_cord = Atom::from_value(&mut slab, HOON_TXT) .unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) .as_noun(); let bootstrap_poke = T(&mut slab, &[D(tas!(b"boot")), hoon_cord]); slab.set_root(bootstrap_poke); nockapp .add_io_driver(nockapp::one_punch_driver(slab, Operation::Poke)) .await; let mut slab = NounSlab::new(); let entry_contents = { let mut contents_vec: Vec = vec![]; let mut file = File::open(&entry).await?; file.read_to_end(&mut contents_vec).await?; Atom::from_value(&mut slab, contents_vec)?.as_noun() }; let entry_string = canonicalize_and_string(&entry); let entry_path = Atom::from_value(&mut slab, entry_string)?.as_noun(); let mut directory_noun = D(0); let directory = canonicalize_and_string(&deps_dir); let walker = WalkDir::new(&directory).follow_links(true).into_iter(); for entry_result in walker.filter_entry(is_valid_file_or_dir) { let entry = entry_result?; let is_file = entry.metadata()?.is_file(); if is_file { let path_str = entry .path() .to_str() .expect("Failed to convert path to string") .strip_prefix(&directory) .expect("Failed to strip prefix"); debug!("Path: {:?}", path_str); let path_cord = Atom::from_value(&mut slab, path_str)?.as_noun(); let contents = { let mut contents_vec: Vec = vec![]; let mut file = File::open(entry.path()).await?; file.read_to_end(&mut contents_vec).await?; Atom::from_value(&mut slab, contents_vec)?.as_noun() }; let entry_cell = T(&mut slab, &[path_cord, contents]); directory_noun = T(&mut slab, &[entry_cell, directory_noun]); } } let out_path_string = if let Some(path) = &out { let parent = if path.is_dir() { path } else { path.parent().unwrap_or_else(|| Path::new(".")) }; let filename = if path.is_dir() { OsStr::new(OUT_JAM_NAME) } else { path.file_name().unwrap_or_else(|| OsStr::new(OUT_JAM_NAME)) }; info!("Filename: {:?}", filename); let parent_canonical = canonicalize_and_string(parent); format!("{}/{}", parent_canonical, filename.to_string_lossy()) } else { let parent_dir = current_dir().expect("Failed to get current directory"); format!("{}/{}", canonicalize_and_string(&parent_dir), OUT_JAM_NAME) }; debug!("Output path: {:?}", out_path_string); let out_path = Atom::from_value(&mut slab, out_path_string.clone())?.as_noun(); let arbitrary_noun = if arbitrary { D(0) } else { D(1) }; let poke = T( &mut slab, &[ D(tas!(b"build")), entry_path, entry_contents, directory_noun, arbitrary_noun, out_path, ], ); slab.set_root(poke); nockapp .add_io_driver(nockapp::one_punch_driver(slab, Operation::Poke)) .await; nockapp.add_io_driver(nockapp::file_driver()).await; nockapp.add_io_driver(nockapp::exit_driver()).await; Ok((nockapp, out_path_string.into())) } pub fn is_valid_file_or_dir(entry: &DirEntry) -> bool { let is_dir = entry .metadata() .unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) .is_dir(); let is_valid = entry .file_name() .to_str() .map(|s| { s.ends_with(".jock") || s.ends_with(".hoon") || s.ends_with(".txt") || s.ends_with(".jam") }) .unwrap_or(false); is_dir || is_valid } #[instrument] pub fn canonicalize_and_string(path: &std::path::Path) -> String { trace!("Canonicalizing path: {:?}", path); let path = path.canonicalize().expect("Failed to canonicalize path"); debug!("Canonicalized path: {:?}", path); let path = path.to_str().expect("Failed to convert path to string"); path.to_string() } /// Run the build and verify the output file, used to build files outside of cli. pub async fn run_build( mut nockapp: nockapp::NockApp, out_path: Option, ) -> Result, Error> { nockapp.run().await?; let out_path = out_path.unwrap_or_else(|| { std::env::current_dir() .unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) .join(OUT_JAM_NAME) }); Ok(fs::read(out_path).await?) } #[cfg(test)] mod tests { #[test] #[cfg_attr(miri, ignore)] fn test_canonicalize_and_string() { // Create a temp dir that will definitely exist let temp_dir = std::env::temp_dir(); // Use canonicalize_and_string on the temp dir let result = super::canonicalize_and_string(&temp_dir); // Compare with direct canonicalization let canonical = temp_dir.canonicalize().unwrap_or_else(|_| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }); assert_eq!( result, canonical.to_str().unwrap_or_else(|| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) ); } }