use crate::entities::{ Described, Description, EntitiesManager, EntitiesManagerComponents, Entity, Usr, }; use anyhow::{Context, Result}; use thiserror::Error; use threadpool::ThreadPool; use std::collections::HashMap; use std::path::Path; use std::sync::{mpsc::channel, Arc}; pub(crate) fn generate(base_dir: &Path, manager: EntitiesManager) -> Result<()> { let EntitiesManagerComponents { toplevel_entities, descriptions, } = manager.decompose(); let md_output_dir = base_dir.join("markdown"); let html_output_dir = base_dir.join("html"); std::fs::create_dir_all(&md_output_dir) .context("Failed to create the markdown output directory")?; std::fs::create_dir_all(&html_output_dir) .context("Failed to create the HTML output directory")?; let pool = ThreadPool::new(num_cpus::get()); let descriptions = Arc::new(descriptions); let (tx, rx) = channel::<()>(); for (usr, entity) in toplevel_entities { generate_recursively( usr, entity, pool.clone(), tx.clone(), descriptions.clone(), &md_output_dir, &html_output_dir, ); } drop(tx); // This is not really idiomatic, but iter returns None when every Sender is destroyed, so just // by passing around Senders in generate_recursively, we wait for every job. rx.iter().for_each(drop); Ok(()) } fn generate_recursively( usr: Usr, entity: Entity, pool: ThreadPool, tx: std::sync::mpsc::Sender<()>, descriptions: Arc>, md_output_dir: impl AsRef, html_output_dir: impl AsRef, ) { let descriptions = descriptions.clone(); let md_output_dir = md_output_dir.as_ref().to_owned(); let html_output_dir = html_output_dir.as_ref().to_owned(); let pool2 = pool.clone(); pool.execute(move || { trace!("Trying to generate {}", usr.0); let entity = Described:: { entity, description: descriptions.get(&usr).unwrap().clone(), }; let leftovers = generate_single( &usr, entity, &descriptions, &md_output_dir, &html_output_dir, ) .unwrap(); for (usr, entity) in leftovers { let pool = pool2.clone(); let tx = tx.clone(); generate_recursively( usr, entity, pool, tx, descriptions.clone(), md_output_dir.clone(), html_output_dir.clone(), ); } }); } fn generate_single( usr: &Usr, entity: Described, descriptions: &HashMap, md_output_dir: impl AsRef, html_output_dir: impl AsRef, ) -> Result> { use std::io::prelude::*; use std::process::{Command, Stdio}; let md_output_file = md_output_dir.as_ref().join(&usr.0); let html_output_file = html_output_dir.as_ref().join(&usr.0); let mut command = Command::new("pandoc"); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .args(&[ "--from=json", "--to=markdown", "--output", md_output_file .to_str() .context("Entity name is not valid UTF-8")?, ]); debug!("Launching command: {:?}", command); let mut pandoc = command .spawn() .context("Failed to execute Pandoc command")?; let (pandoc_ast, leftovers) = entity.into_pandoc(&descriptions); if log_enabled!(log::Level::Trace) { let json = serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?; trace!("Sending json: {}", json); write!( pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), "{}", &json, ) .context("Failed to convert Pandoc AST to JSON")?; } else { serde_json::to_writer( pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), &pandoc_ast, ) .context("Failed to convert Pandoc AST to JSON")?; } let output = pandoc .wait_with_output() .expect("Pandoc command wasn't running"); if !output.status.success() { Err(CommandError { status: output.status, stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), }) .context("Pandoc command failed")?; } let mut command = Command::new("pandoc"); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .args(&[ "--from=markdown", "--to=html", "--css=res/style.css", "--standalone", "--self-contained", md_output_file .to_str() .context("Entity name is not valid UTF-8")?, "--output", html_output_file .to_str() .context("Entity name is not valid UTF-8")?, ]); debug!("Launching command: {:?}", command); let output = command .output() .context("Failed to execute Pandoc command")?; if !output.status.success() { Err(CommandError { status: output.status, stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), }) .context("Pandoc command failed")?; } Ok(leftovers) } #[derive(Debug, Clone, Error)] #[error("Command returned status {:?} and stderr {:?}", status, stderr)] struct CommandError { status: std::process::ExitStatus, stderr: String, }