跳到正文
本頁目錄

內部機制

讀完本頁,你就知道 lip 如何查找套件的版本、如何不借助 git 與 git 儲存庫通信、快取了什麼、如何在版本之間做選擇,以及如何為伺服器準備系統。本頁寫給好奇的人和套件的作者。lip 的工作原理給出了總覽,確切的定義則在 lip 協議裡。

一次操作的步驟

lip setup、lip install、lip uninstall 和 lip update 執行的是同樣的步驟:

  1. 加鎖並恢復。 佔用工作區鎖 .lip/lock。把被中斷的操作做完或復原。
  2. 狀態檔。 從 .lip/state.json 讀取已安裝的套件。沒有這個檔案的工作區就沒有套件,除非它有 lip v0.x 的 tooth_lock.json,這時改為讀取後者。
  3. 環境。 在每種系統上,套件都是按執行端 server 和平臺 win-x64 來選擇的。
  4. 解析。 把每個來源變成一串候選:它指向的各個版本,最新的在前,每個版本帶上所請求變體在該環境下的條目。
  5. 求解。 為每個請求和每個相依選定一個候選,使每個套件只出現一次,並且沒有兩個模組同名。
  6. 計劃。 計劃解除安裝離開集合的套件(按安裝順序的逆序),再安裝進入集合的套件(相依在前)。做計劃時會下載所有將要寫入的歸檔和資源,並檢查一切可能失敗的地方:每個檔案歸誰所有、preserve、路徑、每條放置規則的 src、每個模組在其 manifest.json 裡的名字,以及 mods 是不是連結。
  7. 準備系統,如果要執行 Windows 程式。見系統相依。
  8. 應用。 mods 不存在就建立它,然後執行計劃好的鉤子命令和檔案變更,每一項都先記入日誌。
  9. 狀態檔。 每一步都成功之後,儲存一次狀態檔。

第 8 步之前的失敗不會動工作區。第 8 步當中的失敗,或者 Ctrl+C,會讓 lip 復原這次操作的每一處變更。鉤子命令做過的事不會被復原。

試執行在第 6 步之後停下,第 1 步裡不加鎖,也不做任何修復。

同一時間只有一次操作

一次操作會獨佔地開啟 .lip/lock:在 Windows 上是不共享地開啟,在 Linux 和 macOS 上是獨佔的 flock。這個檔案會一直保留,從不刪除。對同一工作區的第二次操作不會等待,而是立刻失敗,並給出:

text
lip cannot take the workspace lock '<workspace>/.lip/lock': most likely another lip command is changing the workspace '<workspace>'. Wait for the other lip command to finish, then try again. If no other lip command is running, a program may be holding the lock file open: close it, then try again.

lip run 只在啟動伺服器之前、恢復被中斷操作的那一刻佔用同一把鎖。伺服器執行期間,它以同樣的方式佔用的是伺服器鎖 .lip/server.lock。同一工作區的第二個伺服器會失敗,並給出:

text
A server of the workspace '<workspace>' is already running: another lip run, the interface of lip or the daemon of lip runs it, and holds the server lock '<workspace>/.lip/server.lock'. Stop that server first, or attach to it with 'lip run --managed' when it is a managed server.

這兩把鎖互不相干。處理程序被殺掉時,鎖會隨它的檔案句柄一起釋放,所以絕不會有鎖遺留下來。

套件的解析

lip 沒有中央儲存庫,也沒有模塊代理伺服器。像 github.com/user/repo 這樣的套件 ID,就是 git 儲存庫 https://github.com/user/repo.git。

  • 版本。 儲存庫的版本,是它名字恰好為 v 加 SemVer 2.0.0 版本號的標籤,例如 v1.2.3。其他標籤會被忽略,包括 v1.2 或 v01.2.3 這樣不嚴格的寫法。附註標籤按它所指的提交計算。
  • 引用。 帶引用的來源只有一個候選。@branch:<name> 和 @tag:<name> 每條命令查詢一次。@rev:<commit> 不需要查詢:直接按提交 ID 取得。縮寫的提交 ID 會被拒絕,因為解析它需要完整的歷史,而 lip 從不下載完整歷史。
  • 惰性候選。 儲存庫的候選按從新到舊的順序、只在需要時才產生。lip 只會開啟求解過程真正用到的版本。
  • 怎樣才算候選。 一個版本要成為候選,它的 lip.toml 必須有效,其中的 id 必須是儲存庫的路徑,version 必須等於標籤的版本,並且所請求的變體在該環境下有條目。否則 lip 繼續看下一個版本,並記下原因,以便在沒有任何版本合適時告訴你。
  • 沒有標籤版本時。 在分支、提交,或者不是版本的標籤上,套件的版本就是它 lip.toml 裡的 version。
  • 遊戲是例外。github.com/LiteLDev/bds 不是儲存庫。lip 根據 https://levimc.org/api/v4/mc/versions/ 上的伺服器組建清單自己生成它的套件,並從 minecraft.net 下載伺服器。
  • 其他來源。 URL 指向一個通過 HTTPS 下載的 ZIP 壓縮檔。本機路徑指向一個 ZIP 檔案或一個資料夾。它們各有一個候選。

