Skip to main content

nichlink_run_method/authoring/external_graft/
plan.rs

1//! Persistent declarations for immutable external grafts.
2//! 不可变外部 graft 的持久化声明。
3//!
4//! A plan file is an authoring record, not a declaration the compiler sees and
5//! not an overlay application. It names the logical slot (`target_path`), the
6//! base face's identity (`target`), the external implementation's selector, and
7//! whether the whole subtree or only the node is replaced. `GraftPlanDocument`
8//! in the kernel owns the text format; this module owns the filesystem.
9//! 计划文件是创作记录:它不是编译器看到的声明,也不是覆盖应用。它记录逻辑槽位
10//! (`target_path`)、原注册面的身份(`target`)、外部实现的选择器,以及替换整棵
11//! 子树还是只替换节点。文本格式由 kernel 的 `GraftPlanDocument` 拥有,本模块只
12//! 负责文件系统。
13
14use std::fs;
15use std::path::PathBuf;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use crate::runtime::{LoadedGraft, graft_record_root, load_graft_record, load_graft_records};
19use crate::{GraftPlanDocument, NodeId, Registry};
20use nichlink::lexicon;
21
22use super::super::filesystem::atomic_write;
23use super::super::validation::package_root;
24
25/// The directory that owns every external graft plan.
26/// 拥有全部外部 graft 计划的目录。
27///
28/// The layout lives in the non-gated runtime loader now, so Studio and a
29/// runtime host resolve the same path from the same constant list.
30/// 版式现在住在不受门控的运行期加载器里,因此 Studio 与运行期宿主按同一份常量表解析
31/// 同一条路径。
32pub fn external_graft_root() -> PathBuf {
33    graft_record_root(&package_root())
34}
35
36/// An external overlay declaration owned by the host package.
37/// 宿主包拥有的外部覆盖声明。
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct ExternalGraftPlanFile {
40    /// The plan's directory name under `.nichlink/external-grafts/`.
41    /// 计划在 `.nichlink/external-grafts/` 下的目录名。
42    pub selector: String,
43    /// The parsed plan body.
44    /// 解析后的计划主体。
45    pub document: GraftPlanDocument,
46    /// The plan directory itself.
47    /// 该计划所在目录。
48    pub root: PathBuf,
49}
50
51impl ExternalGraftPlanFile {
52    /// The path of the plan file inside `root`.
53    /// `root` 内计划文件的路径。
54    pub fn plan_path(&self) -> PathBuf {
55        self.root.join(lexicon::GRAFT_PLAN_FILE)
56    }
57
58    /// The identity of the base face this plan replaces.
59    /// 该计划所替换原注册面的身份。
60    pub fn target(&self) -> NodeId {
61        self.document.target
62    }
63
64    /// The logical slot path this plan replaces.
65    /// 该计划替换的逻辑槽位路径。
66    pub fn target_path(&self) -> &str {
67        &self.document.target_path
68    }
69
70    /// The selector naming the external implementation.
71    /// 命名外部实现的选择器。
72    pub fn graft(&self) -> &str {
73        &self.document.graft
74    }
75
76    /// Whether the whole subtree is replaced rather than only the node.
77    /// 替换整棵子树还是仅替换该节点。
78    pub fn full(&self) -> bool {
79        self.document.full
80    }
81}
82
83/// One directory under `.nichlink/external-grafts/`.
84/// `.nichlink/external-grafts/` 下的一个目录。
85///
86/// A plan the tooling can read and a plan it cannot are both listed: a broken
87/// file is something the author has to see, not something to hide.
88/// 读得懂与读不懂的计划都会被列出:坏文件是作者必须看见的东西,不该被藏起来。
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct ExternalGraftPlanEntry {
91    /// The plan's directory name under `.nichlink/external-grafts/`.
92    /// 计划在 `.nichlink/external-grafts/` 下的目录名。
93    pub selector: String,
94    /// The plan directory itself.
95    /// 该计划所在目录。
96    pub root: PathBuf,
97    /// The parsed plan, or the reason it could not be read.
98    /// 解析后的计划,或无法读取的原因。
99    pub document: Result<GraftPlanDocument, String>,
100}
101
102impl ExternalGraftPlanEntry {
103    /// The path of the plan file inside `root`.
104    /// `root` 内计划文件的路径。
105    pub fn plan_path(&self) -> PathBuf {
106        self.root.join(lexicon::GRAFT_PLAN_FILE)
107    }
108
109    /// Borrow the parsed plan, or the stored reason it is unusable.
110    /// 借出解析后的计划,或它不可用的已存原因。
111    pub fn document(&self) -> Result<&GraftPlanDocument, &str> {
112        self.document.as_ref().map_err(String::as_str)
113    }
114}
115
116/// Reject a selector the plan format could not read back.
117/// 拒绝计划格式读不回来的选择器。
118fn checked_selector(selector: &str) -> Result<&str, String> {
119    let selector = selector.trim();
120    crate::validate_graft_selector(selector)?;
121    Ok(selector)
122}
123
124/// The directory one selector's plan lives in, without reading the plan.
125/// 某个选择器的计划所在目录,不读取计划本身。
126///
127/// Deleting must not require a parse. An unreadable record is exactly the one a
128/// reader most needs to be able to remove, and while the delete path read first
129/// it refused every broken record instead. Reading stays the job of
130/// [`read_external_graft`]; this only resolves and checks the directory.
131/// 删除不得以解析为前提。读不懂的记录恰恰是读者最需要能删掉的,而删除路径此前先读,于是
132/// 拒绝了每一条坏记录。读仍是 [`read_external_graft`] 的职责;这里只解析并检查目录。
133pub fn external_graft_directory(selector: &str) -> Result<PathBuf, String> {
134    let selector = checked_selector(selector)?;
135    let root = external_graft_root().join(selector);
136    if !root.is_dir() {
137        return Err(format!(
138            "external graft `{selector}` does not exist at {}",
139            root.display()
140        ));
141    }
142    Ok(root)
143}
144
145/// Create an external graft plan without touching the base source tree.
146/// 创建外部 graft 计划,不接触原树源码。
147pub fn create_external_graft(
148    registry: &Registry,
149    target: NodeId,
150    graft: impl Into<String>,
151    full: bool,
152) -> Result<ExternalGraftPlanFile, String> {
153    let target_path = registry
154        .path_for(target)
155        .ok_or_else(|| format!("graft target `{target}` is not registered"))?;
156    let selector = checked_selector(&graft.into())?.to_owned();
157
158    let root = external_graft_root().join(&selector);
159    if root.exists() {
160        return Err(format!(
161            "external graft `{selector}` already exists at {}",
162            root.join(lexicon::GRAFT_PLAN_FILE).display()
163        ));
164    }
165    let document = GraftPlanDocument::new(target, target_path, selector.clone(), full);
166    fs::create_dir_all(&root)
167        .map_err(|error| format!("cannot create external graft directory: {error}"))?;
168    if let Err(error) = atomic_write(&root.join(lexicon::GRAFT_PLAN_FILE), &document.render()) {
169        let _ = fs::remove_dir_all(&root);
170        return Err(format!("cannot write external graft plan: {error}"));
171    }
172
173    Ok(ExternalGraftPlanFile {
174        selector,
175        document,
176        root,
177    })
178}
179
180/// Read one plan back, so an authoring surface can show and edit it.
181/// 读回一条计划,供创作界面显示与编辑。
182///
183/// The read and parse go through the runtime loader, so Studio and a host refuse
184/// exactly the same documents.
185/// 读取与解析都走运行期加载器,因此 Studio 与宿主拒绝的文档完全相同。
186pub fn read_external_graft(selector: &str) -> Result<ExternalGraftPlanFile, String> {
187    let selector = checked_selector(selector)?;
188    let document = load_graft_record(&package_root(), selector)?;
189    Ok(ExternalGraftPlanFile {
190        selector: selector.to_owned(),
191        document,
192        root: external_graft_root().join(selector),
193    })
194}
195
196/// Every plan directory, including the ones that do not parse.
197/// 列出全部计划目录,包括解析失败的。
198pub fn list_external_grafts() -> Result<Vec<ExternalGraftPlanEntry>, String> {
199    let root = external_graft_root();
200    let loaded = load_graft_records(&package_root())?;
201    Ok(loaded
202        .into_iter()
203        .map(|entry| match entry {
204            LoadedGraft::Record(record) => ExternalGraftPlanEntry {
205                root: root.join(&record.selector),
206                selector: record.selector.clone(),
207                document: Ok(record.document),
208            },
209            LoadedGraft::Unreadable { selector, reason } => ExternalGraftPlanEntry {
210                root: root.join(&selector),
211                selector,
212                document: Err(reason),
213            },
214        })
215        .collect())
216}
217
218/// Change whether a plan replaces the whole subtree.
219/// 改变一条计划是否替换整棵子树。
220pub fn rewrite_external_graft(selector: &str, full: bool) -> Result<ExternalGraftPlanFile, String> {
221    let mut plan = read_external_graft(selector)?;
222    if plan.document.full == full {
223        return Ok(plan);
224    }
225    plan.document.full = full;
226    let path = plan.plan_path();
227    atomic_write(&path, &plan.document.render())?;
228    Ok(plan)
229}
230
231/// Move one plan directory to the recoverable NichLink trash, returning its path.
232/// 把一个计划目录移到可恢复的 NichLink 回收目录,并返回其路径。
233///
234/// The plan is not parsed: a record whose text is broken still has a directory,
235/// and removing it is the repair. The trash keeps the bytes, so a removal that
236/// turns out to be a mistake remains reversible by hand.
237/// 计划不被解析:文本损坏的记录仍有目录,删掉它就是修复。回收目录保留字节,因此删错时仍可
238/// 手工恢复。
239pub fn remove_external_graft(selector: &str) -> Result<PathBuf, String> {
240    let selector = checked_selector(selector)?;
241    let root = external_graft_directory(selector)?;
242    let stamp = SystemTime::now()
243        .duration_since(UNIX_EPOCH)
244        .map_err(|error| format!("clock error: {error}"))?
245        .as_nanos();
246    let trash = package_root()
247        .join(lexicon::NICHLINK_DIR)
248        .join("trash")
249        .join(lexicon::EXTERNAL_GRAFT_DIR)
250        .join(format!("{selector}-{stamp}"));
251    fs::create_dir_all(trash.parent().expect("trash has a parent"))
252        .map_err(|error| format!("cannot create NichLink trash: {error}"))?;
253    fs::rename(&root, &trash).map_err(|error| {
254        format!(
255            "cannot move {} to {}: {error}",
256            root.display(),
257            trash.display()
258        )
259    })?;
260    Ok(trash)
261}
262
263#[cfg(test)]
264#[path = "plan_tests.rs"]
265mod plan_tests;