From b541daf50e4eef2af80bd22a24d55e41035429e4 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:57:27 +0800 Subject: [PATCH] feat: CLI compile documents with lock updates (#1218) --- crates/tinymist-project/src/args.rs | 48 ++++++++- crates/tinymist-project/src/compiler.rs | 13 ++- crates/tinymist-project/src/lock.rs | 9 +- crates/tinymist-project/src/model.rs | 13 +-- crates/tinymist-project/src/world.rs | 77 +++++++++++++- crates/tinymist/src/args.rs | 15 +-- crates/tinymist/src/main.rs | 63 +++--------- crates/tinymist/src/state/input.rs | 4 +- crates/tinymist/src/tool/project.rs | 130 ++++++++++++------------ tinymist.lock | 10 +- 10 files changed, 238 insertions(+), 144 deletions(-) diff --git a/crates/tinymist-project/src/args.rs b/crates/tinymist-project/src/args.rs index 1f939523..b3256fdd 100644 --- a/crates/tinymist-project/src/args.rs +++ b/crates/tinymist-project/src/args.rs @@ -22,11 +22,7 @@ pub enum DocCommands { /// Project task commands. #[derive(Debug, Clone, clap::Subcommand)] #[clap(rename_all = "kebab-case")] -// clippy bug: TaskCompileArgs is evaluated as 0 bytes -#[allow(clippy::large_enum_variant)] pub enum TaskCommands { - /// Declare a compile task (output). - Compile(TaskCompileArgs), /// Declare a preview task. Preview(TaskPreviewArgs), } @@ -48,6 +44,50 @@ pub struct DocNewArgs { pub package: CompilePackageArgs, } +impl DocNewArgs { + /// Converts to project input. + pub fn to_input(&self) -> ProjectInput { + let id: Id = (&self.id).into(); + + let root = self + .root + .as_ref() + .map(|root| ResourcePath::from_user_sys(Path::new(root))); + let main = ResourcePath::from_user_sys(Path::new(&self.id.input)); + + let font_paths = self + .font + .font_paths + .iter() + .map(|p| ResourcePath::from_user_sys(p)) + .collect::>(); + + let package_path = self + .package + .package_path + .as_ref() + .map(|p| ResourcePath::from_user_sys(p)); + + let package_cache_path = self + .package + .package_cache_path + .as_ref() + .map(|p| ResourcePath::from_user_sys(p)); + + ProjectInput { + id: id.clone(), + root, + main, + // todo: inputs + inputs: vec![], + font_paths, + system_fonts: !self.font.ignore_system_fonts, + package_path, + package_cache_path, + } + } +} + /// The id of a document. /// /// If an identifier is not provided, the document's path is used as the id. diff --git a/crates/tinymist-project/src/compiler.rs b/crates/tinymist-project/src/compiler.rs index 8e5f9cba..5a842ac0 100644 --- a/crates/tinymist-project/src/compiler.rs +++ b/crates/tinymist-project/src/compiler.rs @@ -37,7 +37,7 @@ pub struct ProjectInsId(EcoString); /// /// Whether to export depends on the current state of the document and the user /// settings. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct ExportSignal { /// Whether the revision is annotated by memory events. pub by_mem_events: bool, @@ -62,6 +62,17 @@ pub struct CompileSnapshot { } impl CompileSnapshot { + /// Creates a snapshot from the world. + pub fn from_world(world: CompilerWorld) -> Self { + Self { + id: ProjectInsId("primary".into()), + signal: ExportSignal::default(), + env: CompileEnv::default(), + world, + success_doc: None, + } + } + /// Forks a new snapshot that compiles a different document. /// /// Note: the resulting document should not be shared in system, because we diff --git a/crates/tinymist-project/src/lock.rs b/crates/tinymist-project/src/lock.rs index 8699a921..4cdaa855 100644 --- a/crates/tinymist-project/src/lock.rs +++ b/crates/tinymist-project/src/lock.rs @@ -23,6 +23,10 @@ impl LockFile { self.document.iter().find(|i| &i.id == id) } + pub fn get_task(&self, id: &Id) -> Option<&ApplyProjectTask> { + self.task.iter().find(|i| &i.id == id) + } + pub fn replace_document(&mut self, input: ProjectInput) { let id = input.id.clone(); let index = self.document.iter().position(|i| i.id == id); @@ -263,10 +267,13 @@ impl LockFileUpdate { let _ = package_cache_path; let _ = package_path; + // todo: freeze the sys.inputs + let input = ProjectInput { id: id.clone(), root: Some(root), - main: Some(main), + main, + inputs: vec![], font_paths, system_fonts: true, // !args.font.ignore_system_fonts, package_path: None, diff --git a/crates/tinymist-project/src/model.rs b/crates/tinymist-project/src/model.rs index 1e984f0b..9d9be484 100644 --- a/crates/tinymist-project/src/model.rs +++ b/crates/tinymist-project/src/model.rs @@ -407,13 +407,13 @@ impl ResourcePath { } } /// Converts the resource path to an absolute file system path. - pub fn to_abs_path(&self, rel: &Path) -> Option { + pub fn to_abs_path(&self, base: &Path) -> Option { if self.0 == "file" { let path = Path::new(&self.1); if path.is_absolute() { Some(path.to_owned()) } else { - Some(rel.join(path)) + Some(base.join(path)) } } else { None @@ -481,12 +481,13 @@ pub struct LockFile { pub struct ProjectInput { /// The project's ID. pub id: Id, - /// The project's root directory. + /// The path to the root directory of the project. #[serde(skip_serializing_if = "Option::is_none")] pub root: Option, - /// The project's main file. - #[serde(skip_serializing_if = "Option::is_none")] - pub main: Option, + /// The path to the main file of the project. + pub main: ResourcePath, + /// The key-value pairs visible through `sys.inputs` + pub inputs: Vec<(String, String)>, /// The project's font paths. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub font_paths: Vec, diff --git a/crates/tinymist-project/src/world.rs b/crates/tinymist-project/src/world.rs index a1aec06a..e8913de6 100644 --- a/crates/tinymist-project/src/world.rs +++ b/crates/tinymist-project/src/world.rs @@ -13,7 +13,7 @@ use std::path::Path; use std::{borrow::Cow, sync::Arc}; use tinymist_std::error::prelude::*; -use tinymist_std::ImmutPath; +use tinymist_std::{bail, ImmutPath}; use tinymist_world::font::system::SystemFontSearcher; use tinymist_world::package::{http::HttpRegistry, RegistryPathMapper}; use tinymist_world::vfs::{system::SystemAccessModel, Vfs}; @@ -22,6 +22,7 @@ use typst::foundations::{Dict, Str, Value}; use typst::utils::LazyHash; use crate::font::TinymistFontResolver; +use crate::ProjectInput; /// Compiler feature for LSP universe and worlds without typst.ts to implement /// more for tinymist. type trait of [`CompilerUniverse`]. @@ -114,6 +115,80 @@ impl WorldProvider for CompileOnceArgs { } } +// todo: merge me with the above impl +impl WorldProvider for (ProjectInput, ImmutPath) { + fn resolve(&self) -> Result { + let (proj, lock_dir) = self; + let entry = self.entry()?.try_into()?; + let inputs = proj + .inputs + .iter() + .map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str())))) + .collect(); + let fonts = LspUniverseBuilder::resolve_fonts(CompileFontArgs { + font_paths: { + proj.font_paths + .iter() + .flat_map(|p| p.to_abs_path(lock_dir)) + .collect::>() + }, + ignore_system_fonts: !proj.system_fonts, + })?; + let package = LspUniverseBuilder::resolve_package( + // todo: recover certificate path + None, + Some(&CompilePackageArgs { + package_path: proj + .package_path + .as_ref() + .and_then(|p| p.to_abs_path(lock_dir)), + package_cache_path: proj + .package_cache_path + .as_ref() + .and_then(|p| p.to_abs_path(lock_dir)), + }), + ); + + LspUniverseBuilder::build( + entry, + Arc::new(LazyHash::new(inputs)), + Arc::new(fonts), + package, + ) + .context("failed to create universe") + } + + fn entry(&self) -> Result { + let (proj, lock_dir) = self; + + let entry = proj + .main + .to_abs_path(lock_dir) + .context("failed to resolve entry file")?; + + let root = if let Some(root) = &proj.root { + root.to_abs_path(lock_dir) + .context("failed to resolve root")? + } else { + lock_dir.as_ref().to_owned() + }; + + if !entry.starts_with(&root) { + bail!("entry file must be in the root directory, {entry:?}, {root:?}"); + } + + let relative_entry = match entry.strip_prefix(&root) { + Ok(relative_entry) => relative_entry, + Err(_) => bail!("entry path must be inside the root: {}", entry.display()), + }; + + Ok(EntryOpts::new_rooted( + root.clone(), + Some(relative_entry.to_owned()), + )) + } +} + /// Builder for LSP universe. pub struct LspUniverseBuilder; diff --git a/crates/tinymist/src/args.rs b/crates/tinymist/src/args.rs index ca6bf4bf..f0242915 100644 --- a/crates/tinymist/src/args.rs +++ b/crates/tinymist/src/args.rs @@ -4,6 +4,7 @@ use sync_lsp::transport::MirrorArgs; use tinymist::{ project::{DocCommands, TaskCommands}, + tool::project::CompileArgs, CompileFontArgs, CompileOnceArgs, }; use tinymist_core::LONG_VERSION; @@ -32,8 +33,7 @@ pub enum Commands { #[cfg(feature = "preview")] Preview(tinymist::tool::preview::PreviewCliArgs), - /// Runs compile commands - #[clap(hide(true))] // still in development + /// Runs compile command like `typst-cli compile` Compile(CompileArgs), /// Runs language query #[clap(hide(true))] // still in development @@ -146,17 +146,6 @@ pub struct TraceLspArgs { pub compile: CompileOnceArgs, } -/// Common arguments of compile, watch, and query. -#[derive(Debug, Clone, Default, clap::Parser)] -pub struct CompileArgs { - #[clap(flatten)] - pub compile: CompileOnceArgs, - - /// Path to output file - #[clap(value_name = "OUTPUT")] - pub output: Option, -} - #[derive(Debug, Clone, Default, clap::Parser)] pub struct LspArgs { #[clap(flatten)] diff --git a/crates/tinymist/src/main.rs b/crates/tinymist/src/main.rs index 52fc9f02..8ff19386 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist/src/main.rs @@ -16,18 +16,18 @@ use futures::future::MaybeDone; use lsp_server::RequestId; use once_cell::sync::Lazy; use reflexo::ImmutPath; -use reflexo_typst::{package::PackageSpec, Compiler, TypstDict}; +use reflexo_typst::{package::PackageSpec, TypstDict}; use serde_json::Value as JsonValue; use sync_lsp::{ internal_error, transport::{with_stdio_transport, MirrorArgs}, LspBuilder, LspClientRoot, LspResult, }; +use tinymist::world::TaskInputs; use tinymist::{ - tool::project::{project_main, task_main}, + tool::project::{compile_main, project_main, task_main}, CompileConfig, Config, RegularInit, ServerState, SuperInit, UserActionTask, }; -use tinymist::{world::TaskInputs, world::WorldProvider}; use tinymist_core::LONG_VERSION; use tinymist_project::EntryResolver; use tinymist_query::package::PackageInfo; @@ -65,27 +65,29 @@ fn main() -> Result<()> { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); + // Parse command line arguments + let args = CliArguments::parse(); + + let is_transient_cmd = matches!(args.command, Some(Commands::Compile(..))); + // Start logging let _ = { use log::LevelFilter::*; + let base_level = if is_transient_cmd { Warn } else { Info }; + env_logger::builder() - .filter_module("tinymist", Info) + .filter_module("tinymist", base_level) .filter_module("typst_preview", Debug) - .filter_module("typlite", Info) - .filter_module("reflexo", Info) - .filter_module("sync_lsp", Info) - .filter_module("reflexo_typst::service::compile", Info) - .filter_module("reflexo_typst::service::watch", Info) + .filter_module("typlite", base_level) + .filter_module("reflexo", base_level) + .filter_module("sync_lsp", base_level) .filter_module("reflexo_typst::diag::console", Info) .try_init() }; - // Parse command line arguments - let args = CliArguments::parse(); - match args.command.unwrap_or_default() { Commands::Completion(args) => completion(args), - Commands::Compile(args) => compile(args), + Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)), Commands::Query(query_cmds) => query_main(query_cmds), Commands::Lsp(args) => lsp_main(args), Commands::TraceLsp(args) => trace_lsp_main(args), @@ -114,41 +116,6 @@ pub fn completion(args: ShellCompletionArgs) -> Result<()> { Ok(()) } -/// Runs compilation -pub fn compile(args: CompileArgs) -> Result<()> { - use std::io::Write; - - let input = args - .compile - .input - .as_ref() - .context("Missing required argument: INPUT")?; - let output = match args.output { - Some(stdout_path) if stdout_path == "-" => None, - Some(output_path) => Some(PathBuf::from(output_path)), - None => Some(Path::new(input).with_extension("pdf")), - }; - - let universe = args.compile.resolve()?; - let world = universe.snapshot(); - - let converter = std::marker::PhantomData.compile(&world, &mut Default::default()); - let pdf = typst_pdf::pdf(&converter.unwrap().output, &Default::default()); - - match (pdf, output) { - (Ok(pdf), None) => { - std::io::stdout().write_all(&pdf).unwrap(); - } - (Ok(pdf), Some(output)) => std::fs::write(output, pdf).unwrap(), - (Err(err), ..) => { - eprintln!("{err:?}"); - std::process::exit(1); - } - } - - Ok(()) -} - /// The main entry point for the language server. pub fn lsp_main(args: LspArgs) -> Result<()> { let pairs = LONG_VERSION.trim().split('\n'); diff --git a/crates/tinymist/src/state/input.rs b/crates/tinymist/src/state/input.rs index bc3d216b..57410f0c 100644 --- a/crates/tinymist/src/state/input.rs +++ b/crates/tinymist/src/state/input.rs @@ -263,8 +263,8 @@ impl ServerState { .unwrap_or_else(|| lock_dir.clone()); let main = input .main - .as_ref() - .and_then(|main| Some(main.to_abs_path(lock_dir)?.as_path().into())) + .to_abs_path(lock_dir) + .map(|path| path.as_path().into()) .unwrap_or_else(|| path.clone()); let entry = self .entry_resolver() diff --git a/crates/tinymist/src/tool/project.rs b/crates/tinymist/src/tool/project.rs index 662ef121..13396f6c 100644 --- a/crates/tinymist/src/tool/project.rs +++ b/crates/tinymist/src/tool/project.rs @@ -1,76 +1,34 @@ //! Project management tools. -use std::path::Path; +use std::path::{Path, PathBuf}; +use reflexo::ImmutPath; use tinymist_std::error::prelude::*; -use crate::project::*; +use crate::{project::*, task::ExportTask}; + +/// Common arguments of compile, watch, and query. +#[derive(Debug, Clone, clap::Parser)] +pub struct CompileArgs { + /// Inherits the compile task arguments. + #[clap(flatten)] + pub compile: TaskCompileArgs, + + /// Saves the compilation arguments to the lock file. + #[clap(long)] + pub save_lock: bool, + + /// Specifies the path to the lock file. If the path is + /// set, the lock file will be saved. + #[clap(long)] + pub lockfile: Option, +} trait LockFileExt { - fn declare(&mut self, args: &DocNewArgs) -> Id; fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result; - fn compile(&mut self, args: TaskCompileArgs) -> Result; - fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> Result; } impl LockFileExt for LockFile { - fn declare(&mut self, args: &DocNewArgs) -> Id { - let id: Id = (&args.id).into(); - - let root = args - .root - .as_ref() - .map(|root| ResourcePath::from_user_sys(Path::new(root))); - let main = ResourcePath::from_user_sys(Path::new(&args.id.input)); - - let font_paths = args - .font - .font_paths - .iter() - .map(|p| ResourcePath::from_user_sys(p)) - .collect::>(); - - let package_path = args - .package - .package_path - .as_ref() - .map(|p| ResourcePath::from_user_sys(p)); - - let package_cache_path = args - .package - .package_cache_path - .as_ref() - .map(|p| ResourcePath::from_user_sys(p)); - - let input = ProjectInput { - id: id.clone(), - root, - main: Some(main), - font_paths, - system_fonts: !args.font.ignore_system_fonts, - package_path, - package_cache_path, - }; - - self.replace_document(input); - - id - } - - fn compile(&mut self, args: TaskCompileArgs) -> Result { - let id = self.declare(&args.declare); - self.export(id, args) - } - - fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> Result { - let task = args.to_task(doc_id)?; - let task_id = task.id().clone(); - - self.replace_task(task); - - Ok(task_id) - } - fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result { let task_id = args .name @@ -92,12 +50,51 @@ impl LockFileExt for LockFile { } } +/// Runs project compilation(s) +pub async fn compile_main(args: CompileArgs) -> Result<()> { + // Identifies the input and output + let input = args.compile.declare.to_input(); + let output = args.compile.to_task(input.id.clone())?; + + // Saves the lock file if the flags are set + let save_lock = args.save_lock || args.lockfile.is_some(); + // todo: respect the name of the lock file + let lock_dir: ImmutPath = if let Some(lockfile) = args.lockfile { + lockfile.parent().context("no parent")?.into() + } else { + std::env::current_dir().context("lock directory")?.into() + }; + + if save_lock { + LockFile::update(&lock_dir, |state| { + state.replace_document(input.clone()); + state.replace_task(output.clone()); + + Ok(()) + })?; + } + + // Prepares for the compilation + let universe = (input, lock_dir.clone()).resolve()?; + let world = universe.snapshot(); + let snap = CompileSnapshot::from_world(world); + + // Compiles the project + let compiled = snap.compile(); + + // Exports the compiled project + let lock_dir = save_lock.then_some(lock_dir); + ExportTask::do_export(output.task, compiled, lock_dir).await?; + + Ok(()) +} + /// Project document commands' main pub fn project_main(args: DocCommands) -> Result<()> { LockFile::update(Path::new("."), |state| { match args { DocCommands::New(args) => { - state.declare(&args); + state.replace_document(args.to_input()); } DocCommands::Configure(args) => { let id: Id = (&args.id).into(); @@ -117,11 +114,10 @@ pub fn project_main(args: DocCommands) -> Result<()> { pub fn task_main(args: TaskCommands) -> Result<()> { LockFile::update(Path::new("."), |state| { match args { - TaskCommands::Compile(args) => { - let _ = state.compile(args); - } TaskCommands::Preview(args) => { - let id = state.declare(&args.declare); + let input = args.declare.to_input(); + let id = input.id.clone(); + state.replace_document(input); let _ = state.preview(id, &args); } } diff --git a/tinymist.lock b/tinymist.lock index ec8c0d72..7626485b 100644 --- a/tinymist.lock +++ b/tinymist.lock @@ -4,10 +4,16 @@ version = "0.1.0-beta0" [[document]] id = "file:docs/tinymist/book.typ" +inputs = [] +main = "file:docs/tinymist/book.typ" +root = "file:." system-fonts = true [[document]] id = "file:docs/tinymist/ebook.typ" +inputs = [] +main = "file:docs/tinymist/ebook.typ" +root = "file:." system-fonts = true [[task]] @@ -23,7 +29,9 @@ type = "export-svg" when = "never" [[task.transform]] -pages = ["1-1"] + +[task.transform.pages] +ranges = ["1-1"] [[task]] document = "file:docs/tinymist/ebook.typ"