mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Read SOURCE_DATE_EPOCH
for better reproducibility (#3809)
This commit is contained in:
parent
0619ae98a8
commit
d4b3ae0925
@ -62,6 +62,7 @@ zip = { workspace = true, optional = true }
|
|||||||
openssl = { workspace = true }
|
openssl = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true, features = ["string"] }
|
clap = { workspace = true, features = ["string"] }
|
||||||
clap_complete = { workspace = true }
|
clap_complete = { workspace = true }
|
||||||
clap_mangen = { workspace = true }
|
clap_mangen = { workspace = true }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use clap::builder::ValueParser;
|
use clap::builder::ValueParser;
|
||||||
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum};
|
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
@ -167,6 +168,12 @@ pub struct SharedArgs {
|
|||||||
)]
|
)]
|
||||||
pub font_paths: Vec<PathBuf>,
|
pub font_paths: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// The document's creation date formatted as a UNIX timestamp.
|
||||||
|
///
|
||||||
|
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
|
||||||
|
#[clap(env = "SOURCE_DATE_EPOCH", value_parser = parse_source_date_epoch)]
|
||||||
|
pub source_date_epoch: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
/// The format to emit diagnostics in
|
/// The format to emit diagnostics in
|
||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
@ -176,6 +183,15 @@ pub struct SharedArgs {
|
|||||||
pub diagnostic_format: DiagnosticFormat,
|
pub diagnostic_format: DiagnosticFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
|
||||||
|
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
|
||||||
|
let timestamp: i64 = raw
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
|
||||||
|
DateTime::from_timestamp(timestamp, 0)
|
||||||
|
.ok_or_else(|| "timestamp out of range".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// An input that is either stdin or a real path.
|
/// An input that is either stdin or a real path.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Input {
|
pub enum Input {
|
||||||
|
@ -166,7 +166,10 @@ fn export(
|
|||||||
|
|
||||||
/// Export to a PDF.
|
/// Export to a PDF.
|
||||||
fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> {
|
fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> {
|
||||||
let buffer = typst_pdf::pdf(document, Smart::Auto, now());
|
let timestamp = convert_datetime(
|
||||||
|
command.common.source_date_epoch.unwrap_or_else(chrono::Utc::now),
|
||||||
|
);
|
||||||
|
let buffer = typst_pdf::pdf(document, Smart::Auto, timestamp);
|
||||||
command
|
command
|
||||||
.output()
|
.output()
|
||||||
.write(&buffer)
|
.write(&buffer)
|
||||||
@ -174,16 +177,15 @@ fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current date and time in UTC.
|
/// Convert [`chrono::DateTime`] to [`Datetime`]
|
||||||
fn now() -> Option<Datetime> {
|
fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<Datetime> {
|
||||||
let now = chrono::Local::now().naive_utc();
|
|
||||||
Datetime::from_ymd_hms(
|
Datetime::from_ymd_hms(
|
||||||
now.year(),
|
date_time.year(),
|
||||||
now.month().try_into().ok()?,
|
date_time.month().try_into().ok()?,
|
||||||
now.day().try_into().ok()?,
|
date_time.day().try_into().ok()?,
|
||||||
now.hour().try_into().ok()?,
|
date_time.hour().try_into().ok()?,
|
||||||
now.minute().try_into().ok()?,
|
date_time.minute().try_into().ok()?,
|
||||||
now.second().try_into().ok()?,
|
date_time.second().try_into().ok()?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::{fmt, fs, io, mem};
|
use std::{fmt, fs, io, mem};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Local};
|
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
|
||||||
use comemo::Prehashed;
|
use comemo::Prehashed;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
@ -42,8 +42,9 @@ pub struct SystemWorld {
|
|||||||
/// Maps file ids to source files and buffers.
|
/// Maps file ids to source files and buffers.
|
||||||
slots: Mutex<HashMap<FileId, FileSlot>>,
|
slots: Mutex<HashMap<FileId, FileSlot>>,
|
||||||
/// 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.
|
||||||
now: OnceLock<DateTime<Local>>,
|
/// Reset between compilations if not [`Now::Fixed`].
|
||||||
|
now: Now,
|
||||||
/// The export cache, used for caching output files in `typst watch`
|
/// The export cache, used for caching output files in `typst watch`
|
||||||
/// sessions.
|
/// sessions.
|
||||||
export_cache: ExportCache,
|
export_cache: ExportCache,
|
||||||
@ -104,6 +105,11 @@ impl SystemWorld {
|
|||||||
let mut searcher = FontSearcher::new();
|
let mut searcher = FontSearcher::new();
|
||||||
searcher.search(&command.font_paths);
|
searcher.search(&command.font_paths);
|
||||||
|
|
||||||
|
let now = match command.source_date_epoch {
|
||||||
|
Some(time) => Now::Fixed(time),
|
||||||
|
None => Now::System(OnceLock::new()),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
workdir: std::env::current_dir().ok(),
|
workdir: std::env::current_dir().ok(),
|
||||||
root,
|
root,
|
||||||
@ -112,7 +118,7 @@ impl SystemWorld {
|
|||||||
book: Prehashed::new(searcher.book),
|
book: Prehashed::new(searcher.book),
|
||||||
fonts: searcher.fonts,
|
fonts: searcher.fonts,
|
||||||
slots: Mutex::new(HashMap::new()),
|
slots: Mutex::new(HashMap::new()),
|
||||||
now: OnceLock::new(),
|
now,
|
||||||
export_cache: ExportCache::new(),
|
export_cache: ExportCache::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -146,7 +152,9 @@ impl SystemWorld {
|
|||||||
for slot in self.slots.get_mut().values_mut() {
|
for slot in self.slots.get_mut().values_mut() {
|
||||||
slot.reset();
|
slot.reset();
|
||||||
}
|
}
|
||||||
self.now.take();
|
if let Now::System(time_lock) = &mut self.now {
|
||||||
|
time_lock.take();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup a source file by id.
|
/// Lookup a source file by id.
|
||||||
@ -187,17 +195,24 @@ impl World for SystemWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
||||||
let now = self.now.get_or_init(chrono::Local::now);
|
let now = match &self.now {
|
||||||
|
Now::Fixed(time) => time,
|
||||||
|
Now::System(time) => time.get_or_init(Utc::now),
|
||||||
|
};
|
||||||
|
|
||||||
let naive = match offset {
|
// The time with the specified UTC offset, or within the local time zone.
|
||||||
None => now.naive_local(),
|
let with_offset = match offset {
|
||||||
Some(o) => now.naive_utc() + chrono::Duration::try_hours(o)?,
|
None => now.with_timezone(&Local).fixed_offset(),
|
||||||
|
Some(hours) => {
|
||||||
|
let seconds = i32::try_from(hours).ok()?.checked_mul(3600)?;
|
||||||
|
now.with_timezone(&FixedOffset::east_opt(seconds)?)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Datetime::from_ymd(
|
Datetime::from_ymd(
|
||||||
naive.year(),
|
with_offset.year(),
|
||||||
naive.month().try_into().ok()?,
|
with_offset.month().try_into().ok()?,
|
||||||
naive.day().try_into().ok()?,
|
with_offset.day().try_into().ok()?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,6 +399,15 @@ fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
|
|||||||
Ok(std::str::from_utf8(buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf))?)
|
Ok(std::str::from_utf8(buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The current date and time.
|
||||||
|
enum Now {
|
||||||
|
/// The date and time if the environment `SOURCE_DATE_EPOCH` is set.
|
||||||
|
/// Used for reproducible builds.
|
||||||
|
Fixed(DateTime<Utc>),
|
||||||
|
/// The current date and time if the time is not externally fixed.
|
||||||
|
System(OnceLock<DateTime<Utc>>),
|
||||||
|
}
|
||||||
|
|
||||||
/// An error that occurs during world construction.
|
/// An error that occurs during world construction.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WorldCreationError {
|
pub enum WorldCreationError {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user