|
1 | | -use std::path::{Component, Path, PathBuf}; |
| 1 | +use std::collections::HashSet; |
| 2 | +use std::path::Path; |
2 | 3 |
|
3 | 4 | use codespan_reporting::diagnostic::{Diagnostic, Label}; |
4 | 5 | use ignore::overrides::Override; |
5 | | -use typst::syntax::{FileId, VirtualPath}; |
6 | 6 |
|
7 | | -use crate::check::{Diagnostics, Result, TryExt}; |
| 7 | +use crate::check::path::PackagePath; |
| 8 | +use crate::check::Diagnostics; |
8 | 9 |
|
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); |
11 | 17 |
|
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 }; |
18 | 25 | let Ok(metadata) = ch.metadata() else { |
19 | 26 | continue; |
20 | 27 | }; |
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; |
29 | 39 | } |
| 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); |
30 | 51 | } |
31 | | - Ok(result) |
32 | 52 | } |
33 | 53 |
|
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( |
36 | 65 | 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 |
45 | 73 |
|
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 | + } |
67 | 155 | } |
| 156 | + |
| 157 | + // TODO: ideally this should be async |
| 158 | + std::fs::remove_file(out).ok(); |
68 | 159 | } |
| 160 | +} |
69 | 161 |
|
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 | + } |
71 | 180 | } |
72 | 181 |
|
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 | + ); |
85 | 201 | } |
0 commit comments