跳到正文
本頁目錄

內部機制

本指南說明 lip 的底層工作方式。

架構

lip 提供兩個主要入口:

  1. CLI(lip):獨立命令行工具,內置包管理邏輯,不依賴守護進程。
  2. Daemon(lipd):獨立進程,供外部應用(IDE、GUI、腳本)以編程方式調用 lip。

CLI 與 Daemon 共享同一套核心邏輯,保證無論通過哪種接口,行為都一致。

Daemon(lipd)通過 stdio 上的 JSON-RPC 通信,使工具無需為每個命令反覆拉起進程,也無需解析文本輸出。

流程

lip install、lip uninstall 和 lip update 執行相同的步驟:

  1. 鎖:從用戶數據目錄加載該工作區的鎖,或讀取早期版本留在工作區中的 lip_lock.json 或舊的 tooth_lock.json(參見鎖文件)。
  2. 環境:確定平臺和 RID(參見環境)。
  3. 解析器:把每個 ref 轉換為一個具體包候選序列,途中跟隨路由文檔。
  4. 依賴圖求解器:為每個請求和每條安裝邊各選出一個候選,使每個身份只出現一次,且不違反任何 conflicts 條目。
  5. 規劃:規劃按安裝順序的逆序卸載離開集合的包、再按依賴優先的順序安裝進入集合的包,每一步都基於之前各步完成後的工作區。規劃會下載所有要寫入的文件,並檢查一切可能失敗的情形:文件所有權、preserveFiles、路徑、放置來源以及 plugins/mods 目錄佈局。tooth.json 包在解析時已被轉換為第 4 版的文件包(參見舊格式:tooth.json),因此所有包都由同一個安裝器處理。
  6. 執行:必要時讓 plugins 成為指向 mods 的鏈接(參見安裝依賴),然後運行規劃好的腳本與文件修改。放到 plugins/ 下的文件會寫入 mods/ 並按 mods/ 記錄。
  7. 鎖:所有步驟成功之後,保存一次鎖。

操作要麼全部完成,要麼全部不做(規範 §7"原子性")。第 6 步之前的任何失敗都不會修改工作區。執行時某一步失敗(文件無法寫入、腳本以非零碼退出,或按下 Ctrl+C),lip 會撤銷該操作的全部修改(包括目錄佈局),鎖保持原樣。腳本所做的事無法撤銷:postInstall 或 preUninstall 腳本失敗時,操作同樣失敗並撤銷 lip 的文件修改,但已運行腳本的效果不會被撤銷。

包解析

lip 不使用中心化的包註冊表或模塊代理。像 github.com/user/repo 這樣的包路徑直接對應 git 倉庫 https://github.com/user/repo.git,lip 通過內置的 git smart-HTTP 協議(第 2 版)客戶端與其通信。無需安裝 git 命令行工具,lip 也從不調用它。

  • 版本:lip 通過 ls-refs 列出倉庫標籤(只請求 refs/tags/v*)。可用版本即名稱恰為 v<SemVer 2.0.0> 的標籤(例如 v1.2.3);其他標籤,包括 v1.2 或 v01.2.3 這類寬鬆寫法,都會被忽略。附註標籤會被解析到其指向的提交。
  • git 引用:帶 git 引用的包路徑只有一個候選。@branch:<name> 和 @tag:<name> 通過 ls-refs 恰好請求 refs/heads/<name> 或 refs/tags/<name> 來解析(附註標籤會被解析到其提交),每條命令只解析一次;@rev:<commit> 無需請求,提交按其 ID 獲取。不接受縮寫的提交 ID:解析縮寫需要倉庫的全部歷史,而 lip 從不下載全部歷史。
  • 惰性候選:包路徑的候選從最新標籤開始按需產出,因此 lip 只會打開求解器實際需要的標籤。
  • 文檔:讀取某個版本的 manifest.json(或 tooth.json)時,lip 只獲取該標籤提交的樹對象(使用 filter blob:none 的淺層部分獲取,不傳輸任何文件內容),然後只下載這一個文件。因此無論倉庫多大,解析依賴時每個版本只需幾 KB 流量。
  • 路由:路由文檔按 variant 和環境選出一個條目,並按順序解析其 refs。超過 8 跳的鏈,或兩次經過同一歸檔和 variant 的鏈,不產出候選。
  • 安裝:只有安裝器需要時才下載文件:mod 下載整棵樹,文件包只下載要放置的文件。tooth.json 包的發佈資源在安裝時下載,與其他 HTTP 下載相同。
  • 完整性:每個下載的 git 對象都會按其 git 對象 ID(SHA-1)校驗。ZIP 歸檔以其 SHA-256 標識。
  • 緩存:全局緩存按數據的傳輸方式命名每個條目。下載的 git 文件存為 git-<对象 ID>,相同的文件只下載一次,可在不同版本和不同包之間複用。通過 HTTP(S) 下載的內容一律存為 http-<URL 的 SHA-256>,因此同一 URL 只下載一次;重新打開鎖中記錄的 ZIP 歸檔時,lip 會用鎖中的 SHA-256 校驗緩存文件,缺失或不一致時重新下載。位於另一個歸檔中的 ZIP 歸檔不單獨緩存:lip 會再次從其父歸檔中讀取。下載內容先寫入緩存中隱藏的 .staging 目錄,完成後再移動到位;lip 不向系統臨時目錄寫入任何內容。標籤列表和樹對象僅在單次命令執行期間緩存在內存中;每次運行都會重新查詢標籤,因此被移動的標籤會立即生效。
  • 重定向:已重命名或轉移的 GitHub 倉庫會通過 GitHub 的 HTTP 重定向自動跟隨。github.com 的所有者和倉庫名不區分大小寫。
  • 代理:若配置了 github_proxy,對 github.com 倉庫的請求會先經由該代理發送;若代理請求失敗,lip 會直接向 github.com 重試。從 github.com URL 下載發佈資源時遵循同樣的規則。

