Skip to content

Commit 06fafa5

Browse files
committed
Refactor file checking
1 parent 4bec9b5 commit 06fafa5

5 files changed

Lines changed: 335 additions & 291 deletions

File tree

src/check.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ use typst::{
66
WorldExt,
77
};
88

9-
use crate::{
10-
check::{files::forbid_font_files, readme::check_readme},
11-
world::SystemWorld,
12-
};
9+
use crate::check::readme::check_readme;
10+
use crate::world::SystemWorld;
1311

1412
pub mod authors;
1513
mod compile;
@@ -18,6 +16,7 @@ mod files;
1816
mod imports;
1917
mod kebab_case;
2018
mod manifest;
19+
mod path;
2120
mod readme;
2221

2322
pub use diagnostics::{Diagnostics, Result, TryExt};
@@ -41,9 +40,6 @@ pub async fn all_checks(
4140
diags.extend(template_diags, template_dir);
4241
}
4342

44-
let res = forbid_font_files(&package_dir, &mut diags);
45-
diags.maybe_emit(res);
46-
4743
let res = check_readme(&worlds.package, &mut diags).await;
4844
diags.maybe_emit(res);
4945

src/check/files.rs

Lines changed: 180 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,201 @@
1-
use std::path::{Component, Path, PathBuf};
1+
use std::collections::HashSet;
2+
use std::path::Path;
23

34
use codespan_reporting::diagnostic::{Diagnostic, Label};
45
use ignore::overrides::Override;
5-
use typst::syntax::{FileId, VirtualPath};
66

7-
use crate::check::{Diagnostics, Result, TryExt};
7+
use crate::check::path::PackagePath;
8+
use crate::check::Diagnostics;
89

