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