內部機制
讀完本頁,你就知道 lip 如何查找套件的版本、如何不借助 git 與 git 儲存庫通信、快取了什麼、如何在版本之間做選擇,以及如何為伺服器準備系統。本頁寫給好奇的人和套件的作者。lip 的工作原理給出了總覽,確切的定義則在 lip 協議裡。
一次操作的步驟
lip setup、lip install、lip uninstall 和 lip update 執行的是同樣的步驟:
- 加鎖並恢復。 佔用工作區鎖
.lip/lock。把被中斷的操作做完或復原。 - 狀態檔。 從
.lip/state.json讀取已安裝的套件。沒有這個檔案的工作區就沒有套件,除非它有 lip v0.x 的tooth_lock.json,這時改為讀取後者。 - 環境。 在每種系統上,套件都是按執行端
server和平臺win-x64來選擇的。 - 解析。 把每個來源變成一串候選:它指向的各個版本,最新的在前,每個版本帶上所請求變體在該環境下的條目。
- 求解。 為每個請求和每個相依選定一個候選,使每個套件只出現一次,並且沒有兩個模組同名。
- 計劃。 計劃解除安裝離開集合的套件(按安裝順序的逆序),再安裝進入集合的套件(相依在前)。做計劃時會下載所有將要寫入的歸檔和資源,並檢查一切可能失敗的地方:每個檔案歸誰所有、
preserve、路徑、每條放置規則的src、每個模組在其manifest.json裡的名字,以及mods是不是連結。 - 準備系統,如果要執行 Windows 程式。見系統相依。
- 應用。
mods不存在就建立它,然後執行計劃好的鉤子命令和檔案變更,每一項都先記入日誌。 - 狀態檔。 每一步都成功之後,儲存一次狀態檔。
第 8 步之前的失敗不會動工作區。第 8 步當中的失敗,或者 Ctrl+C,會讓 lip 復原這次操作的每一處變更。鉤子命令做過的事不會被復原。
試執行在第 6 步之後停下,第 1 步裡不加鎖,也不做任何修復。
同一時間只有一次操作
一次操作會獨佔地開啟 .lip/lock:在 Windows 上是不共享地開啟,在 Linux 和 macOS 上是獨佔的 flock。這個檔案會一直保留,從不刪除。對同一工作區的第二次操作不會等待,而是立刻失敗,並給出:
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。同一工作區的第二個伺服器會失敗,並給出:
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 是它的客戶端。
- 連線。 在 Windows 上通過命名管道
\\.\pipe\lip-<SID>,只有你的使用者能開啟它。在 Linux 和 macOS 上通過套接字$XDG_RUNTIME_DIR/lip/daemon.sock;XDG_RUNTIME_DIR未設定時,是狀態目錄裡的daemon.sock。套接字的權限是0600,所在目錄的權限是0700。 - 啟動。 沒有處理程序在監聽時,客戶端用自己的可執行檔啟動常駐程式(脫離終端機),等它開始監聽後再連線。兩個客戶端同時這樣做,最後也只有一個常駐程式:第二個建立不了端點,會自己結束。
- 取代。 常駐程式是另一個版本或另一個可執行檔時(例如
lip self-update之後),客戶端會請它結束,並啟動一個新的。正在執行命令或執行託管伺服器的常駐程式會留下。這時客戶端自己執行命令,只有lip run --managed例外,它由舊的常駐程式執行。 - 傳送。 客戶端傳送命令,連同它的資料夾、環境變數、標準流是不是終端機,以及終端機的大小和顏色。常駐程式據此執行命令。不同客戶端的命令並行執行,每條都有自己的資料夾、環境、代理伺服器和失敗報告。
- 轉發。 常駐程式只在命令需要時才讀取客戶端的輸入,並把命令及其程式的輸出發回去。客戶端收到的信號會被傳過去。客戶端以命令的結束代碼結束。
介面也是這樣執行的。常駐程式負責繪製,客戶端把繪製的內容寫到終端機上,並把你按的鍵和終端機的大小傳過去。
常駐程式執行的程式,包括伺服器,都不在你終端機的會話裡。它們的輸出通過管道而不是終端機到達 lip。
常駐程式在沒有連線、也沒有託管伺服器持續 10 分鐘後結束,在 lip self-update 取代了 lip 之後結束,或者在 lip daemon stop 時結束。確切的規則見 lip-daemon(1)。
關於規範的說明
lip 只管理伺服器工作區:執行端始終是 server。客戶端工作區留給實現同一協議的啟動器,例如 LeviLauncher。
lip 實現的是 lip 協議。本頁與協議不一致時,以協議為準。