typst 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/typst/src/compiler-new.rs +771 -0
- metadata +18 -4
- data/ext/typst/Cargo.lock +0 -2840
@@ -0,0 +1,771 @@
|
|
1
|
+
use std::ffi::OsStr;
|
2
|
+
use std::fs::{self, File};
|
3
|
+
use std::io::{self, Write};
|
4
|
+
use std::path::{Path, PathBuf};
|
5
|
+
|
6
|
+
use chrono::{DateTime, Datelike, Timelike, Utc};
|
7
|
+
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
8
|
+
use codespan_reporting::term;
|
9
|
+
use ecow::eco_format;
|
10
|
+
//use parking_lot::RwLock;
|
11
|
+
use pathdiff::diff_paths;
|
12
|
+
//use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
13
|
+
use typst::diag::{
|
14
|
+
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
15
|
+
};
|
16
|
+
use typst::foundations::{Datetime, Smart};
|
17
|
+
use typst::html::HtmlDocument;
|
18
|
+
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
19
|
+
use typst::syntax::{FileId, Source, Span};
|
20
|
+
use typst::WorldExt;
|
21
|
+
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
22
|
+
|
23
|
+
use typst_cli::args::{
|
24
|
+
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
|
25
|
+
PdfStandard, WatchCommand,
|
26
|
+
};
|
27
|
+
//#[cfg(feature = "http-server")]
|
28
|
+
//use crate::server::HtmlServer;
|
29
|
+
//use crate::timings::Timer;
|
30
|
+
|
31
|
+
//use crate::watch::Status;
|
32
|
+
use crate::world::SystemWorld;
|
33
|
+
//use crate::{set_failed, terminal};
|
34
|
+
|
35
|
+
type CodespanResult<T> = Result<T, CodespanError>;
|
36
|
+
type CodespanError = codespan_reporting::files::Error;
|
37
|
+
|
38
|
+
/*
|
39
|
+
/// Execute a compilation command.
|
40
|
+
pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> {
|
41
|
+
let mut config = CompileConfig::new(command)?;
|
42
|
+
let mut world =
|
43
|
+
SystemWorld::new(&command.args.input, &command.args.world, &command.args.process)
|
44
|
+
.map_err(|err| eco_format!("{err}"))?;
|
45
|
+
timer.record(&mut world, |world| compile_once(world, &mut config))?
|
46
|
+
}
|
47
|
+
*/
|
48
|
+
|
49
|
+
/// A preprocessed `CompileCommand`.
|
50
|
+
pub struct CompileConfig {
|
51
|
+
/// Whether we are watching.
|
52
|
+
pub watching: bool,
|
53
|
+
/// Path to input Typst file or stdin.
|
54
|
+
pub input: Input,
|
55
|
+
/// Path to output file (PDF, PNG, SVG, or HTML).
|
56
|
+
pub output: Output,
|
57
|
+
/// The format of the output file.
|
58
|
+
pub output_format: OutputFormat,
|
59
|
+
/// Which pages to export.
|
60
|
+
pub pages: Option<PageRanges>,
|
61
|
+
/// The document's creation date formatted as a UNIX timestamp, with UTC suffix.
|
62
|
+
pub creation_timestamp: Option<DateTime<Utc>>,
|
63
|
+
/// The format to emit diagnostics in.
|
64
|
+
pub diagnostic_format: DiagnosticFormat,
|
65
|
+
/// Opens the output file with the default viewer or a specific program after
|
66
|
+
/// compilation.
|
67
|
+
pub open: Option<Option<String>>,
|
68
|
+
/// One (or multiple comma-separated) PDF standards that Typst will enforce
|
69
|
+
/// conformance with.
|
70
|
+
pub pdf_standards: PdfStandards,
|
71
|
+
/// A path to write a Makefile rule describing the current compilation.
|
72
|
+
pub make_deps: Option<PathBuf>,
|
73
|
+
/// The PPI (pixels per inch) to use for PNG export.
|
74
|
+
pub ppi: f32,
|
75
|
+
/// The export cache for images, used for caching output files in `typst
|
76
|
+
/// watch` sessions with images.
|
77
|
+
pub export_cache: ExportCache,
|
78
|
+
// /// Server for `typst watch` to HTML.
|
79
|
+
// #[cfg(feature = "http-server")]
|
80
|
+
// pub server: Option<HtmlServer>,
|
81
|
+
}
|
82
|
+
|
83
|
+
impl CompileConfig {
|
84
|
+
/// Preprocess a `CompileCommand`, producing a compilation config.
|
85
|
+
pub fn new(command: &CompileCommand) -> StrResult<Self> {
|
86
|
+
Self::new_impl(&command.args, None)
|
87
|
+
}
|
88
|
+
|
89
|
+
/// Preprocess a `WatchCommand`, producing a compilation config.
|
90
|
+
pub fn watching(command: &WatchCommand) -> StrResult<Self> {
|
91
|
+
Self::new_impl(&command.args, Some(command))
|
92
|
+
}
|
93
|
+
|
94
|
+
/// The shared implementation of [`CompileConfig::new`] and
|
95
|
+
/// [`CompileConfig::watching`].
|
96
|
+
fn new_impl(args: &CompileArgs, watch: Option<&WatchCommand>) -> StrResult<Self> {
|
97
|
+
let input = args.input.clone();
|
98
|
+
|
99
|
+
let output_format = if let Some(specified) = args.format {
|
100
|
+
specified
|
101
|
+
} else if let Some(Output::Path(output)) = &args.output {
|
102
|
+
match output.extension() {
|
103
|
+
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
|
104
|
+
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
|
105
|
+
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
|
106
|
+
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
|
107
|
+
_ => bail!(
|
108
|
+
"could not infer output format for path {}.\n\
|
109
|
+
consider providing the format manually with `--format/-f`",
|
110
|
+
output.display()
|
111
|
+
),
|
112
|
+
}
|
113
|
+
} else {
|
114
|
+
OutputFormat::Pdf
|
115
|
+
};
|
116
|
+
|
117
|
+
let output = args.output.clone().unwrap_or_else(|| {
|
118
|
+
let Input::Path(path) = &input else {
|
119
|
+
panic!("output must be specified when input is from stdin, as guarded by the CLI");
|
120
|
+
};
|
121
|
+
Output::Path(path.with_extension(
|
122
|
+
match output_format {
|
123
|
+
OutputFormat::Pdf => "pdf",
|
124
|
+
OutputFormat::Png => "png",
|
125
|
+
OutputFormat::Svg => "svg",
|
126
|
+
OutputFormat::Html => "html",
|
127
|
+
},
|
128
|
+
))
|
129
|
+
});
|
130
|
+
|
131
|
+
let pages = args.pages.as_ref().map(|export_ranges| {
|
132
|
+
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
|
133
|
+
});
|
134
|
+
|
135
|
+
let pdf_standards = {
|
136
|
+
let list = args
|
137
|
+
.pdf_standard
|
138
|
+
.iter()
|
139
|
+
.map(|standard| match standard {
|
140
|
+
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
|
141
|
+
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
|
142
|
+
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
|
143
|
+
})
|
144
|
+
.collect::<Vec<_>>();
|
145
|
+
PdfStandards::new(&list)?
|
146
|
+
};
|
147
|
+
|
148
|
+
/*
|
149
|
+
#[cfg(feature = "http-server")]
|
150
|
+
let server = match watch {
|
151
|
+
Some(command)
|
152
|
+
if output_format == OutputFormat::Html && !command.server.no_serve =>
|
153
|
+
{
|
154
|
+
Some(HtmlServer::new(&input, &command.server)?)
|
155
|
+
}
|
156
|
+
_ => None,
|
157
|
+
};
|
158
|
+
*/
|
159
|
+
|
160
|
+
Ok(Self {
|
161
|
+
watching: watch.is_some(),
|
162
|
+
input,
|
163
|
+
output,
|
164
|
+
output_format,
|
165
|
+
pages,
|
166
|
+
pdf_standards,
|
167
|
+
creation_timestamp: args.world.creation_timestamp,
|
168
|
+
make_deps: args.make_deps.clone(),
|
169
|
+
ppi: args.ppi,
|
170
|
+
diagnostic_format: args.process.diagnostic_format,
|
171
|
+
open: args.open.clone(),
|
172
|
+
export_cache: ExportCache::new(),
|
173
|
+
//#[cfg(feature = "http-server")]
|
174
|
+
//server,
|
175
|
+
})
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
/// Compile a single time.
|
180
|
+
///
|
181
|
+
/// Returns whether it compiled without errors.
|
182
|
+
#[typst_macros::time(name = "compile once")]
|
183
|
+
pub fn compile_once(
|
184
|
+
world: &mut SystemWorld,
|
185
|
+
config: &mut CompileConfig,
|
186
|
+
) -> StrResult<()> {
|
187
|
+
let start = std::time::Instant::now();
|
188
|
+
if config.watching {
|
189
|
+
Status::Compiling.print(config).unwrap();
|
190
|
+
}
|
191
|
+
|
192
|
+
let Warned { output, warnings } = compile_and_export(world, config);
|
193
|
+
|
194
|
+
match output {
|
195
|
+
// Export the PDF / PNG.
|
196
|
+
Ok(outputs) => {
|
197
|
+
let duration = start.elapsed();
|
198
|
+
|
199
|
+
if config.watching {
|
200
|
+
if warnings.is_empty() {
|
201
|
+
Status::Success(duration).print(config).unwrap();
|
202
|
+
} else {
|
203
|
+
Status::PartialSuccess(duration).print(config).unwrap();
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
print_diagnostics(world, &[], &warnings, config.diagnostic_format)
|
208
|
+
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
209
|
+
|
210
|
+
write_make_deps(world, config, outputs)?;
|
211
|
+
open_output(config)?;
|
212
|
+
}
|
213
|
+
|
214
|
+
// Print diagnostics.
|
215
|
+
Err(errors) => {
|
216
|
+
set_failed();
|
217
|
+
|
218
|
+
if config.watching {
|
219
|
+
Status::Error.print(config).unwrap();
|
220
|
+
}
|
221
|
+
|
222
|
+
print_diagnostics(world, &errors, &warnings, config.diagnostic_format)
|
223
|
+
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
Ok(())
|
228
|
+
}
|
229
|
+
|
230
|
+
/// Compile and then export the document.
|
231
|
+
fn compile_and_export(
|
232
|
+
world: &mut SystemWorld,
|
233
|
+
config: &mut CompileConfig,
|
234
|
+
) -> Warned<SourceResult<Vec<Output>>> {
|
235
|
+
match config.output_format {
|
236
|
+
OutputFormat::Html => {
|
237
|
+
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
238
|
+
let result = output.and_then(|document| export_html(&document, config));
|
239
|
+
Warned {
|
240
|
+
output: result.map(|()| vec![config.output.clone()]),
|
241
|
+
warnings,
|
242
|
+
}
|
243
|
+
}
|
244
|
+
_ => {
|
245
|
+
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
|
246
|
+
let result = output.and_then(|document| export_paged(&document, config));
|
247
|
+
Warned { output: result, warnings }
|
248
|
+
}
|
249
|
+
}
|
250
|
+
}
|
251
|
+
|
252
|
+
/// Export to HTML.
|
253
|
+
fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<()> {
|
254
|
+
let html = typst_html::html(document)?;
|
255
|
+
let result = config.output.write(html.as_bytes());
|
256
|
+
|
257
|
+
// #[cfg(feature = "http-server")]
|
258
|
+
// if let Some(server) = &config.server {
|
259
|
+
// server.update(html);
|
260
|
+
// }
|
261
|
+
|
262
|
+
result
|
263
|
+
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
|
264
|
+
.at(Span::detached())
|
265
|
+
}
|
266
|
+
|
267
|
+
/// Export to a paged target format.
|
268
|
+
fn export_paged(
|
269
|
+
document: &PagedDocument,
|
270
|
+
config: &CompileConfig,
|
271
|
+
) -> SourceResult<Vec<Output>> {
|
272
|
+
match config.output_format {
|
273
|
+
OutputFormat::Pdf => {
|
274
|
+
export_pdf(document, config).map(|()| vec![config.output.clone()])
|
275
|
+
}
|
276
|
+
OutputFormat::Png => {
|
277
|
+
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
|
278
|
+
}
|
279
|
+
OutputFormat::Svg => {
|
280
|
+
export_image(document, config, ImageExportFormat::Svg).at(Span::detached())
|
281
|
+
}
|
282
|
+
OutputFormat::Html => unreachable!(),
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
286
|
+
/// Export to a PDF.
|
287
|
+
fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
|
288
|
+
// If the timestamp is provided through the CLI, use UTC suffix,
|
289
|
+
// else, use the current local time and timezone.
|
290
|
+
let timestamp = match config.creation_timestamp {
|
291
|
+
Some(timestamp) => convert_datetime(timestamp).map(Timestamp::new_utc),
|
292
|
+
None => {
|
293
|
+
let local_datetime = chrono::Local::now();
|
294
|
+
convert_datetime(local_datetime).and_then(|datetime| {
|
295
|
+
Timestamp::new_local(
|
296
|
+
datetime,
|
297
|
+
local_datetime.offset().local_minus_utc() / 60,
|
298
|
+
)
|
299
|
+
})
|
300
|
+
}
|
301
|
+
};
|
302
|
+
let options = PdfOptions {
|
303
|
+
ident: Smart::Auto,
|
304
|
+
timestamp,
|
305
|
+
page_ranges: config.pages.clone(),
|
306
|
+
standards: config.pdf_standards.clone(),
|
307
|
+
};
|
308
|
+
let buffer = typst_pdf::pdf(document, &options)?;
|
309
|
+
config
|
310
|
+
.output
|
311
|
+
.write(&buffer)
|
312
|
+
.map_err(|err| eco_format!("failed to write PDF file ({err})"))
|
313
|
+
.at(Span::detached())?;
|
314
|
+
Ok(())
|
315
|
+
}
|
316
|
+
|
317
|
+
/// Convert [`chrono::DateTime`] to [`Datetime`]
|
318
|
+
fn convert_datetime<Tz: chrono::TimeZone>(
|
319
|
+
date_time: chrono::DateTime<Tz>,
|
320
|
+
) -> Option<Datetime> {
|
321
|
+
Datetime::from_ymd_hms(
|
322
|
+
date_time.year(),
|
323
|
+
date_time.month().try_into().ok()?,
|
324
|
+
date_time.day().try_into().ok()?,
|
325
|
+
date_time.hour().try_into().ok()?,
|
326
|
+
date_time.minute().try_into().ok()?,
|
327
|
+
date_time.second().try_into().ok()?,
|
328
|
+
)
|
329
|
+
}
|
330
|
+
|
331
|
+
/// An image format to export in.
|
332
|
+
#[derive(Clone, Copy)]
|
333
|
+
enum ImageExportFormat {
|
334
|
+
Png,
|
335
|
+
Svg,
|
336
|
+
}
|
337
|
+
|
338
|
+
/// Export to one or multiple images.
|
339
|
+
fn export_image(
|
340
|
+
document: &PagedDocument,
|
341
|
+
config: &CompileConfig,
|
342
|
+
fmt: ImageExportFormat,
|
343
|
+
) -> StrResult<Vec<Output>> {
|
344
|
+
// Determine whether we have indexable templates in output
|
345
|
+
let can_handle_multiple = match config.output {
|
346
|
+
Output::Stdout => false,
|
347
|
+
Output::Path(ref output) => {
|
348
|
+
output_template::has_indexable_template(output.to_str().unwrap_or_default())
|
349
|
+
}
|
350
|
+
};
|
351
|
+
|
352
|
+
let exported_pages = document
|
353
|
+
.pages
|
354
|
+
.iter()
|
355
|
+
.enumerate()
|
356
|
+
.filter(|(i, _)| {
|
357
|
+
config.pages.as_ref().map_or(true, |exported_page_ranges| {
|
358
|
+
exported_page_ranges.includes_page_index(*i)
|
359
|
+
})
|
360
|
+
})
|
361
|
+
.collect::<Vec<_>>();
|
362
|
+
|
363
|
+
if !can_handle_multiple && exported_pages.len() > 1 {
|
364
|
+
let err = match config.output {
|
365
|
+
Output::Stdout => "to stdout",
|
366
|
+
Output::Path(_) => {
|
367
|
+
"without a page number template ({p}, {0p}) in the output path"
|
368
|
+
}
|
369
|
+
};
|
370
|
+
bail!("cannot export multiple images {err}");
|
371
|
+
}
|
372
|
+
|
373
|
+
// The results are collected in a `Vec<()>` which does not allocate.
|
374
|
+
exported_pages
|
375
|
+
.par_iter()
|
376
|
+
.map(|(i, page)| {
|
377
|
+
// Use output with converted path.
|
378
|
+
let output = match &config.output {
|
379
|
+
Output::Path(path) => {
|
380
|
+
let storage;
|
381
|
+
let path = if can_handle_multiple {
|
382
|
+
storage = output_template::format(
|
383
|
+
path.to_str().unwrap_or_default(),
|
384
|
+
i + 1,
|
385
|
+
document.pages.len(),
|
386
|
+
);
|
387
|
+
Path::new(&storage)
|
388
|
+
} else {
|
389
|
+
path
|
390
|
+
};
|
391
|
+
|
392
|
+
// If we are not watching, don't use the cache.
|
393
|
+
// If the frame is in the cache, skip it.
|
394
|
+
// If the file does not exist, always create it.
|
395
|
+
if config.watching
|
396
|
+
&& config.export_cache.is_cached(*i, &page.frame)
|
397
|
+
&& path.exists()
|
398
|
+
{
|
399
|
+
return Ok(Output::Path(path.to_path_buf()));
|
400
|
+
}
|
401
|
+
|
402
|
+
Output::Path(path.to_owned())
|
403
|
+
}
|
404
|
+
Output::Stdout => Output::Stdout,
|
405
|
+
};
|
406
|
+
|
407
|
+
export_image_page(config, page, &output, fmt)?;
|
408
|
+
Ok(output)
|
409
|
+
})
|
410
|
+
.collect::<StrResult<Vec<Output>>>()
|
411
|
+
}
|
412
|
+
|
413
|
+
mod output_template {
|
414
|
+
const INDEXABLE: [&str; 3] = ["{p}", "{0p}", "{n}"];
|
415
|
+
|
416
|
+
pub fn has_indexable_template(output: &str) -> bool {
|
417
|
+
INDEXABLE.iter().any(|template| output.contains(template))
|
418
|
+
}
|
419
|
+
|
420
|
+
pub fn format(output: &str, this_page: usize, total_pages: usize) -> String {
|
421
|
+
// Find the base 10 width of number `i`
|
422
|
+
fn width(i: usize) -> usize {
|
423
|
+
1 + i.checked_ilog10().unwrap_or(0) as usize
|
424
|
+
}
|
425
|
+
|
426
|
+
let other_templates = ["{t}"];
|
427
|
+
INDEXABLE.iter().chain(other_templates.iter()).fold(
|
428
|
+
output.to_string(),
|
429
|
+
|out, template| {
|
430
|
+
let replacement = match *template {
|
431
|
+
"{p}" => format!("{this_page}"),
|
432
|
+
"{0p}" | "{n}" => format!("{:01$}", this_page, width(total_pages)),
|
433
|
+
"{t}" => format!("{total_pages}"),
|
434
|
+
_ => unreachable!("unhandled template placeholder {template}"),
|
435
|
+
};
|
436
|
+
out.replace(template, replacement.as_str())
|
437
|
+
},
|
438
|
+
)
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
/// Export single image.
|
443
|
+
fn export_image_page(
|
444
|
+
config: &CompileConfig,
|
445
|
+
page: &Page,
|
446
|
+
output: &Output,
|
447
|
+
fmt: ImageExportFormat,
|
448
|
+
) -> StrResult<()> {
|
449
|
+
match fmt {
|
450
|
+
ImageExportFormat::Png => {
|
451
|
+
let pixmap = typst_render::render(page, config.ppi / 72.0);
|
452
|
+
let buf = pixmap
|
453
|
+
.encode_png()
|
454
|
+
.map_err(|err| eco_format!("failed to encode PNG file ({err})"))?;
|
455
|
+
output
|
456
|
+
.write(&buf)
|
457
|
+
.map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
|
458
|
+
}
|
459
|
+
ImageExportFormat::Svg => {
|
460
|
+
let svg = typst_svg::svg(page);
|
461
|
+
output
|
462
|
+
.write(svg.as_bytes())
|
463
|
+
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
|
464
|
+
}
|
465
|
+
}
|
466
|
+
Ok(())
|
467
|
+
}
|
468
|
+
|
469
|
+
impl Output {
|
470
|
+
fn write(&self, buffer: &[u8]) -> StrResult<()> {
|
471
|
+
match self {
|
472
|
+
Output::Stdout => std::io::stdout().write_all(buffer),
|
473
|
+
Output::Path(path) => fs::write(path, buffer),
|
474
|
+
}
|
475
|
+
.map_err(|err| eco_format!("{err}"))
|
476
|
+
}
|
477
|
+
}
|
478
|
+
|
479
|
+
/// Caches exported files so that we can avoid re-exporting them if they haven't
|
480
|
+
/// changed.
|
481
|
+
///
|
482
|
+
/// This is done by having a list of size `files.len()` that contains the hashes
|
483
|
+
/// of the last rendered frame in each file. If a new frame is inserted, this
|
484
|
+
/// will invalidate the rest of the cache, this is deliberate as to decrease the
|
485
|
+
/// complexity and memory usage of such a cache.
|
486
|
+
pub struct ExportCache {
|
487
|
+
/// The hashes of last compilation's frames.
|
488
|
+
pub cache: RwLock<Vec<u128>>,
|
489
|
+
}
|
490
|
+
|
491
|
+
impl ExportCache {
|
492
|
+
/// Creates a new export cache.
|
493
|
+
pub fn new() -> Self {
|
494
|
+
Self { cache: RwLock::new(Vec::with_capacity(32)) }
|
495
|
+
}
|
496
|
+
|
497
|
+
/// Returns true if the entry is cached and appends the new hash to the
|
498
|
+
/// cache (for the next compilation).
|
499
|
+
pub fn is_cached(&self, i: usize, frame: &Frame) -> bool {
|
500
|
+
let hash = typst::utils::hash128(frame);
|
501
|
+
|
502
|
+
let mut cache = self.cache.upgradable_read();
|
503
|
+
if i >= cache.len() {
|
504
|
+
cache.with_upgraded(|cache| cache.push(hash));
|
505
|
+
return false;
|
506
|
+
}
|
507
|
+
|
508
|
+
cache.with_upgraded(|cache| std::mem::replace(&mut cache[i], hash) == hash)
|
509
|
+
}
|
510
|
+
}
|
511
|
+
|
512
|
+
/// Writes a Makefile rule describing the relationship between the output and
|
513
|
+
/// its dependencies to the path specified by the --make-deps argument, if it
|
514
|
+
/// was provided.
|
515
|
+
fn write_make_deps(
|
516
|
+
world: &mut SystemWorld,
|
517
|
+
config: &CompileConfig,
|
518
|
+
outputs: Vec<Output>,
|
519
|
+
) -> StrResult<()> {
|
520
|
+
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
|
521
|
+
let Ok(output_paths) = outputs
|
522
|
+
.into_iter()
|
523
|
+
.filter_map(|o| match o {
|
524
|
+
Output::Path(path) => Some(path.into_os_string().into_string()),
|
525
|
+
Output::Stdout => None,
|
526
|
+
})
|
527
|
+
.collect::<Result<Vec<_>, _>>()
|
528
|
+
else {
|
529
|
+
bail!("failed to create make dependencies file because output path was not valid unicode")
|
530
|
+
};
|
531
|
+
if output_paths.is_empty() {
|
532
|
+
bail!("failed to create make dependencies file because output was stdout")
|
533
|
+
}
|
534
|
+
|
535
|
+
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
|
536
|
+
// perfect as some special characters can't be escaped.
|
537
|
+
fn munge(s: &str) -> String {
|
538
|
+
let mut res = String::with_capacity(s.len());
|
539
|
+
let mut slashes = 0;
|
540
|
+
for c in s.chars() {
|
541
|
+
match c {
|
542
|
+
'\\' => slashes += 1,
|
543
|
+
'$' => {
|
544
|
+
res.push('$');
|
545
|
+
slashes = 0;
|
546
|
+
}
|
547
|
+
':' => {
|
548
|
+
res.push('\\');
|
549
|
+
slashes = 0;
|
550
|
+
}
|
551
|
+
' ' | '\t' => {
|
552
|
+
// `munge`'s source contains a comment here that says: "A
|
553
|
+
// space or tab preceded by 2N+1 backslashes represents N
|
554
|
+
// backslashes followed by space..."
|
555
|
+
for _ in 0..slashes + 1 {
|
556
|
+
res.push('\\');
|
557
|
+
}
|
558
|
+
slashes = 0;
|
559
|
+
}
|
560
|
+
'#' => {
|
561
|
+
res.push('\\');
|
562
|
+
slashes = 0;
|
563
|
+
}
|
564
|
+
_ => slashes = 0,
|
565
|
+
};
|
566
|
+
res.push(c);
|
567
|
+
}
|
568
|
+
res
|
569
|
+
}
|
570
|
+
|
571
|
+
fn write(
|
572
|
+
make_deps_path: &Path,
|
573
|
+
output_paths: Vec<String>,
|
574
|
+
root: PathBuf,
|
575
|
+
dependencies: impl Iterator<Item = PathBuf>,
|
576
|
+
) -> io::Result<()> {
|
577
|
+
let mut file = File::create(make_deps_path)?;
|
578
|
+
let current_dir = std::env::current_dir()?;
|
579
|
+
let relative_root = diff_paths(&root, ¤t_dir).unwrap_or(root.clone());
|
580
|
+
|
581
|
+
for (i, output_path) in output_paths.into_iter().enumerate() {
|
582
|
+
if i != 0 {
|
583
|
+
file.write_all(b" ")?;
|
584
|
+
}
|
585
|
+
file.write_all(munge(&output_path).as_bytes())?;
|
586
|
+
}
|
587
|
+
file.write_all(b":")?;
|
588
|
+
for dependency in dependencies {
|
589
|
+
let relative_dependency = match dependency.strip_prefix(&root) {
|
590
|
+
Ok(root_relative_dependency) => {
|
591
|
+
relative_root.join(root_relative_dependency)
|
592
|
+
}
|
593
|
+
Err(_) => dependency,
|
594
|
+
};
|
595
|
+
let Some(relative_dependency) = relative_dependency.to_str() else {
|
596
|
+
// Silently skip paths that aren't valid unicode so we still
|
597
|
+
// produce a rule that will work for the other paths that can be
|
598
|
+
// processed.
|
599
|
+
continue;
|
600
|
+
};
|
601
|
+
|
602
|
+
file.write_all(b" ")?;
|
603
|
+
file.write_all(munge(relative_dependency).as_bytes())?;
|
604
|
+
}
|
605
|
+
file.write_all(b"\n")?;
|
606
|
+
|
607
|
+
Ok(())
|
608
|
+
}
|
609
|
+
|
610
|
+
write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
|
611
|
+
.map_err(|err| {
|
612
|
+
eco_format!("failed to create make dependencies file due to IO error ({err})")
|
613
|
+
})
|
614
|
+
}
|
615
|
+
|
616
|
+
/// Opens the output if desired.
|
617
|
+
fn open_output(config: &mut CompileConfig) -> StrResult<()> {
|
618
|
+
let Some(viewer) = config.open.take() else { return Ok(()) };
|
619
|
+
|
620
|
+
// #[cfg(feature = "http-server")]
|
621
|
+
// if let Some(server) = &config.server {
|
622
|
+
// let url = format!("http://{}", server.addr());
|
623
|
+
// return open_path(OsStr::new(&url), viewer.as_deref());
|
624
|
+
// }
|
625
|
+
|
626
|
+
// Can't open stdout.
|
627
|
+
let Output::Path(path) = &config.output else { return Ok(()) };
|
628
|
+
|
629
|
+
// Some resource openers require the path to be canonicalized.
|
630
|
+
let path = path
|
631
|
+
.canonicalize()
|
632
|
+
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
|
633
|
+
|
634
|
+
open_path(path.as_os_str(), viewer.as_deref())
|
635
|
+
}
|
636
|
+
|
637
|
+
/// Opens the given file using:
|
638
|
+
///
|
639
|
+
/// - The default file viewer if `app` is `None`.
|
640
|
+
/// - The given viewer provided by `app` if it is `Some`.
|
641
|
+
fn open_path(path: &OsStr, viewer: Option<&str>) -> StrResult<()> {
|
642
|
+
if let Some(viewer) = viewer {
|
643
|
+
open::with_detached(path, viewer)
|
644
|
+
.map_err(|err| eco_format!("failed to open file with {} ({})", viewer, err))
|
645
|
+
} else {
|
646
|
+
open::that_detached(path).map_err(|err| {
|
647
|
+
let openers = open::commands(path)
|
648
|
+
.iter()
|
649
|
+
.map(|command| command.get_program().to_string_lossy())
|
650
|
+
.collect::<Vec<_>>()
|
651
|
+
.join(", ");
|
652
|
+
eco_format!(
|
653
|
+
"failed to open file with any of these resource openers: {} ({})",
|
654
|
+
openers,
|
655
|
+
err,
|
656
|
+
)
|
657
|
+
})
|
658
|
+
}
|
659
|
+
}
|
660
|
+
|
661
|
+
/// Print diagnostic messages to the terminal.
|
662
|
+
pub fn print_diagnostics(
|
663
|
+
world: &SystemWorld,
|
664
|
+
errors: &[SourceDiagnostic],
|
665
|
+
warnings: &[SourceDiagnostic],
|
666
|
+
diagnostic_format: DiagnosticFormat,
|
667
|
+
) -> Result<(), codespan_reporting::files::Error> {
|
668
|
+
let mut config = term::Config { tab_width: 2, ..Default::default() };
|
669
|
+
if diagnostic_format == DiagnosticFormat::Short {
|
670
|
+
config.display_style = term::DisplayStyle::Short;
|
671
|
+
}
|
672
|
+
|
673
|
+
for diagnostic in warnings.iter().chain(errors) {
|
674
|
+
let diag = match diagnostic.severity {
|
675
|
+
Severity::Error => Diagnostic::error(),
|
676
|
+
Severity::Warning => Diagnostic::warning(),
|
677
|
+
}
|
678
|
+
.with_message(diagnostic.message.clone())
|
679
|
+
.with_notes(
|
680
|
+
diagnostic
|
681
|
+
.hints
|
682
|
+
.iter()
|
683
|
+
.map(|e| (eco_format!("hint: {e}")).into())
|
684
|
+
.collect(),
|
685
|
+
)
|
686
|
+
.with_labels(label(world, diagnostic.span).into_iter().collect());
|
687
|
+
|
688
|
+
term::emit(&mut terminal::out(), &config, world, &diag)?;
|
689
|
+
|
690
|
+
// Stacktrace-like helper diagnostics.
|
691
|
+
for point in &diagnostic.trace {
|
692
|
+
let message = point.v.to_string();
|
693
|
+
let help = Diagnostic::help()
|
694
|
+
.with_message(message)
|
695
|
+
.with_labels(label(world, point.span).into_iter().collect());
|
696
|
+
|
697
|
+
term::emit(&mut terminal::out(), &config, world, &help)?;
|
698
|
+
}
|
699
|
+
}
|
700
|
+
|
701
|
+
Ok(())
|
702
|
+
}
|
703
|
+
|
704
|
+
/// Create a label for a span.
|
705
|
+
fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
|
706
|
+
Some(Label::primary(span.id()?, world.range(span)?))
|
707
|
+
}
|
708
|
+
|
709
|
+
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
|
710
|
+
type FileId = FileId;
|
711
|
+
type Name = String;
|
712
|
+
type Source = Source;
|
713
|
+
|
714
|
+
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
|
715
|
+
let vpath = id.vpath();
|
716
|
+
Ok(if let Some(package) = id.package() {
|
717
|
+
format!("{package}{}", vpath.as_rooted_path().display())
|
718
|
+
} else {
|
719
|
+
// Try to express the path relative to the working directory.
|
720
|
+
vpath
|
721
|
+
.resolve(self.root())
|
722
|
+
.and_then(|abs| pathdiff::diff_paths(abs, self.workdir()))
|
723
|
+
.as_deref()
|
724
|
+
.unwrap_or_else(|| vpath.as_rootless_path())
|
725
|
+
.to_string_lossy()
|
726
|
+
.into()
|
727
|
+
})
|
728
|
+
}
|
729
|
+
|
730
|
+
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
|
731
|
+
Ok(self.lookup(id))
|
732
|
+
}
|
733
|
+
|
734
|
+
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
|
735
|
+
let source = self.lookup(id);
|
736
|
+
source
|
737
|
+
.byte_to_line(given)
|
738
|
+
.ok_or_else(|| CodespanError::IndexTooLarge {
|
739
|
+
given,
|
740
|
+
max: source.len_bytes(),
|
741
|
+
})
|
742
|
+
}
|
743
|
+
|
744
|
+
fn line_range(
|
745
|
+
&'a self,
|
746
|
+
id: FileId,
|
747
|
+
given: usize,
|
748
|
+
) -> CodespanResult<std::ops::Range<usize>> {
|
749
|
+
let source = self.lookup(id);
|
750
|
+
source
|
751
|
+
.line_to_range(given)
|
752
|
+
.ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() })
|
753
|
+
}
|
754
|
+
|
755
|
+
fn column_number(
|
756
|
+
&'a self,
|
757
|
+
id: FileId,
|
758
|
+
_: usize,
|
759
|
+
given: usize,
|
760
|
+
) -> CodespanResult<usize> {
|
761
|
+
let source = self.lookup(id);
|
762
|
+
source.byte_to_column(given).ok_or_else(|| {
|
763
|
+
let max = source.len_bytes();
|
764
|
+
if given <= max {
|
765
|
+
CodespanError::InvalidCharBoundary { given }
|
766
|
+
} else {
|
767
|
+
CodespanError::IndexTooLarge { given, max }
|
768
|
+
}
|
769
|
+
})
|
770
|
+
}
|
771
|
+
}
|