fix: remove system time deps from crates (#1621)

* fix: remove system time deps from crates

* fix: remove system time deps from crates

* fix: smater feature gate

* docs: add some todos

* Update time.rs

* Update Cargo.toml

* build: remove hard dep chrono
This commit is contained in:
Myriad-Dreamin 2025-04-08 01:46:05 +08:00 committed by GitHub
parent 35b718452e
commit 769fc93df9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 190 additions and 90 deletions

4
Cargo.lock generated
View file

@ -4192,7 +4192,6 @@ name = "tinymist-project"
version = "0.13.12-rc1"
dependencies = [
"anyhow",
"chrono",
"clap",
"comemo",
"dirs",
@ -4223,7 +4222,6 @@ dependencies = [
"anyhow",
"base64",
"biblatex",
"chrono",
"comemo",
"dashmap",
"dirs",
@ -4312,6 +4310,7 @@ dependencies = [
"serde_with",
"siphasher",
"tempfile",
"time",
"typst",
"typst-shim",
"wasm-bindgen",
@ -4324,7 +4323,6 @@ name = "tinymist-task"
version = "0.13.12-rc1"
dependencies = [
"anyhow",
"chrono",
"clap",
"comemo",
"dirs",

View file

@ -41,6 +41,7 @@ open = { version = "5.1.3" }
parking_lot = "0.12.1"
walkdir = "2"
chrono = "0.4"
time = "0.3"
dirs = "6"
fontdb = "0.21"
notify = "6"

View file

@ -14,7 +14,6 @@ rust-version.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
comemo.workspace = true
dirs.workspace = true

View file

@ -33,7 +33,6 @@ walkdir.workspace = true
indexmap.workspace = true
ecow.workspace = true
siphasher.workspace = true
chrono.workspace = true
rpds.workspace = true
rayon.workspace = true

View file

@ -28,7 +28,8 @@ serde_repr.workspace = true
serde_json.workspace = true
serde_with.workspace = true
siphasher.workspace = true
web-time.workspace = true
web-time = { workspace = true, optional = true }
time.workspace = true
lsp-types.workspace = true
tempfile = { workspace = true, optional = true }
same-file = { workspace = true, optional = true }
@ -74,7 +75,7 @@ typst = ["dep:typst", "dep:typst-shim"]
rkyv = ["rkyv/alloc", "rkyv/archive_le"]
rkyv-validation = ["rkyv/validation"]
__web = ["wasm-bindgen", "js-sys"]
__web = ["wasm-bindgen", "js-sys", "web-time"]
web = ["__web"]
system = ["tempfile", "same-file"]
bi-hash = []

View file

@ -1,8 +1,18 @@
//! Cross platform time utilities.
pub use std::time::SystemTime as Time;
pub use time::UtcDateTime;
#[cfg(not(feature = "web"))]
pub use std::time::{Duration, Instant};
#[cfg(feature = "web")]
pub use web_time::{Duration, Instant};
/// Returns the current datetime in utc (UTC+0).
pub fn utc_now() -> UtcDateTime {
now().into()
}
/// Returns the current system time (UTC+0).
#[cfg(any(feature = "system", feature = "web"))]
pub fn now() -> Time {
@ -22,3 +32,30 @@ pub fn now() -> Time {
pub fn now() -> Time {
Time::UNIX_EPOCH
}
/// The trait helping convert to a [`UtcDateTime`].
pub trait ToUtcDateTime {
/// Converts to a [`UtcDateTime`].
fn to_utc_datetime(self) -> Option<UtcDateTime>;
}
impl ToUtcDateTime for i64 {
/// Converts a UNIX timestamp to a [`UtcDateTime`].
fn to_utc_datetime(self) -> Option<UtcDateTime> {
UtcDateTime::from_unix_timestamp(self).ok()
}
}
impl ToUtcDateTime for Time {
/// Converts a system time to a [`UtcDateTime`].
fn to_utc_datetime(self) -> Option<UtcDateTime> {
Some(UtcDateTime::from(self))
}
}
/// Converts a [`UtcDateTime`] to typst's datetime.
#[cfg(feature = "typst")]
pub fn to_typst_time(timestamp: UtcDateTime) -> typst::foundations::Datetime {
let datetime = ::time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
typst::foundations::Datetime::Datetime(datetime)
}

View file

@ -14,7 +14,6 @@ rust-version.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
comemo.workspace = true
dirs.workspace = true

View file

@ -1,8 +1,7 @@
use tinymist_std::time::ToUtcDateTime;
pub use typst_pdf::pdf;
pub use typst_pdf::PdfStandard as TypstPdfStandard;
use tinymist_world::args::convert_source_date_epoch;
use typst::foundations::Datetime;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
use super::*;
@ -19,13 +18,14 @@ impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PdfExport {
doc: &Arc<TypstPagedDocument>,
config: &ExportPdfTask,
) -> Result<Bytes> {
// todo: timestamp world.now()
let creation_timestamp = config
.creation_timestamp
.map(convert_source_date_epoch)
.transpose()
.context_ut("prepare pdf creation timestamp")?
.unwrap_or_else(chrono::Utc::now);
.map(|ts| ts.to_utc_datetime().context("timestamp is out of range"))
.transpose()?
.unwrap_or_else(tinymist_std::time::utc_now);
// todo: this seems different from `Timestamp::new_local` which also embeds the
// timezone information.
let timestamp = Timestamp::new_utc(tinymist_std::time::to_typst_time(creation_timestamp));
let standards = PdfStandards::new(
&config
@ -45,23 +45,10 @@ impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PdfExport {
Ok(Bytes::new(typst_pdf::pdf(
doc,
&PdfOptions {
timestamp: convert_datetime(creation_timestamp),
timestamp: Some(timestamp),
standards,
..Default::default()
},
)?))
}
}
/// Convert [`chrono::DateTime`] to [`Timestamp`]
pub fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<Timestamp> {
use chrono::{Datelike, Timelike};
Some(Timestamp::new_utc(Datetime::from_ymd_hms(
date_time.year(),
date_time.month().try_into().ok()?,
date_time.day().try_into().ok()?,
date_time.hour().try_into().ok()?,
date_time.minute().try_into().ok()?,
date_time.second().try_into().ok()?,
)?))
}

View file

@ -15,7 +15,10 @@ rust-version.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
chrono = { workspace = true, default-features = false, optional = true, features = [
"std",
"clock",
] }
clap.workspace = true
codespan-reporting.workspace = true
comemo.workspace = true
@ -49,8 +52,10 @@ web-sys = { workspace = true, optional = true, features = ["console"] }
default = []
browser-embedded-fonts = ["typst-assets/fonts"]
http-registry = ["dep:reqwest"]
http-registry = ["reqwest"]
web = [
"chrono",
"chrono/wasmbind",
"wasm-bindgen",
"web-sys",
"js-sys",
@ -60,8 +65,9 @@ web = [
]
browser = ["tinymist-vfs/browser", "web"]
system = [
"dep:dirs",
"dep:fontdb",
"dirs",
"fontdb",
"chrono",
"http-registry",
"tinymist-std/system",
"tinymist-vfs/system",

View file

@ -4,7 +4,6 @@ use std::{
sync::Arc,
};
use chrono::{DateTime, Utc};
use clap::{builder::ValueParser, ArgAction, Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use tinymist_std::{bail, error::prelude::*};
@ -211,11 +210,6 @@ pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
.map_err(|err| format!("timestamp must be decimal integer ({err})"))
}
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
pub fn convert_source_date_epoch(seconds: i64) -> Result<chrono::DateTime<Utc>, String> {
DateTime::from_timestamp(seconds, 0).ok_or_else(|| "timestamp out of range".to_string())
}
macro_rules! display_possible_values {
($ty:ty) => {
impl fmt::Display for $ty {

View file

@ -6,7 +6,6 @@ use std::{
sync::{Arc, LazyLock, OnceLock},
};
use chrono::{DateTime, Datelike, Local};
use tinymist_std::error::prelude::*;
use tinymist_vfs::{
FsProvider, PathResolution, RevisingVfs, SourceCache, TypstFileId, Vfs, WorkspaceResolver,
@ -432,6 +431,11 @@ fn is_revision_changed(a: Option<NonZeroUsize>, b: Option<NonZeroUsize>) -> bool
a.is_none() || b.is_none() || a != b
}
#[cfg(any(feature = "web", feature = "system"))]
type NowStorage = chrono::DateTime<chrono::Local>;
#[cfg(not(any(feature = "web", feature = "system")))]
type NowStorage = tinymist_std::time::UtcDateTime;
pub struct CompilerWorld<F: CompilerFeat> {
/// State for the *root & entry* of compilation.
/// The world forbids direct access to files outside this directory.
@ -455,7 +459,7 @@ pub struct CompilerWorld<F: CompilerFeat> {
source_db: SourceDb,
/// The current datetime if requested. This is stored here to ensure it is
/// always the same within one compilation. Reset between compilations.
now: OnceLock<DateTime<Local>>,
now: OnceLock<NowStorage>,
}
impl<F: CompilerFeat> Clone for CompilerWorld<F> {
@ -707,12 +711,15 @@ impl<F: CompilerFeat> World for CompilerWorld<F> {
///
/// If this function returns `None`, Typst's `datetime` function will
/// return an error.
#[cfg(any(feature = "web", feature = "system"))]
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
use chrono::{Datelike, Duration};
// todo: typst respects creation_timestamp, but we don't...
let now = self.now.get_or_init(|| tinymist_std::time::now().into());
let naive = match offset {
None => now.naive_local(),
Some(o) => now.naive_utc() + chrono::Duration::try_hours(o)?,
Some(o) => now.naive_utc() + Duration::try_hours(o)?,
};
Datetime::from_ymd(
@ -721,6 +728,31 @@ impl<F: CompilerFeat> World for CompilerWorld<F> {
naive.day().try_into().ok()?,
)
}
/// Get the current date.
///
/// If no offset is specified, the local date should be chosen. Otherwise,
/// the UTC date should be chosen with the corresponding offset in hours.
///
/// If this function returns `None`, Typst's `datetime` function will
/// return an error.
#[cfg(not(any(feature = "web", feature = "system")))]
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
use tinymist_std::time::{now, to_typst_time, Duration};
// todo: typst respects creation_timestamp, but we don't...
let now = self.now.get_or_init(|| now().into());
let now = offset
.and_then(|offset| {
let dur = Duration::from_secs(offset.checked_mul(3600)? as u64)
.try_into()
.ok()?;
now.checked_add(dur)
})
.unwrap_or(*now);
Some(to_typst_time(now))
}
}
impl<F: CompilerFeat> EntryReader for CompilerWorld<F> {

View file

@ -6,24 +6,22 @@ use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, OnceLock};
use reflexo::ImmutPath;
use reflexo_typst::CompilationTask;
use reflexo_typst::{Bytes, CompilationTask, ExportComputation};
use tinymist_project::LspWorld;
use tinymist_std::error::prelude::*;
use tinymist_std::fs::paths::write_atomic;
use tinymist_std::typst::TypstDocument;
use tinymist_task::{convert_datetime, get_page_selection, ExportTarget, TextExport};
use tinymist_task::{get_page_selection, ExportTarget, PdfExport, TextExport};
use tokio::sync::mpsc;
use typlite::Typlite;
use typst::foundations::IntoValue;
use typst::visualize::Color;
use typst_pdf::PdfOptions;
use super::{FutureFolder, SyncTaskFactory};
use crate::project::{
convert_source_date_epoch, ApplyProjectTask, CompiledArtifact, EntryReader, ExportHtmlTask,
ExportMarkdownTask, ExportPdfTask, ExportPngTask, ExportSvgTask,
ExportTask as ProjectExportTask, ExportTextTask, LspCompiledArtifact, ProjectTask, QueryTask,
TaskWhen,
ApplyProjectTask, CompiledArtifact, EntryReader, ExportHtmlTask, ExportMarkdownTask,
ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask, ExportTextTask,
LspCompiledArtifact, ProjectTask, QueryTask, TaskWhen,
};
use crate::{actor::editor::EditorRequest, tool::word_count};
@ -186,7 +184,7 @@ impl ExportTask {
// Prepare data.
let kind2 = task.clone();
let data = FutureFolder::compute(move |_| -> Result<Vec<u8>> {
let data = FutureFolder::compute(move |_| -> Result<Bytes> {
let doc = &doc;
// static BLANK: Lazy<Page> = Lazy::new(Page::default);
@ -222,28 +220,9 @@ impl ExportTask {
.context("no first page to export")
};
Ok(match kind2 {
Preview(..) => vec![],
Preview(..) => Bytes::new([]),
// todo: more pdf flags
ExportPdf(ExportPdfTask {
creation_timestamp, ..
}) => {
// todo: timestamp world.now()
let creation_timestamp = creation_timestamp
.map(convert_source_date_epoch)
.transpose()
.context_ut("parse pdf creation timestamp")?
.unwrap_or_else(chrono::Utc::now);
// todo: Some(pdf_uri.as_str())
typst_pdf::pdf(
paged_doc()?,
&PdfOptions {
timestamp: convert_datetime(creation_timestamp),
..PdfOptions::default()
},
)
.map_err(|e| anyhow::anyhow!("failed to convert to pdf: {e:?}"))?
}
ExportPdf(config) => PdfExport::run(&graph, paged_doc()?, &config)?,
Query(QueryTask {
export: _,
output_extension: _,
@ -271,37 +250,37 @@ impl ExportTask {
let Some(value) = mapped.first() else {
bail!("no such field found for element");
};
serialize(value, &format, pretty).map(String::into_bytes)?
serialize(value, &format, pretty).map(Bytes::from_string)?
} else {
serialize(&mapped, &format, pretty).map(String::into_bytes)?
serialize(&mapped, &format, pretty).map(Bytes::from_string)?
}
}
ExportHtml(ExportHtmlTask { export: _ }) => typst_html::html(html_doc()?)
.map_err(|e| format!("export error: {e:?}"))
.context_ut("failed to export to html")?
.into_bytes(),
ExportSvgHtml(ExportHtmlTask { export: _ }) => {
reflexo_vec2svg::render_svg_html::<DefaultExportFeature>(paged_doc()?)
.into_bytes()
}
ExportHtml(ExportHtmlTask { export: _ }) => Bytes::from_string(
typst_html::html(html_doc()?)
.map_err(|e| format!("export error: {e:?}"))
.context_ut("failed to export to html")?,
),
ExportSvgHtml(ExportHtmlTask { export: _ }) => Bytes::from_string(
reflexo_vec2svg::render_svg_html::<DefaultExportFeature>(paged_doc()?),
),
ExportText(ExportTextTask { export: _ }) => {
TextExport::run_on_doc(doc)?.into_bytes()
Bytes::from_string(TextExport::run_on_doc(doc)?)
}
ExportMd(ExportMarkdownTask { export: _ }) => {
let conv = Typlite::new(Arc::new(graph.world().clone()))
.convert()
.map_err(|e| anyhow::anyhow!("failed to convert to markdown: {e}"))?;
conv.as_bytes().to_owned()
Bytes::from_string(conv)
}
ExportSvg(ExportSvgTask { export }) => {
let (is_first, merged_gap) = get_page_selection(&export)?;
if is_first {
typst_svg::svg(first_page()?).into_bytes()
Bytes::from_string(if is_first {
typst_svg::svg(first_page()?)
} else {
typst_svg::svg_merged(paged_doc()?, merged_gap).into_bytes()
}
typst_svg::svg_merged(paged_doc()?, merged_gap)
})
}
ExportPng(ExportPngTask { export, ppi, fill }) => {
let ppi = ppi.to_f32();
@ -323,9 +302,11 @@ impl ExportTask {
typst_render::render_merged(paged_doc()?, ppi / 72., merged_gap, Some(fill))
};
pixmap
.encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?
Bytes::new(
pixmap
.encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?,
)
}
})
})
@ -510,4 +491,70 @@ mod tests {
assert!(needs_run);
}
use chrono::{DateTime, Utc};
use tinymist_std::time::*;
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
pub fn convert_source_date_epoch(seconds: i64) -> Result<DateTime<Utc>, String> {
DateTime::from_timestamp(seconds, 0).ok_or_else(|| "timestamp out of range".to_string())
}
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
pub fn convert_system_time(seconds: i64) -> Result<Time, String> {
if seconds < 0 {
return Err("negative timestamp since unix epoch".to_string());
}
Time::UNIX_EPOCH
.checked_add(Duration::new(seconds as u64, 0))
.ok_or_else(|| "timestamp out of range".to_string())
}
#[test]
fn test_timestamp_chrono() {
let timestamp = 1_000_000_000;
let date_time = convert_source_date_epoch(timestamp).unwrap();
assert_eq!(date_time.timestamp(), timestamp);
}
#[test]
fn test_timestamp_system() {
let timestamp = 1_000_000_000;
let date_time = convert_system_time(timestamp).unwrap();
assert_eq!(
date_time
.duration_since(Time::UNIX_EPOCH)
.unwrap()
.as_secs(),
timestamp as u64
);
}
use typst::foundations::Datetime as TypstDatetime;
fn convert_datetime_chrono(date_time: DateTime<Utc>) -> Option<TypstDatetime> {
use chrono::{Datelike, Timelike};
TypstDatetime::from_ymd_hms(
date_time.year(),
date_time.month().try_into().ok()?,
date_time.day().try_into().ok()?,
date_time.hour().try_into().ok()?,
date_time.minute().try_into().ok()?,
date_time.second().try_into().ok()?,
)
}
#[test]
fn test_timestamp_pdf() {
let timestamp = 1_000_000_000;
let date_time = convert_source_date_epoch(timestamp).unwrap();
assert_eq!(date_time.timestamp(), timestamp);
let chrono_pdf_ts = convert_datetime_chrono(date_time).unwrap();
let timestamp = 1_000_000_000;
let date_time = convert_system_time(timestamp).unwrap();
let system_pdf_ts = tinymist_std::time::to_typst_time(date_time.into());
assert_eq!(chrono_pdf_ts, system_pdf_ts);
}
}