只有當某個版本的標籤仍存在於倉庫中時,該版本才能被安裝。目前只支持可通過 HTTPS 訪問的公開倉庫。

標籤處帶有 version 的 manifest.json 必須聲明該標籤的版本。標籤處的舊 tooth.json 若聲明瞭其他版本,則按標籤版本安裝,lip 會給出警告。git 引用所給的分支、提交或名稱不是 v<SemVer> 的標籤沒有標籤版本:使用文檔自身的 version。

求解

  • 固定:已安裝的包按原樣進入求解器,從鎖中重建,無需訪問網絡。若這樣無法滿足請求,lip 會只固定顯式安裝的包重新求解,此時依賴可以改變版本。
  • 優先級:靠前的候選優先:包路徑的最新版本,以及條目或依賴中的第一個 ref。
  • 順序:先安裝依賴;依賴成環的包按名稱排序。
  • 清理:不再被任何顯式安裝的包需要的依賴會被卸載。
  • 按鎖重裝:當 lip 再次需要已安裝包的文件時,會打開鎖中記錄的歸檔並校驗其標識(SHA-256 或提交),而不是重新解析該包。

清單校驗

各格式由倉庫 schemas/ 目錄中的 JSON Schema 描述:manifest.json 為 manifest.v4.schema.json,鎖為 lock.v4.schema.json;舊格式為 manifest.schema.json(tooth.json 格式 3)、manifest.v1.schema.json、manifest.v2.schema.json 和 workspace_state.schema.json(tooth_lock.json)。這些 schema 供編輯器和其他工具使用,lip 本身不執行 JSON Schema 校驗。

  • 校驗:lip 嚴格解析 manifest.json(UTF-8、不允許註釋、不允許重複鍵),並拒絕鍵類型錯誤的文檔,例如無效的 name、version、ref、範圍或模板。指向無效文檔的 ref 不產出候選,lip 會報告原因。
  • 版本:帶有 formatVersion 4 和格式 UUID 的 manifest.json 是第 4 版文檔;兩者都沒有的是普通 LeviLamina 清單。格式版本為 1 或 2 的 tooth.json 會在讀取時於內存中轉換為版本 3,再轉換為第 4 版的文件包(規範 §9);如需把包倉庫從 tooth.json 遷移到 manifest.json,請在倉庫中運行 lip migrate。

工作區狀態

lip 在保存於工作區之外、用戶數據目錄中的鎖文件裡維護已安裝包狀態(參見鎖文件),記錄:

  • 用戶顯式請求的包與隱式依賴;
  • 精確安裝版本,以及其來源歸檔;
  • 每個包擁有的文件;
  • 工作區的平臺和 RID。

為保證操作原子性而寫入的文件都是隱藏文件,並位於目標旁邊:鎖先寫入同目錄下的 .<name>.tmp,再重命名覆蓋鎖;操作要覆蓋或刪除的文件先在同一目錄中重命名為 .<name>.bak(若該名稱已被佔用,則為 .<name>.bak1、.<name>.bak2……)。鎖保存後刪除這些備份(無法刪除的備份,例如被某個程序打開的,會保留並在警告中列出),操作失敗時則改回原名。卸載後變空的目錄也在此時刪除。在 Windows 上,這些文件還會被設置隱藏屬性。

  • 日誌:操作修改工作區期間,lip 在做出每項修改之前,先把它記錄到鎖旁邊的 <hash>.journal 中。若 lip 被終止,下一次對該工作區的 lip install、uninstall 或 update 會先讀取日誌:撤銷被中斷的操作(恢復備份)並報告;若該操作已進行到保存鎖的階段,則刪除備份以完成它。試運行(dry run)只報告日誌的存在。
  • 同一時間只有一個操作:操作期間獨佔打開鎖旁邊的 <hash>.lock(操作結束時刪除)。對同一工作區的第二個 lip install、uninstall 或 update 會立即失敗,並提示"Another lip process is changing the workspace";請在第一個操作結束後重新運行。

關於規範的說明

lip 只管理服務端工作區:它從不從工作區判斷平臺,而是總是使用 server,如同由用戶顯式指定(CLI 規範 §3,規範 §5.1)。客戶端工作區交由實現同一規範的啟動器(如 LeviLauncher)處理。

lip 實現 lip 包規範(格式版本 4)。規範沒有規定 lip 如何繼續管理舊版本寫入的鎖(規範 §8 "Earlier formats"),並允許通過包含嵌套 ZIP 歸檔的歸檔來標識它(規範 §8)。lip 的做法如下:

  1. 遷移而來的鎖條目:從 tooth_lock.json 遷移而來的條目不記錄歸檔標識。當 lip 需要其歸檔時,會重新解析標籤 path@version。
  2. 嵌套的 ZIP 歸檔:位於另一個歸檔中的 ZIP 歸檔只按 SHA-256 鎖定,且不單獨緩存。要再次打開它,lip 會重新解析記錄的解析鏈,從而重新打開父歸檔並解出該 ZIP 文件,並且只接受 SHA-256 一致的結果。
  3. 同一身份的多個條目:同一個 tooth.json 包的各變體身份不同(path 與 path#label,規範 §9.1),從 tooth_lock.json 遷移時也是如此;對於該規則之前寫入的鎖,lip 會為其條目補上所要求的身份。若鎖中仍有同一身份的兩個條目,兩者都會保留並列出,並可分別用 path#variant 卸載。新的求解要求身份唯一。