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
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
@ -187,14 +191,9 @@ pub struct SharedArgs {
)]
pub inputs: Vec<(String, String)>,
/// Adds additional directories to search for fonts
#[clap(
long = "font-path",
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
)]
pub font_paths: Vec<PathBuf>,
/// Common font arguments
#[clap(flatten)]
pub font_args: FontArgs,
/// The document's creation date formatted as a UNIX timestamp.
///
@ -214,6 +213,26 @@ pub struct SharedArgs {
value_parser = clap::value_parser!(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/>
@ -343,6 +362,18 @@ fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
/// Lists all discovered fonts in system and custom font paths
#[derive(Debug, Clone, Parser)]
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
#[clap(
long = "font-path",
@ -352,9 +383,10 @@ pub struct FontsCommand {
)]
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)]
pub variants: bool,
pub ignore_system_fonts: bool,
}
/// 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
/// `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,
/// 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.

View File

@ -12,7 +12,7 @@ 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);
searcher.search(&command.font_args.font_paths, command.font_args.ignore_system_fonts);
for (name, infos) in searcher.book.families() {
println!("{name}");
@ -66,7 +66,7 @@ impl FontSearcher {
}
/// 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();
// Font paths have highest priority.
@ -74,8 +74,10 @@ impl FontSearcher {
db.load_fonts_dir(path);
}
// System fonts have second priority.
db.load_system_fonts();
if !ignore_system_fonts {
// System fonts have second priority.
db.load_system_fonts();
}
for face in db.faces() {
let path = match &face.source {

View File

@ -10,9 +10,12 @@ use typst::syntax::package::{
};
use crate::args::InitCommand;
use crate::package::PackageStorage;
/// Execute an initialization command.
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,
// we try to figure it out automatically by downloading the package index
// 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
// normal package spec parsing if it fails.
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))
})?;
// Find or download the package.
let package_path = crate::package::prepare_package(&spec)?;
let package_path = package_storage.prepare_package(&spec)?;
// Parse the manifest.
let manifest = parse_manifest(&package_path)?;

View File

