tinymist_project/lock/
system.rs1use std::cmp::Ordering;
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::{path::Path, sync::Arc};
4
5use ecow::{EcoVec, eco_vec};
6use tinymist_std::error::prelude::*;
7use tinymist_std::path::unix_slash;
8use tinymist_std::{ImmutPath, bail};
9#[cfg(feature = "lsp")]
10use tinymist_task::CtxPath;
11#[cfg(feature = "lsp")]
12use typst::World;
13use typst::diag::EcoString;
14
15#[cfg(feature = "lsp")]
16use crate::model::ResourcePath;
17use crate::model::{ApplyProjectTask, Id, ProjectInput, ProjectRoute};
18use crate::{LOCK_FILENAME, LOCK_VERSION, LockFile, LockFileCompat, ProjectPathMaterial};
19
20impl LockFile {
21 pub fn get_document(&self, id: &Id) -> Option<&ProjectInput> {
23 self.document.iter().find(|i| &i.id == id)
24 }
25
26 pub fn get_task(&self, id: &Id) -> Option<&ApplyProjectTask> {
28 self.task.iter().find(|i| &i.id == id)
29 }
30
31 pub fn replace_document(&mut self, mut input: ProjectInput) {
33 input.lock_dir = None;
34 let input = input;
35 let id = input.id.clone();
36 let index = self.document.iter().position(|i| i.id == id);
37 if let Some(index) = index {
38 self.document[index] = input;
39 } else {
40 self.document.push(input);
41 }
42 }
43
44 pub fn replace_task(&mut self, mut task: ApplyProjectTask) {
46 if let Some(pat) = task.task.as_export_mut().and_then(|t| t.output.as_mut()) {
47 let rel = pat.clone().relative_to(self.lock_dir.as_ref().unwrap());
48 *pat = rel;
49 }
50
51 let task = task;
52
53 let id = task.id().clone();
54 let index = self.task.iter().position(|i| *i.id() == id);
55 if let Some(index) = index {
56 self.task[index] = task;
57 } else {
58 self.task.push(task);
59 }
60 }
61
62 pub fn replace_route(&mut self, route: ProjectRoute) {
64 let id = route.id.clone();
65
66 self.route.retain(|i| i.id != id);
67 self.route.push(route);
68 }
69
70 pub fn sort(&mut self) {
72 self.document.sort_by(|a, b| a.id.cmp(&b.id));
73 self.task
74 .sort_by(|a, b| a.doc_id().cmp(b.doc_id()).then_with(|| a.id().cmp(b.id())));
75 }
77
78 pub fn serialize_resolve(&self) -> String {
80 let content = toml::Table::try_from(self).unwrap();
81
82 let mut out = String::new();
83
84 let marker_line = "# This file is automatically @generated by tinymist.";
88 let extra_line = "# It is not intended for manual editing.";
89
90 out.push_str(marker_line);
91 out.push('\n');
92 out.push_str(extra_line);
93 out.push('\n');
94
95 out.push_str(&format!("version = {LOCK_VERSION:?}\n"));
96
97 let document = content.get("document");
98 if let Some(document) = document {
99 for document in document.as_array().unwrap() {
100 out.push('\n');
101 out.push_str("[[document]]\n");
102 emit_document(document, &mut out);
103 }
104 }
105
106 let route = content.get("route");
107 if let Some(route) = route {
108 for route in route.as_array().unwrap() {
109 out.push('\n');
110 out.push_str("[[route]]\n");
111 emit_route(route, &mut out);
112 }
113 }
114
115 let task = content.get("task");
116 if let Some(task) = task {
117 for task in task.as_array().unwrap() {
118 out.push('\n');
119 out.push_str("[[task]]\n");
120 emit_output(task, &mut out);
121 }
122 }
123
124 return out;
125
126 fn emit_document(input: &toml::Value, out: &mut String) {
127 let table = input.as_table().unwrap();
128 out.push_str(&table.to_string());
129 }
130
131 fn emit_output(output: &toml::Value, out: &mut String) {
132 let mut table = output.clone();
133 let table = table.as_table_mut().unwrap();
134 if let Some(transform) = table.remove("transform") {
136 let mut task_table = toml::Table::new();
137 task_table.insert("transform".to_string(), transform);
138
139 table.insert("task".to_string(), task_table.into());
140 }
141
142 out.push_str(&table.to_string());
143 }
144
145 fn emit_route(route: &toml::Value, out: &mut String) {
146 let table = route.as_table().unwrap();
147 out.push_str(&table.to_string());
148 }
149 }
150
151 pub fn update(cwd: &Path, f: impl FnOnce(&mut Self) -> Result<()>) -> Result<()> {
153 let fs = tinymist_std::fs::flock::Filesystem::new(cwd.to_owned());
154
155 let mut lock_file = fs
156 .open_rw_exclusive_create(LOCK_FILENAME, "project commands")
157 .context("tinymist.lock")?;
158
159 let mut data = vec![];
160 lock_file.read_to_end(&mut data).context("read lock")?;
161
162 let old_data =
163 std::str::from_utf8(&data).context("tinymist.lock file is not valid utf-8")?;
164
165 let mut state = if old_data.trim().is_empty() {
166 LockFile {
167 lock_dir: Some(ImmutPath::from(cwd)),
169 document: vec![],
170 task: vec![],
171 route: eco_vec![],
172 }
173 } else {
174 let old_state = toml::from_str::<LockFileCompat>(old_data)
175 .context_ut("tinymist.lock file is not a valid TOML file")?;
176
177 let version = old_state.version()?;
178 match Version(version).partial_cmp(&Version(LOCK_VERSION)) {
179 Some(Ordering::Equal | Ordering::Less) => {}
180 Some(Ordering::Greater) => {
181 bail!(
182 "trying to update lock file having a future version, current tinymist-cli supports {LOCK_VERSION}, the lock file is {version}",
183 );
184 }
185 None => {
186 bail!(
187 "cannot compare version, are version strings in right format? current tinymist-cli supports {LOCK_VERSION}, the lock file is {version}",
188 );
189 }
190 }
191
192 let mut lf = old_state.migrate()?;
193 lf.lock_dir = Some(ImmutPath::from(cwd));
194 lf
195 };
196
197 f(&mut state)?;
198
199 state.sort();
201 let new_data = state.serialize_resolve();
202
203 if old_data == new_data {
206 return Ok(());
207 }
208
209 lock_file.file().set_len(0).context(LOCK_FILENAME)?;
215 lock_file.seek(SeekFrom::Start(0)).context(LOCK_FILENAME)?;
216 lock_file
217 .write_all(new_data.as_bytes())
218 .context(LOCK_FILENAME)?;
219
220 Ok(())
221 }
222
223 pub fn read(dir: &Path) -> Result<Self> {
225 let fs = tinymist_std::fs::flock::Filesystem::new(dir.to_owned());
226
227 let mut lock_file = fs
228 .open_ro_shared(LOCK_FILENAME, "project commands")
229 .context(LOCK_FILENAME)?;
230
231 let mut data = vec![];
232 lock_file.read_to_end(&mut data).context(LOCK_FILENAME)?;
233
234 let data = std::str::from_utf8(&data).context("tinymist.lock file is not valid utf-8")?;
235
236 let state = toml::from_str::<LockFileCompat>(data)
237 .context_ut("tinymist.lock file is not a valid TOML file")?;
238
239 let mut lf = state.migrate()?;
240 lf.lock_dir = Some(dir.into());
241 Ok(lf)
242 }
243}
244
245pub fn update_lock(root: ImmutPath) -> LockFileUpdate {
247 LockFileUpdate {
248 root,
249 updates: vec![],
250 }
251}
252
253enum LockUpdate {
254 #[cfg(feature = "lsp")]
255 Input(ProjectInput),
256 Task(ApplyProjectTask),
257 Material(ProjectPathMaterial),
258 Route(ProjectRoute),
259}
260
261pub struct LockFileUpdate {
263 root: Arc<Path>,
264 updates: Vec<LockUpdate>,
265}
266
267impl LockFileUpdate {
268 #[cfg(feature = "lsp")]
270 pub fn compiled(&mut self, world: &crate::LspWorld, ctx: CtxPath) -> Option<Id> {
271 let id = Id::from_world(world, ctx)?;
272
273 let root = ResourcePath::from_user_sys(Path::new("."), ctx);
274 let main =
275 ResourcePath::from_user_sys(world.path_for_id(world.main()).ok()?.as_path(), ctx);
276
277 let font_resolver = &world.font_resolver;
278 let font_paths = font_resolver
279 .font_paths()
280 .iter()
281 .map(|p| ResourcePath::from_user_sys(p, ctx))
282 .collect::<Vec<_>>();
283
284 let registry = &world.registry;
287 let package_path = registry
288 .package_path()
289 .map(|p| ResourcePath::from_user_sys(p, ctx));
290 let package_cache_path = registry
291 .package_cache_path()
292 .map(|p| ResourcePath::from_user_sys(p, ctx));
293
294 let _ = package_cache_path;
296 let _ = package_path;
297
298 let input = ProjectInput {
301 id: id.clone(),
302 lock_dir: Some(ctx.1.to_path_buf()),
303 root: Some(root),
304 main,
305 inputs: vec![],
306 font_paths,
307 system_fonts: true, package_path: None,
309 package_cache_path: None,
310 };
311
312 self.updates.push(LockUpdate::Input(input));
313
314 Some(id)
315 }
316
317 pub fn task(&mut self, task: ApplyProjectTask) {
319 self.updates.push(LockUpdate::Task(task));
320 }
321
322 pub fn update_materials(&mut self, doc_id: Id, files: EcoVec<ImmutPath>) {
324 self.updates
325 .push(LockUpdate::Material(ProjectPathMaterial::from_deps(
326 doc_id, files,
327 )));
328 }
329
330 pub fn route(&mut self, doc_id: Id, priority: u32) {
332 self.updates.push(LockUpdate::Route(ProjectRoute {
333 id: doc_id,
334 priority,
335 }));
336 }
337
338 pub fn commit(self) {
340 crate::LockFile::update(&self.root, |l| {
341 let root: EcoString = unix_slash(&self.root).into();
342 let root_hash = tinymist_std::hash::hash128(&root);
343 for update in self.updates {
344 match update {
345 #[cfg(feature = "lsp")]
346 LockUpdate::Input(input) => {
347 l.replace_document(input);
348 }
349 LockUpdate::Task(task) => {
350 l.replace_task(task);
351 }
352 LockUpdate::Material(mut mat) => {
353 let root: EcoString = unix_slash(&self.root).into();
354 mat.root = root.clone();
355 let cache_dir = dirs::cache_dir();
356 if let Some(cache_dir) = cache_dir {
357 let id = tinymist_std::hash::hash128(&mat.id);
358 let root_lo = root_hash & 0xfff;
359 let root_hi = root_hash >> 12;
360 let id_lo = id & 0xfff;
361 let id_hi = id >> 12;
362
363 let hash_str =
364 format!("{root_lo:03x}/{root_hi:013x}/{id_lo:03x}/{id_hi:013x}");
365
366 let cache_dir = cache_dir.join("tinymist/projects").join(hash_str);
367 let _ = std::fs::create_dir_all(&cache_dir);
368
369 let data = serde_json::to_string(&mat).unwrap();
370 let path = cache_dir.join("path-material.json");
371 tinymist_std::fs::paths::write_atomic(path, data)
372 .log_error("ProjectCompiler: write material error");
373
374 }
376 }
378 LockUpdate::Route(route) => {
379 l.replace_route(route);
380 }
381 }
382 }
383
384 Ok(())
385 })
386 .log_error("ProjectCompiler: lock file error");
387 }
388}
389
390struct Version<'a>(&'a str);
394
395impl PartialEq for Version<'_> {
396 fn eq(&self, other: &Self) -> bool {
397 semver::Version::parse(self.0)
398 .ok()
399 .and_then(|a| semver::Version::parse(other.0).ok().map(|b| a == b))
400 .unwrap_or(false)
401 }
402}
403
404impl PartialOrd for Version<'_> {
405 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
406 let lhs = semver::Version::parse(self.0).ok()?;
407 let rhs = semver::Version::parse(other.0).ok()?;
408 Some(lhs.cmp(&rhs))
409 }
410}