pub mod cache;
pub mod extract;
pub mod fetch;
pub mod imports;
pub mod manifest;
pub mod pipeline;
pub mod spec;
pub use pipeline::{InstallSummary, install, install_with_transitive};
pub use spec::PackageSpec;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub enum InstallError {
InvalidSpec {
raw: String,
reason: String,
},
Http {
url: String,
reason: String,
},
HttpStatus {
url: String,
status: u16,
},
Io {
context: String,
source: std::io::Error,
},
Extract {
context: String,
source: std::io::Error,
},
ManifestMissing {
expected: PathBuf,
},
ManifestParse {
reason: String,
},
ManifestMismatch {
expected: String,
found: String,
},
CacheDirUnresolved,
TransitiveDepFailed {
parent: String,
child: String,
source: Box<InstallError>,
},
}
impl fmt::Display for InstallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InstallError::InvalidSpec { raw, reason } => {
write!(
f,
"invalid package spec `{raw}`: {reason} \
(expected format: @preview/<name>:<version>)"
)
}
InstallError::Http { url, reason } => {
write!(f, "failed to fetch {url}: {reason}")
}
InstallError::HttpStatus { url, status } => {
write!(
f,
"registry returned HTTP {status} for {url} \
(check the package name and version at \
https://typst.app/universe/)"
)
}
InstallError::Io { context, source } => {
write!(f, "{context}: {source}")
}
InstallError::Extract { context, source } => {
write!(f, "{context}: {source}")
}
InstallError::ManifestMissing { expected } => {
write!(
f,
"package tarball is missing typst.toml at {}",
expected.display(),
)
}
InstallError::ManifestParse { reason } => {
write!(f, "failed to parse typst.toml: {reason}")
}
InstallError::ManifestMismatch { expected, found } => {
write!(
f,
"manifest mismatch: asked for {expected}, tarball declared {found}",
)
}
InstallError::CacheDirUnresolved => {
write!(
f,
"could not determine the user cache directory; \
set FERROCV_CACHE_DIR to an explicit path and retry",
)
}
InstallError::TransitiveDepFailed {
parent,
child,
source,
} => {
write!(
f,
"failed to install transitive dep {child} required by {parent}: {source}",
)
}
}
}
}
impl std::error::Error for InstallError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
InstallError::Io { source, .. } | InstallError::Extract { source, .. } => Some(source),
InstallError::TransitiveDepFailed { source, .. } => Some(&**source),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transitive_dep_failed_displays_both_specs() {
let err = InstallError::TransitiveDepFailed {
parent: "@preview/parent:1.0.0".to_owned(),
child: "@preview/child:2.0.0".to_owned(),
source: Box::new(InstallError::HttpStatus {
url: "https://packages.typst.org/preview/child-2.0.0.tar.gz".to_owned(),
status: 404,
}),
};
let msg = err.to_string();
assert!(
msg.contains("@preview/parent:1.0.0"),
"Display must mention parent: {msg}",
);
assert!(
msg.contains("@preview/child:2.0.0"),
"Display must mention child: {msg}",
);
}
#[test]
fn transitive_dep_failed_chains_source() {
use std::error::Error;
let inner = InstallError::HttpStatus {
url: "https://packages.typst.org/preview/child-2.0.0.tar.gz".to_owned(),
status: 404,
};
let err = InstallError::TransitiveDepFailed {
parent: "@preview/parent:1.0.0".to_owned(),
child: "@preview/child:2.0.0".to_owned(),
source: Box::new(inner),
};
let chained = err.source().expect("source must be reachable");
assert!(
chained.to_string().contains("404"),
"chained source must surface the inner cause: {chained}",
);
}
}