一個版本只有在它的標籤還存在於儲存庫裡時才能安裝。只支援能通過 HTTPS 訪問的公開儲存庫。比較套件 ID 時不區分 ASCII 字母的大小寫。

git 客戶端

lip 用自己的客戶端與 git 儲存庫通信,使用的是 git 智能 HTTP 協議的第 2 版。不需要 git 程式,lip 也從不執行它。

  • 列出標籤。 lip 用 ls-refs 向伺服器查詢引用,範圍限定在 refs/tags/v。對於分支或標籤引用,它只查詢 refs/heads/<name> 或 refs/tags/<name> 本身。
  • 讀取 lip.toml。 lip 只取得那一個提交的樹:一次淺的、部分的取得(deepen 1、filter blob:none),不傳輸任何檔案內容。然後它只取得那一個檔案;沒有 lip.toml 時取得 tooth.json。因此,無論儲存庫多大,檢視一個版本只需要幾千位元組。
  • 安裝。 檔案內容只在安裝套件時才取得,而且只取得這個套件從自己的歸檔裡放置的那些檔案。
  • 完整性。 每個 git 物件都會對照它的物件 ID 進行校驗。
  • 代理伺服器。 github_proxy 設為 URL 後,對 github.com 儲存庫的請求先走代理伺服器,失敗後再直接發往 github.com。從 github.com 的 URL 下載的檔案遵循同樣的規則。見 GitHub 很慢或被屏蔽時。

伺服器必須像 GitHub 那樣支援這些特性。只轉發檔案下載的代理伺服器無法提供儲存庫訪問。

快取

快取目錄存放 lip 通過 HTTPS 下載的東西:URL 來源指向的 ZIP 壓縮檔、套件的資源,以及遊戲。

  • 條目。 每個下載儲存為 http-<hash>,其中的雜湊是它的 URL 的 SHA-256。這個 URL 是套件裡寫的那個,與使用了什麼代理伺服器無關。
  • 寫入。 下載先寫到快取目錄裡的 .tmp 檔案,完成後再重命名到位。所以存在的條目一定是完整的,多個 lip 處理程序可以共用快取。
  • 修復。 lip 需要一個 ZIP 壓縮檔而快取裡的檔案不是時,它會刪除這個條目並重新下載,只做一次。
  • 不快取的東西。 git 儲存庫的任何內容都不快取到磁碟。標籤清單和樹在一條命令執行期間儲存在記憶體裡,下一條命令會重新查詢,所以被移動的標籤立刻生效。
  • 不校驗內容。 ZIP 壓縮檔和資源只檢查是不是有效的 ZIP 壓縮檔。不記錄也不比較其內容的校驗和。

lip cache clean 會刪除整個目錄。試執行會下載它需要檢查的東西,但不往快取裡寫任何東西。lip 從不寫入系統的臨時目錄。

求解