@ -2,6 +2,7 @@ use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::args::PackageStorageArgs;
use codespan_reporting::term::{self, termcolor};
use ecow::eco_format;
use termcolor::WriteColor;
@ -14,64 +15,83 @@ use crate::download::{download, download_with_progress};
use crate::terminal;
const HOST: &str = "https://packages.typst.org";
const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
/// 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);
/// Holds information about where packages should be stored.
pub struct PackageStorage {
pub package_cache_path: Option<PathBuf>,
pub package_path: Option<PathBuf>,
}
if let Some(data_dir) = dirs::data_dir() {
let dir = data_dir.join(&subdir);
if dir.exists() {
return Ok(dir);
}
impl PackageStorage {
pub fn from_args(args: &PackageStorageArgs) -> Self {
let package_cache_path = args.package_cache_path.clone().or_else(|| {
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() {
let dir = cache_dir.join(&subdir);
if dir.exists() {
return Ok(dir);
}
/// Make a package available in the on-disk cache.
pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
// Download from network if it doesn't exist yet.
if spec.namespace == "preview" {
download_package(spec, &dir)?;
if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir);
if dir.exists() {
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(
spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> {
if spec.namespace == "preview" {
// For `@preview`, download the package index and find the latest
// version.
download_index()?
.iter()
.filter(|package| package.name == spec.name)
.map(|package| package.version)
.max()
.ok_or_else(|| eco_format!("failed to find package {spec}"))
} else {
// For other namespaces, search locally. We only search in the data
// directory and not the cache directory, because the latter is not
// intended for storage of local packages.
let subdir = format!("typst/packages/{}/{}", spec.namespace, spec.name);
dirs::data_dir()
.into_iter()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.max()
.ok_or_else(|| eco_format!("please specify the desired version"))
/// Try to determine the latest version of a package.
pub fn determine_latest_version(
&self,
spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> {
if spec.namespace == "preview" {
// For `@preview`, download the package index and find the latest
// version.
download_index()?
.iter()
.filter(|package| package.name == spec.name)
.map(|package| package.version)
.max()
.ok_or_else(|| eco_format!("failed to find package {spec}"))
} else {
// For other namespaces, search locally. We only search in the data
// directory and not the cache directory, because the latter is not
// intended for storage of local packages.
let subdir = format!("{}/{}", spec.namespace, spec.name);
self.package_path
.iter()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.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 !backup_path.exists() {
bail!(
@ -213,14 +220,19 @@ fn update_needed(release: &Release) -> StrResult<bool> {
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 platform:
/// The backup will be placed as `typst_backup.part` in one of the following
/// directories, depending on the platform:
/// - `$XDG_STATE_HOME` or `~/.local/state` on Linux
/// - `$XDG_DATA_HOME` or `~/.local/share` if the above path isn't available
/// - `~/Library/Application Support` on macOS
/// - `%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> {
#[cfg(target_os = "linux")]
let root_backup_dir = dirs::state_dir()
@ -231,10 +243,5 @@ fn backup_path() -> StrResult<PathBuf> {
let root_backup_dir =
dirs::data_dir().ok_or("unable to locate local data directory")?;
let backup_dir = root_backup_dir.join("typst");
fs::create_dir_all(&backup_dir)
.map_err(|err| eco_format!("failed to create backup directory ({err})"))?;
Ok(backup_dir.join("typst_backup.part"))
Ok(root_backup_dir.join("typst").join("typst_backup.part"))
}

View File

@ -19,6 +19,7 @@ use typst_timing::{timed, TimingScope};
use crate::args::{Input, SharedArgs};
use crate::compile::ExportCache;
use crate::fonts::{FontSearcher, FontSlot};
use crate::package::PackageStorage;
/// Static `FileId` allocated for stdin.
/// This is to ensure that a file is read in the correct way.
@ -41,6 +42,8 @@ pub struct SystemWorld {
fonts: Vec<FontSlot>,
/// Maps file ids to source files and buffers.
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
/// always the same within one compilation.
/// Reset between compilations if not [`Now::Fixed`].
@ -103,13 +106,16 @@ impl SystemWorld {
};
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 {
Some(time) => Now::Fixed(time),
None => Now::System(OnceLock::new()),
};
let package_storage = PackageStorage::from_args(&command.package_storage_args);
Ok(Self {
workdir: std::env::current_dir().ok(),
root,
@ -118,6 +124,7 @@ impl SystemWorld {
book: LazyHash::new(searcher.book),
fonts: searcher.fonts,
slots: Mutex::new(HashMap::new()),
package_storage,
now,
export_cache: ExportCache::new(),
})
@ -144,7 +151,9 @@ impl SystemWorld {
.get_mut()
.values()
.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.
@ -183,11 +192,11 @@ impl World for SystemWorld {
}
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> {
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> {
@ -259,9 +268,13 @@ impl FileSlot {
}
/// 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(
|| read(self.id, project_root),
|| read(self.id, project_root, package_storage),
|data, prev| {
let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
let _scope = TimingScope::new(name, None);
@ -277,9 +290,15 @@ impl FileSlot {
}
/// Retrieve the file's bytes.
fn file(&mut self, project_root: &Path) -> FileResult<Bytes> {
self.file
.get_or_init(|| read(self.id, project_root), |data, _| Ok(data.into()))
fn file(
&mut self,
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
/// 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
// will be resolved.
let buf;
let mut root = project_root;
if let Some(spec) = id.package() {
buf = crate::package::prepare_package(spec)?;
buf = package_storage.prepare_package(spec)?;
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,
/// 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 {
read_from_stdin()
} 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
/// priority is: `--font-paths` > system fonts > embedded fonts. Run
/// `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
/// #set text(font: "PT Sans")