Add more environment control parameters to CLI (#4227)

Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
Co-authored-by: Tulio Martins <tulioml240@gmail.com>
Co-authored-by: PepinhoJp <pepinho.jp@gmail.com>
This commit is contained in:
LuizAugustoPapa 2024-06-06 14:22:54 -03:00 committed by GitHub
parent 753213c40a
commit cc3e9c8602
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 190 additions and 88 deletions

View File

@ -131,6 +131,10 @@ pub struct InitCommand {
/// The project directory, defaults to the template's name /// The project directory, defaults to the template's name
pub dir: Option<String>, pub dir: Option<String>,
/// Arguments related to storage of packages in the system
#[clap(flatten)]
pub package_storage_args: PackageStorageArgs,
} }
/// Processes an input file to extract provided metadata /// Processes an input file to extract provided metadata
@ -187,14 +191,9 @@ pub struct SharedArgs {
)] )]
pub inputs: Vec<(String, String)>, pub inputs: Vec<(String, String)>,
/// Adds additional directories to search for fonts /// Common font arguments
#[clap( #[clap(flatten)]
long = "font-path", pub font_args: FontArgs,
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
)]
pub font_paths: Vec<PathBuf>,
/// The document's creation date formatted as a UNIX timestamp. /// The document's creation date formatted as a UNIX timestamp.
/// ///
@ -214,6 +213,26 @@ pub struct SharedArgs {
value_parser = clap::value_parser!(DiagnosticFormat) value_parser = clap::value_parser!(DiagnosticFormat)
)] )]
pub diagnostic_format: DiagnosticFormat, pub diagnostic_format: DiagnosticFormat,
/// Arguments related to storage of packages in the system
#[clap(flatten)]
pub package_storage_args: PackageStorageArgs,
}
/// Arguments related to where packages are stored in the system.
#[derive(Debug, Clone, Args)]
pub struct PackageStorageArgs {
/// Custom path to local packages, defaults to system-dependent location
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>,
/// Custom path to package cache, defaults to system-dependent location
#[clap(
long = "package-cache-path",
env = "TYPST_PACKAGE_CACHE_PATH",
value_name = "DIR"
)]
pub package_cache_path: Option<PathBuf>,
} }
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/> /// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
@ -343,6 +362,18 @@ fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
/// Lists all discovered fonts in system and custom font paths /// Lists all discovered fonts in system and custom font paths
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
pub struct FontsCommand { pub struct FontsCommand {
/// Common font arguments
#[clap(flatten)]
pub font_args: FontArgs,
/// Also lists style variants of each font family
#[arg(long)]
pub variants: bool,
}
/// Common arguments to customize available fonts
#[derive(Debug, Clone, Parser)]
pub struct FontArgs {
/// Adds additional directories to search for fonts /// Adds additional directories to search for fonts
#[clap( #[clap(
long = "font-path", long = "font-path",
@ -352,9 +383,10 @@ pub struct FontsCommand {
)] )]
pub font_paths: Vec<PathBuf>, pub font_paths: Vec<PathBuf>,
/// Also lists style variants of each font family /// Ensures system fonts won't be searched, unless explicitly included via
/// `--font-path`
#[arg(long)] #[arg(long)]
pub variants: bool, pub ignore_system_fonts: bool,
} }
/// Which format to use for diagnostics. /// Which format to use for diagnostics.
@ -385,8 +417,18 @@ pub struct UpdateCommand {
/// Reverts to the version from before the last update (only possible if /// Reverts to the version from before the last update (only possible if
/// `typst update` has previously ran) /// `typst update` has previously ran)
#[clap(long, default_value_t = false, exclusive = true)] #[clap(
long,
default_value_t = false,
conflicts_with = "version",
conflicts_with = "force"
)]
pub revert: bool, pub revert: bool,
/// Custom path to the backup file created on update and used by `--revert`,
/// defaults to system-dependent location
#[clap(long = "backup-path", env = "TYPST_UPDATE_BACKUP_PATH", value_name = "FILE")]
pub backup_path: Option<PathBuf>,
} }
/// Which format to use for the generated output file. /// Which format to use for the generated output file.

View File

