mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: add package related arguments to typstExtraArgs (#923)
* feat: use typst-kit's package functions * feat: add package related arguments to `typstExtraArgs` * feat: don't use typst-kit's downloader * dev: improve a bit * dev: less lazy * dev: less interfaces
This commit is contained in:
parent
531b8d6737
commit
032c81e93a
13 changed files with 665 additions and 785 deletions
|
@ -8,7 +8,7 @@ use parking_lot::Mutex;
|
|||
use reflexo_typst::typst::prelude::*;
|
||||
use reflexo_typst::{package::PackageSpec, TypstFileId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_world::https::HttpsRegistry;
|
||||
use tinymist_world::package::HttpsRegistry;
|
||||
use typst::diag::{EcoString, StrResult};
|
||||
use typst::syntax::package::PackageManifest;
|
||||
use typst::syntax::VirtualPath;
|
||||
|
|
|
@ -131,6 +131,7 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBu
|
|||
};
|
||||
let mut world = LspUniverseBuilder::build(
|
||||
EntryState::new_rooted(root.as_path().into(), None),
|
||||
Default::default(),
|
||||
Arc::new(
|
||||
LspUniverseBuilder::resolve_fonts(CompileFontArgs {
|
||||
ignore_system_fonts: true,
|
||||
|
@ -138,8 +139,7 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBu
|
|||
})
|
||||
.unwrap(),
|
||||
),
|
||||
Default::default(),
|
||||
None,
|
||||
LspUniverseBuilder::resolve_package(None, None),
|
||||
)
|
||||
.unwrap();
|
||||
let sources = source.split("-----");
|
||||
|
|
|
@ -23,6 +23,7 @@ log.workspace = true
|
|||
reflexo-typst.workspace = true
|
||||
reflexo-typst-shim = { workspace = true, features = ["nightly"] }
|
||||
typst.workspace = true
|
||||
typst-kit.workspace = true
|
||||
|
||||
tinymist-assets = { workspace = true }
|
||||
typst-assets = { workspace = true, features = ["fonts"] }
|
||||
|
@ -33,14 +34,14 @@ flate2 = "1"
|
|||
tar = "0.4"
|
||||
|
||||
[target.'cfg(not(any(target_arch = "riscv64", target_arch = "wasm32", all(target_os = "windows", target_arch = "aarch64"))))'.dependencies]
|
||||
reqwest = { version = "^0.11", default-features = false, features = [
|
||||
reqwest = { version = "^0.12", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"blocking",
|
||||
"multipart",
|
||||
] }
|
||||
|
||||
[target.'cfg(any(target_arch = "riscv64", all(target_os = "windows", target_arch = "aarch64")))'.dependencies]
|
||||
reqwest = { version = "^0.11", default-features = false, features = [
|
||||
reqwest = { version = "^0.12", default-features = false, features = [
|
||||
"native-tls",
|
||||
"blocking",
|
||||
"multipart",
|
||||
|
|
|
@ -1,257 +0,0 @@
|
|||
//! Https registry for tinymist.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use log::error;
|
||||
use parking_lot::Mutex;
|
||||
use reflexo_typst::package::{DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec};
|
||||
use reflexo_typst::typst::{
|
||||
diag::{eco_format, EcoString},
|
||||
syntax::package::PackageVersion,
|
||||
};
|
||||
use reqwest::{blocking::Response, Certificate};
|
||||
|
||||
/// The http registry without typst.ts to implement more for tinymist.
|
||||
pub struct HttpsRegistry {
|
||||
notifier: Arc<Mutex<dyn Notifier + Send>>,
|
||||
|
||||
packages: OnceLock<Vec<(PackageSpec, Option<EcoString>)>>,
|
||||
|
||||
cert_path: Option<PathBuf>,
|
||||
|
||||
data_dir_cache: OnceLock<Option<Arc<Path>>>,
|
||||
cache_dir_cache: OnceLock<Option<Arc<Path>>>,
|
||||
// package_dir_cache: RwLock<HashMap<PackageSpec, Result<Arc<Path>, PackageError>>>,
|
||||
}
|
||||
|
||||
impl Default for HttpsRegistry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
|
||||
|
||||
// todo: reset cache
|
||||
packages: OnceLock::new(),
|
||||
|
||||
// Default to None
|
||||
cert_path: None,
|
||||
|
||||
data_dir_cache: OnceLock::new(),
|
||||
cache_dir_cache: OnceLock::new(),
|
||||
// package_dir_cache: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpsRegistry {
|
||||
/// Create a new registry.
|
||||
pub fn new(cert_path: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
cert_path,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get local path option
|
||||
pub fn local_path(&self) -> Option<Box<Path>> {
|
||||
if let Some(data_dir) = self.data_dir() {
|
||||
if data_dir.exists() {
|
||||
return Some(data_dir.join("typst/packages").into());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> Option<&Arc<Path>> {
|
||||
self.data_dir_cache
|
||||
.get_or_init(|| dirs::data_dir().map(From::from))
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn cache_dir(&self) -> Option<&Arc<Path>> {
|
||||
self.cache_dir_cache
|
||||
.get_or_init(|| dirs::cache_dir().map(From::from))
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Get data & cache dir
|
||||
pub fn paths(&self) -> Vec<Box<Path>> {
|
||||
let mut res = vec![];
|
||||
if let Some(data_dir) = self.data_dir() {
|
||||
let dir: Box<Path> = data_dir.join("typst/packages").into();
|
||||
if dir.exists() {
|
||||
res.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cache_dir) = self.cache_dir() {
|
||||
let dir: Box<Path> = cache_dir.join("typst/packages").into();
|
||||
if dir.exists() {
|
||||
res.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Make a package available in the on-disk cache.
|
||||
pub fn prepare_package(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError> {
|
||||
// let cache = self.package_dir_cache.read();
|
||||
// if let Some(dir) = cache.get(spec) {
|
||||
// return dir.clone();
|
||||
// }
|
||||
|
||||
// drop(cache);
|
||||
// let mut cache = self.package_dir_cache.write();
|
||||
// if let Some(dir) = cache.get(spec) {
|
||||
// return dir.clone();
|
||||
// }
|
||||
|
||||
// let dir = self.prepare_package_(spec);
|
||||
// cache.insert(spec.clone(), dir.clone());
|
||||
// dir
|
||||
self.prepare_package_(spec)
|
||||
}
|
||||
|
||||
/// Make a package available in the on-disk cache.
|
||||
pub fn prepare_package_(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError> {
|
||||
let subdir = format!(
|
||||
"typst/packages/{}/{}/{}",
|
||||
spec.namespace, spec.name, spec.version
|
||||
);
|
||||
|
||||
if let Some(data_dir) = self.data_dir() {
|
||||
let dir = data_dir.join(&subdir);
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cache_dir) = self.cache_dir() {
|
||||
let dir = cache_dir.join(&subdir);
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" && !dir.exists() {
|
||||
self.download_package(spec, &dir)?;
|
||||
}
|
||||
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
}
|
||||
}
|
||||
|
||||
Err(PackageError::NotFound(spec.clone()))
|
||||
}
|
||||
|
||||
/// Download a package over the network.
|
||||
fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> Result<(), PackageError> {
|
||||
let url = format!(
|
||||
"https://packages.typst.org/preview/{}-{}.tar.gz",
|
||||
spec.name, spec.version
|
||||
);
|
||||
|
||||
self.notifier.lock().downloading(spec);
|
||||
threaded_http(&url, self.cert_path.as_deref(), |resp| {
|
||||
let reader = match resp.and_then(|r| r.error_for_status()) {
|
||||
Ok(response) => response,
|
||||
Err(err) if matches!(err.status().map(|s| s.as_u16()), Some(404)) => {
|
||||
return Err(PackageError::NotFound(spec.clone()))
|
||||
}
|
||||
Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
|
||||
};
|
||||
|
||||
let decompressed = flate2::read::GzDecoder::new(reader);
|
||||
tar::Archive::new(decompressed)
|
||||
.unpack(package_dir)
|
||||
.map_err(|err| {
|
||||
std::fs::remove_dir_all(package_dir).ok();
|
||||
PackageError::MalformedArchive(Some(eco_format!("{err}")))
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| PackageError::Other(Some(eco_format!("cannot spawn http thread"))))?
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageRegistry for HttpsRegistry {
|
||||
fn resolve(&self, spec: &PackageSpec) -> Result<std::sync::Arc<Path>, PackageError> {
|
||||
self.prepare_package(spec)
|
||||
}
|
||||
|
||||
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||
self.packages.get_or_init(|| {
|
||||
let url = "https://packages.typst.org/preview/index.json";
|
||||
|
||||
threaded_http(url, self.cert_path.as_deref(), |resp| {
|
||||
let reader = match resp.and_then(|r| r.error_for_status()) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
// todo: silent error
|
||||
error!("Failed to fetch package index: {err} from {url}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RemotePackageIndex {
|
||||
name: EcoString,
|
||||
version: PackageVersion,
|
||||
description: Option<EcoString>,
|
||||
}
|
||||
|
||||
let index: Vec<RemotePackageIndex> = match serde_json::from_reader(reader) {
|
||||
Ok(index) => index,
|
||||
Err(err) => {
|
||||
error!("Failed to parse package index: {err} from {url}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
index
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
(
|
||||
PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
name: e.name,
|
||||
version: e.version,
|
||||
},
|
||||
e.description,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn threaded_http<T: Send + Sync>(
|
||||
url: &str,
|
||||
cert_path: Option<&Path>,
|
||||
f: impl FnOnce(Result<Response, reqwest::Error>) -> T + Send + Sync,
|
||||
) -> Option<T> {
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(move || {
|
||||
let client_builder = reqwest::blocking::Client::builder();
|
||||
|
||||
let client = if let Some(cert_path) = cert_path {
|
||||
let cert = std::fs::read(cert_path)
|
||||
.ok()
|
||||
.and_then(|buf| Certificate::from_pem(&buf).ok());
|
||||
if let Some(cert) = cert {
|
||||
client_builder.add_root_certificate(cert).build().unwrap()
|
||||
} else {
|
||||
client_builder.build().unwrap()
|
||||
}
|
||||
} else {
|
||||
client_builder.build().unwrap()
|
||||
};
|
||||
|
||||
f(client.get(url).send())
|
||||
})
|
||||
.join()
|
||||
.ok()
|
||||
})
|
||||
}
|
|
@ -18,11 +18,11 @@ use reflexo_typst::error::prelude::*;
|
|||
use reflexo_typst::font::system::SystemFontSearcher;
|
||||
use reflexo_typst::foundations::{Str, Value};
|
||||
use reflexo_typst::vfs::{system::SystemAccessModel, Vfs};
|
||||
use reflexo_typst::{CompilerFeat, CompilerUniverse, CompilerWorld, TypstDict};
|
||||
use reflexo_typst::{CompilerFeat, CompilerUniverse, CompilerWorld, ImmutPath, TypstDict};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod https;
|
||||
use https::HttpsRegistry;
|
||||
pub mod package;
|
||||
use package::HttpsRegistry;
|
||||
|
||||
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
|
||||
|
||||
|
@ -65,6 +65,22 @@ pub struct CompileFontArgs {
|
|||
pub ignore_system_fonts: bool,
|
||||
}
|
||||
|
||||
/// Arguments related to where packages are stored in the system.
|
||||
#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
|
||||
pub struct CompilePackageArgs {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Common arguments of compile, watch, and query.
|
||||
#[derive(Debug, Clone, Parser, Default)]
|
||||
pub struct CompileOnceArgs {
|
||||
|
@ -89,6 +105,10 @@ pub struct CompileOnceArgs {
|
|||
#[clap(flatten)]
|
||||
pub font: CompileFontArgs,
|
||||
|
||||
/// Package related arguments.
|
||||
#[clap(flatten)]
|
||||
pub package: CompilePackageArgs,
|
||||
|
||||
/// The document's creation date formatted as a UNIX timestamp.
|
||||
///
|
||||
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
|
||||
|
@ -104,26 +124,29 @@ pub struct CompileOnceArgs {
|
|||
/// Path to CA certificate file for network access, especially for
|
||||
/// downloading typst packages.
|
||||
#[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
|
||||
pub certification: Option<PathBuf>,
|
||||
pub cert: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl CompileOnceArgs {
|
||||
/// Get a universe instance from the given arguments.
|
||||
pub fn resolve(&self) -> anyhow::Result<LspUniverse> {
|
||||
let entry = self.entry()?.try_into()?;
|
||||
let fonts = LspUniverseBuilder::resolve_fonts(self.font.clone())?;
|
||||
let inputs = self
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str()))))
|
||||
.collect();
|
||||
let cert_path = self.certification.clone();
|
||||
let fonts = LspUniverseBuilder::resolve_fonts(self.font.clone())?;
|
||||
let package = LspUniverseBuilder::resolve_package(
|
||||
self.cert.as_deref().map(From::from),
|
||||
Some(&self.package),
|
||||
);
|
||||
|
||||
LspUniverseBuilder::build(
|
||||
entry,
|
||||
Arc::new(fonts),
|
||||
Arc::new(LazyHash::new(inputs)),
|
||||
cert_path,
|
||||
Arc::new(fonts),
|
||||
package,
|
||||
)
|
||||
.context("failed to create universe")
|
||||
}
|
||||
|
@ -185,15 +208,15 @@ impl LspUniverseBuilder {
|
|||
/// See [`LspCompilerFeat`] for instantiation details.
|
||||
pub fn build(
|
||||
entry: EntryState,
|
||||
font_resolver: Arc<FontResolverImpl>,
|
||||
inputs: ImmutDict,
|
||||
cert_path: Option<PathBuf>,
|
||||
font_resolver: Arc<FontResolverImpl>,
|
||||
package_registry: HttpsRegistry,
|
||||
) -> ZResult<LspUniverse> {
|
||||
Ok(LspUniverse::new_raw(
|
||||
entry,
|
||||
Some(inputs),
|
||||
Vfs::new(SystemAccessModel {}),
|
||||
HttpsRegistry::new(cert_path),
|
||||
package_registry,
|
||||
font_resolver,
|
||||
))
|
||||
}
|
||||
|
@ -209,6 +232,14 @@ impl LspUniverseBuilder {
|
|||
})?;
|
||||
Ok(searcher.into())
|
||||
}
|
||||
|
||||
/// Resolve package registry from given options.
|
||||
pub fn resolve_package(
|
||||
cert_path: Option<ImmutPath>,
|
||||
args: Option<&CompilePackageArgs>,
|
||||
) -> HttpsRegistry {
|
||||
HttpsRegistry::new(cert_path, args)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses key/value pairs split by the first equal sign.
|
||||
|
|
325
crates/tinymist-world/src/package.rs
Normal file
325
crates/tinymist-world/src/package.rs
Normal file
|
@ -0,0 +1,325 @@
|
|||
//! Https registry for tinymist.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use reflexo_typst::package::{DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec};
|
||||
use reflexo_typst::typst::diag::EcoString;
|
||||
use reflexo_typst::ImmutPath;
|
||||
use reqwest::blocking::Response;
|
||||
use reqwest::Certificate;
|
||||
use typst::diag::{eco_format, PackageResult, StrResult};
|
||||
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
|
||||
|
||||
use crate::CompilePackageArgs;
|
||||
|
||||
/// The https package registry for tinymist.
|
||||
pub struct HttpsRegistry {
|
||||
/// The path at which local packages (`@local` packages) are stored.
|
||||
local_dir: Option<ImmutPath>,
|
||||
/// The path at which non-local packages (`@preview` packages) should be
|
||||
/// stored when downloaded.
|
||||
cache_dir: Option<ImmutPath>,
|
||||
/// lazily initialized package storage.
|
||||
storage: OnceLock<PackageStorage>,
|
||||
/// The path to the certificate file to use for HTTPS requests.
|
||||
cert_path: Option<ImmutPath>,
|
||||
/// The notifier to use for progress updates.
|
||||
notifier: Arc<Mutex<dyn Notifier + Send>>,
|
||||
// package_dir_cache: RwLock<HashMap<PackageSpec, Result<ImmutPath, PackageError>>>,
|
||||
}
|
||||
|
||||
impl Default for HttpsRegistry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
|
||||
cert_path: None,
|
||||
local_dir: None,
|
||||
cache_dir: None,
|
||||
|
||||
storage: OnceLock::new(),
|
||||
// package_dir_cache: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HttpsRegistry {
|
||||
type Target = PackageStorage;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.storage()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpsRegistry {
|
||||
/// Create a new registry.
|
||||
pub fn new(cert_path: Option<ImmutPath>, args: Option<&CompilePackageArgs>) -> Self {
|
||||
Self {
|
||||
cert_path,
|
||||
local_dir: args.and_then(|args| Some(args.package_path.as_deref()?.into())),
|
||||
cache_dir: args.and_then(|args| Some(args.package_cache_path.as_deref()?.into())),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `typst-kit` implementing package storage
|
||||
pub fn storage(&self) -> &PackageStorage {
|
||||
self.storage.get_or_init(|| {
|
||||
PackageStorage::new(
|
||||
self.cache_dir
|
||||
.clone()
|
||||
.or_else(|| Some(dirs::cache_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
|
||||
self.local_dir
|
||||
.clone()
|
||||
.or_else(|| Some(dirs::data_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
|
||||
self.cert_path.clone(),
|
||||
self.notifier.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get local path option
|
||||
pub fn local_path(&self) -> Option<ImmutPath> {
|
||||
self.storage().package_path().cloned()
|
||||
}
|
||||
|
||||
/// Get data & cache dir
|
||||
pub fn paths(&self) -> Vec<ImmutPath> {
|
||||
let data_dir = self.storage().package_path().cloned();
|
||||
let cache_dir = self.storage().package_cache_path().cloned();
|
||||
data_dir.into_iter().chain(cache_dir).collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageRegistry for HttpsRegistry {
|
||||
fn resolve(&self, spec: &PackageSpec) -> Result<ImmutPath, PackageError> {
|
||||
self.storage().prepare_package(spec)
|
||||
}
|
||||
|
||||
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||
self.storage().download_index()
|
||||
}
|
||||
}
|
||||
|
||||
/// The default Typst registry.
|
||||
pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org";
|
||||
|
||||
/// The default packages sub directory within the package and package cache
|
||||
/// paths.
|
||||
pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
|
||||
|
||||
/// Holds information about where packages should be stored and downloads them
|
||||
/// on demand, if possible.
|
||||
pub struct PackageStorage {
|
||||
/// The path at which non-local packages should be stored when downloaded.
|
||||
package_cache_path: Option<ImmutPath>,
|
||||
/// The path at which local packages are stored.
|
||||
package_path: Option<ImmutPath>,
|
||||
/// The downloader used for fetching the index and packages.
|
||||
cert_path: Option<ImmutPath>,
|
||||
/// The cached index of the preview namespace.
|
||||
index: OnceLock<Vec<(PackageSpec, Option<EcoString>)>>,
|
||||
notifier: Arc<Mutex<dyn Notifier + Send>>,
|
||||
}
|
||||
|
||||
impl PackageStorage {
|
||||
/// Creates a new package storage for the given package paths.
|
||||
/// It doesn't fallback directories, thus you can disable the related
|
||||
/// storage by passing `None`.
|
||||
pub fn new(
|
||||
package_cache_path: Option<ImmutPath>,
|
||||
package_path: Option<ImmutPath>,
|
||||
cert_path: Option<ImmutPath>,
|
||||
notifier: Arc<Mutex<dyn Notifier + Send>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
package_cache_path,
|
||||
package_path,
|
||||
cert_path,
|
||||
notifier,
|
||||
index: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path at which non-local packages should be stored when
|
||||
/// downloaded.
|
||||
pub fn package_cache_path(&self) -> Option<&ImmutPath> {
|
||||
self.package_cache_path.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the path at which local packages are stored.
|
||||
pub fn package_path(&self) -> Option<&ImmutPath> {
|
||||
self.package_path.as_ref()
|
||||
}
|
||||
|
||||
/// Make a package available in the on-disk cache.
|
||||
pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<ImmutPath> {
|
||||
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
|
||||
|
||||
if let Some(packages_dir) = &self.package_path {
|
||||
let dir = packages_dir.join(&subdir);
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cache_dir) = &self.package_cache_path {
|
||||
let dir = cache_dir.join(&subdir);
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
}
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" {
|
||||
self.download_package(spec, &dir)?;
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(PackageError::NotFound(spec.clone()))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
self.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"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Download the package index. The result of this is cached for efficiency.
|
||||
pub fn download_index(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||
self.index.get_or_init(|| {
|
||||
let url = format!("{DEFAULT_REGISTRY}/preview/index.json");
|
||||
|
||||
threaded_http(&url, self.cert_path.as_deref(), |resp| {
|
||||
let reader = match resp.and_then(|r| r.error_for_status()) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
// todo: silent error
|
||||
log::error!("Failed to fetch package index: {err} from {url}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RemotePackageIndex {
|
||||
name: EcoString,
|
||||
version: PackageVersion,
|
||||
description: Option<EcoString>,
|
||||
}
|
||||
|
||||
let index: Vec<RemotePackageIndex> = match serde_json::from_reader(reader) {
|
||||
Ok(index) => index,
|
||||
Err(err) => {
|
||||
log::error!("Failed to parse package index: {err} from {url}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
index
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
(
|
||||
PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
name: e.name,
|
||||
version: e.version,
|
||||
},
|
||||
e.description,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Download a package over the network.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the package spec namespace isn't `preview`.
|
||||
pub fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
|
||||
assert_eq!(spec.namespace, "preview");
|
||||
|
||||
let url = format!(
|
||||
"{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz",
|
||||
spec.name, spec.version
|
||||
);
|
||||
|
||||
self.notifier.lock().downloading(spec);
|
||||
threaded_http(&url, self.cert_path.as_deref(), |resp| {
|
||||
let reader = match resp.and_then(|r| r.error_for_status()) {
|
||||
Ok(response) => response,
|
||||
Err(err) if matches!(err.status().map(|s| s.as_u16()), Some(404)) => {
|
||||
return Err(PackageError::NotFound(spec.clone()))
|
||||
}
|
||||
Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
|
||||
};
|
||||
|
||||
let decompressed = flate2::read::GzDecoder::new(reader);
|
||||
tar::Archive::new(decompressed)
|
||||
.unpack(package_dir)
|
||||
.map_err(|err| {
|
||||
std::fs::remove_dir_all(package_dir).ok();
|
||||
PackageError::MalformedArchive(Some(eco_format!("{err}")))
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| PackageError::Other(Some(eco_format!("cannot spawn http thread"))))?
|
||||
}
|
||||
}
|
||||
|
||||
fn threaded_http<T: Send + Sync>(
|
||||
url: &str,
|
||||
cert_path: Option<&Path>,
|
||||
f: impl FnOnce(Result<Response, reqwest::Error>) -> T + Send + Sync,
|
||||
) -> Option<T> {
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(move || {
|
||||
let client_builder = reqwest::blocking::Client::builder();
|
||||
|
||||
let client = if let Some(cert_path) = cert_path {
|
||||
let cert = std::fs::read(cert_path)
|
||||
.ok()
|
||||
.and_then(|buf| Certificate::from_pem(&buf).ok());
|
||||
if let Some(cert) = cert {
|
||||
client_builder.add_root_certificate(cert).build().unwrap()
|
||||
} else {
|
||||
client_builder.build().unwrap()
|
||||
}
|
||||
} else {
|
||||
client_builder.build().unwrap()
|
||||
};
|
||||
|
||||
f(client.get(url).send())
|
||||
})
|
||||
.join()
|
||||
.ok()
|
||||
})
|
||||
}
|
|
@ -134,12 +134,16 @@ impl LanguageState {
|
|||
let compile_handle = handle.clone();
|
||||
let cache = self.cache.clone();
|
||||
let cert_path = self.compile_config().determine_certification_path();
|
||||
let package = self.compile_config().determine_package_opts();
|
||||
|
||||
self.client.handle.spawn_blocking(move || {
|
||||
// Create the world
|
||||
let font_resolver = font_resolver.wait().clone();
|
||||
let verse = LspUniverseBuilder::build(entry_.clone(), font_resolver, inputs, cert_path)
|
||||
.expect("incorrect options");
|
||||
let package_registry =
|
||||
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
|
||||
let verse =
|
||||
LspUniverseBuilder::build(entry_.clone(), inputs, font_resolver, package_registry)
|
||||
.expect("incorrect options");
|
||||
|
||||
// Create the actor
|
||||
let server = CompileServerActor::new_with(
|
||||
|
|
|
@ -334,7 +334,7 @@ impl LanguageState {
|
|||
|
||||
/// Initialize a new template.
|
||||
pub fn init_template(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
use crate::tool::package::{self, determine_latest_version, TemplateSource};
|
||||
use crate::tool::package::{self, TemplateSource};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -359,7 +359,7 @@ impl LanguageState {
|
|||
// Try to parse without version, but prefer the error message of the
|
||||
// normal package spec parsing if it fails.
|
||||
let spec: VersionlessPackageSpec = from_source.parse().map_err(|_| err)?;
|
||||
let version = determine_latest_version(&snap.world, &spec)?;
|
||||
let version = snap.world.registry.determine_latest_version(&spec)?;
|
||||
StrResult::Ok(spec.at(version))
|
||||
})
|
||||
.map_err(map_string_err("failed to parse package spec"))
|
||||
|
@ -386,7 +386,7 @@ impl LanguageState {
|
|||
|
||||
/// Get the entry of a template.
|
||||
pub fn get_template_entry(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
use crate::tool::package::{self, determine_latest_version, TemplateSource};
|
||||
use crate::tool::package::{self, TemplateSource};
|
||||
|
||||
let from_source = get_arg!(args[0] as String);
|
||||
|
||||
|
@ -404,7 +404,7 @@ impl LanguageState {
|
|||
// Try to parse without version, but prefer the error message of the
|
||||
// normal package spec parsing if it fails.
|
||||
let spec: VersionlessPackageSpec = from_source.parse().map_err(|_| err)?;
|
||||
let version = determine_latest_version(&snap.world, &spec)?;
|
||||
let version = snap.world.registry.determine_latest_version(&spec)?;
|
||||
StrResult::Ok(spec.at(version))
|
||||
})
|
||||
.map_err(map_string_err("failed to parse package spec"))
|
||||
|
@ -547,6 +547,7 @@ impl LanguageState {
|
|||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
let paths = snap.world.registry.paths();
|
||||
let paths = paths.iter().map(|p| p.as_ref()).collect::<Vec<_>>();
|
||||
serde_json::to_value(paths).map_err(|e| internal_error(e.to_string()))
|
||||
})
|
||||
}
|
||||
|
@ -559,12 +560,8 @@ impl LanguageState {
|
|||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
let paths = snap
|
||||
.world
|
||||
.registry
|
||||
.local_path()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let paths = snap.world.registry.local_path();
|
||||
let paths = paths.as_deref().into_iter().collect::<Vec<_>>();
|
||||
serde_json::to_value(paths).map_err(|e| internal_error(e.to_string()))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -576,8 +576,9 @@ impl CompileConfig {
|
|||
root_dir: command.root,
|
||||
inputs: Arc::new(LazyHash::new(inputs)),
|
||||
font: command.font,
|
||||
package: command.package,
|
||||
creation_timestamp: command.creation_timestamp,
|
||||
cert: command.certification,
|
||||
cert: command.cert.as_deref().map(From::from),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -723,6 +724,14 @@ impl CompileConfig {
|
|||
opts
|
||||
}
|
||||
|
||||
/// Determines the package options.
|
||||
pub fn determine_package_opts(&self) -> CompilePackageArgs {
|
||||
if let Some(extras) = &self.typst_extra_args {
|
||||
return extras.package.clone();
|
||||
}
|
||||
CompilePackageArgs::default()
|
||||
}
|
||||
|
||||
/// Determines the font resolver.
|
||||
pub fn determine_fonts(&self) -> Deferred<Arc<FontResolverImpl>> {
|
||||
// todo: on font resolving failure, downgrade to a fake font book
|
||||
|
@ -762,7 +771,7 @@ impl CompileConfig {
|
|||
}
|
||||
|
||||
/// Determines the certification path.
|
||||
pub fn determine_certification_path(&self) -> Option<PathBuf> {
|
||||
pub fn determine_certification_path(&self) -> Option<ImmutPath> {
|
||||
let extras = self.typst_extra_args.as_ref()?;
|
||||
extras.cert.clone()
|
||||
}
|
||||
|
@ -880,10 +889,12 @@ pub struct CompileExtraOpts {
|
|||
pub inputs: ImmutDict,
|
||||
/// Additional font paths.
|
||||
pub font: CompileFontArgs,
|
||||
/// Package related arguments.
|
||||
pub package: CompilePackageArgs,
|
||||
/// The creation timestamp for various output.
|
||||
pub creation_timestamp: Option<chrono::DateTime<chrono::Utc>>,
|
||||
/// Path to certification file
|
||||
pub cert: Option<PathBuf>,
|
||||
pub cert: Option<ImmutPath>,
|
||||
}
|
||||
|
||||
/// The path pattern that could be substituted.
|
||||
|
|
|
@ -1,42 +1,4 @@
|
|||
//! Package management tools.
|
||||
|
||||
use reflexo_typst::package::PackageRegistry;
|
||||
use typst::diag::{eco_format, StrResult};
|
||||
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
|
||||
|
||||
use crate::LspWorld;
|
||||
|
||||
mod init;
|
||||
pub use init::*;
|
||||
|
||||
/// Try to determine the latest version of a package.
|
||||
pub fn determine_latest_version(
|
||||
world: &LspWorld,
|
||||
spec: &VersionlessPackageSpec,
|
||||
) -> StrResult<PackageVersion> {
|
||||
if spec.namespace == "preview" {
|
||||
let packages = world.registry.packages();
|
||||
packages
|
||||
.iter()
|
||||
.filter(|(package, _)| package.namespace == "preview" && 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);
|
||||
world
|
||||
.registry
|
||||
.local_path()
|
||||
.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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,20 +10,19 @@ use typst_syntax::Source;
|
|||
use super::*;
|
||||
|
||||
fn conv_(s: &str, for_docs: bool) -> EcoString {
|
||||
static FONT_RESOLVER: LazyLock<Result<Arc<FontResolverImpl>>> = LazyLock::new(|| {
|
||||
Ok(Arc::new(
|
||||
static FONT_RESOLVER: LazyLock<Arc<FontResolverImpl>> = LazyLock::new(|| {
|
||||
Arc::new(
|
||||
LspUniverseBuilder::resolve_fonts(CompileFontArgs::default())
|
||||
.map_err(|e| format!("{e:?}"))?,
|
||||
))
|
||||
.expect("cannot resolve default fonts"),
|
||||
)
|
||||
});
|
||||
|
||||
let font_resolver = FONT_RESOLVER.clone();
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let main = Source::detached(s);
|
||||
let mut universe = LspUniverseBuilder::build(
|
||||
EntryState::new_rooted(cwd.as_path().into(), Some(main.id())),
|
||||
font_resolver.unwrap(),
|
||||
Default::default(),
|
||||
FONT_RESOLVER.clone(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue