diff options
Diffstat (limited to 'cli')
| -rw-r--r-- | cli/Cargo.toml | 59 | ||||
| -rw-r--r-- | cli/build.rs | 60 | ||||
| -rw-r--r-- | cli/src/args.rs | 119 | ||||
| -rw-r--r-- | cli/src/compile.rs | 239 | ||||
| -rw-r--r-- | cli/src/fonts.rs | 183 | ||||
| -rw-r--r-- | cli/src/main.rs | 79 | ||||
| -rw-r--r-- | cli/src/package.rs | 77 | ||||
| -rw-r--r-- | cli/src/tracing.rs | 138 | ||||
| -rw-r--r-- | cli/src/watch.rs | 170 | ||||
| -rw-r--r-- | cli/src/world.rs | 256 |
10 files changed, 0 insertions, 1380 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index e8058df0..00000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "typst-cli" -description = "The command line interface for Typst." -categories = ["compilers", "command-line-utilities"] -keywords = ["typst", "cli"] -version.workspace = true -rust-version.workspace = true -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true -license.workspace = true - -[[bin]] -name = "typst" -path = "src/main.rs" -test = false -doctest = false -bench = false -doc = false - -[dependencies] -typst = { path = ".." } -typst-library = { path = "../library" } -chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } -clap = { version = "4.2.4", features = ["derive", "env"] } -codespan-reporting = "0.11" -comemo = "0.3" -dirs = "5" -flate2 = "1" -inferno = "0.11.15" -memmap2 = "0.5" -notify = "5" -once_cell = "1" -open = "4.0.2" -same-file = "1" -siphasher = "0.3" -tar = "0.4" -tempfile = "3.5.0" -tracing = "0.1.37" -tracing-error = "0.2" -tracing-flame = "0.2.0" -tracing-subscriber = "0.3.17" -ureq = "2" -walkdir = "2" - -[build-dependencies] -clap = { version = "4.2.4", features = ["derive", "string"] } -clap_complete = "4.2.1" -clap_mangen = "0.2.10" - -[features] -default = ["embed-fonts"] - -# Embeds some fonts into the binary: -# - For text: Linux Libertine, New Computer Modern -# - For math: New Computer Modern Math -# - For code: Deja Vu Sans Mono -embed-fonts = [] diff --git a/cli/build.rs b/cli/build.rs deleted file mode 100644 index 86325e1d..00000000 --- a/cli/build.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::env; -use std::fs::{create_dir_all, File}; -use std::path::Path; -use std::process::Command; - -use clap::{CommandFactory, ValueEnum}; -use clap_complete::{generate_to, Shell}; -use clap_mangen::Man; - -#[path = "src/args.rs"] -#[allow(dead_code)] -mod args; - -fn main() { - println!("cargo:rerun-if-env-changed=TYPST_VERSION"); - println!("cargo:rerun-if-env-changed=GEN_ARTIFACTS"); - - if option_env!("TYPST_VERSION").is_none() { - println!("cargo:rustc-env=TYPST_VERSION={}", typst_version()); - } - - if let Some(dir) = env::var_os("GEN_ARTIFACTS") { - let out = &Path::new(&dir); - create_dir_all(out).unwrap(); - let cmd = &mut args::CliArguments::command(); - - Man::new(cmd.clone()) - .render(&mut File::create(out.join("typst.1")).unwrap()) - .unwrap(); - - for subcmd in cmd.get_subcommands() { - let name = format!("typst-{}", subcmd.get_name()); - Man::new(subcmd.clone().name(&name)) - .render(&mut File::create(out.join(format!("{name}.1"))).unwrap()) - .unwrap(); - } - - for shell in Shell::value_variants() { - generate_to(*shell, cmd, "typst", out).unwrap(); - } - } -} - -/// Also used by `args.rs`. -fn typst_version() -> String { - if let Some(version) = option_env!("TYPST_VERSION") { - return version.to_owned(); - } - - let pkg = env!("CARGO_PKG_VERSION"); - let hash = Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .ok() - .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout.get(..8)?.into()).ok()) - .unwrap_or_else(|| "unknown hash".into()); - - format!("{pkg} ({hash})") -} diff --git a/cli/src/args.rs b/cli/src/args.rs deleted file mode 100644 index 7fdb041b..00000000 --- a/cli/src/args.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::fmt::{self, Display, Formatter}; -use std::path::PathBuf; - -use clap::{ArgAction, Parser, Subcommand, ValueEnum}; - -/// typst creates PDF files from .typ files -#[derive(Debug, Clone, Parser)] -#[clap(name = "typst", version = crate::typst_version(), author)] -pub struct CliArguments { - /// The typst command to run - #[command(subcommand)] - pub command: Command, - - /// Sets the level of logging verbosity: - /// -v = warning & error, -vv = info, -vvv = debug, -vvvv = trace - #[clap(short, long, action = ArgAction::Count)] - pub verbosity: u8, -} - -/// What to do. -#[derive(Debug, Clone, Subcommand)] -#[command()] -pub enum Command { - /// Compiles the input file into a PDF file - #[command(visible_alias = "c")] - Compile(CompileCommand), - - /// Watches the input file and recompiles on changes - #[command(visible_alias = "w")] - Watch(CompileCommand), - - /// List all discovered fonts in system and custom font paths - Fonts(FontsCommand), -} - -/// Compiles the input file into a PDF file -#[derive(Debug, Clone, Parser)] -pub struct CompileCommand { - /// Path to input Typst file - pub input: PathBuf, - - /// Path to output PDF file or PNG file(s) - pub output: Option<PathBuf>, - - /// Configure the project root - #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")] - pub root: Option<PathBuf>, - - /// Add additional directories to search for fonts - #[clap( - long = "font-path", - env = "TYPST_FONT_PATHS", - value_name = "DIR", - action = ArgAction::Append, - )] - pub font_paths: Vec<PathBuf>, - - /// Opens the output file after compilation using the default PDF viewer - #[arg(long = "open")] - pub open: Option<Option<String>>, - - /// The PPI to use if exported as PNG - #[arg(long = "ppi", default_value_t = 144.0)] - pub ppi: f32, - - /// In which format to emit diagnostics - #[clap( - long, - default_value_t = DiagnosticFormat::Human, - value_parser = clap::value_parser!(DiagnosticFormat) - )] - pub diagnostic_format: DiagnosticFormat, - - /// Produces a flamegraph of the compilation process - #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] - pub flamegraph: Option<Option<PathBuf>>, -} - -impl CompileCommand { - /// The output path. - pub fn output(&self) -> PathBuf { - self.output - .clone() - .unwrap_or_else(|| self.input.with_extension("pdf")) - } -} - -/// List all discovered fonts in system and custom font paths -#[derive(Debug, Clone, Parser)] -pub struct FontsCommand { - /// Add additional directories to search for fonts - #[clap( - long = "font-path", - env = "TYPST_FONT_PATHS", - value_name = "DIR", - action = ArgAction::Append, - )] - pub font_paths: Vec<PathBuf>, - - /// Also list style variants of each font family - #[arg(long)] - pub variants: bool, -} - -/// Which format to use for diagnostics. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] -pub enum DiagnosticFormat { - Human, - Short, -} - -impl Display for DiagnosticFormat { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } -} diff --git a/cli/src/compile.rs b/cli/src/compile.rs deleted file mode 100644 index 3250202b..00000000 --- a/cli/src/compile.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::fs; -use std::path::Path; - -use codespan_reporting::diagnostic::{Diagnostic, Label}; -use codespan_reporting::term::{self, termcolor}; -use termcolor::{ColorChoice, StandardStream}; -use typst::diag::{bail, SourceError, StrResult}; -use typst::doc::Document; -use typst::eval::eco_format; -use typst::file::FileId; -use typst::geom::Color; -use typst::syntax::Source; -use typst::World; - -use crate::args::{CompileCommand, DiagnosticFormat}; -use crate::watch::Status; -use crate::world::SystemWorld; -use crate::{color_stream, set_failed}; - -type CodespanResult<T> = Result<T, CodespanError>; -type CodespanError = codespan_reporting::files::Error; - -/// Execute a compilation command. -pub fn compile(mut command: CompileCommand) -> StrResult<()> { - let mut world = SystemWorld::new(&command)?; - compile_once(&mut world, &mut command, false)?; - Ok(()) -} - -/// Compile a single time. -/// -/// Returns whether it compiled without errors. -#[tracing::instrument(skip_all)] -pub fn compile_once( - world: &mut SystemWorld, - command: &mut CompileCommand, - watching: bool, -) -> StrResult<()> { - tracing::info!("Starting compilation"); - - let start = std::time::Instant::now(); - if watching { - Status::Compiling.print(command).unwrap(); - } - - // Reset everything and ensure that the main file is still present. - world.reset(); - world.source(world.main()).map_err(|err| err.to_string())?; - - let result = typst::compile(world); - let duration = start.elapsed(); - - match result { - // Export the PDF / PNG. - Ok(document) => { - export(&document, command)?; - - tracing::info!("Compilation succeeded in {duration:?}"); - if watching { - Status::Success(duration).print(command).unwrap(); - } - - if let Some(open) = command.open.take() { - open_file(open.as_deref(), &command.output())?; - } - } - - // Print diagnostics. - Err(errors) => { - set_failed(); - tracing::info!("Compilation failed"); - - if watching { - Status::Error.print(command).unwrap(); - } - - print_diagnostics(world, *errors, command.diagnostic_format) - .map_err(|_| "failed to print diagnostics")?; - } - } - - Ok(()) -} - -/// Export into the target format. -fn export(document: &Document, command: &CompileCommand) -> StrResult<()> { - match command.output().extension() { - Some(ext) if ext.eq_ignore_ascii_case("png") => export_png(document, command), - _ => export_pdf(document, command), - } -} - -/// Export to a PDF. -fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> { - let output = command.output(); - let buffer = typst::export::pdf(document); - fs::write(output, buffer).map_err(|_| "failed to write PDF file")?; - Ok(()) -} - -/// Export to one or multiple PNGs. -fn export_png(document: &Document, command: &CompileCommand) -> StrResult<()> { - // Determine whether we have a `{n}` numbering. - let output = command.output(); - let string = output.to_str().unwrap_or_default(); - let numbered = string.contains("{n}"); - if !numbered && document.pages.len() > 1 { - bail!("cannot export multiple PNGs without `{{n}}` in output path"); - } - - // Find a number width that accommodates all pages. For instance, the - // first page should be numbered "001" if there are between 100 and - // 999 pages. - let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; - let mut storage; - - for (i, frame) in document.pages.iter().enumerate() { - let pixmap = typst::export::render(frame, command.ppi / 72.0, Color::WHITE); - let path = if numbered { - storage = string.replace("{n}", &format!("{:0width$}", i + 1)); - Path::new(&storage) - } else { - output.as_path() - }; - pixmap.save_png(path).map_err(|_| "failed to write PNG file")?; - } - - Ok(()) -} - -/// Opens the given file using: -/// - The default file viewer if `open` is `None`. -/// - The given viewer provided by `open` if it is `Some`. -fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { - if let Some(app) = open { - open::with_in_background(path, app); - } else { - open::that_in_background(path); - } - - Ok(()) -} - -/// Print diagnostic messages to the terminal. -fn print_diagnostics( - world: &SystemWorld, - errors: Vec<SourceError>, - diagnostic_format: DiagnosticFormat, -) -> Result<(), codespan_reporting::files::Error> { - let mut w = match diagnostic_format { - DiagnosticFormat::Human => color_stream(), - DiagnosticFormat::Short => StandardStream::stderr(ColorChoice::Never), - }; - - let mut config = term::Config { tab_width: 2, ..Default::default() }; - if diagnostic_format == DiagnosticFormat::Short { - config.display_style = term::DisplayStyle::Short; - } - - for error in errors { - // The main diagnostic. - let diag = Diagnostic::error() - .with_message(error.message) - .with_notes( - error - .hints - .iter() - .map(|e| (eco_format!("hint: {e}")).into()) - .collect(), - ) - .with_labels(vec![Label::primary(error.span.id(), error.span.range(world))]); - - term::emit(&mut w, &config, world, &diag)?; - - // Stacktrace-like helper diagnostics. - for point in error.trace { - let message = point.v.to_string(); - let help = Diagnostic::help().with_message(message).with_labels(vec![ - Label::primary(point.span.id(), point.span.range(world)), - ]); - - term::emit(&mut w, &config, world, &help)?; - } - } - - Ok(()) -} - -impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { - type FileId = FileId; - type Name = FileId; - type Source = Source; - - fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> { - Ok(id) - } - - fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> { - Ok(self.lookup(id)) - } - - fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> { - let source = self.lookup(id); - source - .byte_to_line(given) - .ok_or_else(|| CodespanError::IndexTooLarge { - given, - max: source.len_bytes(), - }) - } - - fn line_range( - &'a self, - id: FileId, - given: usize, - ) -> CodespanResult<std::ops::Range<usize>> { - let source = self.lookup(id); - source - .line_to_range(given) - .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) - } - - fn column_number( - &'a self, - id: FileId, - _: usize, - given: usize, - ) -> CodespanResult<usize> { - let source = self.lookup(id); - source.byte_to_column(given).ok_or_else(|| { - let max = source.len_bytes(); - if given <= max { - CodespanError::InvalidCharBoundary { given } - } else { - CodespanError::IndexTooLarge { given, max } - } - }) - } -} diff --git a/cli/src/fonts.rs b/cli/src/fonts.rs deleted file mode 100644 index 75835741..00000000 --- a/cli/src/fonts.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::cell::OnceCell; -use std::env; -use std::fs::{self, File}; -use std::path::{Path, PathBuf}; - -use memmap2::Mmap; -use typst::diag::StrResult; -use typst::font::{Font, FontBook, FontInfo, FontVariant}; -use typst::util::Bytes; -use walkdir::WalkDir; - -use crate::args::FontsCommand; - -/// Execute a font listing command. -pub fn fonts(command: FontsCommand) -> StrResult<()> { - let mut searcher = FontSearcher::new(); - searcher.search(&command.font_paths); - - for (name, infos) in searcher.book.families() { - println!("{name}"); - if command.variants { - for info in infos { - let FontVariant { style, weight, stretch } = info.variant; - println!("- Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}"); - } - } - } - - Ok(()) -} - -/// Searches for fonts. -pub struct FontSearcher { - /// Metadata about all discovered fonts. - pub book: FontBook, - /// Slots that the fonts are loaded into. - pub fonts: Vec<FontSlot>, -} - -/// Holds details about the location of a font and lazily the font itself. -pub struct FontSlot { - /// The path at which the font can be found on the system. - path: PathBuf, - /// The index of the font in its collection. Zero if the path does not point - /// to a collection. - index: u32, - /// The lazily loaded font. - font: OnceCell<Option<Font>>, -} - -impl FontSlot { - /// Get the font for this slot. - pub fn get(&self) -> Option<Font> { - self.font - .get_or_init(|| { - let data = fs::read(&self.path).ok()?.into(); - Font::new(data, self.index) - }) - .clone() - } -} - -impl FontSearcher { - /// Create a new, empty system searcher. - pub fn new() -> Self { - Self { book: FontBook::new(), fonts: vec![] } - } - - /// Search everything that is available. - pub fn search(&mut self, font_paths: &[PathBuf]) { - self.search_system(); - - #[cfg(feature = "embed-fonts")] - self.add_embedded(); - - for path in font_paths { - self.search_dir(path) - } - } - - /// Add fonts that are embedded in the binary. - #[cfg(feature = "embed-fonts")] - fn add_embedded(&mut self) { - let mut process = |bytes: &'static [u8]| { - let buffer = Bytes::from_static(bytes); - for (i, font) in Font::iter(buffer).enumerate() { - self.book.push(font.info().clone()); - self.fonts.push(FontSlot { - path: PathBuf::new(), - index: i as u32, - font: OnceCell::from(Some(font)), - }); - } - }; - - macro_rules! add { - ($filename:literal) => { - process(include_bytes!(concat!("../../assets/fonts/", $filename,))); - }; - } - - // Embed default fonts. - add!("LinLibertine_R.ttf"); - add!("LinLibertine_RB.ttf"); - add!("LinLibertine_RBI.ttf"); - add!("LinLibertine_RI.ttf"); - add!("NewCMMath-Book.otf"); - add!("NewCMMath-Regular.otf"); - add!("NewCM10-Regular.otf"); - add!("NewCM10-Bold.otf"); - add!("NewCM10-Italic.otf"); - add!("NewCM10-BoldItalic.otf"); - add!("DejaVuSansMono.ttf"); - add!("DejaVuSansMono-Bold.ttf"); - add!("DejaVuSansMono-Oblique.ttf"); - add!("DejaVuSansMono-BoldOblique.ttf"); - } - - /// Search for fonts in the linux system font directories. - fn search_system(&mut self) { - if cfg!(target_os = "macos") { - self.search_dir("/Library/Fonts"); - self.search_dir("/Network/Library/Fonts"); - self.search_dir("/System/Library/Fonts"); - } else if cfg!(unix) { - self.search_dir("/usr/share/fonts"); - self.search_dir("/usr/local/share/fonts"); - } else if cfg!(windows) { - self.search_dir( - env::var_os("WINDIR") - .map(PathBuf::from) - .unwrap_or_else(|| "C:\\Windows".into()) - .join("Fonts"), - ); - - if let Some(roaming) = dirs::config_dir() { - self.search_dir(roaming.join("Microsoft\\Windows\\Fonts")); - } - - if let Some(local) = dirs::cache_dir() { - self.search_dir(local.join("Microsoft\\Windows\\Fonts")); - } - } - - if let Some(dir) = dirs::font_dir() { - self.search_dir(dir); - } - } - - /// Search for all fonts in a directory recursively. - fn search_dir(&mut self, path: impl AsRef<Path>) { - for entry in WalkDir::new(path) - .follow_links(true) - .sort_by(|a, b| a.file_name().cmp(b.file_name())) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path(); - if matches!( - path.extension().and_then(|s| s.to_str()), - Some("ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC"), - ) { - self.search_file(path); - } - } - } - - /// Index the fonts in the file at the given path. - fn search_file(&mut self, path: &Path) { - if let Ok(file) = File::open(path) { - if let Ok(mmap) = unsafe { Mmap::map(&file) } { - for (i, info) in FontInfo::iter(&mmap).enumerate() { - self.book.push(info); - self.fonts.push(FontSlot { - path: path.into(), - index: i as u32, - font: OnceCell::new(), - }); - } - } - } - } -} diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index 425d05fd..00000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod args; -mod compile; -mod fonts; -mod package; -mod tracing; -mod watch; -mod world; - -use std::cell::Cell; -use std::env; -use std::io::{self, IsTerminal, Write}; -use std::process::ExitCode; - -use clap::Parser; -use codespan_reporting::term::{self, termcolor}; -use termcolor::{ColorChoice, WriteColor}; - -use crate::args::{CliArguments, Command}; - -thread_local! { - /// The CLI's exit code. - static EXIT: Cell<ExitCode> = Cell::new(ExitCode::SUCCESS); -} - -/// Entry point. -fn main() -> ExitCode { - let arguments = CliArguments::parse(); - let _guard = match crate::tracing::setup_tracing(&arguments) { - Ok(guard) => guard, - Err(err) => { - eprintln!("failed to initialize tracing {}", err); - None - } - }; - - let res = match arguments.command { - Command::Compile(command) => crate::compile::compile(command), - Command::Watch(command) => crate::watch::watch(command), - Command::Fonts(command) => crate::fonts::fonts(command), - }; - - if let Err(msg) = res { - set_failed(); - print_error(&msg).expect("failed to print error"); - } - - EXIT.with(|cell| cell.get()) -} - -/// Ensure a failure exit code. -fn set_failed() { - EXIT.with(|cell| cell.set(ExitCode::FAILURE)); -} - -/// Print an application-level error (independent from a source file). -fn print_error(msg: &str) -> io::Result<()> { - let mut w = color_stream(); - let styles = term::Styles::default(); - - w.set_color(&styles.header_error)?; - write!(w, "error")?; - - w.reset()?; - writeln!(w, ": {msg}.") -} - -/// Get stderr with color support if desirable. -fn color_stream() -> termcolor::StandardStream { - termcolor::StandardStream::stderr(if std::io::stderr().is_terminal() { - ColorChoice::Auto - } else { - ColorChoice::Never - }) -} - -/// Used by `args.rs`. -fn typst_version() -> &'static str { - env!("TYPST_VERSION") -} diff --git a/cli/src/package.rs b/cli/src/package.rs deleted file mode 100644 index 6853796b..00000000 --- a/cli/src/package.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::fs; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; - -use codespan_reporting::term::{self, termcolor}; -use termcolor::WriteColor; -use typst::diag::{PackageError, PackageResult}; -use typst::file::PackageSpec; - -use super::color_stream; - -/// Make a package available in the on-disk cache. -pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> { - let subdir = - format!("typst/packages/{}/{}-{}", spec.namespace, spec.name, spec.version); - - if let Some(data_dir) = dirs::data_dir() { - let dir = data_dir.join(&subdir); - if dir.exists() { - return Ok(dir); - } - } - - if let Some(cache_dir) = dirs::cache_dir() { - let dir = cache_dir.join(&subdir); - - // Download from network if it doesn't exist yet. - if spec.namespace == "preview" && !dir.exists() { - download_package(spec, &dir)?; - } - - if dir.exists() { - return Ok(dir); - } - } - - Err(PackageError::NotFound(spec.clone())) -} - -/// Download a package over the network. -fn download_package(spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> { - // The `@preview` namespace is the only namespace that supports on-demand - // fetching. - assert_eq!(spec.namespace, "preview"); - - let url = format!( - "https://packages.typst.org/preview/{}-{}.tar.gz", - spec.name, spec.version - ); - - print_downloading(spec).unwrap(); - let reader = match ureq::get(&url).call() { - Ok(response) => response.into_reader(), - Err(ureq::Error::Status(404, _)) => { - return Err(PackageError::NotFound(spec.clone())) - } - Err(_) => return Err(PackageError::NetworkFailed), - }; - - let decompressed = flate2::read::GzDecoder::new(reader); - tar::Archive::new(decompressed).unpack(package_dir).map_err(|_| { - fs::remove_dir_all(package_dir).ok(); - PackageError::MalformedArchive - }) -} - -/// Print that a package downloading is happening. -fn print_downloading(spec: &PackageSpec) -> io::Result<()> { - let mut w = color_stream(); - let styles = term::Styles::default(); - - w.set_color(&styles.header_help)?; - write!(w, "downloading")?; - - w.reset()?; - writeln!(w, " {spec}") -} diff --git a/cli/src/tracing.rs b/cli/src/tracing.rs deleted file mode 100644 index 80c2ff65..00000000 --- a/cli/src/tracing.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::fs::File; -use std::io::{self, BufReader, BufWriter, Seek, SeekFrom}; -use std::path::PathBuf; - -use inferno::flamegraph::Options; -use tracing::metadata::LevelFilter; -use tracing_error::ErrorLayer; -use tracing_flame::{FlameLayer, FlushGuard}; -use tracing_subscriber::fmt; -use tracing_subscriber::prelude::*; - -use crate::args::{CliArguments, Command}; - -/// Initializes the tracing system and returns a guard that will flush the -/// flamegraph to disk when dropped. -pub fn setup_tracing(args: &CliArguments) -> io::Result<Option<impl Drop>> { - let flamegraph = match &args.command { - Command::Compile(command) => command.flamegraph.as_ref(), - Command::Watch(command) if command.flamegraph.is_some() => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "cannot use --flamegraph with watch command", - )); - } - _ => None, - }; - - // Short circuit if we don't need to initialize flamegraph or debugging. - if flamegraph.is_none() && args.verbosity == 0 { - tracing_subscriber::fmt() - .without_time() - .with_max_level(level_filter(args)) - .init(); - - return Ok(None); - } - - // Build the FMT layer printing to the console. - let fmt_layer = fmt::Layer::default().without_time().with_filter(level_filter(args)); - - // Error layer for building backtraces - let error_layer = ErrorLayer::default(); - - // Build the registry. - let registry = tracing_subscriber::registry().with(fmt_layer).with(error_layer); - - let Some(path) = flamegraph else { - registry.init(); - return Ok(None); - }; - - // Create a temporary file to store the flamegraph data. - let temp_file = tempfile::tempfile()?; - let writer = BufWriter::new(temp_file.try_clone()?); - - // Build the flamegraph layer. - let flame_layer = FlameLayer::new(writer) - .with_empty_samples(false) - .with_threads_collapsed(true) - .with_module_path(false) - .with_file_and_line(true); - let flush_guard = flame_layer.flush_on_drop(); - - // Build the subscriber. - registry.with(flame_layer).init(); - - tracing::warn!( - "Flamegraph is enabled, this can create a large temporary \ - file and slow down the compilation process." - ); - - Ok(Some(TracingGuard { - flush_guard: Some(flush_guard), - temp_file, - output_svg: path.clone().unwrap_or_else(|| "flamegraph.svg".into()), - })) -} - -/// Returns the log level filter for the given verbosity level. -fn level_filter(args: &CliArguments) -> LevelFilter { - match args.verbosity { - 0 => LevelFilter::OFF, - 1 => LevelFilter::WARN, - 2 => LevelFilter::INFO, - 3 => LevelFilter::DEBUG, - _ => LevelFilter::TRACE, - } -} - -/// Will flush the flamegraph to disk when dropped. -struct TracingGuard { - flush_guard: Option<FlushGuard<BufWriter<File>>>, - temp_file: File, - output_svg: PathBuf, -} - -impl TracingGuard { - fn finish(&mut self) -> io::Result<()> { - if self.flush_guard.is_none() { - return Ok(()); - } - - tracing::info!("Flushing tracing flamegraph..."); - - // At this point, we're done tracing, so we can drop the guard. - // This will flush the tracing output to disk. - // We can then read the file and generate the flamegraph. - drop(self.flush_guard.take()); - - // Reset the file pointer to the beginning. - self.temp_file.seek(SeekFrom::Start(0))?; - - // Create the readers and writers. - let reader = BufReader::new(&mut self.temp_file); - let output = BufWriter::new(File::create(&self.output_svg)?); - - // Create the options: default in flame chart mode - let mut options = Options::default(); - options.flame_chart = true; - - inferno::flamegraph::from_reader(&mut options, reader, output) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - Ok(()) - } -} - -impl Drop for TracingGuard { - fn drop(&mut self) { - if !std::thread::panicking() { - if let Err(e) = self.finish() { - // Since we are finished, we cannot rely on tracing to log the - // error. - eprintln!("failed to flush tracing flamegraph: {e}"); - } - } - } -} diff --git a/cli/src/watch.rs b/cli/src/watch.rs deleted file mode 100644 index 2ad73f85..00000000 --- a/cli/src/watch.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::collections::HashSet; -use std::io::{self, IsTerminal, Write}; -use std::path::{Path, PathBuf}; - -use codespan_reporting::term::{self, termcolor}; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use same_file::is_same_file; -use termcolor::WriteColor; -use typst::diag::StrResult; -use typst::eval::eco_format; - -use crate::args::CompileCommand; -use crate::color_stream; -use crate::compile::compile_once; -use crate::world::SystemWorld; - -/// Execute a watching compilation command. -pub fn watch(mut command: CompileCommand) -> StrResult<()> { - // Create the world that serves sources, files, and fonts. - let mut world = SystemWorld::new(&command)?; - - // Perform initial compilation. - compile_once(&mut world, &mut command, true)?; - - // Setup file watching. - let (tx, rx) = std::sync::mpsc::channel(); - let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()) - .map_err(|_| "failed to setup file watching")?; - - // Watch all the files that are used by the input file and its dependencies. - watch_dependencies(&mut world, &mut watcher, HashSet::new())?; - - // Handle events. - let timeout = std::time::Duration::from_millis(100); - let output = command.output(); - loop { - let mut recompile = false; - for event in rx - .recv() - .into_iter() - .chain(std::iter::from_fn(|| rx.recv_timeout(timeout).ok())) - { - let event = event.map_err(|_| "failed to watch directory")?; - recompile |= is_event_relevant(&event, &output); - } - - if recompile { - // Retrieve the dependencies of the last compilation. - let previous: HashSet<PathBuf> = - world.dependencies().map(ToOwned::to_owned).collect(); - - // Recompile. - compile_once(&mut world, &mut command, true)?; - comemo::evict(10); - - // Adjust the watching. - watch_dependencies(&mut world, &mut watcher, previous)?; - } - } -} - -/// Adjust the file watching. Watches all new dependencies and unwatches -/// all `previous` dependencies that are not relevant anymore. -#[tracing::instrument(skip_all)] -fn watch_dependencies( - world: &mut SystemWorld, - watcher: &mut dyn Watcher, - mut previous: HashSet<PathBuf>, -) -> StrResult<()> { - // Watch new paths that weren't watched yet. - for path in world.dependencies() { - let watched = previous.remove(path); - if path.exists() && !watched { - tracing::info!("Watching {}", path.display()); - watcher - .watch(path, RecursiveMode::NonRecursive) - .map_err(|_| eco_format!("failed to watch {path:?}"))?; - } - } - - // Unwatch old paths that don't need to be watched anymore. - for path in previous { - tracing::info!("Unwatching {}", path.display()); - watcher.unwatch(&path).ok(); - } - - Ok(()) -} - -/// Whether a watch event is relevant for compilation. -fn is_event_relevant(event: ¬ify::Event, output: &Path) -> bool { - // Never recompile because the output file changed. - if event - .paths - .iter() - .all(|path| is_same_file(path, output).unwrap_or(false)) - { - return false; - } - - match &event.kind { - notify::EventKind::Any => true, - notify::EventKind::Access(_) => false, - notify::EventKind::Create(_) => true, - notify::EventKind::Modify(kind) => match kind { - notify::event::ModifyKind::Any => true, - notify::event::ModifyKind::Data(_) => true, - notify::event::ModifyKind::Metadata(_) => false, - notify::event::ModifyKind::Name(_) => true, - notify::event::ModifyKind::Other => false, - }, - notify::EventKind::Remove(_) => true, - notify::EventKind::Other => false, - } -} - -/// The status in which the watcher can be. -pub enum Status { - Compiling, - Success(std::time::Duration), - Error, -} - -impl Status { - /// Clear the terminal and render the status message. - pub fn print(&self, command: &CompileCommand) -> io::Result<()> { - let output = command.output(); - let timestamp = chrono::offset::Local::now().format("%H:%M:%S"); - let color = self.color(); - - let mut w = color_stream(); - if std::io::stderr().is_terminal() { - // Clear the terminal. - let esc = 27 as char; - write!(w, "{esc}c{esc}[1;1H")?; - } - - w.set_color(&color)?; - write!(w, "watching")?; - w.reset()?; - writeln!(w, " {}", command.input.display())?; - - w.set_color(&color)?; - write!(w, "writing to")?; - w.reset()?; - writeln!(w, " {}", output.display())?; - - writeln!(w)?; - writeln!(w, "[{timestamp}] {}", self.message())?; - writeln!(w)?; - - w.flush() - } - - fn message(&self) -> String { - match self { - Self::Compiling => "compiling ...".into(), - Self::Success(duration) => format!("compiled successfully in {duration:.2?}"), - Self::Error => "compiled with errors".into(), - } - } - - fn color(&self) -> termcolor::ColorSpec { - let styles = term::Styles::default(); - match self { - Self::Error => styles.header_error, - _ => styles.header_note, - } - } -} diff --git a/cli/src/world.rs b/cli/src/world.rs deleted file mode 100644 index f09a3f6c..00000000 --- a/cli/src/world.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::cell::{OnceCell, RefCell, RefMut}; -use std::collections::HashMap; -use std::fs; -use std::hash::Hash; -use std::path::{Path, PathBuf}; - -use chrono::Datelike; -use comemo::Prehashed; -use same_file::Handle; -use siphasher::sip128::{Hasher128, SipHasher13}; -use typst::diag::{FileError, FileResult, StrResult}; -use typst::eval::{eco_format, Datetime, Library}; -use typst::file::FileId; -use typst::font::{Font, FontBook}; -use typst::syntax::Source; -use typst::util::{Bytes, PathExt}; -use typst::World; - -use crate::args::CompileCommand; -use crate::fonts::{FontSearcher, FontSlot}; -use crate::package::prepare_package; - -/// A world that provides access to the operating system. -pub struct SystemWorld { - /// The root relative to which absolute paths are resolved. - root: PathBuf, - /// The input path. - main: FileId, - /// Typst's standard library. - library: Prehashed<Library>, - /// Metadata about discovered fonts. - book: Prehashed<FontBook>, - /// Locations of and storage for lazily loaded fonts. - fonts: Vec<FontSlot>, - /// Maps package-path combinations to canonical hashes. All package-path - /// combinations that point to the same file are mapped to the same hash. To - /// be used in conjunction with `paths`. - hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>, - /// Maps canonical path hashes to source files and buffers. - paths: RefCell<HashMap<PathHash, PathSlot>>, - /// The current date if requested. This is stored here to ensure it is - /// always the same within one compilation. Reset between compilations. - today: OnceCell<Option<Datetime>>, -} - -impl SystemWorld { - /// Create a new system world. - pub fn new(command: &CompileCommand) -> StrResult<Self> { - let mut searcher = FontSearcher::new(); - searcher.search(&command.font_paths); - - // Resolve the system-global input path. - let system_input = command.input.canonicalize().map_err(|_| { - eco_format!("input file not found (searched at {})", command.input.display()) - })?; - - // Resolve the system-global root directory. - let root = { - let path = command - .root - .as_deref() - .or_else(|| system_input.parent()) - .unwrap_or(Path::new(".")); - path.canonicalize().map_err(|_| { - eco_format!("root directory not found (searched at {})", path.display()) - })? - }; - - // Resolve the input path within the project. - let project_input = system_input - .strip_prefix(&root) - .map(|path| Path::new("/").join(path)) - .map_err(|_| "input file must be contained in project root")?; - - Ok(Self { - root, - main: FileId::new(None, &project_input), - library: Prehashed::new(typst_library::build()), - book: Prehashed::new(searcher.book), - fonts: searcher.fonts, - hashes: RefCell::default(), - paths: RefCell::default(), - today: OnceCell::new(), - }) - } - - /// The id of the main source file. - pub fn main(&self) -> FileId { - self.main - } - - /// Return all paths the last compilation depended on. - pub fn dependencies(&mut self) -> impl Iterator<Item = &Path> { - self.paths.get_mut().values().map(|slot| slot.system_path.as_path()) - } - - /// Reset the compilation state in preparation of a new compilation. - pub fn reset(&mut self) { - self.hashes.borrow_mut().clear(); - self.paths.borrow_mut().clear(); - self.today.take(); - } - - /// Lookup a source file by id. - #[track_caller] - pub fn lookup(&self, id: FileId) -> Source { - self.source(id).expect("file id does not point to any source file") - } -} - -impl World for SystemWorld { - fn library(&self) -> &Prehashed<Library> { - &self.library - } - - fn book(&self) -> &Prehashed<FontBook> { - &self.book - } - - fn main(&self) -> Source { - self.source(self.main).unwrap() - } - - fn source(&self, id: FileId) -> FileResult<Source> { - self.slot(id)?.source() - } - - fn file(&self, id: FileId) -> FileResult<Bytes> { - self.slot(id)?.file() - } - - fn font(&self, index: usize) -> Option<Font> { - self.fonts[index].get() - } - - fn today(&self, offset: Option<i64>) -> Option<Datetime> { - *self.today.get_or_init(|| { - let naive = match offset { - None => chrono::Local::now().naive_local(), - Some(o) => (chrono::Utc::now() + chrono::Duration::hours(o)).naive_utc(), - }; - - Datetime::from_ymd( - naive.year(), - naive.month().try_into().ok()?, - naive.day().try_into().ok()?, - ) - }) - } -} - -impl SystemWorld { - /// Access the canonical slot for the given file id. - #[tracing::instrument(skip_all)] - fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> { - let mut system_path = PathBuf::new(); - let hash = self - .hashes - .borrow_mut() - .entry(id) - .or_insert_with(|| { - // Determine the root path relative to which the file path - // will be resolved. - let root = match id.package() { - Some(spec) => prepare_package(spec)?, - None => self.root.clone(), - }; - - // Join the path to the root. If it tries to escape, deny - // access. Note: It can still escape via symlinks. - system_path = - root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?; - - PathHash::new(&system_path) - }) - .clone()?; - - Ok(RefMut::map(self.paths.borrow_mut(), |paths| { - paths.entry(hash).or_insert_with(|| PathSlot { - id, - // This will only trigger if the `or_insert_with` above also - // triggered. - system_path, - source: OnceCell::new(), - buffer: OnceCell::new(), - }) - })) - } -} - -/// Holds canonical data for all paths pointing to the same entity. -/// -/// Both fields can be populated if the file is both imported and read(). -struct PathSlot { - /// The slot's canonical file id. - id: FileId, - /// The slot's path on the system. - system_path: PathBuf, - /// The lazily loaded source file for a path hash. - source: OnceCell<FileResult<Source>>, - /// The lazily loaded buffer for a path hash. - buffer: OnceCell<FileResult<Bytes>>, -} - -impl PathSlot { - fn source(&self) -> FileResult<Source> { - self.source - .get_or_init(|| { - let buf = read(&self.system_path)?; - let text = decode_utf8(buf)?; - Ok(Source::new(self.id, text)) - }) - .clone() - } - - fn file(&self) -> FileResult<Bytes> { - self.buffer - .get_or_init(|| read(&self.system_path).map(Bytes::from)) - .clone() - } -} - -/// A hash that is the same for all paths pointing to the same entity. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -struct PathHash(u128); - -impl PathHash { - fn new(path: &Path) -> FileResult<Self> { - let f = |e| FileError::from_io(e, path); - let handle = Handle::from_path(path).map_err(f)?; - let mut state = SipHasher13::new(); - handle.hash(&mut state); - Ok(Self(state.finish128().as_u128())) - } -} - -/// Read a file. -fn read(path: &Path) -> FileResult<Vec<u8>> { - let f = |e| FileError::from_io(e, path); - if fs::metadata(path).map_err(f)?.is_dir() { - Err(FileError::IsDirectory) - } else { - fs::read(path).map_err(f) - } -} - -/// Decode UTF-8 with an optional BOM. -fn decode_utf8(buf: Vec<u8>) -> FileResult<String> { - Ok(if buf.starts_with(b"\xef\xbb\xbf") { - // Remove UTF-8 BOM. - std::str::from_utf8(&buf[3..])?.into() - } else { - // Assume UTF-8. - String::from_utf8(buf)? - }) -} |
