lib.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. use std::env::current_dir;
  2. use std::ffi::OsStr;
  3. use std::path::{Path, PathBuf};
  4. use clap::{arg, command, ColorChoice, Parser};
  5. use nockapp::driver::Operation;
  6. use nockapp::kernel::boot::{self, default_boot_cli, Cli as BootCli};
  7. use nockapp::noun::slab::NounSlab;
  8. use nockapp::{system_data_dir, AtomExt, Noun, NounExt};
  9. use nockvm::interpreter::{self, Context};
  10. use nockvm::noun::{Atom, D, T};
  11. use nockvm_macros::tas;
  12. use tokio::fs::{self, File};
  13. use tokio::io::AsyncReadExt;
  14. use tracing::{debug, info, instrument, trace};
  15. use walkdir::{DirEntry, WalkDir};
  16. pub const OUT_JAM_NAME: &str = "out.jam";
  17. pub type Error = Box<dyn std::error::Error>;
  18. static KERNEL_JAM: &[u8] = include_bytes!("../bootstrap/hoonc.jam");
  19. static HOON_TXT: &[u8] = include_bytes!("../hoon/hoon-138.hoon");
  20. #[derive(Parser, Debug)]
  21. #[command(about = "Tests various poke types for the kernel", author = "zorp", version, color = ColorChoice::Auto)]
  22. pub struct ChooCli {
  23. #[command(flatten)]
  24. pub boot: BootCli,
  25. // TODO: REPRODUCIBILITY:
  26. // make entry path relative to the dependency directory
  27. // we may have to go back to requiring that the entry exists in the dependency directory
  28. #[arg(help = "Path to file to compile")]
  29. pub entry: std::path::PathBuf,
  30. #[arg(help = "Path to root of dependency directory", default_value = "hoon")]
  31. pub directory: std::path::PathBuf,
  32. #[arg(
  33. long,
  34. help = "Build raw, without file hash injection",
  35. default_value = "false"
  36. )]
  37. pub arbitrary: bool,
  38. #[arg(long, help = "Output file path", default_value = None)]
  39. pub output: Option<std::path::PathBuf>,
  40. }
  41. pub async fn hoonc_data_dir() -> PathBuf {
  42. let hoonc_data_dir = system_data_dir().join("hoonc");
  43. if !hoonc_data_dir.exists() {
  44. fs::create_dir_all(&hoonc_data_dir)
  45. .await
  46. .unwrap_or_else(|_| {
  47. panic!(
  48. "Panicked at {}:{} (git sha: {:?})",
  49. file!(),
  50. line!(),
  51. option_env!("GIT_SHA")
  52. )
  53. });
  54. }
  55. hoonc_data_dir
  56. }
  57. /// Builds and interprets a Hoon generator.
  58. ///
  59. /// This function:
  60. /// 1. Builds the specified Hoon generator into a jam
  61. /// 2. Decodes the jam into a Nock noun
  62. /// 3. Interprets the noun with a kick operation to run the generator
  63. ///
  64. /// # Parameters
  65. /// - `context`: The Nock interpreter context
  66. /// - `path`: Path to the Hoon generator file
  67. ///
  68. /// # Returns
  69. /// - A noun
  70. pub async fn build_and_kick_jam(
  71. context: &mut Context,
  72. path: &str,
  73. deps_dir: PathBuf,
  74. out_dir: Option<PathBuf>,
  75. ) -> Noun {
  76. let jam = build_jam(path, deps_dir, out_dir, true, false)
  77. .await
  78. .expect("failed to build page");
  79. debug!("Built jam");
  80. let generator_trap =
  81. Noun::cue_bytes_slice(&mut context.stack, &jam).expect("invalid generator jam");
  82. let kick = T(&mut context.stack, &[D(9), D(2), D(0), D(1)]);
  83. debug!("Kicking trap");
  84. interpreter::interpret(context, generator_trap, kick).unwrap_or_else(|_| {
  85. panic!(
  86. "Panicked at {}:{} (git sha: {:?})",
  87. file!(),
  88. line!(),
  89. option_env!("GIT_SHA")
  90. )
  91. })
  92. }
  93. pub async fn save_generator(
  94. context: &mut Context,
  95. path: &str,
  96. deps_dir: PathBuf,
  97. out_dir: Option<PathBuf>,
  98. ) -> Result<(), Error> {
  99. let cli = default_boot_cli(true);
  100. boot::init_default_tracing(&cli);
  101. let kicked = build_and_kick_jam(context, path, deps_dir, out_dir.clone()).await;
  102. let jammed = kicked.jam_self(&mut context.stack);
  103. let file_name = Path::new(path)
  104. .file_stem()
  105. .unwrap_or_else(|| OsStr::new("generator"))
  106. .to_string_lossy()
  107. .to_string();
  108. let output_file = out_dir
  109. .clone()
  110. .unwrap_or_else(|| current_dir().expect("Failed to get current directory"))
  111. .join(format!("{}.jam", file_name));
  112. if let Some(parent) = output_file.parent() {
  113. fs::create_dir_all(parent).await?;
  114. }
  115. fs::write(&output_file, jammed).await?;
  116. println!("Generator saved to: {}", output_file.display());
  117. Ok(())
  118. }
  119. /// Builds a jam (serialized Nock noun) from a Hoon source file
  120. ///
  121. /// This function:
  122. /// 1. Locates the source file relative to the hoon directory
  123. /// 2. Creates a temporary directory for build artifacts
  124. /// 3. Initializes a Nock app with the hoonc build system
  125. /// 4. Builds the source file and returns the resulting jam as bytes
  126. ///
  127. /// # Parameters
  128. /// - `entry`: Path to the Hoon source file, relative to the hoon directory
  129. /// - `arbitrary`: Whether to build with arbitrary mode enabled
  130. /// - `new`: Whether to force a clean build
  131. ///
  132. /// # Returns
  133. /// - A Result containing either the jam bytes or a hoonc error
  134. pub async fn build_jam(
  135. entry: &str,
  136. deps_dir: PathBuf,
  137. out_dir: Option<PathBuf>,
  138. arbitrary: bool,
  139. new: bool,
  140. ) -> Result<Vec<u8>, Error> {
  141. info!("Dependencies directory: {:?}", deps_dir);
  142. info!("Entry file: {:?}", entry);
  143. let (nockapp, out_path) =
  144. initialize_with_default_cli(entry.into(), deps_dir, out_dir, arbitrary, new).await?;
  145. info!("Output path: {:?}", out_path);
  146. run_build(nockapp, Some(out_path.clone())).await
  147. }
  148. pub async fn initialize_hoonc(cli: ChooCli) -> Result<(nockapp::NockApp, PathBuf), Error> {
  149. initialize_hoonc_(
  150. cli.entry,
  151. cli.directory,
  152. cli.arbitrary,
  153. cli.output,
  154. cli.boot.clone(),
  155. )
  156. .await
  157. }
  158. pub async fn initialize_with_default_cli(
  159. entry: std::path::PathBuf,
  160. deps_dir: std::path::PathBuf,
  161. out: Option<std::path::PathBuf>,
  162. arbitrary: bool,
  163. new: bool,
  164. ) -> Result<(nockapp::NockApp, PathBuf), Error> {
  165. let cli = default_boot_cli(new);
  166. initialize_hoonc_(entry, deps_dir, arbitrary, out, cli).await
  167. }
  168. pub async fn initialize_hoonc_(
  169. entry: std::path::PathBuf,
  170. deps_dir: std::path::PathBuf,
  171. arbitrary: bool,
  172. out: Option<std::path::PathBuf>,
  173. boot_cli: BootCli,
  174. ) -> Result<(nockapp::NockApp, PathBuf), Error> {
  175. debug!("Dependencies directory: {:?}", deps_dir);
  176. debug!("Entry file: {:?}", entry);
  177. let data_dir = system_data_dir();
  178. let mut nockapp = boot::setup(
  179. KERNEL_JAM,
  180. Some(boot_cli.clone()),
  181. &[],
  182. "hoonc",
  183. Some(data_dir),
  184. )
  185. .await?;
  186. let mut slab = NounSlab::new();
  187. let hoon_cord = Atom::from_value(&mut slab, HOON_TXT)
  188. .unwrap_or_else(|_| {
  189. panic!(
  190. "Panicked at {}:{} (git sha: {:?})",
  191. file!(),
  192. line!(),
  193. option_env!("GIT_SHA")
  194. )
  195. })
  196. .as_noun();
  197. let bootstrap_poke = T(&mut slab, &[D(tas!(b"boot")), hoon_cord]);
  198. slab.set_root(bootstrap_poke);
  199. nockapp
  200. .add_io_driver(nockapp::one_punch_driver(slab, Operation::Poke))
  201. .await;
  202. let mut slab = NounSlab::new();
  203. let entry_contents = {
  204. let mut contents_vec: Vec<u8> = vec![];
  205. let mut file = File::open(&entry).await?;
  206. file.read_to_end(&mut contents_vec).await?;
  207. Atom::from_value(&mut slab, contents_vec)?.as_noun()
  208. };
  209. let entry_string = canonicalize_and_string(&entry);
  210. let entry_path = Atom::from_value(&mut slab, entry_string)?.as_noun();
  211. let mut directory_noun = D(0);
  212. let directory = canonicalize_and_string(&deps_dir);
  213. let walker = WalkDir::new(&directory).follow_links(true).into_iter();
  214. for entry_result in walker.filter_entry(is_valid_file_or_dir) {
  215. let entry = entry_result?;
  216. let is_file = entry.metadata()?.is_file();
  217. if is_file {
  218. let path_str = entry
  219. .path()
  220. .to_str()
  221. .expect("Failed to convert path to string")
  222. .strip_prefix(&directory)
  223. .expect("Failed to strip prefix");
  224. debug!("Path: {:?}", path_str);
  225. let path_cord = Atom::from_value(&mut slab, path_str)?.as_noun();
  226. let contents = {
  227. let mut contents_vec: Vec<u8> = vec![];
  228. let mut file = File::open(entry.path()).await?;
  229. file.read_to_end(&mut contents_vec).await?;
  230. Atom::from_value(&mut slab, contents_vec)?.as_noun()
  231. };
  232. let entry_cell = T(&mut slab, &[path_cord, contents]);
  233. directory_noun = T(&mut slab, &[entry_cell, directory_noun]);
  234. }
  235. }
  236. let out_path_string = if let Some(path) = &out {
  237. let parent = if path.is_dir() {
  238. path
  239. } else {
  240. path.parent().unwrap_or_else(|| Path::new("."))
  241. };
  242. let filename = if path.is_dir() {
  243. OsStr::new(OUT_JAM_NAME)
  244. } else {
  245. path.file_name().unwrap_or_else(|| OsStr::new(OUT_JAM_NAME))
  246. };
  247. info!("Filename: {:?}", filename);
  248. let parent_canonical = canonicalize_and_string(parent);
  249. format!("{}/{}", parent_canonical, filename.to_string_lossy())
  250. } else {
  251. let parent_dir = current_dir().expect("Failed to get current directory");
  252. format!("{}/{}", canonicalize_and_string(&parent_dir), OUT_JAM_NAME)
  253. };
  254. debug!("Output path: {:?}", out_path_string);
  255. let out_path = Atom::from_value(&mut slab, out_path_string.clone())?.as_noun();
  256. let arbitrary_noun = if arbitrary { D(0) } else { D(1) };
  257. let poke = T(
  258. &mut slab,
  259. &[
  260. D(tas!(b"build")),
  261. entry_path,
  262. entry_contents,
  263. directory_noun,
  264. arbitrary_noun,
  265. out_path,
  266. ],
  267. );
  268. slab.set_root(poke);
  269. nockapp
  270. .add_io_driver(nockapp::one_punch_driver(slab, Operation::Poke))
  271. .await;
  272. nockapp.add_io_driver(nockapp::file_driver()).await;
  273. nockapp.add_io_driver(nockapp::exit_driver()).await;
  274. Ok((nockapp, out_path_string.into()))
  275. }
  276. pub fn is_valid_file_or_dir(entry: &DirEntry) -> bool {
  277. let is_dir = entry
  278. .metadata()
  279. .unwrap_or_else(|_| {
  280. panic!(
  281. "Panicked at {}:{} (git sha: {:?})",
  282. file!(),
  283. line!(),
  284. option_env!("GIT_SHA")
  285. )
  286. })
  287. .is_dir();
  288. let is_valid = entry
  289. .file_name()
  290. .to_str()
  291. .map(|s| {
  292. s.ends_with(".jock")
  293. || s.ends_with(".hoon")
  294. || s.ends_with(".txt")
  295. || s.ends_with(".jam")
  296. })
  297. .unwrap_or(false);
  298. is_dir || is_valid
  299. }
  300. #[instrument]
  301. pub fn canonicalize_and_string(path: &std::path::Path) -> String {
  302. trace!("Canonicalizing path: {:?}", path);
  303. let path = path.canonicalize().expect("Failed to canonicalize path");
  304. debug!("Canonicalized path: {:?}", path);
  305. let path = path.to_str().expect("Failed to convert path to string");
  306. path.to_string()
  307. }
  308. /// Run the build and verify the output file, used to build files outside of cli.
  309. pub async fn run_build(
  310. mut nockapp: nockapp::NockApp,
  311. out_path: Option<PathBuf>,
  312. ) -> Result<Vec<u8>, Error> {
  313. nockapp.run().await?;
  314. let out_path = out_path.unwrap_or_else(|| {
  315. std::env::current_dir()
  316. .unwrap_or_else(|_| {
  317. panic!(
  318. "Panicked at {}:{} (git sha: {:?})",
  319. file!(),
  320. line!(),
  321. option_env!("GIT_SHA")
  322. )
  323. })
  324. .join(OUT_JAM_NAME)
  325. });
  326. Ok(fs::read(out_path).await?)
  327. }
  328. #[cfg(test)]
  329. mod tests {
  330. #[test]
  331. #[cfg_attr(miri, ignore)]
  332. fn test_canonicalize_and_string() {
  333. // Create a temp dir that will definitely exist
  334. let temp_dir = std::env::temp_dir();
  335. // Use canonicalize_and_string on the temp dir
  336. let result = super::canonicalize_and_string(&temp_dir);
  337. // Compare with direct canonicalization
  338. let canonical = temp_dir.canonicalize().unwrap_or_else(|_| {
  339. panic!(
  340. "Panicked at {}:{} (git sha: {:?})",
  341. file!(),
  342. line!(),
  343. option_env!("GIT_SHA")
  344. )
  345. });
  346. assert_eq!(
  347. result,
  348. canonical.to_str().unwrap_or_else(|| {
  349. panic!(
  350. "Panicked at {}:{} (git sha: {:?})",
  351. file!(),
  352. line!(),
  353. option_env!("GIT_SHA")
  354. )
  355. })
  356. );
  357. }
  358. }