求解就是為每個請求和每個相依選定要安裝的候選。

  • 固定。 已安裝的套件按原樣進入求解,不聯網,直接從狀態檔重建。如果這樣無法滿足請求,lip 會重新求解,這次只固定由你安裝的套件,相依的版本可以變動。相依已安裝的版本會被優先嚐試,所以只有必須變動的才會變動。
  • 更新。 lip update 從一開始就按第二種方式求解,這樣已安裝的相依絕不會拖住你要更新的套件,使它得不到最新版本。
  • 偏好。 靠前的候選優先:也就是儲存庫的最新版本。
  • 遊戲和載入器不動。 lip install 和 lip update 圍繞它們求解。找不到合適的組合時,失敗資訊會給出它們的版本,並提示 lip setup。安裝伺服器時則按新的遊戲和載入器求解,只有當某個模組沒有任何版本與之相配時才把它排除在外。
  • 順序。 套件按相依在前的順序安裝。循環相依裡的套件按標識排序。
  • 清除。 不再被任何由你安裝的套件需要的相依會被解除安裝。
  • 同一個套件。 只有當 lip 解析出的套件在標識、版本或提交上與已安裝的不同時,才會取代它。來自 URL 或本機路徑的套件沒有提交。
  • 根據狀態檔重新安裝。 lip 再次需要某個已安裝套件的檔案時,會使用狀態檔裡記錄的套件檔來安裝,而不是重新解析:對儲存庫來說,從記錄的提交安裝;否則從其來源現在指向的歸檔安裝。

套件檔的校驗

lip.toml 由 lip.schema.toml 描述,使用者設定由 config.schema.toml 描述,工作區設定由 workspace-config.schema.toml 描述,狀態檔由 state.schema.json 描述。

  • 在 schema 之外,lip 還會檢查 lip.toml(5) 的要求:有效的套件 ID 和版本,有效的相依鍵和範圍,執行端和平臺,同一變體裡沒有兩個條目針對相同的執行端和平臺,每個條目最多一個 mcmod 資源,以及有效的 glob。lip.toml 無效的歸檔不是套件。它不產生候選,lip 會報告原因。
  • lip v0.x 的檔案。 沒有 lip.toml 的歸檔會從它格式 1、2 或 3 的 tooth.json 讀取,解析時就把它完整地轉換成套件檔。因此所有的套件都由同一個安裝器處理。位於版本標籤上的 tooth.json 按標籤的版本安裝,不管它自己的 version 寫的是什麼。tooth_lock.json 會變成 .lip/state.json,liprc.json 會變成 config.toml,都在 lip 第一次處理它們時自動完成。見 lip-compatibility(7)。

套件的作者可以在 lip.toml 一頁找到格式說明。

工作區狀態和日誌

狀態檔 .lip/state.json 為每個已安裝的套件記錄:是否由你安裝、安裝時使用的套件檔、來源、來自儲存庫時的提交,以及它擁有的檔案。

操作之所以能「全有或全無」,靠的是三類檔案:

  • 日誌 .lip/journal。每次變更在執行之前都先記在這裡,並刷寫到磁碟。其中的路徑是相對於工作區的,所以被移動或複製的工作區會帶著被中斷的操作一起走。
  • 備份。 操作要取代或刪除的檔案,會先在同一目錄裡重命名為 .<名字>.bak;這個名字已被佔用時,依次用 .<名字>.bak1、.<名字>.bak2,以此類推。在 Windows 上它還會帶上隱藏屬性。
  • 暫存的狀態檔。 新的狀態先寫到 .lip/state.json.tmp,再重命名覆寫 .lip/state.json。

操作成功時,lip 把日誌標記為已提交,把狀態檔重命名到位,刪除備份和被操作清空的目錄,再刪除日誌。因為被某個程式開啟而刪不掉的備份會被留下,並在警告裡指出。

操作失敗或被取消時,lip 倒著走一遍日誌:刪除它建立的東西,把每個備份重命名回去。

lip 被殺掉時,日誌會留下。下一次操作,或者下一次 lip run,會在佔用工作區鎖之後讀取它。帶提交標記的日誌會被做完。不帶的會被復原。如果有些東西暫時還復原不了,日誌會被改寫成只含剩餘的部分,命令會一直失敗,直到能復原為止。

這些格式在 journal(5) 和 lip-workspace(7) 裡固定下來,以便其他程式也能參與。

系統相依