9-
/// Size (in bytes) after which a file is considered large.
10-
const SIZE_THRESHOLD: u64 = 1024 * 1024; // 1 MB
10+
pub fn check_files(
11+
diags: &mut Diagnostics,
12+
package_dir: &Path,
13+
exclude: &Override,
14+
thumbnail_path: Option<PackagePath>,
15+
) {
16+
let thumbnail_path = thumbnail_path.as_ref().map(PackagePath::as_path);
1117

12-
pub fn find_large_files(dir: &Path, exclude: Override) -> Result<Vec<(PathBuf, u64)>> {
13-
let mut result = Vec::new();
14-
for ch in ignore::WalkBuilder::new(dir).overrides(exclude).build() {
15-
let Ok(ch) = ch else {
16-
continue;
17-
};
18+
// Manually keep track of excluded directories, to figure out if nested
19+
// files are ignored. This is done, so we can generate diagnostics for
20+
// excluded files.
21+
let mut excluded_dirs = HashSet::new();
22+
23+
for ch in ignore::WalkBuilder::new(package_dir).hidden(false).build() {
24+
let Ok(ch) = ch else { continue };
1825
let Ok(metadata) = ch.metadata() else {
1926
continue;
2027
};
21-
if metadata.is_file() && metadata.len() > SIZE_THRESHOLD {
22-
result.push((
23-
ch.path()
24-
.strip_prefix(dir)
25-
.error("internal", "Prefix striping failed even though child path (`ch`) was constructed from parent path (`dir`)")?
26-
.to_owned(),
27-
metadata.len(),
28-
))
28+
29+
let file_path = PackagePath::from_full(package_dir, ch.path());
30+
31+
if metadata.is_dir() {
32+
// If the parent directory is ignored, all children are ignored too.
33+
if parent_is_excluded(&excluded_dirs, file_path)
34+
|| exclude.matched(file_path.relative(), true).is_ignore()
35+
{
36+
excluded_dirs.insert(ch.into_path());
37+
}
38+
continue;
2939
}
40+
41+
// The thumbnail is always excluded.
42+
let is_thumbnail = Some(file_path) == thumbnail_path;
43+
let excluded = is_thumbnail
44+
|| parent_is_excluded(&excluded_dirs, file_path)
45+
|| exclude.matched(file_path.relative(), false).is_ignore();
46+
47+
forbid_font_files(diags, file_path);
48+
49+
exclude_large_files(diags, file_path, excluded, metadata.len());
50+
exclude_examples_and_tests(diags, file_path, excluded);
3051
}
31-
Ok(result)
3252
}
3353

34-
pub fn forbid_font_files(
35-
package_dir: &Path,
54+
fn parent_is_excluded(
55+
excluded_dirs: &HashSet<std::path::PathBuf>,
56+
file_path: PackagePath<&Path>,
57+
) -> bool {
58+
file_path
59+
.full()
60+
.parent()
61+
.is_some_and(|parent| excluded_dirs.contains(parent))
62+
}
63+
64+
fn exclude_large_files(
3665
diags: &mut Diagnostics,
37-
) -> std::result::Result<(), Diagnostic<FileId>> {
38-
for ch in ignore::WalkBuilder::new(package_dir).build() {
39-
let Ok(ch) = ch else {
40-
continue;
41-
};
42-
let Ok(metadata) = ch.metadata() else {
43-
continue;
44-
};
66+
path: PackagePath<&Path>,
67+
excluded: bool,
68+
size: u64,
69+
) {
70+
/// Size (in bytes) after which a file is considered large.
71+
const LARGE: u64 = 1024 * 1024; // 1 MB
72+
const REALLY_LARGE: u64 = 50 * 1024 * 1024; // 50 MB
4573

46-
let ext = ch
47-
.path()
48-
.extension()
49-
.and_then(|e| e.to_str())
50-
.unwrap_or_default()
51-
.to_lowercase();
52-
if metadata.is_file() && (&ext == "otf" || &ext == "ttf") {
53-
let file_id = FileId::new(None, VirtualPath::new(ch.path().strip_prefix(package_dir)
54-
.error("internal", "Prefix striping failed even though child path (`ch`) was constructed from parent path (`dir`)")?
55-
));
56-
diags.emit(
57-
Diagnostic::error()
58-
.with_label(Label::primary(file_id, 0..0))
59-
.with_code("files/fonts")
60-
.with_message(
61-
"Font files are not allowed.\n\n\
62-
Delete them and instruct your users to install them manually, \
63-
in your README and/or in a documentation comment.\n\n\
64-
More details: https://github.com/typst/packages/blob/main/docs/resources.md#fonts-are-not-supported-in-packages",
65-
),
66-
);
74+
if size < LARGE {
75+
return;
76+
}
77+
78+
if path.full().extension().is_some_and(|ext| ext == "wasm") {
79+
check_wasm_file_size(diags, path, size);
80+
// Don't suggest to exclude WASM files, they are generally necessary
81+
// for the package to work.
82+
return;
83+
}
84+
85+
let (code, message) = if size > REALLY_LARGE {
86+
(
87+
"size/extra-large",
88+
format!(
89+
"This file is really large ({size}MB). \
90+
If possible, do not include it in this repository at all.",
91+
size = size / 1024 / 1024
92+
),
93+
)
94+
} else if !excluded {
95+
(
96+
"size/large",
97+
format!(
98+
"This file is quite large ({size}MB). \
99+
If it is not required to use the package \
100+
(i.e. it is a documentation file, or part of an example), \
101+
it should be added to `exclude` in your `typst.toml`.",
102+
size = size / 1024 / 1024
103+
),
104+
)
105+
} else {
106+
return;
107+
};
108+
109+
diags.emit(
110+
Diagnostic::warning()
111+
.with_code(code)
112+
.with_label(Label::primary(path.file_id(), 0..0))
113+
.with_message(message),
114+
)
115+
}
116+
117+
fn check_wasm_file_size(diags: &mut Diagnostics, path: PackagePath<&Path>, original_size: u64) {
118+
let Some(file_name) = path.full().file_name() else {
119+
return;
120+
};
121+
let out = std::env::temp_dir().join(file_name);
122+
123+
let wasm_opt_result = wasm_opt::OptimizationOptions::new_optimize_for_size()
124+
// Explicitely enable and disable features to best match what wasmi supports
125+
// https://github.com/wasmi-labs/wasmi?tab=readme-ov-file#webassembly-proposals
126+
.enable_feature(wasm_opt::Feature::MutableGlobals)
127+
.enable_feature(wasm_opt::Feature::TruncSat)
128+
.enable_feature(wasm_opt::Feature::SignExt)
129+
.enable_feature(wasm_opt::Feature::Multivalue)
130+
.enable_feature(wasm_opt::Feature::BulkMemory)
131+
.enable_feature(wasm_opt::Feature::ReferenceTypes)
132+
.enable_feature(wasm_opt::Feature::TailCall)
133+
.enable_feature(wasm_opt::Feature::ExtendedConst)
134+
.enable_feature(wasm_opt::Feature::MultiMemory)
135+
.enable_feature(wasm_opt::Feature::Simd)
136+
.disable_feature(wasm_opt::Feature::RelaxedSimd)
137+
.disable_feature(wasm_opt::Feature::Gc)
138+
.disable_feature(wasm_opt::Feature::ExceptionHandling)
139+
.run(path.full(), &out);
140+
141+
if wasm_opt_result.is_ok() {
142+
if let Ok(new_size) = std::fs::metadata(&out).map(|m| m.len()) {
143+
let diff = (original_size - new_size) / 1024;
144+
145+
if diff > 20 {
146+
diags.emit(
147+
Diagnostic::warning()
148+
.with_label(Label::primary(path.file_id(), 0..0))
149+
.with_code("size/wasm")
150+
.with_message(format!(
151+
"This file could be {diff}kB smaller with `wasm-opt -Os`."
152+
)),
153+
);
154+
}
67155
}
156+
157+
// TODO: ideally this should be async
158+
std::fs::remove_file(out).ok();
68159
}
160+
}
69161

70-
Ok(())
162+
fn exclude_examples_and_tests(diags: &mut Diagnostics, path: PackagePath<&Path>, excluded: bool) {
163+
if excluded {
164+
return;
165+
}
166+
167+
let file_name = path.file_name().to_string_lossy();
168+
let warning = || Diagnostic::warning().with_label(Label::primary(path.file_id(), 0..0));
169+
if file_name.contains("example") {
170+
diags.emit(warning().with_code("exclude/example").with_message(
171+
"This file seems to be an example, \
172+
and should probably be added to `exclude` in your `typst.toml`.",
173+
));
174+
} else if file_name.contains("test") {
175+
diags.emit(warning().with_code("exclude/test").with_message(
176+
"This file seems to be a test, \
177+
and should probably be added to `exclude` in your `typst.toml`.",
178+
));
179+
}
71180
}
72181

73-
/// Stips any any leading root components (`/` or `\`) of the path before
74-
/// joining it to the `root` path.
75-
///
76-
/// Absolute paths (starting with `/` or `\`) replace the complete path when
77-
/// `join`ed with a parent path.
78-
pub fn path_relative_to(root: &Path, path: &Path) -> PathBuf {
79-
let components = path
80-
.components()
81-
.skip_while(|c| matches!(c, Component::RootDir))
82-
.map(|c| Path::new(c.as_os_str()));
83-
84-
PathBuf::from_iter(std::iter::once(root).chain(components))
182+
fn forbid_font_files(diags: &mut Diagnostics, path: PackagePath<&Path>) {
183+
let Some(ext) = path.full().extension() else {
184+
return;
185+
};
186+
if !(ext == "otf" || ext == "ttf") {
187+
return;
188+
}
189+
190+
diags.emit(
191+
Diagnostic::error()
192+
.with_label(Label::primary(path.file_id(), 0..0))
193+
.with_code("files/fonts")
194+
.with_message(
195+
"Font files are not allowed.\n\n\
196+
Delete them and instruct your users to install them manually, \
197+
in your README and/or in a documentation comment.\n\n\
198+
More details: https://github.com/typst/packages/blob/main/docs/resources.md#fonts-are-not-supported-in-packages",
199+
),
200+
);
85201
}

0 commit comments

Comments
 (0)