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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,8 @@ serde_repr.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_with.workspace = true serde_with.workspace = true
siphasher.workspace = true siphasher.workspace = true
web-time.workspace = true web-time = { workspace = true, optional = true }
time.workspace = true
lsp-types.workspace = true lsp-types.workspace = true
tempfile = { workspace = true, optional = true } tempfile = { workspace = true, optional = true }
same-file = { 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 = ["rkyv/alloc", "rkyv/archive_le"]
rkyv-validation = ["rkyv/validation"] rkyv-validation = ["rkyv/validation"]
__web = ["wasm-bindgen", "js-sys"] __web = ["wasm-bindgen", "js-sys", "web-time"]
web = ["__web"] web = ["__web"]
system = ["tempfile", "same-file"] system = ["tempfile", "same-file"]
bi-hash = [] bi-hash = []

View file

@ -1,8 +1,18 @@
//! Cross platform time utilities. //! Cross platform time utilities.
pub use std::time::SystemTime as Time; 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}; 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). /// Returns the current system time (UTC+0).
#[cfg(any(feature = "system", feature = "web"))] #[cfg(any(feature = "system", feature = "web"))]
pub fn now() -> Time { pub fn now() -> Time {
@ -22,3 +32,30 @@ pub fn now() -> Time {
pub fn now() -> Time { pub fn now() -> Time {
Time::UNIX_EPOCH 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] [dependencies]
anyhow.workspace = true anyhow.workspace = true
chrono.workspace = true
clap.workspace = true clap.workspace = true
comemo.workspace = true comemo.workspace = true
dirs.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::pdf;
pub use typst_pdf::PdfStandard as TypstPdfStandard; 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 typst_pdf::{PdfOptions, PdfStandards, Timestamp};
use super::*; use super::*;
@ -19,13 +18,14 @@ impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PdfExport {
doc: &Arc<TypstPagedDocument>, doc: &Arc<TypstPagedDocument>,
config: &ExportPdfTask, config: &ExportPdfTask,
) -> Result<Bytes> { ) -> Result<Bytes> {
// todo: timestamp world.now()
let creation_timestamp = config let creation_timestamp = config
.creation_timestamp .creation_timestamp
.map(convert_source_date_epoch) .map(|ts| ts.to_utc_datetime().context("timestamp is out of range"))
.transpose() .transpose()?
.context_ut("prepare pdf creation timestamp")? .unwrap_or_else(tinymist_std::time::utc_now);
.unwrap_or_else(chrono::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( let standards = PdfStandards::new(
&config &config
@ -45,23 +45,10 @@ impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PdfExport {
Ok(Bytes::new(typst_pdf::pdf( Ok(Bytes::new(typst_pdf::pdf(
doc, doc,
&PdfOptions { &PdfOptions {
timestamp: convert_datetime(creation_timestamp), timestamp: Some(timestamp),
standards, standards,
..Default::default() ..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] [dependencies]
anyhow.workspace = true anyhow.workspace = true
chrono.workspace = true chrono = { workspace = true, default-features = false, optional = true, features = [
"std",
"clock",
] }
clap.workspace = true clap.workspace = true
codespan-reporting.workspace = true codespan-reporting.workspace = true
comemo.workspace = true comemo.workspace = true
@ -49,8 +52,10 @@ web-sys = { workspace = true, optional = true, features = ["console"] }
default = [] default = []
browser-embedded-fonts = ["typst-assets/fonts"] browser-embedded-fonts = ["typst-assets/fonts"]
http-registry = ["dep:reqwest"] http-registry = ["reqwest"]
web = [ web = [
"chrono",
"chrono/wasmbind",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"js-sys", "js-sys",
@ -60,8 +65,9 @@ web = [
] ]
browser = ["tinymist-vfs/browser", "web"] browser = ["tinymist-vfs/browser", "web"]
system = [ system = [
"dep:dirs", "dirs",
"dep:fontdb", "fontdb",
"chrono",
"http-registry", "http-registry",
"tinymist-std/system", "tinymist-std/system",
"tinymist-vfs/system", "tinymist-vfs/system",

View file

@ -4,7 +4,6 @@ use std::{
sync::Arc, sync::Arc,
}; };
use chrono::{DateTime, Utc};
use clap::{builder::ValueParser, ArgAction, Parser, ValueEnum}; use clap::{builder::ValueParser, ArgAction, Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_std::{bail, error::prelude::*}; 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})")) .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 { macro_rules! display_possible_values {
($ty:ty) => { ($ty:ty) => {
impl fmt::Display for $ty { impl fmt::Display for $ty {

View file

@ -6,7 +6,6 @@ use std::{
sync::{Arc, LazyLock, OnceLock}, sync::{Arc, LazyLock, OnceLock},
}; };
use chrono::{DateTime, Datelike, Local};
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_vfs::{ use tinymist_vfs::{
FsProvider, PathResolution, RevisingVfs, SourceCache, TypstFileId, Vfs, WorkspaceResolver, 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 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> { pub struct CompilerWorld<F: CompilerFeat> {
/// State for the *root & entry* of compilation. /// State for the *root & entry* of compilation.
/// The world forbids direct access to files outside this directory. /// The world forbids direct access to files outside this directory.
@ -455,7 +459,7 @@ pub struct CompilerWorld<F: CompilerFeat> {
source_db: SourceDb, source_db: SourceDb,
/// 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. Reset between compilations. /// always the same within one compilation. Reset between compilations.
now: OnceLock<DateTime<Local>>, now: OnceLock<NowStorage>,
} }
impl<F: CompilerFeat> Clone for CompilerWorld<F> { 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 /// If this function returns `None`, Typst's `datetime` function will
/// return an error. /// return an error.
#[cfg(any(feature = "web", feature = "system"))]
fn today(&self, offset: Option<i64>) -> Option<Datetime> { 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 now = self.now.get_or_init(|| tinymist_std::time::now().into());
let naive = match offset { let naive = match offset {
None => now.naive_local(), 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( Datetime::from_ymd(
@ -721,6 +728,31 @@ impl<F: CompilerFeat> World for CompilerWorld<F> {
naive.day().try_into().ok()?, 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> { impl<F: CompilerFeat> EntryReader for CompilerWorld<F> {

View file

@ -6,24 +6,22 @@ use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use reflexo::ImmutPath; use reflexo::ImmutPath;
use reflexo_typst::CompilationTask; use reflexo_typst::{Bytes, CompilationTask, ExportComputation};
use tinymist_project::LspWorld; use tinymist_project::LspWorld;
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_std::fs::paths::write_atomic; use tinymist_std::fs::paths::write_atomic;
use tinymist_std::typst::TypstDocument; 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 tokio::sync::mpsc;
use typlite::Typlite; use typlite::Typlite;
use typst::foundations::IntoValue; use typst::foundations::IntoValue;
use typst::visualize::Color; use typst::visualize::Color;
use typst_pdf::PdfOptions;
use super::{FutureFolder, SyncTaskFactory}; use super::{FutureFolder, SyncTaskFactory};
use crate::project::{ use crate::project::{
convert_source_date_epoch, ApplyProjectTask, CompiledArtifact, EntryReader, ExportHtmlTask, ApplyProjectTask, CompiledArtifact, EntryReader, ExportHtmlTask, ExportMarkdownTask,
ExportMarkdownTask, ExportPdfTask, ExportPngTask, ExportSvgTask, ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask, ExportTextTask,
ExportTask as ProjectExportTask, ExportTextTask, LspCompiledArtifact, ProjectTask, QueryTask, LspCompiledArtifact, ProjectTask, QueryTask, TaskWhen,
TaskWhen,
}; };
use crate::{actor::editor::EditorRequest, tool::word_count}; use crate::{actor::editor::EditorRequest, tool::word_count};
@ -186,7 +184,7 @@ impl ExportTask {
// Prepare data. // Prepare data.
let kind2 = task.clone(); let kind2 = task.clone();
let data = FutureFolder::compute(move |_| -> Result<Vec<u8>> { let data = FutureFolder::compute(move |_| -> Result<Bytes> {
let doc = &doc; let doc = &doc;
// static BLANK: Lazy<Page> = Lazy::new(Page::default); // static BLANK: Lazy<Page> = Lazy::new(Page::default);
@ -222,28 +220,9 @@ impl ExportTask {
.context("no first page to export") .context("no first page to export")
}; };
Ok(match kind2 { Ok(match kind2 {
Preview(..) => vec![], Preview(..) => Bytes::new([]),
// todo: more pdf flags // todo: more pdf flags
ExportPdf(ExportPdfTask { ExportPdf(config) => PdfExport::run(&graph, paged_doc()?, &config)?,
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:?}"))?
}
Query(QueryTask { Query(QueryTask {
export: _, export: _,
output_extension: _, output_extension: _,
@ -271,37 +250,37 @@ impl ExportTask {
let Some(value) = mapped.first() else { let Some(value) = mapped.first() else {
bail!("no such field found for element"); bail!("no such field found for element");
}; };
serialize(value, &format, pretty).map(String::into_bytes)? serialize(value, &format, pretty).map(Bytes::from_string)?
} else { } 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()?) ExportHtml(ExportHtmlTask { export: _ }) => Bytes::from_string(
.map_err(|e| format!("export error: {e:?}")) typst_html::html(html_doc()?)
.context_ut("failed to export to html")? .map_err(|e| format!("export error: {e:?}"))
.into_bytes(), .context_ut("failed to export to html")?,
ExportSvgHtml(ExportHtmlTask { export: _ }) => { ),
reflexo_vec2svg::render_svg_html::<DefaultExportFeature>(paged_doc()?) ExportSvgHtml(ExportHtmlTask { export: _ }) => Bytes::from_string(
.into_bytes() reflexo_vec2svg::render_svg_html::<DefaultExportFeature>(paged_doc()?),
} ),
ExportText(ExportTextTask { export: _ }) => { ExportText(ExportTextTask { export: _ }) => {
TextExport::run_on_doc(doc)?.into_bytes() Bytes::from_string(TextExport::run_on_doc(doc)?)
} }
ExportMd(ExportMarkdownTask { export: _ }) => { ExportMd(ExportMarkdownTask { export: _ }) => {
let conv = Typlite::new(Arc::new(graph.world().clone())) let conv = Typlite::new(Arc::new(graph.world().clone()))
.convert() .convert()
.map_err(|e| anyhow::anyhow!("failed to convert to markdown: {e}"))?; .map_err(|e| anyhow::anyhow!("failed to convert to markdown: {e}"))?;
conv.as_bytes().to_owned() Bytes::from_string(conv)
} }
ExportSvg(ExportSvgTask { export }) => { ExportSvg(ExportSvgTask { export }) => {
let (is_first, merged_gap) = get_page_selection(&export)?; let (is_first, merged_gap) = get_page_selection(&export)?;
if is_first { Bytes::from_string(if is_first {
typst_svg::svg(first_page()?).into_bytes() typst_svg::svg(first_page()?)
} else { } else {
typst_svg::svg_merged(paged_doc()?, merged_gap).into_bytes() typst_svg::svg_merged(paged_doc()?, merged_gap)
} })
} }
ExportPng(ExportPngTask { export, ppi, fill }) => { ExportPng(ExportPngTask { export, ppi, fill }) => {
let ppi = ppi.to_f32(); let ppi = ppi.to_f32();
@ -323,9 +302,11 @@ impl ExportTask {
typst_render::render_merged(paged_doc()?, ppi / 72., merged_gap, Some(fill)) typst_render::render_merged(paged_doc()?, ppi / 72., merged_gap, Some(fill))
}; };
pixmap Bytes::new(
.encode_png() pixmap
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? .encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?,
)
} }
}) })
}) })
@ -510,4 +491,70 @@ mod tests {
assert!(needs_run); 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);
}
} }