工作區裡的程式,也就是套件的鉤子命令和 Windows 伺服器,需要系統提供兩樣東西。兩樣都由 lip 自己安裝。

  • Wine,在 Linux 和 macOS 上。每個版本的 lip 都指定了一個用於 x64 Linux 的 Wine 組建(來自 Kron4ek/Wine-Builds)和一個用於 macOS 的組建(來自 Gcenx/macOS_Wine_builds),後者在 arm64 上由 Rosetta 2 執行。lip 只下載一次,解壓縮到資料目錄的 wine/ 裡,所有工作區共用。lip 從不執行別的 wine。
  • Microsoft Visual C++ Redistributable(x64)。登錄檔的 HKLM\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64 下有值 Installed = 1 時,視為已安裝。否則 lip 會下載 vc_redist.x64.exe,並以 /install /quiet /norestart 執行它:在 Windows 上以系統管理員身份執行,可能會彈出使用者帳戶控制提示;在其他系統上通過 Wine、在工作區的 Wine 前綴裡執行,前綴的登錄檔用 wine reg query 讀取。

lip 這樣保證安全:

  • 固定的下載。 每個檔案的 URL 和 SHA-256 都寫在 lip 裡。SHA-256 不一致的下載會被刪除,既不解壓縮也不執行。
  • 絕不半途而廢。 Wine 先解壓縮到 .tmp 目錄,再重命名到位。被中途打斷的組建絕不會被執行。
  • 一次一個。 同時進行安裝的多個 lip 處理程序,通過資料目錄裡的 lock 檔案互相等待。
  • 在變更工作區之前。 lip 在會執行鉤子命令的操作開始變更工作區之前、在 lip setup 安裝 Windows 組建期間,以及在 lip run 啟動 Windows 伺服器之前準備系統。在這一步失敗,工作區保持原樣。

Linux 伺服器兩樣都不需要。一切都已安裝時,準備系統只需要查一次登錄檔。

通過 Wine 時,lip 以 wine cmd /s /c <command> 執行鉤子命令,以 wine 加伺服器檔案的絕對路徑執行伺服器,並把 WINEPREFIX 設為工作區裡的 .lip/wine、把 WINEDEBUG 設為 -all,除非你自己設定了它們。見 Linux 與 macOS。

常駐程式

除了 lip daemon 的命令之外,每條命令以及介面,都由使用者的常駐程式執行:一個只帶 --daemon 這一個參數啟動的 lip 處理程序。你啟動的 lip 是它的客戶端。

  1. 連線。 在 Windows 上通過命名管道 \\.\pipe\lip-<SID>,只有你的使用者能開啟它。在 Linux 和 macOS 上通過套接字 $XDG_RUNTIME_DIR/lip/daemon.sock;XDG_RUNTIME_DIR 未設定時,是狀態目錄裡的 daemon.sock。套接字的權限是 0600,所在目錄的權限是 0700。
  2. 啟動。 沒有處理程序在監聽時,客戶端用自己的可執行檔啟動常駐程式(脫離終端機),等它開始監聽後再連線。兩個客戶端同時這樣做,最後也只有一個常駐程式:第二個建立不了端點,會自己結束。
  3. 取代。 常駐程式是另一個版本或另一個可執行檔時(例如 lip self-update 之後),客戶端會請它結束,並啟動一個新的。正在執行命令或執行託管伺服器的常駐程式會留下。這時客戶端自己執行命令,只有 lip run --managed 例外,它由舊的常駐程式執行。
  4. 傳送。 客戶端傳送命令,連同它的資料夾、環境變數、標準流是不是終端機,以及終端機的大小和顏色。常駐程式據此執行命令。不同客戶端的命令並行執行,每條都有自己的資料夾、環境、代理伺服器和失敗報告。
  5. 轉發。 常駐程式只在命令需要時才讀取客戶端的輸入,並把命令及其程式的輸出發回去。客戶端收到的信號會被傳過去。客戶端以命令的結束代碼結束。

介面也是這樣執行的。常駐程式負責繪製,客戶端把繪製的內容寫到終端機上,並把你按的鍵和終端機的大小傳過去。

常駐程式執行的程式,包括伺服器,都不在你終端機的會話裡。它們的輸出通過管道而不是終端機到達 lip。

常駐程式在沒有連線、也沒有託管伺服器持續 10 分鐘後結束,在 lip self-update 取代了 lip 之後結束,或者在 lip daemon stop 時結束。確切的規則見 lip-daemon(1)。

關於規範的說明

lip 只管理伺服器工作區:執行端始終是 server。客戶端工作區留給實現同一協議的啟動器,例如 LeviLauncher。

lip 實現的是 lip 協議。本頁與協議不一致時,以協議為準。