mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-23 04:45:21 +00:00
387 lines
13 KiB
Rust
387 lines
13 KiB
Rust
#![allow(missing_docs)]
|
|
|
|
use std::cmp::Ordering;
|
|
use std::io::{Read, Seek, SeekFrom, Write};
|
|
use std::{path::Path, sync::Arc};
|
|
|
|
use ecow::{eco_vec, EcoVec};
|
|
use tinymist_std::error::prelude::*;
|
|
use tinymist_std::path::unix_slash;
|
|
use tinymist_std::{bail, ImmutPath};
|
|
use tinymist_task::CtxPath;
|
|
use typst::diag::EcoString;
|
|
use typst::World;
|
|
|
|
use crate::model::{ApplyProjectTask, Id, ProjectInput, ProjectRoute, ResourcePath};
|
|
use crate::{LockFile, LockFileCompat, LspWorld, ProjectPathMaterial, LOCK_FILENAME, LOCK_VERSION};
|
|
|
|
impl LockFile {
|
|
pub fn get_document(&self, id: &Id) -> Option<&ProjectInput> {
|
|
self.document.iter().find(|i| &i.id == id)
|
|
}
|
|
|
|
pub fn get_task(&self, id: &Id) -> Option<&ApplyProjectTask> {
|
|
self.task.iter().find(|i| &i.id == id)
|
|
}
|
|
|
|
pub fn replace_document(&mut self, mut input: ProjectInput) {
|
|
input.lock_dir = None;
|
|
let input = input;
|
|
let id = input.id.clone();
|
|
let index = self.document.iter().position(|i| i.id == id);
|
|
if let Some(index) = index {
|
|
self.document[index] = input;
|
|
} else {
|
|
self.document.push(input);
|
|
}
|
|
}
|
|
|
|
pub fn replace_task(&mut self, mut task: ApplyProjectTask) {
|
|
if let Some(pat) = task.task.as_export_mut().and_then(|t| t.output.as_mut()) {
|
|
let rel = pat.clone().relative_to(self.lock_dir.as_ref().unwrap());
|
|
*pat = rel;
|
|
}
|
|
|
|
let task = task;
|
|
|
|
let id = task.id().clone();
|
|
let index = self.task.iter().position(|i| *i.id() == id);
|
|
if let Some(index) = index {
|
|
self.task[index] = task;
|
|
} else {
|
|
self.task.push(task);
|
|
}
|
|
}
|
|
|
|
pub fn replace_route(&mut self, route: ProjectRoute) {
|
|
let id = route.id.clone();
|
|
|
|
self.route.retain(|i| i.id != id);
|
|
self.route.push(route);
|
|
}
|
|
|
|
pub fn sort(&mut self) {
|
|
self.document.sort_by(|a, b| a.id.cmp(&b.id));
|
|
self.task
|
|
.sort_by(|a, b| a.doc_id().cmp(b.doc_id()).then_with(|| a.id().cmp(b.id())));
|
|
// the route's order is important, so we don't sort them.
|
|
}
|
|
|
|
pub fn serialize_resolve(&self) -> String {
|
|
let content = toml::Table::try_from(self).unwrap();
|
|
|
|
let mut out = String::new();
|
|
|
|
// At the start of the file we notify the reader that the file is generated.
|
|
// Specifically Phabricator ignores files containing "@generated", so we use
|
|
// that.
|
|
let marker_line = "# This file is automatically @generated by tinymist.";
|
|
let extra_line = "# It is not intended for manual editing.";
|
|
|
|
out.push_str(marker_line);
|
|
out.push('\n');
|
|
out.push_str(extra_line);
|
|
out.push('\n');
|
|
|
|
out.push_str(&format!("version = {LOCK_VERSION:?}\n"));
|
|
|
|
let document = content.get("document");
|
|
if let Some(document) = document {
|
|
for document in document.as_array().unwrap() {
|
|
out.push('\n');
|
|
out.push_str("[[document]]\n");
|
|
emit_document(document, &mut out);
|
|
}
|
|
}
|
|
|
|
let route = content.get("route");
|
|
if let Some(route) = route {
|
|
for route in route.as_array().unwrap() {
|
|
out.push('\n');
|
|
out.push_str("[[route]]\n");
|
|
emit_route(route, &mut out);
|
|
}
|
|
}
|
|
|
|
let task = content.get("task");
|
|
if let Some(task) = task {
|
|
for task in task.as_array().unwrap() {
|
|
out.push('\n');
|
|
out.push_str("[[task]]\n");
|
|
emit_output(task, &mut out);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
|
|
fn emit_document(input: &toml::Value, out: &mut String) {
|
|
let table = input.as_table().unwrap();
|
|
out.push_str(&table.to_string());
|
|
}
|
|
|
|
fn emit_output(output: &toml::Value, out: &mut String) {
|
|
let mut table = output.clone();
|
|
let table = table.as_table_mut().unwrap();
|
|
// replace transform with task.transforms
|
|
if let Some(transform) = table.remove("transform") {
|
|
let mut task_table = toml::Table::new();
|
|
task_table.insert("transform".to_string(), transform);
|
|
|
|
table.insert("task".to_string(), task_table.into());
|
|
}
|
|
|
|
out.push_str(&table.to_string());
|
|
}
|
|
|
|
fn emit_route(route: &toml::Value, out: &mut String) {
|
|
let table = route.as_table().unwrap();
|
|
out.push_str(&table.to_string());
|
|
}
|
|
}
|
|
|
|
pub fn update(cwd: &Path, f: impl FnOnce(&mut Self) -> Result<()>) -> Result<()> {
|
|
let fs = tinymist_std::fs::flock::Filesystem::new(cwd.to_owned());
|
|
|
|
let mut lock_file = fs
|
|
.open_rw_exclusive_create(LOCK_FILENAME, "project commands")
|
|
.context("tinymist.lock")?;
|
|
|
|
let mut data = vec![];
|
|
lock_file.read_to_end(&mut data).context("read lock")?;
|
|
|
|
let old_data =
|
|
std::str::from_utf8(&data).context("tinymist.lock file is not valid utf-8")?;
|
|
|
|
let mut state = if old_data.trim().is_empty() {
|
|
LockFile {
|
|
// todo: reduce cost
|
|
lock_dir: Some(ImmutPath::from(cwd)),
|
|
document: vec![],
|
|
task: vec![],
|
|
route: eco_vec![],
|
|
}
|
|
} else {
|
|
let old_state = toml::from_str::<LockFileCompat>(old_data)
|
|
.context_ut("tinymist.lock file is not a valid TOML file")?;
|
|
|
|
let version = old_state.version()?;
|
|
match Version(version).partial_cmp(&Version(LOCK_VERSION)) {
|
|
Some(Ordering::Equal | Ordering::Less) => {}
|
|
Some(Ordering::Greater) => {
|
|
bail!(
|
|
"trying to update lock file having a future version, current tinymist-cli supports {LOCK_VERSION}, the lock file is {version}",
|
|
);
|
|
}
|
|
None => {
|
|
bail!(
|
|
"cannot compare version, are version strings in right format? current tinymist-cli supports {LOCK_VERSION}, the lock file is {version}",
|
|
);
|
|
}
|
|
}
|
|
|
|
let mut lf = old_state.migrate()?;
|
|
lf.lock_dir = Some(ImmutPath::from(cwd));
|
|
lf
|
|
};
|
|
|
|
f(&mut state)?;
|
|
|
|
// todo: for read only operations, we don't have to compare it.
|
|
state.sort();
|
|
let new_data = state.serialize_resolve();
|
|
|
|
// If the lock file contents haven't changed so don't rewrite it. This is
|
|
// helpful on read-only filesystems.
|
|
if old_data == new_data {
|
|
return Ok(());
|
|
}
|
|
|
|
// todo: even if cargo, they don't update the lock file atomically. This
|
|
// indicates that we may get data corruption if the process is killed
|
|
// while writing the lock file. This is sensible because `Cargo.lock` is
|
|
// only a "resolved result" of the `Cargo.toml`. Thus, we should inform
|
|
// users that don't only persist configuration in the lock file.
|
|
lock_file.file().set_len(0).context(LOCK_FILENAME)?;
|
|
lock_file.seek(SeekFrom::Start(0)).context(LOCK_FILENAME)?;
|
|
lock_file
|
|
.write_all(new_data.as_bytes())
|
|
.context(LOCK_FILENAME)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn read(dir: &Path) -> Result<Self> {
|
|
let fs = tinymist_std::fs::flock::Filesystem::new(dir.to_owned());
|
|
|
|
let mut lock_file = fs
|
|
.open_ro_shared(LOCK_FILENAME, "project commands")
|
|
.context(LOCK_FILENAME)?;
|
|
|
|
let mut data = vec![];
|
|
lock_file.read_to_end(&mut data).context(LOCK_FILENAME)?;
|
|
|
|
let data = std::str::from_utf8(&data).context("tinymist.lock file is not valid utf-8")?;
|
|
|
|
let state = toml::from_str::<LockFileCompat>(data)
|
|
.context_ut("tinymist.lock file is not a valid TOML file")?;
|
|
|
|
let mut lf = state.migrate()?;
|
|
lf.lock_dir = Some(dir.into());
|
|
Ok(lf)
|
|
}
|
|
}
|
|
|
|
/// Make a new project lock updater.
|
|
pub fn update_lock(root: ImmutPath) -> LockFileUpdate {
|
|
LockFileUpdate {
|
|
root,
|
|
updates: vec![],
|
|
}
|
|
}
|
|
|
|
enum LockUpdate {
|
|
Input(ProjectInput),
|
|
Task(ApplyProjectTask),
|
|
Material(ProjectPathMaterial),
|
|
Route(ProjectRoute),
|
|
}
|
|
|
|
pub struct LockFileUpdate {
|
|
root: Arc<Path>,
|
|
updates: Vec<LockUpdate>,
|
|
}
|
|
|
|
impl LockFileUpdate {
|
|
pub fn compiled(&mut self, world: &LspWorld, ctx: CtxPath) -> Option<Id> {
|
|
let id = Id::from_world(world, ctx)?;
|
|
|
|
let root = ResourcePath::from_user_sys(Path::new("."), ctx);
|
|
let main =
|
|
ResourcePath::from_user_sys(world.path_for_id(world.main()).ok()?.as_path(), ctx);
|
|
|
|
let font_resolver = &world.font_resolver;
|
|
let font_paths = font_resolver
|
|
.font_paths()
|
|
.iter()
|
|
.map(|p| ResourcePath::from_user_sys(p, ctx))
|
|
.collect::<Vec<_>>();
|
|
|
|
// let system_font = font_resolver.system_font();
|
|
|
|
let registry = &world.registry;
|
|
let package_path = registry
|
|
.package_path()
|
|
.map(|p| ResourcePath::from_user_sys(p, ctx));
|
|
let package_cache_path = registry
|
|
.package_cache_path()
|
|
.map(|p| ResourcePath::from_user_sys(p, ctx));
|
|
|
|
// todo: freeze the package paths
|
|
let _ = package_cache_path;
|
|
let _ = package_path;
|
|
|
|
// todo: freeze the sys.inputs
|
|
|
|
let input = ProjectInput {
|
|
id: id.clone(),
|
|
lock_dir: Some(ctx.1.to_path_buf()),
|
|
root: Some(root),
|
|
main,
|
|
inputs: vec![],
|
|
font_paths,
|
|
system_fonts: true, // !args.font.ignore_system_fonts,
|
|
package_path: None,
|
|
package_cache_path: None,
|
|
};
|
|
|
|
self.updates.push(LockUpdate::Input(input));
|
|
|
|
Some(id)
|
|
}
|
|
|
|
pub fn task(&mut self, task: ApplyProjectTask) {
|
|
self.updates.push(LockUpdate::Task(task));
|
|
}
|
|
|
|
pub fn update_materials(&mut self, doc_id: Id, files: EcoVec<ImmutPath>) {
|
|
self.updates
|
|
.push(LockUpdate::Material(ProjectPathMaterial::from_deps(
|
|
doc_id, files,
|
|
)));
|
|
}
|
|
|
|
pub fn route(&mut self, doc_id: Id, priority: u32) {
|
|
self.updates.push(LockUpdate::Route(ProjectRoute {
|
|
id: doc_id,
|
|
priority,
|
|
}));
|
|
}
|
|
|
|
pub fn commit(self) {
|
|
crate::LockFile::update(&self.root, |l| {
|
|
let root: EcoString = unix_slash(&self.root).into();
|
|
let root_hash = tinymist_std::hash::hash128(&root);
|
|
for update in self.updates {
|
|
match update {
|
|
LockUpdate::Input(input) => {
|
|
l.replace_document(input);
|
|
}
|
|
LockUpdate::Task(task) => {
|
|
l.replace_task(task);
|
|
}
|
|
LockUpdate::Material(mut mat) => {
|
|
let root: EcoString = unix_slash(&self.root).into();
|
|
mat.root = root.clone();
|
|
let cache_dir = dirs::cache_dir();
|
|
if let Some(cache_dir) = cache_dir {
|
|
let id = tinymist_std::hash::hash128(&mat.id);
|
|
let root_lo = root_hash & 0xfff;
|
|
let root_hi = root_hash >> 12;
|
|
let id_lo = id & 0xfff;
|
|
let id_hi = id >> 12;
|
|
|
|
let hash_str =
|
|
format!("{root_lo:03x}/{root_hi:013x}/{id_lo:03x}/{id_hi:013x}");
|
|
|
|
let cache_dir = cache_dir.join("tinymist/projects").join(hash_str);
|
|
let _ = std::fs::create_dir_all(&cache_dir);
|
|
|
|
let data = serde_json::to_string(&mat).unwrap();
|
|
let path = cache_dir.join("path-material.json");
|
|
tinymist_std::fs::paths::write_atomic(path, data)
|
|
.log_error("ProjectCompiler: write material error");
|
|
|
|
// todo: clean up old cache
|
|
}
|
|
// l.replace_material(mat);
|
|
}
|
|
LockUpdate::Route(route) => {
|
|
l.replace_route(route);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.log_error("ProjectCompiler: lock file error");
|
|
}
|
|
}
|
|
|
|
struct Version<'a>(&'a str);
|
|
|
|
impl PartialEq for Version<'_> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
semver::Version::parse(self.0)
|
|
.ok()
|
|
.and_then(|a| semver::Version::parse(other.0).ok().map(|b| a == b))
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for Version<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
let lhs = semver::Version::parse(self.0).ok()?;
|
|
let rhs = semver::Version::parse(other.0).ok()?;
|
|
Some(lhs.cmp(&rhs))
|
|
}
|
|
}
|