@ -12,7 +12,7 @@ use crate::args::FontsCommand;
/// Execute a font listing command. /// Execute a font listing command.
pub fn fonts(command: &FontsCommand) -> StrResult<()> { pub fn fonts(command: &FontsCommand) -> StrResult<()> {
let mut searcher = FontSearcher::new(); let mut searcher = FontSearcher::new();
searcher.search(&command.font_paths); searcher.search(&command.font_args.font_paths, command.font_args.ignore_system_fonts);
for (name, infos) in searcher.book.families() { for (name, infos) in searcher.book.families() {
println!("{name}"); println!("{name}");
@ -66,7 +66,7 @@ impl FontSearcher {
} }
/// Search everything that is available. /// Search everything that is available.
pub fn search(&mut self, font_paths: &[PathBuf]) { pub fn search(&mut self, font_paths: &[PathBuf], ignore_system_fonts: bool) {
let mut db = Database::new(); let mut db = Database::new();
// Font paths have highest priority. // Font paths have highest priority.
@ -74,8 +74,10 @@ impl FontSearcher {
db.load_fonts_dir(path); db.load_fonts_dir(path);
} }
// System fonts have second priority. if !ignore_system_fonts {
db.load_system_fonts(); // System fonts have second priority.
db.load_system_fonts();
}
for face in db.faces() { for face in db.faces() {
let path = match &face.source { let path = match &face.source {

View File

@ -10,9 +10,12 @@ use typst::syntax::package::{
}; };
use crate::args::InitCommand; use crate::args::InitCommand;
use crate::package::PackageStorage;
/// Execute an initialization command. /// Execute an initialization command.
pub fn init(command: &InitCommand) -> StrResult<()> { pub fn init(command: &InitCommand) -> StrResult<()> {
let package_storage = PackageStorage::from_args(&command.package_storage_args);
// Parse the package specification. If the user didn't specify the version, // Parse the package specification. If the user didn't specify the version,
// we try to figure it out automatically by downloading the package index // we try to figure it out automatically by downloading the package index
// or searching the disk. // or searching the disk.
@ -20,12 +23,12 @@ pub fn init(command: &InitCommand) -> StrResult<()> {
// Try to parse without version, but prefer the error message of the // Try to parse without version, but prefer the error message of the
// normal package spec parsing if it fails. // normal package spec parsing if it fails.
let spec: VersionlessPackageSpec = command.template.parse().map_err(|_| err)?; let spec: VersionlessPackageSpec = command.template.parse().map_err(|_| err)?;
let version = crate::package::determine_latest_version(&spec)?; let version = package_storage.determine_latest_version(&spec)?;
StrResult::Ok(spec.at(version)) StrResult::Ok(spec.at(version))
})?; })?;
// Find or download the package. // Find or download the package.
let package_path = crate::package::prepare_package(&spec)?; let package_path = package_storage.prepare_package(&spec)?;
// Parse the manifest. // Parse the manifest.
let manifest = parse_manifest(&package_path)?; let manifest = parse_manifest(&package_path)?;

View File

@ -2,6 +2,7 @@ use std::fs;
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::args::PackageStorageArgs;
use codespan_reporting::term::{self, termcolor}; use codespan_reporting::term::{self, termcolor};
use ecow::eco_format; use ecow::eco_format;
use termcolor::WriteColor; use termcolor::WriteColor;
@ -14,64 +15,83 @@ use crate::download::{download, download_with_progress};
use crate::terminal; use crate::terminal;
const HOST: &str = "https://packages.typst.org"; const HOST: &str = "https://packages.typst.org";
const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
/// Make a package available in the on-disk cache. /// Holds information about where packages should be stored.
pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> { pub struct PackageStorage {
let subdir = pub package_cache_path: Option<PathBuf>,
format!("typst/packages/{}/{}/{}", spec.namespace, spec.name, spec.version); pub package_path: Option<PathBuf>,
}
if let Some(data_dir) = dirs::data_dir() { impl PackageStorage {
let dir = data_dir.join(&subdir); pub fn from_args(args: &PackageStorageArgs) -> Self {
if dir.exists() { let package_cache_path = args.package_cache_path.clone().or_else(|| {
return Ok(dir); dirs::cache_dir().map(|cache_dir| cache_dir.join(DEFAULT_PACKAGES_SUBDIR))
} });
let package_path = args.package_path.clone().or_else(|| {
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
});
Self { package_cache_path, package_path }
} }
if let Some(cache_dir) = dirs::cache_dir() { /// Make a package available in the on-disk cache.
let dir = cache_dir.join(&subdir); pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
if dir.exists() { let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
return Ok(dir);
}
// Download from network if it doesn't exist yet. if let Some(packages_dir) = &self.package_path {
if spec.namespace == "preview" { let dir = packages_dir.join(&subdir);
download_package(spec, &dir)?;
if dir.exists() { if dir.exists() {
return Ok(dir); return Ok(dir);
} }
} }
if let Some(cache_dir) = &self.package_cache_path {
let dir = cache_dir.join(&subdir);
if dir.exists() {
return Ok(dir);
}
// Download from network if it doesn't exist yet.
if spec.namespace == "preview" {
download_package(spec, &dir)?;
if dir.exists() {
return Ok(dir);
}
}
}
Err(PackageError::NotFound(spec.clone()))
} }
Err(PackageError::NotFound(spec.clone())) /// Try to determine the latest version of a package.
} pub fn determine_latest_version(
&self,
/// Try to determine the latest version of a package. spec: &VersionlessPackageSpec,
pub fn determine_latest_version( ) -> StrResult<PackageVersion> {
spec: &VersionlessPackageSpec, if spec.namespace == "preview" {
) -> StrResult<PackageVersion> { // For `@preview`, download the package index and find the latest
if spec.namespace == "preview" { // version.
// For `@preview`, download the package index and find the latest download_index()?
// version. .iter()
download_index()? .filter(|package| package.name == spec.name)
.iter() .map(|package| package.version)
.filter(|package| package.name == spec.name) .max()
.map(|package| package.version) .ok_or_else(|| eco_format!("failed to find package {spec}"))
.max() } else {
.ok_or_else(|| eco_format!("failed to find package {spec}")) // For other namespaces, search locally. We only search in the data
} else { // directory and not the cache directory, because the latter is not
// For other namespaces, search locally. We only search in the data // intended for storage of local packages.
// directory and not the cache directory, because the latter is not let subdir = format!("{}/{}", spec.namespace, spec.name);
// intended for storage of local packages. self.package_path
let subdir = format!("typst/packages/{}/{}", spec.namespace, spec.name); .iter()
dirs::data_dir() .flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.into_iter() .flatten()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok()) .filter_map(|entry| entry.ok())
.flatten() .map(|entry| entry.path())
.filter_map(|entry| entry.ok()) .filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.map(|entry| entry.path()) .max()
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok()) .ok_or_else(|| eco_format!("please specify the desired version"))
.max() }
.ok_or_else(|| eco_format!("please specify the desired version"))
} }
} }

View File

@ -40,7 +40,14 @@ pub fn update(command: &UpdateCommand) -> StrResult<()> {
} }
} }
let backup_path = backup_path()?; // Full path to the backup file.
let backup_path = command.backup_path.clone().map(Ok).unwrap_or_else(backup_path)?;
if let Some(backup_dir) = backup_path.parent() {
fs::create_dir_all(backup_dir)
.map_err(|err| eco_format!("failed to create backup directory ({err})"))?;
}
if command.revert { if command.revert {
if !backup_path.exists() { if !backup_path.exists() {
bail!( bail!(
@ -213,14 +220,19 @@ fn update_needed(release: &Release) -> StrResult<bool> {
Ok(new_tag > current_tag) Ok(new_tag > current_tag)
} }
/// Path to a potential backup file. /// Path to a potential backup file in the system.
/// ///
/// The backup will be placed in one of the following directories, depending on /// The backup will be placed as `typst_backup.part` in one of the following
/// the platform: /// directories, depending on the platform:
/// - `$XDG_STATE_HOME` or `~/.local/state` on Linux /// - `$XDG_STATE_HOME` or `~/.local/state` on Linux
/// - `$XDG_DATA_HOME` or `~/.local/share` if the above path isn't available /// - `$XDG_DATA_HOME` or `~/.local/share` if the above path isn't available
/// - `~/Library/Application Support` on macOS /// - `~/Library/Application Support` on macOS
/// - `%APPDATA%` on Windows /// - `%APPDATA%` on Windows
///
/// If a custom backup path is provided via the environment variable
/// `TYPST_UPDATE_BACKUP_PATH`, it will be used instead of the default
/// directories determined by the platform. In that case, this function
/// shouldn't be called.
fn backup_path() -> StrResult<PathBuf> { fn backup_path() -> StrResult<PathBuf> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let root_backup_dir = dirs::state_dir() let root_backup_dir = dirs::state_dir()
@ -231,10 +243,5 @@ fn backup_path() -> StrResult<PathBuf> {
let root_backup_dir = let root_backup_dir =
dirs::data_dir().ok_or("unable to locate local data directory")?; dirs::data_dir().ok_or("unable to locate local data directory")?;
let backup_dir = root_backup_dir.join("typst"); Ok(root_backup_dir.join("typst").join("typst_backup.part"))
fs::create_dir_all(&backup_dir)
.map_err(|err| eco_format!("failed to create backup directory ({err})"))?;
Ok(backup_dir.join("typst_backup.part"))
} }

View File

@ -19,6 +19,7 @@ use typst_timing::{timed, TimingScope};
use crate::args::{Input, SharedArgs}; use crate::args::{Input, SharedArgs};
use crate::compile::ExportCache; use crate::compile::ExportCache;
use crate::fonts::{FontSearcher, FontSlot}; use crate::fonts::{FontSearcher, FontSlot};
use crate::package::PackageStorage;
/// Static `FileId` allocated for stdin. /// Static `FileId` allocated for stdin.
/// This is to ensure that a file is read in the correct way. /// This is to ensure that a file is read in the correct way.
@ -41,6 +42,8 @@ pub struct SystemWorld {
fonts: Vec<FontSlot>, fonts: Vec<FontSlot>,
/// Maps file ids to source files and buffers. /// Maps file ids to source files and buffers.
slots: Mutex<HashMap<FileId, FileSlot>>, slots: Mutex<HashMap<FileId, FileSlot>>,
/// Holds information about where packages are stored.
package_storage: PackageStorage,
/// The current datetime if requested. This is stored here to ensure it is /// The current datetime if requested. This is stored here to ensure it is
/// always the same within one compilation. /// always the same within one compilation.
/// Reset between compilations if not [`Now::Fixed`]. /// Reset between compilations if not [`Now::Fixed`].
@ -103,13 +106,16 @@ impl SystemWorld {
}; };
let mut searcher = FontSearcher::new(); let mut searcher = FontSearcher::new();
searcher.search(&command.font_paths); searcher
.search(&command.font_args.font_paths, command.font_args.ignore_system_fonts);
let now = match command.creation_timestamp { let now = match command.creation_timestamp {
Some(time) => Now::Fixed(time), Some(time) => Now::Fixed(time),
None => Now::System(OnceLock::new()), None => Now::System(OnceLock::new()),
}; };
let package_storage = PackageStorage::from_args(&command.package_storage_args);
Ok(Self { Ok(Self {
workdir: std::env::current_dir().ok(), workdir: std::env::current_dir().ok(),
root, root,
@ -118,6 +124,7 @@ impl SystemWorld {
book: LazyHash::new(searcher.book), book: LazyHash::new(searcher.book),
fonts: searcher.fonts, fonts: searcher.fonts,
slots: Mutex::new(HashMap::new()), slots: Mutex::new(HashMap::new()),
package_storage,
now, now,
export_cache: ExportCache::new(), export_cache: ExportCache::new(),
}) })
@ -144,7 +151,9 @@ impl SystemWorld {
.get_mut() .get_mut()
.values() .values()
.filter(|slot| slot.accessed()) .filter(|slot| slot.accessed())
.filter_map(|slot| system_path(&self.root, slot.id).ok()) .filter_map(|slot| {
system_path(&self.root, slot.id, &self.package_storage).ok()
})
} }
/// Reset the compilation state in preparation of a new compilation. /// Reset the compilation state in preparation of a new compilation.
@ -183,11 +192,11 @@ impl World for SystemWorld {
} }
fn source(&self, id: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {
self.slot(id, |slot| slot.source(&self.root)) self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
} }
fn file(&self, id: FileId) -> FileResult<Bytes> { fn file(&self, id: FileId) -> FileResult<Bytes> {
self.slot(id, |slot| slot.file(&self.root)) self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
} }
fn font(&self, index: usize) -> Option<Font> { fn font(&self, index: usize) -> Option<Font> {
@ -259,9 +268,13 @@ impl FileSlot {
} }
/// Retrieve the source for this file. /// Retrieve the source for this file.
fn source(&mut self, project_root: &Path) -> FileResult<Source> { fn source(
&mut self,
project_root: &Path,
package_storage: &PackageStorage,
) -> FileResult<Source> {
self.source.get_or_init( self.source.get_or_init(
|| read(self.id, project_root), || read(self.id, project_root, package_storage),
|data, prev| { |data, prev| {
let name = if prev.is_some() { "reparsing file" } else { "parsing file" }; let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
let _scope = TimingScope::new(name, None); let _scope = TimingScope::new(name, None);
@ -277,9 +290,15 @@ impl FileSlot {
} }
/// Retrieve the file's bytes. /// Retrieve the file's bytes.
fn file(&mut self, project_root: &Path) -> FileResult<Bytes> { fn file(
self.file &mut self,
.get_or_init(|| read(self.id, project_root), |data, _| Ok(data.into())) project_root: &Path,
package_storage: &PackageStorage,
) -> FileResult<Bytes> {
self.file.get_or_init(
|| read(self.id, project_root, package_storage),
|data, _| Ok(data.into()),
)
} }
} }
@ -344,13 +363,17 @@ impl<T: Clone> SlotCell<T> {
/// Resolves the path of a file id on the system, downloading a package if /// Resolves the path of a file id on the system, downloading a package if
/// necessary. /// necessary.
fn system_path(project_root: &Path, id: FileId) -> FileResult<PathBuf> { fn system_path(
project_root: &Path,
id: FileId,
package_storage: &PackageStorage,
) -> FileResult<PathBuf> {
// Determine the root path relative to which the file path // Determine the root path relative to which the file path
// will be resolved. // will be resolved.
let buf; let buf;
let mut root = project_root; let mut root = project_root;
if let Some(spec) = id.package() { if let Some(spec) = id.package() {
buf = crate::package::prepare_package(spec)?; buf = package_storage.prepare_package(spec)?;
root = &buf; root = &buf;
} }
@ -363,11 +386,15 @@ fn system_path(project_root: &Path, id: FileId) -> FileResult<PathBuf> {
/// ///
/// If the ID represents stdin it will read from standard input, /// If the ID represents stdin it will read from standard input,
/// otherwise it gets the file path of the ID and reads the file from disk. /// otherwise it gets the file path of the ID and reads the file from disk.
fn read(id: FileId, project_root: &Path) -> FileResult<Vec<u8>> { fn read(
id: FileId,
project_root: &Path,
package_storage: &PackageStorage,
) -> FileResult<Vec<u8>> {
if id == *STDIN_ID { if id == *STDIN_ID {
read_from_stdin() read_from_stdin()
} else { } else {
read_from_disk(&system_path(project_root, id)?) read_from_disk(&system_path(project_root, id, package_storage)?)
} }
} }

View File

@ -111,7 +111,8 @@ pub struct TextElem {
/// variable to add directories that should be scanned for fonts. The /// variable to add directories that should be scanned for fonts. The
/// priority is: `--font-paths` > system fonts > embedded fonts. Run /// priority is: `--font-paths` > system fonts > embedded fonts. Run
/// `typst fonts` to see the fonts that Typst has discovered on your /// `typst fonts` to see the fonts that Typst has discovered on your
/// system. /// system. Note that you can pass the `--ignore-system-fonts` parameter
/// to the CLI to ensure Typst won't search for system fonts.
/// ///
/// ```example /// ```example
/// #set text(font: "PT Sans") /// #set text(font: "PT Sans")