<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Linux 安裝與機器初始化 on Tarragon</title><link>https://tarrragon.github.io/blog/linux/install/</link><description>Recent content in Linux 安裝與機器初始化 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/linux/install/index.xml" rel="self" type="application/rss+xml"/><item><title>安裝過程用到的基礎操作</title><link>https://tarrragon.github.io/blog/linux/install/basic-operations/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/basic-operations/</guid><description>&lt;p>這篇是「Linux 安裝與機器初始化」系列的基礎操作篇，系列用一套最小化的 Arch Linux 安裝當貫穿例子。在安裝、補工具、設定 SSH 的過程中，會用到一小撮基礎操作——成為 root、用 nano 改設定檔、shell 的幾個符號。Linux 的指令入門教學網路上已經很豐富，這篇不重複那些，只挑「這個系列實際用到、而且沒太多 Linux 實作經驗的人容易卡住」的那幾個介紹清楚，讓你照著操作時不會被一個沒見過的指令擋在半路。已經熟的，直接跳到 &lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a>。&lt;/p>
&lt;h2 id="成為-rootsu--">成為 root：su -&lt;/h2>
&lt;p>&lt;code>su -&lt;/code> 讓你從一般使用者切換成 root（系統管理員），整個 session 都以 root 身分操作。這個系列用到它，是因為在還沒有 &lt;code>sudo&lt;/code> 的最小系統上，要裝 sudo、改系統設定，得先成為 root——而成為 root 的方式就是 &lt;code>su -&lt;/code>，輸入 root 密碼後進入 root 的 shell。&lt;/p>
&lt;p>那個 &lt;code>-&lt;/code> 決定你載入哪一套環境。&lt;code>su -&lt;/code> 啟動一個 login shell（模擬從頭登入、會跑完整的登入環境初始化），載入 root 自己的完整環境——把 &lt;code>PATH&lt;/code>（shell 搜尋指令的目錄清單）換成 root 的（會包含 &lt;code>/usr/sbin&lt;/code> 這類放管理工具的目錄）、工作目錄切到 root 的家。&lt;code>su&lt;/code>（不加 &lt;code>-&lt;/code>）則不啟動 login shell，環境多半沿用你呼叫時的、可能少掉那些管理工具目錄，於是有些管理指令會因為不在 &lt;code>PATH&lt;/code> 裡而「找不到」，明明你已經是 root。所以要做系統管理，習慣用 &lt;code>su -&lt;/code>。&lt;/p>
&lt;p>&lt;code>su -&lt;/code> 跟 &lt;code>sudo&lt;/code> 解決的是不同情境。&lt;code>sudo&lt;/code> 是「以 root 身分跑單一一條指令」，跑完就回到你自己；&lt;code>su -&lt;/code> 是「整段都當 root」。這個系列先用 &lt;code>su -&lt;/code> 是因為 sudo 還沒裝——一旦 sudo 裝好、wheel 群組（慣例上被授權可用 sudo 的群組）的授權設好，後面就改用 &lt;code>sudo &amp;lt;指令&amp;gt;&lt;/code>，不再整段切 root。做完 root 的事，打 &lt;code>exit&lt;/code> 回到原本的使用者。&lt;/p>
&lt;h2 id="用-nano-改設定檔">用 nano 改設定檔&lt;/h2>
&lt;p>nano 是一個對照 vi 更直覺的文字編輯器，安裝過程改 &lt;code>locale.gen&lt;/code>、&lt;code>hostname&lt;/code>、&lt;code>sudoers&lt;/code> 這些設定檔時會用到它。它的好處是所有快捷鍵都列在畫面最下面兩行，不必背。&lt;/p>
&lt;p>那兩行裡的 &lt;code>^&lt;/code> 符號代表 Ctrl 鍵。&lt;code>^O&lt;/code> 就是 Ctrl+O、&lt;code>^X&lt;/code> 就是 Ctrl+X。這個系列用到的幾個：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>按鍵&lt;/th>
 &lt;th>畫面標示&lt;/th>
 &lt;th>作用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ctrl+O&lt;/td>
 &lt;td>&lt;code>^O Write Out&lt;/code>&lt;/td>
 &lt;td>存檔（會問檔名，按 Enter 確認）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+X&lt;/td>
 &lt;td>&lt;code>^X Exit&lt;/code>&lt;/td>
 &lt;td>離開（有未存變更會問要不要存）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+W&lt;/td>
 &lt;td>&lt;code>^W Where Is&lt;/code>&lt;/td>
 &lt;td>搜尋——在長檔案裡跳到某個字串&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+K&lt;/td>
 &lt;td>&lt;code>^K Cut&lt;/code>&lt;/td>
 &lt;td>剪掉游標所在的整行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+U&lt;/td>
 &lt;td>&lt;code>^U Paste&lt;/code>&lt;/td>
 &lt;td>把剪掉的內容貼回來&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把這幾個串起來就是一次典型的設定檔編輯。以這個系列裡「解開 &lt;code>locale.gen&lt;/code> 某一行的註解」為例：按 Ctrl+W 搜 &lt;code>en_US.UTF-8 UTF-8&lt;/code> 跳到那行、用 Backspace 刪掉行首的 &lt;code>#&lt;/code>、按 Ctrl+O 再 Enter 存檔、按 Ctrl+X 離開。改 &lt;code>hostname&lt;/code> 則是 Ctrl+K 剪掉預設那行、打上新主機名、Ctrl+O、Ctrl+X。Ctrl+K 加 Ctrl+U 合起來就是「剪下再貼上」，是搬移整行的常見組合。&lt;/p>
&lt;h2 id="檔名與指令的大小寫">檔名與指令的大小寫&lt;/h2>
&lt;p>Linux 把大小寫當成不同的字元，這對檔名跟指令都成立。&lt;code>Setup&lt;/code> 跟 &lt;code>setup&lt;/code> 是兩個不同的東西、&lt;code>Documents&lt;/code> 跟 &lt;code>documents&lt;/code> 是兩個不同的資料夾、打 &lt;code>Sudo&lt;/code> 不會執行到 &lt;code>sudo&lt;/code>。這條規則貫穿整個系列：執行 archboot 的安裝程式是 &lt;code>setup&lt;/code>（全小寫），啟動 Hyprland 桌面的指令是 &lt;code>Hyprland&lt;/code>（首字母大寫），兩者差一個字母的大小寫就是不同的目標。&lt;/p>
&lt;p>這對從 macOS 或 Windows 過來的人尤其常見，因為那兩個系統的預設檔案系統不分大小寫——在 Mac 上 &lt;code>File.txt&lt;/code> 跟 &lt;code>file.txt&lt;/code> 指向同一個檔，到了 Linux 就是兩個檔。同一個專案在 Mac 上跑得好好的，搬到 Linux 卻出現「檔案找不到」，常常就是某處大小寫對不上、而 Mac 的不分大小寫把問題藏了起來。&lt;/p>
&lt;p>判讀方式很簡單：在 Linux 上，指令、檔名、路徑一律照原樣的大小寫打。錯誤訊息 &lt;code>command not found&lt;/code> 或 &lt;code>No such file or directory&lt;/code> 在你確定東西明明就在時，先懷疑大小寫。&lt;/p></description><content:encoded><![CDATA[<p>這篇是「Linux 安裝與機器初始化」系列的基礎操作篇，系列用一套最小化的 Arch Linux 安裝當貫穿例子。在安裝、補工具、設定 SSH 的過程中，會用到一小撮基礎操作——成為 root、用 nano 改設定檔、shell 的幾個符號。Linux 的指令入門教學網路上已經很豐富，這篇不重複那些，只挑「這個系列實際用到、而且沒太多 Linux 實作經驗的人容易卡住」的那幾個介紹清楚，讓你照著操作時不會被一個沒見過的指令擋在半路。已經熟的，直接跳到 <a href="../install-option-decisions/">安裝選項判讀</a>。</p>
<h2 id="成為-rootsu--">成為 root：su -</h2>
<p><code>su -</code> 讓你從一般使用者切換成 root（系統管理員），整個 session 都以 root 身分操作。這個系列用到它，是因為在還沒有 <code>sudo</code> 的最小系統上，要裝 sudo、改系統設定，得先成為 root——而成為 root 的方式就是 <code>su -</code>，輸入 root 密碼後進入 root 的 shell。</p>
<p>那個 <code>-</code> 決定你載入哪一套環境。<code>su -</code> 啟動一個 login shell（模擬從頭登入、會跑完整的登入環境初始化），載入 root 自己的完整環境——把 <code>PATH</code>（shell 搜尋指令的目錄清單）換成 root 的（會包含 <code>/usr/sbin</code> 這類放管理工具的目錄）、工作目錄切到 root 的家。<code>su</code>（不加 <code>-</code>）則不啟動 login shell，環境多半沿用你呼叫時的、可能少掉那些管理工具目錄，於是有些管理指令會因為不在 <code>PATH</code> 裡而「找不到」，明明你已經是 root。所以要做系統管理，習慣用 <code>su -</code>。</p>
<p><code>su -</code> 跟 <code>sudo</code> 解決的是不同情境。<code>sudo</code> 是「以 root 身分跑單一一條指令」，跑完就回到你自己；<code>su -</code> 是「整段都當 root」。這個系列先用 <code>su -</code> 是因為 sudo 還沒裝——一旦 sudo 裝好、wheel 群組（慣例上被授權可用 sudo 的群組）的授權設好，後面就改用 <code>sudo &lt;指令&gt;</code>，不再整段切 root。做完 root 的事，打 <code>exit</code> 回到原本的使用者。</p>
<h2 id="用-nano-改設定檔">用 nano 改設定檔</h2>
<p>nano 是一個對照 vi 更直覺的文字編輯器，安裝過程改 <code>locale.gen</code>、<code>hostname</code>、<code>sudoers</code> 這些設定檔時會用到它。它的好處是所有快捷鍵都列在畫面最下面兩行，不必背。</p>
<p>那兩行裡的 <code>^</code> 符號代表 Ctrl 鍵。<code>^O</code> 就是 Ctrl+O、<code>^X</code> 就是 Ctrl+X。這個系列用到的幾個：</p>
<table>
  <thead>
      <tr>
          <th>按鍵</th>
          <th>畫面標示</th>
          <th>作用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ctrl+O</td>
          <td><code>^O Write Out</code></td>
          <td>存檔（會問檔名，按 Enter 確認）</td>
      </tr>
      <tr>
          <td>Ctrl+X</td>
          <td><code>^X Exit</code></td>
          <td>離開（有未存變更會問要不要存）</td>
      </tr>
      <tr>
          <td>Ctrl+W</td>
          <td><code>^W Where Is</code></td>
          <td>搜尋——在長檔案裡跳到某個字串</td>
      </tr>
      <tr>
          <td>Ctrl+K</td>
          <td><code>^K Cut</code></td>
          <td>剪掉游標所在的整行</td>
      </tr>
      <tr>
          <td>Ctrl+U</td>
          <td><code>^U Paste</code></td>
          <td>把剪掉的內容貼回來</td>
      </tr>
  </tbody>
</table>
<p>把這幾個串起來就是一次典型的設定檔編輯。以這個系列裡「解開 <code>locale.gen</code> 某一行的註解」為例：按 Ctrl+W 搜 <code>en_US.UTF-8 UTF-8</code> 跳到那行、用 Backspace 刪掉行首的 <code>#</code>、按 Ctrl+O 再 Enter 存檔、按 Ctrl+X 離開。改 <code>hostname</code> 則是 Ctrl+K 剪掉預設那行、打上新主機名、Ctrl+O、Ctrl+X。Ctrl+K 加 Ctrl+U 合起來就是「剪下再貼上」，是搬移整行的常見組合。</p>
<h2 id="檔名與指令的大小寫">檔名與指令的大小寫</h2>
<p>Linux 把大小寫當成不同的字元，這對檔名跟指令都成立。<code>Setup</code> 跟 <code>setup</code> 是兩個不同的東西、<code>Documents</code> 跟 <code>documents</code> 是兩個不同的資料夾、打 <code>Sudo</code> 不會執行到 <code>sudo</code>。這條規則貫穿整個系列：執行 archboot 的安裝程式是 <code>setup</code>（全小寫），啟動 Hyprland 桌面的指令是 <code>Hyprland</code>（首字母大寫），兩者差一個字母的大小寫就是不同的目標。</p>
<p>這對從 macOS 或 Windows 過來的人尤其常見，因為那兩個系統的預設檔案系統不分大小寫——在 Mac 上 <code>File.txt</code> 跟 <code>file.txt</code> 指向同一個檔，到了 Linux 就是兩個檔。同一個專案在 Mac 上跑得好好的，搬到 Linux 卻出現「檔案找不到」，常常就是某處大小寫對不上、而 Mac 的不分大小寫把問題藏了起來。</p>
<p>判讀方式很簡單：在 Linux 上，指令、檔名、路徑一律照原樣的大小寫打。錯誤訊息 <code>command not found</code> 或 <code>No such file or directory</code> 在你確定東西明明就在時，先懷疑大小寫。</p>
<h2 id="shell-的幾個符號">shell 的幾個符號</h2>
<p>這個系列的指令裡出現了幾個 shell 符號，它們是 shell 本身的語法、不是某個程式的參數，認得它們才讀得懂那些指令在做什麼。</p>
<p><code>&gt;</code> 跟 <code>&gt;&gt;</code> 是重導向，把本來會印到畫面的輸出改寫進檔案。<code>&gt;</code> 覆蓋、<code>&gt;&gt;</code> 追加。系列裡 <code>echo '%wheel ALL=(ALL:ALL) ALL' &gt; /etc/sudoers.d/10-wheel</code> 就是把那行文字寫進一個新檔，而設 <code>authorized_keys</code> 時用 <code>&gt;&gt;</code> 是為了追加、不洗掉既有的 key。</p>
<p><code>|</code>（管線）把左邊指令的輸出，直接餵給右邊指令當輸入。傳 dotfile 進 VM 的 <code>tar czf - . | ssh host 'tar xzf -'</code> 就是把 tar 打包的資料流，不落地直接透過 ssh 送到對面解開。</p>
<p><code>&amp;&amp;</code> 串接兩條指令，而且只有左邊成功才跑右邊。<code>cd ~/dotfiles &amp;&amp; ./scripts/install.sh</code> 的意思是「先切到目錄，切成功了才跑腳本」——如果目錄不存在、<code>cd</code> 失敗，後面的腳本就不會在錯的目錄下執行。</p>
<p><code>$(...)</code> 是命令替換，把括號裡指令的輸出，當成值填進當下這條指令。<code>chsh -s &quot;$(command -v zsh)&quot;</code> 會先跑 <code>command -v zsh</code> 得到 <code>/usr/bin/zsh</code>，再把這個路徑填給 <code>chsh</code>。理解這個語法，也才看得懂 <a href="../minimal-install-verify/">工具驗證篇</a> 講的 <code>which</code> 地雷：當 <code>which</code> 不存在、<code>$(which zsh)</code> 算出空字串，整條指令就拿到一個空值。</p>
<p>跟重導向相關的還有一個經典陷阱：<code>sudo echo '內容' &gt; /etc/某個-root-檔</code> 會失敗。原因是重導向 <code>&gt;</code> 由你的 shell 以你的身分執行，不是被 <code>sudo</code> 提權的那條指令——被提權的只有 <code>echo</code>，真正寫檔的還是你，而你沒權限寫那個 root 檔。解法是 <code>echo '內容' | sudo tee /etc/某個-root-檔</code>：<code>tee</code> 把流進它的 stdin 寫進檔案，而 <code>tee</code> 才是被 <code>sudo</code> 提權的那支，所以寫得進去。這個系列補 <code>sudoers</code> 檔時用的就是這個寫法。</p>
<p>權限對某些檔是硬要求，所以系列裡出現了 <code>chmod</code>。<code>chmod</code> 改檔案權限，那串數字是八進位的權限碼，三位分別代表擁有者/群組/其他人；每一位是讀（4）+ 寫（2）+ 執行（1）的和，所以 <code>7</code> 是全權、<code>6</code> 是可讀寫、<code>4</code> 是只讀、<code>0</code> 是無權。系列裡的 <code>chmod 440</code>（擁有者與群組可讀、其他人無權，給 sudoers 檔）、<code>chmod 600</code>（只有擁有者可讀寫，給私鑰）、<code>chmod 700</code>（只有擁有者全權，給 <code>.ssh</code> 目錄）就是這樣算出來的。這些檔對權限有要求——sudoers 必須是 440、私鑰必須只有自己讀得到，否則對應的工具會拒絕使用它們，所以那幾個 <code>chmod</code> 不是裝飾、是讓 sudo 跟 ssh 願意接受那些檔的條件。</p>
<h2 id="下一步">下一步</h2>
<p>認得這些基礎操作之後，就可以從 <a href="../install-option-decisions/">安裝選項判讀</a> 開始走完整的安裝流程，過程中再遇到這幾個操作就不會卡。</p>
]]></content:encoded></item><item><title>Linux 安裝選項判讀</title><link>https://tarrragon.github.io/blog/linux/install/install-option-decisions/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/install-option-decisions/</guid><description>&lt;p>Linux 安裝程式的每個選項都是一個會往後傳遞代價的決策。選錯的後果不會在當下浮現，而是在重開機進不了系統、磁碟某個分區先爆掉、或裝好的機器一重開就斷網時才出現。判斷一個選項該怎麼選，靠的是同一個問題：這台機器是用來幹嘛的。一台用完即丟的測試 VM、一台跑三年的主力機、一台對外服務的伺服器，同一個選項的正確答案可能完全不同。&lt;/p>
&lt;p>這篇給每個關鍵選項一條判斷軸，而不是一份「照著點」的步驟。底下用一次具體的安裝當作貫穿例子：在 Apple Silicon 的 UTM 上用 archboot ISO 裝 Arch Linux ARM，目標是一台演練 dotfile 部署的測試 VM。例子是具體的，但判斷軸跨發行版通用——locale、分割、bootloader 這些抉擇在多數 Linux 安裝程式裡都會以類似形式出現。&lt;/p>
&lt;p>怎麼建立 VM、燒錄並開機到安裝程式，是跟環境綁定的前置——UTM、VirtualBox、實體機各不相同——不在這條判讀軸的範圍；這篇從「安裝程式已經跑起來、開始問你選項」接手，給的是每個選項的判斷軸，不逐頁帶某個安裝程式的選單怎麼點。底下的指令與選單名稱以 archboot / Arch 呈現，換 Ubuntu（Subiquity 安裝程式）或 Fedora 時，選項的判斷軸一樣成立，但選單長相、套件組名稱、指令會不同。&lt;/p>
&lt;h2 id="系統語系與時間">系統語系與時間&lt;/h2>
&lt;p>系統語系決定的是錯誤訊息、log、系統工具輸出用哪種語言，不是你日常打字的語言。這兩件事容易混為一談。日常輸入中文是桌面層的字型與輸入法問題，跟系統 locale 無關；系統 locale 影響的是當某個服務崩潰、你在 &lt;code>journalctl&lt;/code> 裡讀它吐出來的那行訊息時，那行字是英文還是被翻譯過。&lt;/p>
&lt;p>把系統 locale 留在 &lt;code>en_US.UTF-8&lt;/code> 的理由是可搜尋性。當你把一段錯誤訊息貼到搜尋引擎或問別人，英文訊息能對上絕大多數的文件、issue、Stack Overflow 答案；翻譯過的訊息往往一個結果都搜不到。這條判斷軸對伺服器、開發機、任何你預期會除錯的機器都成立。會選非英文 locale 的情境通常是給終端使用者的桌面，且該使用者不除錯——那是另一種機器。&lt;/p>
&lt;p>時區的選擇影響 log 的時間戳跟排程任務的觸發時刻，挑你所在地即可。另一個相關的決策是「硬體時鐘存 UTC 還是本地時間」：選 UTC。Linux 慣例是硬體時鐘存 UTC、顯示時再換算成時區，這樣跨時區搬機器、或 NTP 校時都不會錯亂。會需要存本地時間的唯一常見情境是跟 Windows 雙開——Windows 預設把硬體時鐘當本地時間——而 VM 或純 Linux 機器沒有這個包袱。&lt;/p>
&lt;h2 id="網路">網路&lt;/h2>
&lt;p>安裝階段的網路設定要回答兩個層次：當下能不能連、以及這份設定會不會帶進裝好的系統。第一層通常很直覺——選到對的網卡、用 DHCP 讓它自動拿 IP。虛擬機的 NAT 網路會自動發 IP，所以選 DHCP、不要手動設 static，省去算網段的麻煩。&lt;/p>
&lt;p>第二層是真正會咬人的地方：安裝程式裡設好的網路，不保證會出現在重開機後的系統裡。這是 VM 裝 Linux 常見的斷網點——安裝時明明能上網裝套件，重開機後卻連不出去，因為安裝環境的網路設定沒被複製到目標系統。判讀方式是裝好首次開機後立刻驗證：看網卡有沒有拿到 IP、能不能解析一個域名。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ip -brief a &lt;span class="c1"># 網卡有沒有 IP、狀態是不是 UP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">ping -c &lt;span class="m">3&lt;/span> archlinux.org &lt;span class="c1"># 解析成功就證明對外連線 + DNS 都通&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DNS 能把域名解析成 IP，本身就證明對外連線是通的（DNS 查詢就是一次網路往返），所以這條 &lt;code>ping&lt;/code> 即使對方不回 ICMP 也已經給了答案。在前述的 archboot 例子裡，網路設定確實有被複製進目標系統並由 systemd-networkd 接手，重開機後免再手動設——但這是該安裝程式的行為，不能假設每個安裝程式都這樣。把「首次開機驗網路」當成固定動作，比預設它一定會通安全。&lt;/p>
&lt;h2 id="套件鏡像">套件鏡像&lt;/h2>
&lt;p>鏡像源決定你從哪裡下載套件，挑地理上接近的那個。基礎系統加上一套桌面動輒上 GB，選對岸的鏡像跟選同城的鏡像，下載時間差好幾倍。安裝程式給的鏡像清單通常按國家排，往下捲找你所在地區的；找不到完全同國的，就退而求其次選同區域、且穩定的大型鏡像。&lt;/p>
&lt;p>這個選擇的另一個作用是順帶確認你裝的是哪個發行版分支。前述例子的鏡像清單全是 &lt;code>archlinuxarm.org&lt;/code>，這證實了 archboot 的 aarch64 ISO 裝出來的是 Arch Linux ARM（ARM 移植版），而不是 x86 的 Arch——同一條安裝路徑產出的是哪個分支，鏡像來源會洩漏給你看。&lt;/p>
&lt;h2 id="磁碟分割">磁碟分割&lt;/h2>
&lt;p>磁碟分割是整個安裝裡選項最多、也最不可逆的一段，但判斷軸只有一條：這台機器需不需要在分區層面做隔離。需要隔離的情境——多系統共存、加密、資料與系統分離以便重灌不丟資料——每多一個，分割就多一層結構。不需要的情境，多切一刀都只是增加「一邊爆一邊空」的風險。下面逐項拆解，但它們服務的是同一個判斷。&lt;/p>
&lt;h3 id="自動分割-vs-手動分割">自動分割 vs 手動分割&lt;/h3>
&lt;p>自動分割（清空整碟、安裝程式幫你建標準佈局）適用於整碟專屬、沒有要保留任何既有資料的機器。測試 VM 的磁碟是全新的、整碟給這個系統用，自動分割沒有任何代價，還省去手動算 EFI 大小、root 大小、對齊、格式化、掛載這一連串容易錯的步驟。&lt;/p>
&lt;p>手動分割的價值在你需要非標準佈局時才浮現：多系統共用某個分區、要留一塊不格式化的資料區、要套 LVM 或 LUKS。這些是真實主力機與伺服器會遇到的需求，但對一台「目標是驗證 dotfile 部署」的 VM 是純雜訊——它們屬於另一個主題，不該混進這次的安裝。判讀訊號很簡單：你說得出一個具體的隔離需求，才手動分割；說不出來，自動分割就是對的。&lt;/p>
&lt;h3 id="分區識別方式partuuid">分區識別方式（PARTUUID）&lt;/h3>
&lt;p>分區識別方式決定 &lt;code>fstab&lt;/code>（開機時決定哪個分區掛到哪的設定檔）跟 bootloader 怎麼指涉每個分區，在 GPT（現代 UEFI 機器的分區表格式）磁碟上選 PARTUUID。這個選擇的後果是「重開機後系統找不找得到自己的分區」。PARTUUID 綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變；相對地，檔案系統層級的 UUID 一重格就變，會讓 &lt;code>fstab&lt;/code> 失效，而 &lt;code>/dev/vda1&lt;/code> 那種 kernel 名稱會隨偵測順序浮動，最不穩。穩定性的排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱，GPT 磁碟用最穩的那個（這三種識別方式的細節見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別卡&lt;/a>）。&lt;/p>
&lt;h3 id="efi-分區的掛載點與大小">EFI 分區的掛載點與大小&lt;/h3>
&lt;p>EFI 系統分區（ESP）放開機載入器與 kernel，掛載點的選擇取決於這台機器是不是單一作業系統。把 ESP 掛在 &lt;code>/boot&lt;/code>（單系統佈局）讓 kernel 跟開機檔住在同一個分區、維護最單純；把 ESP 掛在 &lt;code>/efi&lt;/code>、kernel 另放（多系統佈局）是為了多個 OS 共用同一個 ESP 才需要的結構。單系統的機器選多系統佈局，只是憑空多一層目錄。&lt;/p>
&lt;p>ESP 大小在單系統佈局下要算進 kernel 與 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs&lt;/a>（開機初期把真正的 root 掛起來之前、用來載入驅動的小型臨時根檔系統）。一個 kernel 加上它的 initramfs（含 fallback）大約一兩百 MB，再加上 FAT32 ESP 約 260 MiB 的實務下限，512 MiB 是在下限之上留餘裕。會需要更大的情境是你要同時保留多個 kernel 版本——但單 kernel 的 VM 用不到，給太大只是浪費。&lt;/p></description><content:encoded><![CDATA[<p>Linux 安裝程式的每個選項都是一個會往後傳遞代價的決策。選錯的後果不會在當下浮現，而是在重開機進不了系統、磁碟某個分區先爆掉、或裝好的機器一重開就斷網時才出現。判斷一個選項該怎麼選，靠的是同一個問題：這台機器是用來幹嘛的。一台用完即丟的測試 VM、一台跑三年的主力機、一台對外服務的伺服器，同一個選項的正確答案可能完全不同。</p>
<p>這篇給每個關鍵選項一條判斷軸，而不是一份「照著點」的步驟。底下用一次具體的安裝當作貫穿例子：在 Apple Silicon 的 UTM 上用 archboot ISO 裝 Arch Linux ARM，目標是一台演練 dotfile 部署的測試 VM。例子是具體的，但判斷軸跨發行版通用——locale、分割、bootloader 這些抉擇在多數 Linux 安裝程式裡都會以類似形式出現。</p>
<p>怎麼建立 VM、燒錄並開機到安裝程式，是跟環境綁定的前置——UTM、VirtualBox、實體機各不相同——不在這條判讀軸的範圍；這篇從「安裝程式已經跑起來、開始問你選項」接手，給的是每個選項的判斷軸，不逐頁帶某個安裝程式的選單怎麼點。底下的指令與選單名稱以 archboot / Arch 呈現，換 Ubuntu（Subiquity 安裝程式）或 Fedora 時，選項的判斷軸一樣成立，但選單長相、套件組名稱、指令會不同。</p>
<h2 id="系統語系與時間">系統語系與時間</h2>
<p>系統語系決定的是錯誤訊息、log、系統工具輸出用哪種語言，不是你日常打字的語言。這兩件事容易混為一談。日常輸入中文是桌面層的字型與輸入法問題，跟系統 locale 無關；系統 locale 影響的是當某個服務崩潰、你在 <code>journalctl</code> 裡讀它吐出來的那行訊息時，那行字是英文還是被翻譯過。</p>
<p>把系統 locale 留在 <code>en_US.UTF-8</code> 的理由是可搜尋性。當你把一段錯誤訊息貼到搜尋引擎或問別人，英文訊息能對上絕大多數的文件、issue、Stack Overflow 答案；翻譯過的訊息往往一個結果都搜不到。這條判斷軸對伺服器、開發機、任何你預期會除錯的機器都成立。會選非英文 locale 的情境通常是給終端使用者的桌面，且該使用者不除錯——那是另一種機器。</p>
<p>時區的選擇影響 log 的時間戳跟排程任務的觸發時刻，挑你所在地即可。另一個相關的決策是「硬體時鐘存 UTC 還是本地時間」：選 UTC。Linux 慣例是硬體時鐘存 UTC、顯示時再換算成時區，這樣跨時區搬機器、或 NTP 校時都不會錯亂。會需要存本地時間的唯一常見情境是跟 Windows 雙開——Windows 預設把硬體時鐘當本地時間——而 VM 或純 Linux 機器沒有這個包袱。</p>
<h2 id="網路">網路</h2>
<p>安裝階段的網路設定要回答兩個層次：當下能不能連、以及這份設定會不會帶進裝好的系統。第一層通常很直覺——選到對的網卡、用 DHCP 讓它自動拿 IP。虛擬機的 NAT 網路會自動發 IP，所以選 DHCP、不要手動設 static，省去算網段的麻煩。</p>
<p>第二層是真正會咬人的地方：安裝程式裡設好的網路，不保證會出現在重開機後的系統裡。這是 VM 裝 Linux 常見的斷網點——安裝時明明能上網裝套件，重開機後卻連不出去，因為安裝環境的網路設定沒被複製到目標系統。判讀方式是裝好首次開機後立刻驗證：看網卡有沒有拿到 IP、能不能解析一個域名。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ip -brief a          <span class="c1"># 網卡有沒有 IP、狀態是不是 UP</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ping -c <span class="m">3</span> archlinux.org   <span class="c1"># 解析成功就證明對外連線 + DNS 都通</span></span></span></code></pre></div><p>DNS 能把域名解析成 IP，本身就證明對外連線是通的（DNS 查詢就是一次網路往返），所以這條 <code>ping</code> 即使對方不回 ICMP 也已經給了答案。在前述的 archboot 例子裡，網路設定確實有被複製進目標系統並由 systemd-networkd 接手，重開機後免再手動設——但這是該安裝程式的行為，不能假設每個安裝程式都這樣。把「首次開機驗網路」當成固定動作，比預設它一定會通安全。</p>
<h2 id="套件鏡像">套件鏡像</h2>
<p>鏡像源決定你從哪裡下載套件，挑地理上接近的那個。基礎系統加上一套桌面動輒上 GB，選對岸的鏡像跟選同城的鏡像，下載時間差好幾倍。安裝程式給的鏡像清單通常按國家排，往下捲找你所在地區的；找不到完全同國的，就退而求其次選同區域、且穩定的大型鏡像。</p>
<p>這個選擇的另一個作用是順帶確認你裝的是哪個發行版分支。前述例子的鏡像清單全是 <code>archlinuxarm.org</code>，這證實了 archboot 的 aarch64 ISO 裝出來的是 Arch Linux ARM（ARM 移植版），而不是 x86 的 Arch——同一條安裝路徑產出的是哪個分支，鏡像來源會洩漏給你看。</p>
<h2 id="磁碟分割">磁碟分割</h2>
<p>磁碟分割是整個安裝裡選項最多、也最不可逆的一段，但判斷軸只有一條：這台機器需不需要在分區層面做隔離。需要隔離的情境——多系統共存、加密、資料與系統分離以便重灌不丟資料——每多一個，分割就多一層結構。不需要的情境，多切一刀都只是增加「一邊爆一邊空」的風險。下面逐項拆解，但它們服務的是同一個判斷。</p>
<h3 id="自動分割-vs-手動分割">自動分割 vs 手動分割</h3>
<p>自動分割（清空整碟、安裝程式幫你建標準佈局）適用於整碟專屬、沒有要保留任何既有資料的機器。測試 VM 的磁碟是全新的、整碟給這個系統用，自動分割沒有任何代價，還省去手動算 EFI 大小、root 大小、對齊、格式化、掛載這一連串容易錯的步驟。</p>
<p>手動分割的價值在你需要非標準佈局時才浮現：多系統共用某個分區、要留一塊不格式化的資料區、要套 LVM 或 LUKS。這些是真實主力機與伺服器會遇到的需求，但對一台「目標是驗證 dotfile 部署」的 VM 是純雜訊——它們屬於另一個主題，不該混進這次的安裝。判讀訊號很簡單：你說得出一個具體的隔離需求，才手動分割；說不出來，自動分割就是對的。</p>
<h3 id="分區識別方式partuuid">分區識別方式（PARTUUID）</h3>
<p>分區識別方式決定 <code>fstab</code>（開機時決定哪個分區掛到哪的設定檔）跟 bootloader 怎麼指涉每個分區，在 GPT（現代 UEFI 機器的分區表格式）磁碟上選 PARTUUID。這個選擇的後果是「重開機後系統找不找得到自己的分區」。PARTUUID 綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變；相對地，檔案系統層級的 UUID 一重格就變，會讓 <code>fstab</code> 失效，而 <code>/dev/vda1</code> 那種 kernel 名稱會隨偵測順序浮動，最不穩。穩定性的排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱，GPT 磁碟用最穩的那個（這三種識別方式的細節見 <a href="/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別卡</a>）。</p>
<h3 id="efi-分區的掛載點與大小">EFI 分區的掛載點與大小</h3>
<p>EFI 系統分區（ESP）放開機載入器與 kernel，掛載點的選擇取決於這台機器是不是單一作業系統。把 ESP 掛在 <code>/boot</code>（單系統佈局）讓 kernel 跟開機檔住在同一個分區、維護最單純；把 ESP 掛在 <code>/efi</code>、kernel 另放（多系統佈局）是為了多個 OS 共用同一個 ESP 才需要的結構。單系統的機器選多系統佈局，只是憑空多一層目錄。</p>
<p>ESP 大小在單系統佈局下要算進 kernel 與 <a href="/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs</a>（開機初期把真正的 root 掛起來之前、用來載入驅動的小型臨時根檔系統）。一個 kernel 加上它的 initramfs（含 fallback）大約一兩百 MB，再加上 FAT32 ESP 約 260 MiB 的實務下限，512 MiB 是在下限之上留餘裕。會需要更大的情境是你要同時保留多個 kernel 版本——但單 kernel 的 VM 用不到，給太大只是浪費。</p>
<h3 id="swap">Swap</h3>
<p>Swap 是記憶體不足時的安全墊，大小取決於這台機器的記憶體壓力型態，不是一個固定公式。對一台只有 4 GB RAM、且要在上面從原始碼編譯套件的 VM，編譯瞬間的記憶體尖峰很容易把實體記憶體吃爆、觸發 OOM 把進程殺掉。給 2 GB swap 當緩衝，擋住這種尖峰、避免安裝跑到一半被中斷。</p>
<p>swap 分區的磁碟成本不是一次付清的。<code>mkswap</code> 只寫一個 header，實際沒被換出的頁不會佔用宿主磁碟空間（在稀疏配置的虛擬磁碟上尤其明顯），用到才寫。所以「為了保險多給一點 swap」的代價，比直覺以為的小。判讀軸是看工作負載：會編譯、會跑吃記憶體的服務、RAM 又緊，就給足 swap；純文書、RAM 寬裕，小一點或不給都行。</p>
<p>swap 還有形態的選擇，這裡用分區 swap 是因為在安裝程式階段一併切好最省事，不代表它比另兩種好。swapfile（一個檔案，事後可隨時調大小或移除）避開了「分割最不可逆」的痛點；zram（壓縮記憶體 swap，不碰磁碟）對低 RAM 加編譯尖峰正是設計情境，現代發行版很多預設用它。換句話說，2 GB 這個量是看編譯尖峰定的，而「切成分區」只是配合安裝當下一次到位——若你跳過安裝期的 swap、事後用 swapfile 或 zram 補，是等價的可逆路徑。</p>
<h3 id="檔案系統">檔案系統</h3>
<p>檔案系統的選擇是在「簡單可靠」與「進階功能」之間取捨，預設往簡單那邊靠。<code>ext4</code> 簡單、穩、在各平台行為一致、修復工具成熟，對一台只要求「可靠地存取檔案」的機器是零驚喜的選擇。<code>btrfs</code> 提供快照、subvolume、透明壓縮，但代價是要規劃 subvolume 佈局、還要理解它寫時複製（CoW）的一些行為差異；這些功能在你會用快照回滾的主力機上很有價值，在演練 VM 上是雜訊。<code>xfs</code> 同樣穩定，但對這類用途相對 ext4 沒有決定性優勢。更特定的 <code>zfs</code>、<code>f2fs</code> 不在一般 VM 的考慮範圍——zfs 在 Arch / ARM 上是非主線 kernel 的 out-of-tree 模組、授權與維護成本高，f2fs 是為快閃裝置設計、VM 用不到。</p>
<p>判讀軸是你會不會用到進階功能。會固定用快照回滾系統狀態——選 btrfs 並接受它的佈局複雜度；只是要一個可靠的檔案系統——ext4。快照這類能力跟下面的獨立 <code>/home</code> 一樣，是真實機器的儲存規劃主題，值得另外深入，但別為了「聽起來比較強」就把它的複雜度帶進一台用完即丟的機器。</p>
<h3 id="獨立-home-vs-單一-root">獨立 /home vs 單一 root</h3>
<p>獨立 <code>/home</code> 分區的價值是「重灌系統不丟個人資料」，這是主力機的需求，不是每台機器都需要。把 <code>/home</code> 切成獨立分區，重裝系統時可以只格式化 root、保留 <code>/home</code> 裡的設定與檔案。一台用完即丟的演練 VM 沒有這個需求——它的整個生命週期就是裝起來、驗證、丟掉。如果你想跨多次實驗保留狀態，VM 情境更貼切的手段是宿主層的快照或共享資料夾，而不是在 guest 裡切獨立 <code>/home</code>——後者解決的是「重灌 OS 保資料」，不是「保留實驗狀態」。</p>
<p>把空間切兩半的隱性代價是失衡風險。假設總共十幾 GB，照預設切一塊給 <code>/</code>、剩下給 <code>/home</code>，所有系統套件（桌面全套依賴都裝在 <code>/usr</code>、算 <code>/</code>）擠在前者、容易先滿，而 <code>/home</code> 那邊空著——典型的一邊爆一邊空。把全部空間當一個池子用的單一 root 佈局，不會人為卡死任一邊，對不需要「重灌保資料」的機器最不容易出事。</p>
<h2 id="bootloader">Bootloader</h2>
<p>開機載入器決定韌體怎麼找到並載入 kernel，在虛擬機上選 GRUB 而非直接用 EFISTUB，理由是可靠性（韌體到 kernel 的整條交棒過程見 <a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈卡</a>）。EFISTUB 讓 UEFI 韌體直接載入 kernel、不經過獨立的 bootloader，最精簡，但它完全依賴寫進 UEFI NVRAM（韌體用來存開機項的非揮發記憶體）的開機項。問題在於 QEMU 系的虛擬機（UTM 底層即是）對 EFI 變數的儲存有時不穩，一旦 NVRAM 裡的開機項掉了，韌體就找不到 kernel、機器開不了——這在 VM 環境是會實際踩到的坑。</p>
<p>GRUB 的容錯來自它（以 removable 模式安裝時）多寫了一份。除了 NVRAM 開機項，<code>grub-install --removable</code> 會在 ESP 的標準 fallback 路徑（aarch64 是 <code>\EFI\BOOT\BOOTAA64.EFI</code>）也放一份，就算 NVRAM 開機項丟了，韌體仍會從 fallback 路徑找到 GRUB；VM 環境的安裝程式通常以這個模式裝 GRUB，正是看上這層保險。它還附帶一個開機選單，當 kernel 或 initramfs 出問題時，可以進選單救援、加開機參數除錯——演練時的容錯空間大很多。</p>
<p>判讀軸是環境的 NVRAM 可靠度。在 NVRAM 穩定的實體機上，EFISTUB 的極簡是漂亮的選擇；在 NVRAM 可能不穩的 VM 上，可靠性優先，GRUB 的「多寫一份 fallback + 救援選單」更穩妥。GRUB 不是唯一的可靠選擇——systemd-boot 同樣是有開機選單、能裝到 fallback 路徑的獨立 bootloader，又比 GRUB 輕，在 VM / 單系統同樣站得住；這裡落在 GRUB 是因為 archboot 安裝程式預設以 removable 模式裝它，不是 GRUB 獨佔可靠性。GRUB 自己的設定檔在 VM 上用預設值即可，不需要額外的 kernel 參數。</p>
<h2 id="下一步">下一步</h2>
<p>選項選完、系統裝好、重開機進得去之後，先別急著開始用——「裝好了」跟「能用了」之間往往還缺一截：這台最小系統不一定有你需要的基本工具。<a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a> 就是在補那一截。</p>
<p>從安裝到桌面就緒的完整依賴順序，見 <a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a>；本篇是它「安裝作業系統」那一步的展開。</p>
]]></content:encoded></item><item><title>最小安裝後的工具驗證與補足</title><link>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</guid><description>&lt;p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 &lt;code>base&lt;/code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——&lt;code>sudo&lt;/code>、&lt;code>which&lt;/code>、&lt;code>rsync&lt;/code>——一個都沒有。驗證它們在不在，比假設它們在安全。&lt;/p>
&lt;p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 &lt;code>sudo pacman -S ...&lt;/code>，在一台連 &lt;code>sudo&lt;/code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。&lt;/p>
&lt;h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋&lt;/h2>
&lt;p>&lt;code>sudo&lt;/code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 &lt;code>wheel&lt;/code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。&lt;/p>
&lt;p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（&lt;code>su -&lt;/code> 成為 root、&lt;code>echo &amp;gt; 檔案&lt;/code> 重導向、&lt;code>chmod&lt;/code> 設權限這些基礎操作不熟的話，見 &lt;a href="../basic-operations/">安裝過程用到的基礎操作&lt;/a>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">su - &lt;span class="c1"># 切到 root（輸入 root 密碼）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pacman -S --needed sudo &lt;span class="c1"># root 身分裝 sudo，不需要 sudo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;%wheel ALL=(ALL:ALL) ALL&amp;#39;&lt;/span> &amp;gt; /etc/sudoers.d/10-wheel &lt;span class="c1"># 啟用 wheel 群組授權&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/10-wheel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">visudo -c &lt;span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">exit&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>切回一般使用者後用 &lt;code>sudo -v&lt;/code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。&lt;code>sudo&lt;/code> 是這類前置最典型的一個。&lt;/p>
&lt;p>上面的指令以 Arch 的 &lt;code>pacman&lt;/code> 為例。Fedora 用 &lt;code>dnf&lt;/code>、Debian/Ubuntu 用 &lt;code>apt&lt;/code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。&lt;/p>
&lt;h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷&lt;/h2>
&lt;p>&lt;code>which&lt;/code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 &lt;code>$(which zsh)&lt;/code> 之類的寫法取一支程式的完整路徑；在沒有 &lt;code>which&lt;/code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 &lt;code>chsh -s &amp;quot;$(which zsh)&amp;quot;&lt;/code> 因為 &lt;code>which&lt;/code> 不存在而變成 &lt;code>chsh -s &amp;quot;&amp;quot;&lt;/code>，最後報的是 &lt;code>chsh: shell must be a full path name&lt;/code>——錯誤訊息完全沒提到真正的元兇。&lt;/p>
&lt;p>正確的做法是用 &lt;code>command -v&lt;/code> 取代 &lt;code>which&lt;/code>。&lt;code>command -v&lt;/code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v zsh &lt;span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條判讀對你自己寫的腳本是「把 &lt;code>which&lt;/code> 全換成 &lt;code>command -v&lt;/code>」，對別人的腳本是「在缺 &lt;code>which&lt;/code> 的系統上，先補 &lt;code>which&lt;/code> 套件或改腳本」。它跟 sudo 的差別在於：&lt;code>which&lt;/code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。&lt;/p>
&lt;h2 id="其他常缺的工具">其他常缺的工具&lt;/h2>
&lt;p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>缺了會怎樣&lt;/th>
 &lt;th>補法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>rsync&lt;/code>&lt;/td>
 &lt;td>從本機同步 dotfile 進機器時 &lt;code>rsync: command not found&lt;/code>&lt;/td>
 &lt;td>進套件清單；急用時改用 &lt;code>tar&lt;/code> over SSH 過渡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ca-certificates&lt;/code>&lt;/td>
 &lt;td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）&lt;/td>
 &lt;td>進套件清單；它是下一篇 HTTPS clone 的隱性前置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>hostname&lt;/code>&lt;/td>
 &lt;td>某些腳本呼叫 &lt;code>hostname&lt;/code> 取主機名時失敗&lt;/td>
 &lt;td>補 &lt;code>inetutils&lt;/code>，或改用 &lt;code>hostnamectl&lt;/code> / 讀 &lt;code>/etc/hostname&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯工具鏈&lt;/td>
 &lt;td>從原始碼或社群套件庫編譯時缺 &lt;code>gcc&lt;/code> / &lt;code>make&lt;/code>&lt;/td>
 &lt;td>補發行版的開發工具組（如 &lt;code>base-devel&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>rsync&lt;/code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 &lt;code>tar&lt;/code> 搭配 SSH 過渡：&lt;/p></description><content:encoded><![CDATA[<p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 <code>base</code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——<code>sudo</code>、<code>which</code>、<code>rsync</code>——一個都沒有。驗證它們在不在，比假設它們在安全。</p>
<p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 <code>sudo pacman -S ...</code>，在一台連 <code>sudo</code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。</p>
<h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋</h2>
<p><code>sudo</code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 <code>wheel</code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。</p>
<p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（<code>su -</code> 成為 root、<code>echo &gt; 檔案</code> 重導向、<code>chmod</code> 設權限這些基礎操作不熟的話，見 <a href="../basic-operations/">安裝過程用到的基礎操作</a>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">su -                                          <span class="c1"># 切到 root（輸入 root 密碼）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pacman -S --needed sudo                        <span class="c1"># root 身分裝 sudo，不需要 sudo</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;%wheel ALL=(ALL:ALL) ALL&#39;</span> &gt; /etc/sudoers.d/10-wheel   <span class="c1"># 啟用 wheel 群組授權</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">chmod <span class="m">440</span> /etc/sudoers.d/10-wheel
</span></span><span class="line"><span class="ln">5</span><span class="cl">visudo -c                                      <span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">exit</span></span></code></pre></div><p>切回一般使用者後用 <code>sudo -v</code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。<code>sudo</code> 是這類前置最典型的一個。</p>
<p>上面的指令以 Arch 的 <code>pacman</code> 為例。Fedora 用 <code>dnf</code>、Debian/Ubuntu 用 <code>apt</code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。</p>
<h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷</h2>
<p><code>which</code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 <code>$(which zsh)</code> 之類的寫法取一支程式的完整路徑；在沒有 <code>which</code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 <code>chsh -s &quot;$(which zsh)&quot;</code> 因為 <code>which</code> 不存在而變成 <code>chsh -s &quot;&quot;</code>，最後報的是 <code>chsh: shell must be a full path name</code>——錯誤訊息完全沒提到真正的元兇。</p>
<p>正確的做法是用 <code>command -v</code> 取代 <code>which</code>。<code>command -v</code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">command</span> -v zsh        <span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西</span></span></span></code></pre></div><p>這條判讀對你自己寫的腳本是「把 <code>which</code> 全換成 <code>command -v</code>」，對別人的腳本是「在缺 <code>which</code> 的系統上，先補 <code>which</code> 套件或改腳本」。它跟 sudo 的差別在於：<code>which</code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。</p>
<h2 id="其他常缺的工具">其他常缺的工具</h2>
<p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>缺了會怎樣</th>
          <th>補法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>rsync</code></td>
          <td>從本機同步 dotfile 進機器時 <code>rsync: command not found</code></td>
          <td>進套件清單；急用時改用 <code>tar</code> over SSH 過渡</td>
      </tr>
      <tr>
          <td><code>ca-certificates</code></td>
          <td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）</td>
          <td>進套件清單；它是下一篇 HTTPS clone 的隱性前置</td>
      </tr>
      <tr>
          <td><code>hostname</code></td>
          <td>某些腳本呼叫 <code>hostname</code> 取主機名時失敗</td>
          <td>補 <code>inetutils</code>，或改用 <code>hostnamectl</code> / 讀 <code>/etc/hostname</code></td>
      </tr>
      <tr>
          <td>編譯工具鏈</td>
          <td>從原始碼或社群套件庫編譯時缺 <code>gcc</code> / <code>make</code></td>
          <td>補發行版的開發工具組（如 <code>base-devel</code>）</td>
      </tr>
  </tbody>
</table>
<p><code>rsync</code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 <code>tar</code> 搭配 SSH 過渡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dest &amp;&amp; tar xzf - -C ~/dest&#39;</span></span></span></code></pre></div><p>這條的好處是不依賴目標機有 rsync；缺點是它每次都傳全部、沒有 rsync 的增量。在反覆同步的工作流裡，值得早點把 rsync 補進套件清單換取增量傳輸。</p>
<p><code>ca-certificates</code> 最容易在下一步咬人。最小系統可能沒有 CA 信任根，這時任何 HTTPS 連線——包括下一篇主推的「公開 repo 用 HTTPS clone」——會在 TLS 憑證驗證直接失敗，而錯誤訊息常指向 SSL handshake 而非「缺信任根」，容易誤判成網路問題。打算走 HTTPS 取得 dotfile 的機器，先確認 <code>ca-certificates</code> 在。<code>git</code> 與 <code>curl</code> 同理：它們是 bootstrap 取得程式碼的基本工具，下面的驗證迴圈也會檢查，最小系統若沒有要一併補。</p>
<p>剩下兩項的缺席各有觸發時機。<code>hostname</code> 只在腳本明確呼叫它取主機名時才會浮現缺失，而用 <code>hostnamectl</code> 或直接讀 <code>/etc/hostname</code> 可以繞過，所以它常被當成「補了省事、不補也有替代」的軟缺口。編譯工具鏈則是在你要從原始碼或社群套件庫編譯時才需要——純跑預編譯套件的機器可以不裝，但只要你的 dotfile 流程會編譯任何東西（例如從社群套件庫裝桌面元件），它就得進清單。</p>
<h2 id="系統性的驗證">系統性的驗證</h2>
<p>裝好系統後先跑一輪集中驗證、把缺口一次盤出來，比等腳本跑到一半才逐一踩雷省事。驗證的對象是「你接下來的流程會用到、但最小系統可能沒有」的工具。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> cmd in sudo git curl rsync tar zsh<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;OK   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;缺   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這段刻意用 <code>command -v</code> 來檢查（而不是 <code>which</code>），因為要檢查的對象之一正是「外部工具在不在」，用一個一定存在的內建來檢查才不會自己先掛掉。盤出來的缺口分兩類處理：bootstrap 自身依賴的（如 sudo）當前置手動補；其餘的（如 rsync、編譯工具）進套件清單，交給 bootstrap 一起裝。</p>
<h2 id="跟-bootstrap-套件清單的界線">跟 Bootstrap 套件清單的界線</h2>
<p>這篇的驗證跟 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八的 bootstrap script 設計</a> 是兩件互補的事，界線在「假設」上。bootstrap script 的套件清單假設一個前提：機器已經有能力執行安裝（有 sudo、有 package manager、清單裡的東西都能被裝上）。這篇處理的正是那個前提成立之前的階段——最小系統到底有沒有滿足那些假設，缺的補上，讓 bootstrap 的假設變成事實。</p>
<p>換句話說，套件清單回答「這台機器最終要有哪些套件」，工具驗證回答「這台機器現在夠不夠資格開始跑那份清單」。把兩者分清楚，才不會把 sudo 這種前置誤塞進靠 sudo 安裝的清單裡。</p>
<h2 id="下一步">下一步</h2>
<p>工具補齊、機器有能力執行安裝之後，你還困在一個地方：擠在機器的主控台手打。怎麼從舒適的本機終端機操作它，以及還沒有 SSH key 時怎麼把 dotfile 弄進去，<a href="../ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑</a> 處理這兩件事。</p>
]]></content:encoded></item><item><title>安裝期套件與網路故障排除：pacman / DNS / mirror / keyring</title><link>https://tarrragon.github.io/blog/linux/install/package-and-network-troubleshooting/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/package-and-network-troubleshooting/</guid><description>&lt;p>裝好 OS、第一次跑套件管理器抓 bootstrap 要的東西時，最常撞的一類故障是「套件裝不下來」。這類故障的第一步判讀，是把它拆成兩層完全不同的問題：&lt;strong>連不到（網路 / DNS / mirror）&lt;/strong>，還是&lt;strong>連得到但被拒（套件管理器自己的狀態）&lt;/strong>。這兩層的檢查工具、根因、修法都不一樣，先分對層再往下查，才不會拿修 DNS 的方法去治簽章過期。這篇以 Arch 的 &lt;code>pacman&lt;/code> 為主要案例（本系列 VM 實測踩過的坑），其他發行版的套件管理器概念對應相同。&lt;/p>
&lt;h2 id="第一步分連不到還是連得到但被拒">第一步：分「連不到」還是「連得到但被拒」&lt;/h2>
&lt;p>錯誤訊息本身就能分層，不用猜：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>訊息提到主機名解不出、連線逾時、retrieving file 失敗&lt;/strong> → 連不到，往網路 / DNS / mirror 查。&lt;/li>
&lt;li>&lt;strong>訊息提到 database lock、signature、trust、conflicting、partial&lt;/strong> → 連得到、封包也拿到了，是套件管理器的狀態問題。&lt;/li>
&lt;/ul>
&lt;p>判準是問一句：「它到底有沒有成功連上 mirror？」有連上才談得到簽章、相依、db 狀態；連都沒連上，那些都還輪不到。剛裝好的最小系統最常見的是前者——網路設定還沒到位。&lt;/p>
&lt;h2 id="連不到那層從實體介面往上查到域名">連不到那層：從實體介面往上查到域名&lt;/h2>
&lt;p>網路不通有好幾層，從最底層往上逐層確認，哪一層斷了一目了然。這條鏈跟&lt;a href="../minimal-install-verify/">最小安裝後的驗證&lt;/a>裡的網路檢查同源，這裡聚焦在「抓套件失敗」這個症狀上：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ip -brief a &lt;span class="c1"># 1. 有沒有拿到 IP？介面 UP 且有位址&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">ping -c1 8.8.8.8 &lt;span class="c1"># 2. IP 層對外通不通？（直接打 IP、跳過 DNS）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">getent hosts archlinux.org &lt;span class="c1"># 3. 域名解得出來嗎？&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">timedatectl &lt;span class="c1"># 4. 時間對嗎？（影響下一層的簽章驗證）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第 2 步通、第 3 步不通 = DNS 問題&lt;/strong>，這是最小安裝最典型的落點：IP 層明明通（&lt;code>ping 8.8.8.8&lt;/code> 有回應），但域名解不出來，因為 &lt;code>/etc/resolv.conf&lt;/code> 還沒設 nameserver。這時 pacman 會卡在解析 mirror 主機名。修法是給系統一個 resolver——臨時可直接寫 &lt;code>/etc/resolv.conf&lt;/code>（&lt;code>nameserver 1.1.1.1&lt;/code>）。先看它是什麼（&lt;code>ls -l /etc/resolv.conf&lt;/code>）：啟用了 &lt;code>systemd-resolved&lt;/code> 或 NetworkManager 的系統上它是那些服務管理的 symlink，手寫會被覆蓋，治本要透過該網路管理服務設定 DNS；裸 Arch 最小安裝若沒啟用這些服務，它通常就是一個普通檔案，手寫即持久生效。&lt;/p>
&lt;p>&lt;strong>mirror 逾時 / 抓不到&lt;/strong>：DNS 通了、但某個 mirror 慢或掛了。換 &lt;code>/etc/pacman.d/mirrorlist&lt;/code> 到地理近且快的鏡像（實測不同 mirror 速度可差數倍）。這也接回&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a>裡選 mirror 的決策——裝機當下選錯 mirror，這裡就會慢。&lt;/p>
&lt;h2 id="連得到但被拒那層pacman-自己的狀態">連得到但被拒那層：pacman 自己的狀態&lt;/h2>
&lt;p>連上 mirror、封包也拿到了卻失敗，問題在 pacman 的本地狀態或簽章驗證。這幾種各有明確徵兆與修法：&lt;/p>
&lt;h3 id="database-lock上次沒清乾淨的殘留">database lock：上次沒清乾淨的殘留&lt;/h3>
&lt;p>&lt;code>error: failed to init transaction (unable to lock database)&lt;/code>。pacman 用 &lt;code>/var/lib/pacman/db.lck&lt;/code> 這個鎖檔保證同時只有一個 pacman 在動資料庫；上次 pacman 被中斷（斷電、Ctrl+C、當掉）沒清掉鎖檔就會殘留。&lt;strong>先確認真的沒有 pacman 在跑&lt;/strong>（&lt;code>pgrep -x pacman&lt;/code>），確認沒有再刪鎖檔：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pgrep -x pacman &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;有 pacman 在跑、別刪&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> sudo rm /var/lib/pacman/db.lck&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>先查再刪這個順序重要——盲刪鎖檔時如果真的有另一個 pacman 在跑，兩個同時寫資料庫會弄壞它。&lt;/p>
&lt;h3 id="簽章--keyring-過期十之八九是時間不對">簽章 / keyring 過期：十之八九是時間不對&lt;/h3>
&lt;p>&lt;code>invalid or corrupted package (PGP signature)&lt;/code> 或 &lt;code>signature is unknown trust&lt;/code>。pacman 驗證每個套件的 GPG 簽章，驗證失敗最常見的根因是&lt;strong>系統時間不對&lt;/strong>——這正是第一步要 &lt;code>timedatectl&lt;/code> 的原因。時間差太多（新裝的 VM、主機板電池沒電的老機器）會讓「簽章的有效期」判斷錯誤，明明有效的簽章被判過期。先校時：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo timedatectl set-ntp &lt;span class="nb">true&lt;/span> &lt;span class="c1"># 開 NTP 自動校時（SSH 進最小系統無 polkit 互動代理、裸跑會被拒，要 sudo）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間對了還失敗，才是 keyring 本身的問題（archlinux-keyring 太舊）：&lt;code>sudo pacman -Sy archlinux-keyring&lt;/code> 更新 keyring，必要時 &lt;code>sudo pacman-key --refresh-keys&lt;/code>。順序是先校時再動 keyring，因為時間不對時連 keyring 都更新不了。&lt;/p>
&lt;h3 id="partial-upgrade只同步不升級造成的相依斷裂">partial upgrade：只同步不升級造成的相依斷裂&lt;/h3>
&lt;p>&lt;code>conflicting dependencies&lt;/code> 或裝完某個套件後系統行為異常。根因是在 rolling 發行版上只做了 &lt;code>pacman -Sy&lt;/code>（同步資料庫）就裝新套件，卻沒 &lt;code>-u&lt;/code>（升級既有套件）——新套件依賴新版函式庫，但系統還是舊的，相依對不上。Arch 只支援 full upgrade：&lt;strong>一律 &lt;code>pacman -Syu&lt;/code>，永遠不要單獨 &lt;code>-Sy&lt;/code> 之後裝東西&lt;/strong>。這條規則救掉這一整類故障。&lt;/p></description><content:encoded><![CDATA[<p>裝好 OS、第一次跑套件管理器抓 bootstrap 要的東西時，最常撞的一類故障是「套件裝不下來」。這類故障的第一步判讀，是把它拆成兩層完全不同的問題：<strong>連不到（網路 / DNS / mirror）</strong>，還是<strong>連得到但被拒（套件管理器自己的狀態）</strong>。這兩層的檢查工具、根因、修法都不一樣，先分對層再往下查，才不會拿修 DNS 的方法去治簽章過期。這篇以 Arch 的 <code>pacman</code> 為主要案例（本系列 VM 實測踩過的坑），其他發行版的套件管理器概念對應相同。</p>
<h2 id="第一步分連不到還是連得到但被拒">第一步：分「連不到」還是「連得到但被拒」</h2>
<p>錯誤訊息本身就能分層，不用猜：</p>
<ul>
<li><strong>訊息提到主機名解不出、連線逾時、retrieving file 失敗</strong> → 連不到，往網路 / DNS / mirror 查。</li>
<li><strong>訊息提到 database lock、signature、trust、conflicting、partial</strong> → 連得到、封包也拿到了，是套件管理器的狀態問題。</li>
</ul>
<p>判準是問一句：「它到底有沒有成功連上 mirror？」有連上才談得到簽章、相依、db 狀態；連都沒連上，那些都還輪不到。剛裝好的最小系統最常見的是前者——網路設定還沒到位。</p>
<h2 id="連不到那層從實體介面往上查到域名">連不到那層：從實體介面往上查到域名</h2>
<p>網路不通有好幾層，從最底層往上逐層確認，哪一層斷了一目了然。這條鏈跟<a href="../minimal-install-verify/">最小安裝後的驗證</a>裡的網路檢查同源，這裡聚焦在「抓套件失敗」這個症狀上：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ip -brief a              <span class="c1"># 1. 有沒有拿到 IP？介面 UP 且有位址</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ping -c1 8.8.8.8         <span class="c1"># 2. IP 層對外通不通？（直接打 IP、跳過 DNS）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">getent hosts archlinux.org   <span class="c1"># 3. 域名解得出來嗎？</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">timedatectl              <span class="c1"># 4. 時間對嗎？（影響下一層的簽章驗證）</span></span></span></code></pre></div><p><strong>第 2 步通、第 3 步不通 = DNS 問題</strong>，這是最小安裝最典型的落點：IP 層明明通（<code>ping 8.8.8.8</code> 有回應），但域名解不出來，因為 <code>/etc/resolv.conf</code> 還沒設 nameserver。這時 pacman 會卡在解析 mirror 主機名。修法是給系統一個 resolver——臨時可直接寫 <code>/etc/resolv.conf</code>（<code>nameserver 1.1.1.1</code>）。先看它是什麼（<code>ls -l /etc/resolv.conf</code>）：啟用了 <code>systemd-resolved</code> 或 NetworkManager 的系統上它是那些服務管理的 symlink，手寫會被覆蓋，治本要透過該網路管理服務設定 DNS；裸 Arch 最小安裝若沒啟用這些服務，它通常就是一個普通檔案，手寫即持久生效。</p>
<p><strong>mirror 逾時 / 抓不到</strong>：DNS 通了、但某個 mirror 慢或掛了。換 <code>/etc/pacman.d/mirrorlist</code> 到地理近且快的鏡像（實測不同 mirror 速度可差數倍）。這也接回<a href="../install-option-decisions/">安裝選項判讀</a>裡選 mirror 的決策——裝機當下選錯 mirror，這裡就會慢。</p>
<h2 id="連得到但被拒那層pacman-自己的狀態">連得到但被拒那層：pacman 自己的狀態</h2>
<p>連上 mirror、封包也拿到了卻失敗，問題在 pacman 的本地狀態或簽章驗證。這幾種各有明確徵兆與修法：</p>
<h3 id="database-lock上次沒清乾淨的殘留">database lock：上次沒清乾淨的殘留</h3>
<p><code>error: failed to init transaction (unable to lock database)</code>。pacman 用 <code>/var/lib/pacman/db.lck</code> 這個鎖檔保證同時只有一個 pacman 在動資料庫；上次 pacman 被中斷（斷電、Ctrl+C、當掉）沒清掉鎖檔就會殘留。<strong>先確認真的沒有 pacman 在跑</strong>（<code>pgrep -x pacman</code>），確認沒有再刪鎖檔：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep -x pacman <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;有 pacman 在跑、別刪&#34;</span> <span class="o">||</span> sudo rm /var/lib/pacman/db.lck</span></span></code></pre></div><p>先查再刪這個順序重要——盲刪鎖檔時如果真的有另一個 pacman 在跑，兩個同時寫資料庫會弄壞它。</p>
<h3 id="簽章--keyring-過期十之八九是時間不對">簽章 / keyring 過期：十之八九是時間不對</h3>
<p><code>invalid or corrupted package (PGP signature)</code> 或 <code>signature is unknown trust</code>。pacman 驗證每個套件的 GPG 簽章，驗證失敗最常見的根因是<strong>系統時間不對</strong>——這正是第一步要 <code>timedatectl</code> 的原因。時間差太多（新裝的 VM、主機板電池沒電的老機器）會讓「簽章的有效期」判斷錯誤，明明有效的簽章被判過期。先校時：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo timedatectl set-ntp <span class="nb">true</span>     <span class="c1"># 開 NTP 自動校時（SSH 進最小系統無 polkit 互動代理、裸跑會被拒，要 sudo）</span></span></span></code></pre></div><p>時間對了還失敗，才是 keyring 本身的問題（archlinux-keyring 太舊）：<code>sudo pacman -Sy archlinux-keyring</code> 更新 keyring，必要時 <code>sudo pacman-key --refresh-keys</code>。順序是先校時再動 keyring，因為時間不對時連 keyring 都更新不了。</p>
<h3 id="partial-upgrade只同步不升級造成的相依斷裂">partial upgrade：只同步不升級造成的相依斷裂</h3>
<p><code>conflicting dependencies</code> 或裝完某個套件後系統行為異常。根因是在 rolling 發行版上只做了 <code>pacman -Sy</code>（同步資料庫）就裝新套件，卻沒 <code>-u</code>（升級既有套件）——新套件依賴新版函式庫，但系統還是舊的，相依對不上。Arch 只支援 full upgrade：<strong>一律 <code>pacman -Syu</code>，永遠不要單獨 <code>-Sy</code> 之後裝東西</strong>。這條規則救掉這一整類故障。</p>
<h3 id="stale-db-404裝機當下的資料庫已經過期">stale db 404：裝機當下的資料庫已經過期</h3>
<p><code>error: failed retrieving file '...' 404</code>，而且換好幾個 mirror 都一樣。這是 rolling 發行版特有的時序陷阱：Arch 的 mirror 不保留舊版檔案，你裝機時 ISO 內建的套件資料庫指向的檔名，可能幾天內就被輪替掉了——資料庫說有這個檔、mirror 上已經沒有。修法跟上一條同源：<code>pacman -Syu</code> 先把資料庫同步到最新，檔名對上了就抓得到。這也是為什麼「一律 <code>-Syu</code>」是 Arch 的鐵律，而不只是建議。</p>
<h2 id="判讀總表">判讀總表</h2>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>層</th>
          <th>權威檢查</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主機名解不出</td>
          <td>網路</td>
          <td><code>getent hosts &lt;域名&gt;</code></td>
          <td>設 resolver（注意 symlink）</td>
      </tr>
      <tr>
          <td>ping IP 通、域名不通</td>
          <td>DNS</td>
          <td><code>ping 8.8.8.8</code> vs <code>getent</code></td>
          <td>設 <code>/etc/resolv.conf</code> 或網管服務</td>
      </tr>
      <tr>
          <td>mirror 慢 / 逾時</td>
          <td>網路</td>
          <td>換 mirror 測速</td>
          <td>改 mirrorlist</td>
      </tr>
      <tr>
          <td>unable to lock database</td>
          <td>pacman</td>
          <td><code>pgrep -x pacman</code></td>
          <td>確認無後刪 db.lck</td>
      </tr>
      <tr>
          <td>PGP signature / unknown trust</td>
          <td>pacman</td>
          <td><code>timedatectl</code>（先校時）</td>
          <td>校時 →（仍失敗）更新 keyring</td>
      </tr>
      <tr>
          <td>conflicting / partial</td>
          <td>pacman</td>
          <td>是否只跑了 <code>-Sy</code></td>
          <td><code>pacman -Syu</code>（永遠 full）</td>
      </tr>
      <tr>
          <td>retrieving file 404（多 mirror）</td>
          <td>pacman</td>
          <td>rolling stale db</td>
          <td><code>pacman -Syu</code> 同步再裝</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步">下一步</h2>
<ul>
<li>這幾步用到的網路驗證，完整版在<a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a>。</li>
<li>裝機時選 mirror / locale / 時區的決策，見<a href="../install-option-decisions/">Linux 安裝選項判讀</a>。</li>
<li>跨發行版時「這個套件名 / 這個旗標在別的發行版叫什麼」的差異判讀，見<a href="../platform-divergence-map/">平台與發行版差異的判讀地圖</a>。</li>
<li>套件抓下來了、但 bootstrap 腳本本身失敗要 debug，見<a href="../observable-bootstrap/">可除錯的 bootstrap</a>。</li>
<li>系統跑起來後才出的套件問題（AUR 建置失敗、<code>-bin</code> 包 soname 斷裂等），屬除錯範疇，見<a href="../../debug/">Linux 除錯與診斷</a>。</li>
</ul>
]]></content:encoded></item><item><title>外部連入、SSH key 與無 key 的 bootstrap 路徑</title><link>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</guid><description>&lt;p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。&lt;/p>
&lt;p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。&lt;/p>
&lt;h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入&lt;/h2>
&lt;p>讓機器能被 SSH 連入只需要兩步：裝 SSH 伺服器、啟動它的服務。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pacman -S openssh &lt;span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl &lt;span class="nb">enable&lt;/span> --now sshd &lt;span class="c1"># enable 開機自啟、--now 立刻啟動&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora &lt;code>dnf&lt;/code>、Debian/Ubuntu &lt;code>apt&lt;/code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 &lt;code>ssh&lt;/code> 不是 &lt;code>sshd&lt;/code>，那邊要 &lt;code>systemctl enable --now ssh&lt;/code>。&lt;/p>
&lt;p>從本機連的時候用一般使用者、不要用 root：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@&amp;lt;機器 IP&amp;gt; &lt;span class="c1"># IP 來自機器上的 ip -brief a&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（&lt;code>PermitRootLogin prohibit-password&lt;/code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。&lt;/p>
&lt;p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引&lt;/a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 &lt;code>systemctl enable sshd&lt;/code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。&lt;/p>
&lt;h2 id="ssh-key-免密碼">SSH key 免密碼&lt;/h2>
&lt;p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。&lt;/p>
&lt;p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 SSH 設定裡給它一個好記的別名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span> -C &lt;span class="s2">&amp;#34;vm_arch host-&amp;gt;target&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在 ~/.ssh/config 加一段別名：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># Host vm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># HostName &amp;lt;機器 IP&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># User &amp;lt;你的使用者&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentityFile ~/.ssh/vm_arch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentitiesOnly yes&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，&lt;code>ssh vm&lt;/code> 就免密碼連入，後面的 &lt;code>rsync&lt;/code>、&lt;code>scp&lt;/code> 也跟著免密碼。&lt;/p>
&lt;p>把公鑰放進機器有兩條路。標準工具是 &lt;code>ssh-copy-id&lt;/code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 &lt;code>authorized_keys&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p ~/.ssh &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> chmod &lt;span class="m">700&lt;/span> ~/.ssh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;ssh-ed25519 AAAA... 你的公鑰內容&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> ~/.ssh/authorized_keys&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩條路等價，選哪條看你當下在哪——還沒連上就用 &lt;code>ssh-copy-id&lt;/code>，已經連上就直接貼，少一次切換。&lt;/p>
&lt;h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去&lt;/h2>
&lt;p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。&lt;/p>
&lt;p>&lt;strong>repo 是公開的（在 GitHub 之類）&lt;/strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git clone https://github.com/&amp;lt;帳號&amp;gt;/dotfiles ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ./scripts/install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。&lt;/p></description><content:encoded><![CDATA[<p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。</p>
<p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。</p>
<h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入</h2>
<p>讓機器能被 SSH 連入只需要兩步：裝 SSH 伺服器、啟動它的服務。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pacman -S openssh             <span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl <span class="nb">enable</span> --now sshd   <span class="c1"># enable 開機自啟、--now 立刻啟動</span></span></span></code></pre></div><p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora <code>dnf</code>、Debian/Ubuntu <code>apt</code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 <code>ssh</code> 不是 <code>sshd</code>，那邊要 <code>systemctl enable --now ssh</code>。</p>
<p>從本機連的時候用一般使用者、不要用 root：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@&lt;機器 IP&gt;            <span class="c1"># IP 來自機器上的 ip -brief a</span></span></span></code></pre></div><p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（<code>PermitRootLogin prohibit-password</code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。</p>
<p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 <a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引</a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 <code>systemctl enable sshd</code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。</p>
<h2 id="ssh-key-免密碼">SSH key 免密碼</h2>
<p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。</p>
<p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 SSH 設定裡給它一個好記的別名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N <span class="s2">&#34;&#34;</span> -C <span class="s2">&#34;vm_arch host-&gt;target&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 在 ~/.ssh/config 加一段別名：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">#   Host vm</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#       HostName &lt;機器 IP&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">#       User &lt;你的使用者&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#       IdentityFile ~/.ssh/vm_arch</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#       IdentitiesOnly yes</span></span></span></code></pre></div><p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，<code>ssh vm</code> 就免密碼連入，後面的 <code>rsync</code>、<code>scp</code> 也跟著免密碼。</p>
<p>把公鑰放進機器有兩條路。標準工具是 <code>ssh-copy-id</code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 <code>authorized_keys</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p ~/.ssh <span class="o">&amp;&amp;</span> chmod <span class="m">700</span> ~/.ssh
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;ssh-ed25519 AAAA... 你的公鑰內容&#34;</span> &gt;&gt; ~/.ssh/authorized_keys
</span></span><span class="line"><span class="ln">3</span><span class="cl">chmod <span class="m">600</span> ~/.ssh/authorized_keys</span></span></code></pre></div><p>兩條路等價，選哪條看你當下在哪——還沒連上就用 <code>ssh-copy-id</code>，已經連上就直接貼，少一次切換。</p>
<h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去</h2>
<p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。</p>
<p><strong>repo 是公開的（在 GitHub 之類）</strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/&lt;帳號&gt;/dotfiles ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> ./scripts/install.sh</span></span></code></pre></div><p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。</p>
<p><strong>repo 是私有的、但機器能上網</strong>：機器可以直接 clone，用 GitHub Personal Access Token（PAT）走 HTTPS——這是私有 repo 免 SSH key 的標準解。clone 時把 PAT 當密碼填進認證，機器就拉得到，一樣不必在它上面設 SSH key。</p>
<p><strong>repo 還沒推到任何遠端、或機器離線</strong>：從本機把檔案傳進去。如果本機到機器的 SSH 已經能用（即使只是密碼登入），用 <code>tar</code> over SSH 一次傳進去（跟 <code>scp -r</code> 等價，差別只在 tar 能一次打包、又好控制要不要帶 <code>.git</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dotfiles &amp;&amp; tar xzf - -C ~/dotfiles&#39;</span></span></span></code></pre></div><p>這條只需要兩邊都有的 <code>ssh</code> 跟 <code>tar</code>，不依賴目標機有 rsync。從 macOS 傳的時候要關掉 AppleDouble 中繼檔，否則會夾帶一堆 <code>._</code> 開頭的中繼檔到 Linux 上：在指令前加 <code>COPYFILE_DISABLE=1</code>。完全離線、連 SSH 都還沒通時，最後手段是把 repo 放進 USB、掛載到機器上複製出來。</p>
<p>把 dotfile 弄進去之後，跑它的 <code>install.sh</code> 完成基礎安裝。如果安裝腳本一開始就要用 sudo，記得 sudo 必須在工具驗證階段就備好——它是 <a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a> 的前置，bootstrap 自身補不了。</p>
<h2 id="換一台新機器或重裝時ssh-為什麼突然連不上">換一台新機器（或重裝）時，SSH 為什麼突然連不上</h2>
<p>SSH 的別名、金鑰、<code>known_hosts</code> 都是綁在「某一台特定機器」上的，所以當你重裝、或換一台新 VM，先前設好的 <code>ssh &lt;別名&gt;</code> 往往會以看似無關的錯誤失敗——那套設定是為舊機器建的，而重裝後是另一台機器：不同的 IP、不同的 SSH host key、還沒裝 sshd、<code>authorized_keys</code> 也是空的。判讀的起點是把重裝後的機器當成全新的一台，重做第一次連線的設定，而不是沿用舊別名。</p>
<p>失敗會以三種形式出現，各對應不同層、各有各的修法：</p>
<p><code>Permission denied (publickey)</code> 是認證被拒，代表 sshd 有在跑、連線有到（這是進度），卡在金鑰這關。常見於你用的別名設了 <code>IdentitiesOnly yes</code> 只送某一把 key，而新機器的 <code>authorized_keys</code> 還沒有它。修法是改用帳號加 IP 直連、走密碼，繞過那個鎖死金鑰的別名：<code>ssh user@&lt;新 IP&gt;</code>，密碼是「這次安裝」為該使用者設的（每次重裝各自獨立，不是舊機器那個）。連進去後再把公鑰貼回新機器的 <code>authorized_keys</code>、把別名的 <code>HostName</code> 更新成新 IP，免密碼才會恢復。</p>
<p><code>Host key verification failed</code>（或 <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code>）發生在新機器剛好拿到跟舊機器一樣的 IP 時：你本機 <code>known_hosts</code> 存的是舊機器的 host key，SSH 偵測到同一個 IP 換了 key、當成可能的中間人攻擊而拒連。修法是刪掉那筆舊紀錄，再重連時接受新 key：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -R &lt;IP&gt;       <span class="c1"># 刪掉該 IP 的舊 host key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh-keygen -R &lt;別名&gt;     <span class="c1"># 有用別名的話一併刪</span></span></span></code></pre></div><p><code>Connection refused</code> 代表沒有 sshd 在監聽，也就是新機器還沒把 SSH server 起來。修法回到最開始——在新機器的 console 裝 openssh、啟動服務（見本篇開頭「啟用 sshd」），這一步在每台全新機器上都要重做。</p>
<p>三個症狀的共同根因是同一件事：SSH 的便利設定（別名、金鑰、host key 快取）綁的是機器身分、不會跟著「重裝」自動轉移。把它們當成「為某一台機器設好的」，換機器就重做第一次連線，能省下對著看似無關的錯誤瞎猜的時間。</p>
<h2 id="連入後可能遇到的兩個終端機問題">連入後可能遇到的兩個終端機問題</h2>
<p>SSH 連線本身通了之後，互動 shell 還可能因為終端機環境不對而出現「打字變亂碼、prompt 重繪錯位」。這類問題在你用現代終端機（如 Ghostty、Kitty）連進一台剛裝好的最小 Linux、又跑了 unicode 較重的 prompt（如 Powerlevel10k）時最容易出現，根源是兩個跟字元處理有關的終端機設定，跟你的 shell 配置無關。</p>
<p>第一個是 locale。macOS 的終端機 SSH 連線時常把 <code>LC_CTYPE=UTF-8</code> 送到遠端，但 <code>UTF-8</code> 不是合法的 Linux locale 名稱，Linux 收到後 fallback 成 <code>POSIX</code>/C locale——於是 shell 的行編輯器把輸入當單位元組處理，配上 unicode 字元的 prompt 就重繪成一個字母重複好幾次的累加亂碼。判讀方式是在遠端跑 <code>locale</code>，看 <code>LANG</code> 是不是空的、<code>LC_CTYPE</code> 是不是 <code>POSIX</code>。修法是在 shell 設定裡強制一個合法的 UTF-8 locale（前提是該 locale 已生成，見 <a href="../install-option-decisions/">安裝選項判讀</a> 的 locale 段）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">export</span> <span class="nv">LANG</span><span class="o">=</span>en_US.UTF-8
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">LC_CTYPE</span><span class="o">=</span>en_US.UTF-8</span></span></code></pre></div><p>第二個是 terminfo。現代終端機會把 <code>TERM</code> 設成自己的值（Ghostty 是 <code>xterm-ghostty</code>、Kitty 是 <code>xterm-kitty</code>），而一台剛裝好的 Linux 的 terminfo 資料庫沒有這些條目，shell 的行編輯器做「清行重繪」時找不到對應的控制序列、就把畫面畫壞。判讀方式是在遠端 <code>echo $TERM</code> 看是哪個值、<code>toe | grep &lt;值&gt;</code> 看遠端認不認得。修法有兩條：把你終端機的 terminfo 灌進遠端（保留完整功能），或退而求其次強制一個遠端一定有的 <code>TERM</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 把本機終端機的 terminfo 灌進遠端的 ~/.terminfo（推薦）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">infocmp -x <span class="s2">&#34;</span><span class="nv">$TERM</span><span class="s2">&#34;</span> <span class="p">|</span> ssh remote <span class="s1">&#39;tic -x -&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 或：連線時強制遠端一定有的 TERM（功能略降，但保證能用）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ssh -t remote <span class="s1">&#39;TERM=xterm-256color exec zsh -l&#39;</span></span></span></code></pre></div><p>這兩個問題的共同點是：它們在你裝了 unicode 較重的互動 shell 之後才浮現，而陽春的 shell（ASCII prompt）即使 locale 跟 terminfo 都不對也照樣能用。所以排查時，先確認是不是這層、而不是去懷疑剛裝的 shell 配置壞了。</p>
<h2 id="連入傳輸安裝的順序">連入、傳輸、安裝的順序</h2>
<p>這三件事有一個固定的先後，順序錯了會在中間卡住。先把 sshd 跑起來、從本機連入，取得一個能貼上、可捲動的 session；再把 dotfile 弄進機器（公開 repo 走 HTTPS clone、私有或本地走傳輸）；最後在機器上跑 install.sh 完成安裝。SSH key 是讓「連入」從每次打密碼變成免密碼的優化，可以在任何時候補，不是這條鏈的必要環節、也不是 bootstrap 的前置。</p>
<p><a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a> 把「生成 SSH key、部署公鑰」列為標準流程的一環，那是預設你會建 key 的主路徑。這篇補的是它沒展開的另一面：當你手上還沒有 key、或這台機器的 dotfile 根本不需要 key 就能取得時，怎麼一樣把 bootstrap 跑完。</p>
<h2 id="下一步">下一步</h2>
<p>連入、傳輸、安裝都跑通之後，真正的考驗是當 install.sh 中途失敗時——而它遲早會撞到失敗——你能不能快速看出哪裡錯了。這取決於安裝腳本有沒有把可觀測性內建進去，<a href="../observable-bootstrap/">可除錯的 bootstrap</a> 談的就是怎麼內建。</p>
]]></content:encoded></item><item><title>可除錯的 bootstrap：把可觀測性內建進安裝腳本</title><link>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</guid><description>&lt;p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。&lt;/p>
&lt;p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 &lt;code>install.sh&lt;/code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。&lt;/p>
&lt;h2 id="為什麼會瞎找">為什麼會瞎找&lt;/h2>
&lt;p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。&lt;/p>
&lt;p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀&lt;/a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。&lt;/p>
&lt;h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法&lt;/h2>
&lt;p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。&lt;/p>
&lt;h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案&lt;/h3>
&lt;p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 &lt;code>grep&lt;/code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 &lt;code>exec&lt;/code> 就能把後續所有 stdout 與 stderr 都導去 &lt;code>tee&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XDG_STATE_HOME&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="p">/.local/state&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">/install-&lt;/span>&lt;span class="k">$(&lt;/span>date +%Y%m%d-%H%M%S&lt;span class="k">)&lt;/span>&lt;span class="s2">.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">exec&lt;/span> &amp;gt; &amp;gt;&lt;span class="o">(&lt;/span>tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 &lt;code>XDG_STATE_HOME&lt;/code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。&lt;/p>
&lt;h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令&lt;/h3>
&lt;p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 &lt;code>set -e&lt;/code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 &lt;code>ERR&lt;/code> trap，就能在 &lt;code>set -e&lt;/code> 中斷之前先印出定位資訊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -Eeuo pipefail &lt;span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;log &amp;#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&amp;#34;&amp;#39;&lt;/span> ERR&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>$LINENO&lt;/code> 是出錯的行號、&lt;code>$BASH_COMMAND&lt;/code> 是當下正在執行的那條指令、&lt;code>$?&lt;/code> 是它的結束碼。三者合起來，輸出會長這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範例裡的 &lt;code>pacman&lt;/code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 &lt;code>which&lt;/code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。&lt;code>set -E&lt;/code>（&lt;code>-E&lt;/code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。&lt;/p>
&lt;h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度&lt;/h3>
&lt;p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;[%s] %s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date +%H:%M:%S&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;install.sh start | OS=&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Installing base packages...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stowing configs...&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。&lt;/p>
&lt;h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標&lt;/h2>
&lt;p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計&lt;/a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。&lt;/p>
&lt;p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。&lt;/p>
&lt;h2 id="回到系列">回到系列&lt;/h2>
&lt;p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a> 處理 OS 怎麼裝、&lt;a href="../minimal-install-verify/">工具驗證與補足&lt;/a> 處理裝完缺什麼、&lt;a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap&lt;/a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 &lt;a href="../unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八&lt;/a> 的 dotfile 管理才有立足點。&lt;/p></description><content:encoded><![CDATA[<p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。</p>
<p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 <code>install.sh</code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。</p>
<h2 id="為什麼會瞎找">為什麼會瞎找</h2>
<p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。</p>
<p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 <a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀</a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。</p>
<h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法</h2>
<p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。</p>
<h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案</h3>
<p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 <code>grep</code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 <code>exec</code> 就能把後續所有 stdout 與 stderr 都導去 <code>tee</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_STATE_HOME</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.local/state</span><span class="si">}</span><span class="s2">/dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">LOG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">/install-</span><span class="k">$(</span>date +%Y%m%d-%H%M%S<span class="k">)</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">exec</span> &gt; &gt;<span class="o">(</span>tee -a <span class="s2">&#34;</span><span class="nv">$LOG_FILE</span><span class="s2">&#34;</span><span class="o">)</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span></span></span></code></pre></div><p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 <code>XDG_STATE_HOME</code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。</p>
<h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令</h3>
<p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 <code>set -e</code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 <code>ERR</code> trap，就能在 <code>set -e</code> 中斷之前先印出定位資訊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">set</span> -Eeuo pipefail   <span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;log &#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&#34;&#39;</span> ERR</span></span></code></pre></div><p><code>$LINENO</code> 是出錯的行號、<code>$BASH_COMMAND</code> 是當下正在執行的那條指令、<code>$?</code> 是它的結束碼。三者合起來，輸出會長這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1</span></span></code></pre></div><p>範例裡的 <code>pacman</code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 <code>which</code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。<code>set -E</code>（<code>-E</code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。</p>
<h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度</h3>
<p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">log<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;[%s] %s\n&#39;</span> <span class="s2">&#34;</span><span class="k">$(</span>date +%H:%M:%S<span class="k">)</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$*</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">log <span class="s2">&#34;install.sh start | OS=</span><span class="nv">$OS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">log <span class="s2">&#34;Installing base packages...&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">log <span class="s2">&#34;Stowing configs...&#34;</span></span></span></code></pre></div><p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。</p>
<h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標</h2>
<p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計</a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。</p>
<p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。</p>
<h2 id="回到系列">回到系列</h2>
<p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：<a href="../install-option-decisions/">安裝選項判讀</a> 處理 OS 怎麼裝、<a href="../minimal-install-verify/">工具驗證與補足</a> 處理裝完缺什麼、<a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap</a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 <a href="../unattended-remote-work/">讓機器跑無人值守的長任務</a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八</a> 的 dotfile 管理才有立足點。</p>
]]></content:encoded></item><item><title>讓機器跑無人值守的長任務</title><link>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</guid><description>&lt;p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。&lt;/p>
&lt;p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。&lt;/p>
&lt;h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行&lt;/h2>
&lt;p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 &lt;code>sudo&lt;/code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。&lt;/p>
&lt;p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>whoami&lt;span class="k">)&lt;/span>&lt;span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee /etc/sudoers.d/20-nopasswd &lt;span class="c1"># $(whoami) 會填入你的登入帳號&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/20-nopasswd&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。&lt;/p>
&lt;h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉&lt;/h2>
&lt;p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。&lt;/p>
&lt;p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三&lt;/a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@host
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">zellij &lt;span class="c1"># 起多工器（tmux 同理）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">./run-my-long-task.sh &lt;span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 之後連回來看進度：再 ssh 進去，然後&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">zellij attach &lt;span class="c1"># tmux 是 tmux attach&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。&lt;/p>
&lt;h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做&lt;/h2>
&lt;p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。&lt;/p>
&lt;p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 &lt;code>git push&lt;/code> 就能用——但 &lt;code>gh auth login&lt;/code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">gh auth login &lt;span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 &lt;code>gh&lt;/code>，也可以用 PAT 走 HTTPS，見 &lt;a href="../ssh-keyless-bootstrap/">外部連入篇&lt;/a> 的私有 repo 段。&lt;/p>
&lt;p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。&lt;/p>
&lt;h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務&lt;/h2>
&lt;p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 &lt;code>caffeinate&lt;/code>、Linux 宿主用 &lt;code>systemd-inhibit&lt;/code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。&lt;/p>
&lt;h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent&lt;/h2>
&lt;p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。&lt;/p></description><content:encoded><![CDATA[<p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。</p>
<p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。</p>
<h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行</h2>
<p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 <code>sudo</code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。</p>
<p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>whoami<span class="k">)</span><span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&#34;</span> <span class="p">|</span> sudo tee /etc/sudoers.d/20-nopasswd  <span class="c1"># $(whoami) 會填入你的登入帳號</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo chmod <span class="m">440</span> /etc/sudoers.d/20-nopasswd</span></span></code></pre></div><p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。</p>
<h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉</h2>
<p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。</p>
<p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 <a href="/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三</a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@host
</span></span><span class="line"><span class="ln">2</span><span class="cl">zellij                       <span class="c1"># 起多工器（tmux 同理）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">./run-my-long-task.sh        <span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 之後連回來看進度：再 ssh 進去，然後</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">zellij attach                <span class="c1"># tmux 是 tmux attach</span></span></span></code></pre></div><p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。</p>
<h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做</h2>
<p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。</p>
<p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 <code>git push</code> 就能用——但 <code>gh auth login</code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">gh auth login    <span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證</span></span></span></code></pre></div><p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 <code>gh</code>，也可以用 PAT 走 HTTPS，見 <a href="../ssh-keyless-bootstrap/">外部連入篇</a> 的私有 repo 段。</p>
<p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。</p>
<h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務</h2>
<p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 <code>caffeinate</code>、Linux 宿主用 <code>systemd-inhibit</code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。</p>
<h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent</h2>
<p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。</p>
<h2 id="下一步">下一步</h2>
<p>把這三到四個障礙解決掉，一台機器就能在你離開後自己跑完工作、把成果送回你手上。這篇是 <a href="../ssh-keyless-bootstrap/">外部連入</a>（怎麼連進去）的延伸——從「我連進去手動操作」進到「我設好讓它自己跑」。而要讓那個無人值守的任務在失敗時還留得下可診斷的痕跡，回到 <a href="../observable-bootstrap/">可除錯的 bootstrap</a> 的原則：無人盯著的任務尤其需要把可觀測性內建進去，因為你不在場、只能事後從 log 重建發生了什麼。</p>
]]></content:encoded></item><item><title>GUI 應用的安裝驗證：拆包、首跑對話框與播放判讀</title><link>https://tarrragon.github.io/blog/linux/install/gui-apps-install-verify/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/gui-apps-install-verify/</guid><description>&lt;p>GUI 應用的安裝驗證跟 CLI 工具走不同的判讀鏈：CLI 工具裝完 &lt;code>command -v&lt;/code> 加一次試跑就能定案，GUI 應用則有三個 CLI 沒有的失敗層——依賴鏈拆包（裝了本體、缺功能模組）、首跑同意對話框（程式要求使用者決策才繼續）、播放輸出鏈（視窗有了、聲音或畫面沒有）。這三層都有各自的權威判讀位置，本篇以一輪 VM 實測（檔案管理器、瀏覽器、媒體播放器、音樂串流）把它們走一遍。&lt;/p>
&lt;h2 id="拆包生態裝了本體不等於裝了功能">拆包生態：裝了本體不等於裝了功能&lt;/h2>
&lt;p>發行版為了控制依賴體積，會把一個應用的核心跟功能模組拆成多個套件，預設只裝核心。這個設計讓「安裝成功」跟「功能可用」變成兩件事，而缺件的症狀往往是靜默的：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>VLC 的解碼器是獨立 plugin&lt;/strong>：Arch 的 &lt;code>vlc&lt;/code> 本體開得起來、UI 完整，播 H.264 影片卻回報 &lt;code>Codec 'h264' is not supported&lt;/code>——解碼能力在 &lt;code>vlc-plugin-ffmpeg&lt;/code>（或整組 &lt;code>vlc-plugins-all&lt;/code>）。judgment 訊號是「應用正常啟動、特定格式失敗」，權威來源是應用自己的 log（&lt;code>vlc --verbose=2&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>pipewire 的 session manager 是獨立套件&lt;/strong>：&lt;code>pipewire&lt;/code> 常被依賴鏈拉進來，但沒有 &lt;code>wireplumber&lt;/code> 就沒有人建立音訊 graph——daemon 在跑、&lt;code>wpctl status&lt;/code> 的 Sinks 段是空的、所有應用無聲且不報錯。補 &lt;code>wireplumber&lt;/code> + &lt;code>pipewire-pulse&lt;/code>（多數 GUI 應用走 PulseAudio API）後輸出裝置立即出現。&lt;/li>
&lt;li>&lt;strong>optional dependency 不會自動安裝&lt;/strong>：套件宣告的 optdepends 是「裝了會多什麼功能」的提示、不是安裝動作。影片縮圖、壓縮格式支援、硬體加速常落在這層，&lt;code>pacman -Qi &amp;lt;pkg&amp;gt;&lt;/code> 的 Optional Deps 段列出哪些沒裝。&lt;/li>
&lt;/ul>
&lt;p>判讀原則：GUI 應用「開得起來但某個功能不動」時，先查發行版有沒有把那個功能拆成獨立套件，再懷疑設定或相容性。&lt;/p>
&lt;h2 id="首跑同意對話框程式在等使用者決策">首跑同意對話框：程式在等使用者決策&lt;/h2>
&lt;p>不少 GUI 應用第一次啟動會彈出需要使用者決策的對話框，最典型的是 VLC 的「Privacy and Network Access Policy」：&lt;/p>
&lt;p>VLC 聲明自己不蒐集、不傳輸任何個人資料，但它能自動向第三方網路服務抓取播放清單裡媒體的中繼資料（封面圖、曲名、演出者）——這個行為等於把「你在播哪些檔案」暴露給第三方服務，所以 VLC 開發者要求使用者明示同意（Allow metadata network access 勾選框、預設勾選）後才允許自動連網。&lt;/p>
&lt;p>這個對話框的判讀是用途導向：拿 VLC 播本機影片、看下載的影片檔，中繼資料抓取沒有用處、取消勾選讓播放器完全離線工作；拿它管理音樂庫、想要自動補封面跟曲目資訊，才需要同意。同意與否都能在偏好設定（Privacy / Network Interaction）事後改。&lt;/p>
&lt;p>首跑對話框對自動化流程有一層額外影響：無人值守安裝驗證時，應用會停在對話框等輸入、腳本側只看到「程式起了但沒繼續」。VLC 把這兩個決策記在 &lt;code>~/.config/vlc/vlcrc&lt;/code> 的 &lt;code>qt-privacy-ask&lt;/code> 與 &lt;code>metadata-network-access&lt;/code> 兩個鍵——首跑後檔案才生成，而且 VLC 退出時會整檔重寫（幾千行的完整設定 dump），把它納入 dotfile 版控會持續產生無意義的 diff，比較合理的處理是讓首跑對話框留給人、或在自動化腳本裡預先寫入只含這兩鍵的最小 vlcrc。&lt;/p>
&lt;p>同型的首跑決策也出現在瀏覽器（預設瀏覽器詢問、錯誤回報同意）跟大型 GUI 應用（遙測同意）。它們的共通判讀：對話框問的是「要不要讓程式自動連外 / 回傳資料」，答案取決於這台機器的用途與隱私要求，安裝驗證流程要把「首跑會有互動」納入預期、不是當成故障。&lt;/p>
&lt;h2 id="播放驗證鏈三個權威位置">播放驗證鏈：三個權威位置&lt;/h2>
&lt;p>「有沒有真的在播」的驗證不靠肉眼跟喇叭，三個權威位置各管一段：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>驗證對象&lt;/th>
 &lt;th>權威來源&lt;/th>
 &lt;th>工具與判準&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>視窗存在&lt;/td>
 &lt;td>compositor 的視窗表&lt;/td>
 &lt;td>&lt;code>hyprctl clients&lt;/code> 有該應用的 class 條目&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>音訊真的在出&lt;/td>
 &lt;td>音訊伺服器 graph&lt;/td>
 &lt;td>&lt;code>wpctl status&lt;/code> Streams 段有該應用的 stream 且 &lt;code>[active]&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗的原因&lt;/td>
 &lt;td>程式自己的 log&lt;/td>
 &lt;td>&lt;code>vlc --verbose=2&lt;/code>、瀏覽器 &lt;code>--enable-logging=stderr&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把「管線通不通」跟「應用會不會播」拆開驗證能大幅縮短歸因：先用本機音檔 &lt;code>pw-play &amp;lt;file&amp;gt;&lt;/code> 打通音訊路徑（stream 出現 &lt;code>[active]&lt;/code> 代表 guest 側無誤），再測應用層；應用層失敗就跟管線無關，往解碼器、DRM、應用 log 查。串流再多拆一層：先用無 DRM 的串流（一般影音網站）確立網路串流基線，DRM 內容（Spotify、Netflix 類）的失敗才能歸因到 DRM 層——DRM 在非 x86_64 架構的可用性判讀見 &lt;a href="../platform-divergence-map/">平台與發行版差異的判讀地圖&lt;/a> 的套件存在性段。&lt;/p>
&lt;h2 id="vm-特有硬體解碼回退">VM 特有：硬體解碼回退&lt;/h2>
&lt;p>在 VM 裡播放影片，第一次開檔常會閃一個錯誤對話框（&lt;code>failed to create video output&lt;/code>）然後正常播放——這是硬體解碼回退的痕跡：播放器預設先嘗試硬體加速解碼（VDPAU / VAAPI），虛擬 GPU（如 virtio-gpu）沒有視訊解碼能力，嘗試失敗後回退軟體解碼重建輸出。log 上的特徵是一次性的 decoder error 加上之後穩定的 &lt;code>avcodec decoder&lt;/code> 軟體解碼行；實體機器有 GPU 解碼時不會出現。VM 裡想要乾淨啟動，在播放器偏好設定停用 hardware-accelerated decoding 即可——這是機器特性設定，適合留在該機器本機、不進共用 dotfile。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>套件在這個平台 / 架構存不存在、名字叫什麼：&lt;a href="../platform-divergence-map/">平台與發行版差異的判讀地圖&lt;/a>&lt;/li>
&lt;li>音訊、行程、服務狀態的權威判讀：&lt;a href="../../debug/">Linux 除錯與診斷&lt;/a>&lt;/li>
&lt;li>GUI 應用清單怎麼進 bootstrap：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八：Bootstrap script 設計&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>GUI 應用的安裝驗證跟 CLI 工具走不同的判讀鏈：CLI 工具裝完 <code>command -v</code> 加一次試跑就能定案，GUI 應用則有三個 CLI 沒有的失敗層——依賴鏈拆包（裝了本體、缺功能模組）、首跑同意對話框（程式要求使用者決策才繼續）、播放輸出鏈（視窗有了、聲音或畫面沒有）。這三層都有各自的權威判讀位置，本篇以一輪 VM 實測（檔案管理器、瀏覽器、媒體播放器、音樂串流）把它們走一遍。</p>
<h2 id="拆包生態裝了本體不等於裝了功能">拆包生態：裝了本體不等於裝了功能</h2>
<p>發行版為了控制依賴體積，會把一個應用的核心跟功能模組拆成多個套件，預設只裝核心。這個設計讓「安裝成功」跟「功能可用」變成兩件事，而缺件的症狀往往是靜默的：</p>
<ul>
<li><strong>VLC 的解碼器是獨立 plugin</strong>：Arch 的 <code>vlc</code> 本體開得起來、UI 完整，播 H.264 影片卻回報 <code>Codec 'h264' is not supported</code>——解碼能力在 <code>vlc-plugin-ffmpeg</code>（或整組 <code>vlc-plugins-all</code>）。judgment 訊號是「應用正常啟動、特定格式失敗」，權威來源是應用自己的 log（<code>vlc --verbose=2</code>）。</li>
<li><strong>pipewire 的 session manager 是獨立套件</strong>：<code>pipewire</code> 常被依賴鏈拉進來，但沒有 <code>wireplumber</code> 就沒有人建立音訊 graph——daemon 在跑、<code>wpctl status</code> 的 Sinks 段是空的、所有應用無聲且不報錯。補 <code>wireplumber</code> + <code>pipewire-pulse</code>（多數 GUI 應用走 PulseAudio API）後輸出裝置立即出現。</li>
<li><strong>optional dependency 不會自動安裝</strong>：套件宣告的 optdepends 是「裝了會多什麼功能」的提示、不是安裝動作。影片縮圖、壓縮格式支援、硬體加速常落在這層，<code>pacman -Qi &lt;pkg&gt;</code> 的 Optional Deps 段列出哪些沒裝。</li>
</ul>
<p>判讀原則：GUI 應用「開得起來但某個功能不動」時，先查發行版有沒有把那個功能拆成獨立套件，再懷疑設定或相容性。</p>
<h2 id="首跑同意對話框程式在等使用者決策">首跑同意對話框：程式在等使用者決策</h2>
<p>不少 GUI 應用第一次啟動會彈出需要使用者決策的對話框，最典型的是 VLC 的「Privacy and Network Access Policy」：</p>
<p>VLC 聲明自己不蒐集、不傳輸任何個人資料，但它能自動向第三方網路服務抓取播放清單裡媒體的中繼資料（封面圖、曲名、演出者）——這個行為等於把「你在播哪些檔案」暴露給第三方服務，所以 VLC 開發者要求使用者明示同意（Allow metadata network access 勾選框、預設勾選）後才允許自動連網。</p>
<p>這個對話框的判讀是用途導向：拿 VLC 播本機影片、看下載的影片檔，中繼資料抓取沒有用處、取消勾選讓播放器完全離線工作；拿它管理音樂庫、想要自動補封面跟曲目資訊，才需要同意。同意與否都能在偏好設定（Privacy / Network Interaction）事後改。</p>
<p>首跑對話框對自動化流程有一層額外影響：無人值守安裝驗證時，應用會停在對話框等輸入、腳本側只看到「程式起了但沒繼續」。VLC 把這兩個決策記在 <code>~/.config/vlc/vlcrc</code> 的 <code>qt-privacy-ask</code> 與 <code>metadata-network-access</code> 兩個鍵——首跑後檔案才生成，而且 VLC 退出時會整檔重寫（幾千行的完整設定 dump），把它納入 dotfile 版控會持續產生無意義的 diff，比較合理的處理是讓首跑對話框留給人、或在自動化腳本裡預先寫入只含這兩鍵的最小 vlcrc。</p>
<p>同型的首跑決策也出現在瀏覽器（預設瀏覽器詢問、錯誤回報同意）跟大型 GUI 應用（遙測同意）。它們的共通判讀：對話框問的是「要不要讓程式自動連外 / 回傳資料」，答案取決於這台機器的用途與隱私要求，安裝驗證流程要把「首跑會有互動」納入預期、不是當成故障。</p>
<h2 id="播放驗證鏈三個權威位置">播放驗證鏈：三個權威位置</h2>
<p>「有沒有真的在播」的驗證不靠肉眼跟喇叭，三個權威位置各管一段：</p>
<table>
  <thead>
      <tr>
          <th>驗證對象</th>
          <th>權威來源</th>
          <th>工具與判準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視窗存在</td>
          <td>compositor 的視窗表</td>
          <td><code>hyprctl clients</code> 有該應用的 class 條目</td>
      </tr>
      <tr>
          <td>音訊真的在出</td>
          <td>音訊伺服器 graph</td>
          <td><code>wpctl status</code> Streams 段有該應用的 stream 且 <code>[active]</code></td>
      </tr>
      <tr>
          <td>失敗的原因</td>
          <td>程式自己的 log</td>
          <td><code>vlc --verbose=2</code>、瀏覽器 <code>--enable-logging=stderr</code></td>
      </tr>
  </tbody>
</table>
<p>把「管線通不通」跟「應用會不會播」拆開驗證能大幅縮短歸因：先用本機音檔 <code>pw-play &lt;file&gt;</code> 打通音訊路徑（stream 出現 <code>[active]</code> 代表 guest 側無誤），再測應用層；應用層失敗就跟管線無關，往解碼器、DRM、應用 log 查。串流再多拆一層：先用無 DRM 的串流（一般影音網站）確立網路串流基線，DRM 內容（Spotify、Netflix 類）的失敗才能歸因到 DRM 層——DRM 在非 x86_64 架構的可用性判讀見 <a href="../platform-divergence-map/">平台與發行版差異的判讀地圖</a> 的套件存在性段。</p>
<h2 id="vm-特有硬體解碼回退">VM 特有：硬體解碼回退</h2>
<p>在 VM 裡播放影片，第一次開檔常會閃一個錯誤對話框（<code>failed to create video output</code>）然後正常播放——這是硬體解碼回退的痕跡：播放器預設先嘗試硬體加速解碼（VDPAU / VAAPI），虛擬 GPU（如 virtio-gpu）沒有視訊解碼能力，嘗試失敗後回退軟體解碼重建輸出。log 上的特徵是一次性的 decoder error 加上之後穩定的 <code>avcodec decoder</code> 軟體解碼行；實體機器有 GPU 解碼時不會出現。VM 裡想要乾淨啟動，在播放器偏好設定停用 hardware-accelerated decoding 即可——這是機器特性設定，適合留在該機器本機、不進共用 dotfile。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>套件在這個平台 / 架構存不存在、名字叫什麼：<a href="../platform-divergence-map/">平台與發行版差異的判讀地圖</a></li>
<li>音訊、行程、服務狀態的權威判讀：<a href="../../debug/">Linux 除錯與診斷</a></li>
<li>GUI 應用清單怎麼進 bootstrap：<a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八：Bootstrap script 設計</a></li>
</ul>
]]></content:encoded></item><item><title>平台與發行版差異的判讀地圖</title><link>https://tarrragon.github.io/blog/linux/install/platform-divergence-map/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/platform-divergence-map/</guid><description>&lt;p>同一個工作環境要在多台機器上復現時，差異集中在四個層次：套件管理器、套件名稱、套件存在性、版本節奏。這四層決定了 bootstrap 腳本哪些部分能共用、哪些必須按平台獨立維護，也決定了除錯時要先確認自己站在哪個平台上——很多「工具行為不對」的問題，根因是把 A 平台的經驗直接套到 B 平台。&lt;/p>
&lt;h2 id="差異的四個層次">差異的四個層次&lt;/h2>
&lt;h3 id="套件管理器每個平台各有原生解">套件管理器：每個平台各有原生解&lt;/h3>
&lt;p>macOS 用 Homebrew、Arch 用 pacman、Debian/Ubuntu 用 apt、Fedora 用 dnf。安裝指令、確認旗標、資料庫同步模型都不同，其中兩個差異會直接咬到自動化腳本：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>非互動旗標不對稱&lt;/strong>：apt 的慣例是 &lt;code>-y&lt;/code>，pacman 是 &lt;code>--noconfirm&lt;/code>。腳本只寫了其中一邊，換平台就會卡在確認提示——非 TTY 環境下（SSH 一行式、CI、無人值守）沒人回答 &lt;code>[Y/n]&lt;/code>，pacman 直接以錯誤結束。&lt;/li>
&lt;li>&lt;strong>資料庫同步模型不同&lt;/strong>：Arch 是 rolling release 且鏡像不保留舊版檔案，裝機當下的套件資料庫幾天內就會指向已被輪替掉的檔名，安裝時收到 404（&lt;code>failed retrieving file&lt;/code>）。修法是安裝前先 &lt;code>pacman -Syu&lt;/code> 同步資料庫並全系統升級——只 &lt;code>-Sy&lt;/code> 不 &lt;code>-u&lt;/code> 會造成 partial upgrade（新資料庫裝新套件、舊系統缺新依賴）。Debian stable 的套件庫凍結、沒有這個時序問題，但代價是版本舊。&lt;/li>
&lt;/ul>
&lt;h3 id="套件名稱同一個工具各發行版各叫各的">套件名稱：同一個工具、各發行版各叫各的&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>Arch&lt;/th>
 &lt;th>Debian/Ubuntu&lt;/th>
 &lt;th>Fedora&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>fd&lt;/td>
 &lt;td>&lt;code>fd&lt;/code>&lt;/td>
 &lt;td>&lt;code>fd-find&lt;/code>（執行檔叫 &lt;code>fdfind&lt;/code>）&lt;/td>
 &lt;td>&lt;code>fd-find&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>bat&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>（執行檔叫 &lt;code>batcat&lt;/code>）&lt;/td>
 &lt;td>&lt;code>bat&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>gh&lt;/td>
 &lt;td>&lt;code>github-cli&lt;/code>&lt;/td>
 &lt;td>&lt;code>gh&lt;/code>&lt;/td>
 &lt;td>&lt;code>gh&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CJK 字型&lt;/td>
 &lt;td>&lt;code>noto-fonts-cjk&lt;/code>&lt;/td>
 &lt;td>&lt;code>fonts-noto-cjk&lt;/code>&lt;/td>
 &lt;td>&lt;code>google-noto-sans-cjk-fonts&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meslo Nerd Font&lt;/td>
 &lt;td>&lt;code>ttf-meslo-nerd&lt;/code>&lt;/td>
 &lt;td>未打包（手動裝）&lt;/td>
 &lt;td>未打包&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Debian 的重命名還會連執行檔一起改（&lt;code>fdfind&lt;/code>、&lt;code>batcat&lt;/code>），所以連 shell alias 與腳本內的指令呼叫都要跟著分歧。維護跨發行版清單的可靠做法是逐台實測建立——憑印象抄一份對照表，漂移只是時間問題。&lt;/p>
&lt;h3 id="套件存在性有些概念只存在於特定平台">套件存在性：有些概念只存在於特定平台&lt;/h3>
&lt;p>Hyprland 在 Arch 官方 repo、Fedora 要 COPR、Debian stable 沒有；Quickshell 只有 Arch 打包。反過來，macOS 的 cask app（GUI 應用程式）概念在 Linux 對應的是各桌面環境自己的生態。這層差異沒有翻譯的空間——桌面層的清單是平台專屬的維護對象。&lt;/p>
&lt;p>存在性差異還有一個容易漏看的軸：&lt;strong>CPU 架構&lt;/strong>。發行版 repo 有這個工具、不代表它在你的架構上存在——尤其是專有軟體的二進位發行。實測案例：Arch aarch64（ALARM）的 repo 有 &lt;code>spotify-launcher&lt;/code>（工具本身有 aarch64 建置），但它要下載的 Spotify 官方 client 只發 x86_64/i386 deb，實跑直接回報 &lt;code>There are no packages for your cpu's architecture (cpu=&amp;quot;aarch64&amp;quot;, supported=[&amp;quot;amd64&amp;quot;, &amp;quot;i386&amp;quot;])&lt;/code>。這類失敗的判讀重點是分清「工具沒打包」跟「工具打包了、它依賴的專有 blob 沒有這個架構」——前者可能有 AUR / 第三方 repo 補、後者只能找替代路徑（Spotify 的替代是 Web Player + 從 ChromeOS 鏡像抽出的 arm64 Widevine CDM）。DRM、GPU driver、印表機 driver 這類含專有二進位的軟體，在非 x86_64 架構上都要先查架構支援再排進安裝清單。&lt;/p>
&lt;h3 id="版本節奏rolling-與-stable-的行為差">版本節奏：rolling 與 stable 的行為差&lt;/h3>
&lt;p>Arch rolling 永遠最新，Debian stable 的同名工具可能舊兩年。版本差會讓 config 語法對不上（新版工具的設定選項在舊版不存在）、也會讓「照著文件做卻失敗」——文件寫的是新版行為。除錯時看到「同一份 config 在 A 機器能跑、B 機器報錯」，先比對兩邊的工具版本再懷疑 config 本身。&lt;/p>
&lt;h2 id="除錯前先定平台">除錯前先定平台&lt;/h2>
&lt;p>跨平台差異對除錯的意義：&lt;strong>判讀工具與修法都是平台相依的，先確認自己站在哪，再開始查。&lt;/strong> 三條指令建立座標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">cat /etc/os-release &lt;span class="c1"># 發行版與版本（Linux）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">uname -m &lt;span class="c1"># CPU 架構：x86_64 / aarch64（套件生態差很多）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v pacman apt-get dnf brew &lt;span class="c1"># 哪個套件管理器在場&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>架構那條容易被忽略：aarch64（ARM）的套件生態比 x86_64 小——Homebrew on Linux 對 aarch64 沒有預編譯 bottle、AUR 部分套件不支援 ARM。在 ARM 機器上照 x86 的教學走，會在意想不到的地方碰壁。&lt;/p></description><content:encoded><![CDATA[<p>同一個工作環境要在多台機器上復現時，差異集中在四個層次：套件管理器、套件名稱、套件存在性、版本節奏。這四層決定了 bootstrap 腳本哪些部分能共用、哪些必須按平台獨立維護，也決定了除錯時要先確認自己站在哪個平台上——很多「工具行為不對」的問題，根因是把 A 平台的經驗直接套到 B 平台。</p>
<h2 id="差異的四個層次">差異的四個層次</h2>
<h3 id="套件管理器每個平台各有原生解">套件管理器：每個平台各有原生解</h3>
<p>macOS 用 Homebrew、Arch 用 pacman、Debian/Ubuntu 用 apt、Fedora 用 dnf。安裝指令、確認旗標、資料庫同步模型都不同，其中兩個差異會直接咬到自動化腳本：</p>
<ul>
<li><strong>非互動旗標不對稱</strong>：apt 的慣例是 <code>-y</code>，pacman 是 <code>--noconfirm</code>。腳本只寫了其中一邊，換平台就會卡在確認提示——非 TTY 環境下（SSH 一行式、CI、無人值守）沒人回答 <code>[Y/n]</code>，pacman 直接以錯誤結束。</li>
<li><strong>資料庫同步模型不同</strong>：Arch 是 rolling release 且鏡像不保留舊版檔案，裝機當下的套件資料庫幾天內就會指向已被輪替掉的檔名，安裝時收到 404（<code>failed retrieving file</code>）。修法是安裝前先 <code>pacman -Syu</code> 同步資料庫並全系統升級——只 <code>-Sy</code> 不 <code>-u</code> 會造成 partial upgrade（新資料庫裝新套件、舊系統缺新依賴）。Debian stable 的套件庫凍結、沒有這個時序問題，但代價是版本舊。</li>
</ul>
<h3 id="套件名稱同一個工具各發行版各叫各的">套件名稱：同一個工具、各發行版各叫各的</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>Arch</th>
          <th>Debian/Ubuntu</th>
          <th>Fedora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fd</td>
          <td><code>fd</code></td>
          <td><code>fd-find</code>（執行檔叫 <code>fdfind</code>）</td>
          <td><code>fd-find</code></td>
      </tr>
      <tr>
          <td>bat</td>
          <td><code>bat</code></td>
          <td><code>bat</code>（執行檔叫 <code>batcat</code>）</td>
          <td><code>bat</code></td>
      </tr>
      <tr>
          <td>gh</td>
          <td><code>github-cli</code></td>
          <td><code>gh</code></td>
          <td><code>gh</code></td>
      </tr>
      <tr>
          <td>CJK 字型</td>
          <td><code>noto-fonts-cjk</code></td>
          <td><code>fonts-noto-cjk</code></td>
          <td><code>google-noto-sans-cjk-fonts</code></td>
      </tr>
      <tr>
          <td>Meslo Nerd Font</td>
          <td><code>ttf-meslo-nerd</code></td>
          <td>未打包（手動裝）</td>
          <td>未打包</td>
      </tr>
  </tbody>
</table>
<p>Debian 的重命名還會連執行檔一起改（<code>fdfind</code>、<code>batcat</code>），所以連 shell alias 與腳本內的指令呼叫都要跟著分歧。維護跨發行版清單的可靠做法是逐台實測建立——憑印象抄一份對照表，漂移只是時間問題。</p>
<h3 id="套件存在性有些概念只存在於特定平台">套件存在性：有些概念只存在於特定平台</h3>
<p>Hyprland 在 Arch 官方 repo、Fedora 要 COPR、Debian stable 沒有；Quickshell 只有 Arch 打包。反過來，macOS 的 cask app（GUI 應用程式）概念在 Linux 對應的是各桌面環境自己的生態。這層差異沒有翻譯的空間——桌面層的清單是平台專屬的維護對象。</p>
<p>存在性差異還有一個容易漏看的軸：<strong>CPU 架構</strong>。發行版 repo 有這個工具、不代表它在你的架構上存在——尤其是專有軟體的二進位發行。實測案例：Arch aarch64（ALARM）的 repo 有 <code>spotify-launcher</code>（工具本身有 aarch64 建置），但它要下載的 Spotify 官方 client 只發 x86_64/i386 deb，實跑直接回報 <code>There are no packages for your cpu's architecture (cpu=&quot;aarch64&quot;, supported=[&quot;amd64&quot;, &quot;i386&quot;])</code>。這類失敗的判讀重點是分清「工具沒打包」跟「工具打包了、它依賴的專有 blob 沒有這個架構」——前者可能有 AUR / 第三方 repo 補、後者只能找替代路徑（Spotify 的替代是 Web Player + 從 ChromeOS 鏡像抽出的 arm64 Widevine CDM）。DRM、GPU driver、印表機 driver 這類含專有二進位的軟體，在非 x86_64 架構上都要先查架構支援再排進安裝清單。</p>
<h3 id="版本節奏rolling-與-stable-的行為差">版本節奏：rolling 與 stable 的行為差</h3>
<p>Arch rolling 永遠最新，Debian stable 的同名工具可能舊兩年。版本差會讓 config 語法對不上（新版工具的設定選項在舊版不存在）、也會讓「照著文件做卻失敗」——文件寫的是新版行為。除錯時看到「同一份 config 在 A 機器能跑、B 機器報錯」，先比對兩邊的工具版本再懷疑 config 本身。</p>
<h2 id="除錯前先定平台">除錯前先定平台</h2>
<p>跨平台差異對除錯的意義：<strong>判讀工具與修法都是平台相依的，先確認自己站在哪，再開始查。</strong> 三條指令建立座標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">cat /etc/os-release        <span class="c1"># 發行版與版本（Linux）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">uname -m                   <span class="c1"># CPU 架構：x86_64 / aarch64（套件生態差很多）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">command</span> -v pacman apt-get dnf brew   <span class="c1"># 哪個套件管理器在場</span></span></span></code></pre></div><p>架構那條容易被忽略：aarch64（ARM）的套件生態比 x86_64 小——Homebrew on Linux 對 aarch64 沒有預編譯 bottle、AUR 部分套件不支援 ARM。在 ARM 機器上照 x86 的教學走，會在意想不到的地方碰壁。</p>
<h2 id="bootstrap-的分歧設計判準">Bootstrap 的分歧設計判準</h2>
<p>把差異收進腳本架構的三條判準，決定每段邏輯住在哪：</p>
<ol>
<li><strong>安裝手段跨平台一致</strong>（git clone、curl installer、stow 部署）→ 進共通層，一份邏輯全平台用</li>
<li><strong>只是套件名或套件管理器不同</strong> → 各平台一份安裝腳本 + 一份套件清單，獨立維護、分歧不寫進共通層的 if/else</li>
<li><strong>概念只存在於某平台</strong>（Hyprland、cask）→ 只出現在該平台清單的桌面層</li>
</ol>
<p>這個切法的維護成本結構：共通層改一次全平台生效；平台層只在你真的用那個平台時才付維護成本。沒有實測機器的發行版不預先建清單——那種清單沒有實測支撐、注定漂移。</p>
<h2 id="統一層的誘惑與代價">統一層的誘惑與代價</h2>
<p>「用一個跨平台套件管理器統一所有機器」聽起來能消掉整個分歧層，實際的適用邊界很窄。Homebrew 支援 Linux，但它在 Arch 上會建一套與 pacman 平行的套件世界（獨立 prefix、重複的函式庫、PATH 互搶），而且對 aarch64 Linux 沒有 bottle、全部從原始碼編譯。它真正的適用場景是「發行版套件太舊」（如 Ubuntu LTS 上要新版工具）或「沒有 root 權限」。Nix 能做到真正的跨平台一致，代價是整套心智模型重學。判準是：分歧層的維護成本（每個發行版一份清單）低於統一層的引入成本時，保持原生套件管理器 + 分平台清單。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Bootstrap 腳本本身的設計（log 落地、錯誤定位）見<a href="/blog/linux/install/observable-bootstrap/" data-link-title="可除錯的 bootstrap：把可觀測性內建進安裝腳本" data-link-desc="安裝腳本中途失敗卻只能對著終端機捲動瞎找原因、想在 bootstrap 設計階段就讓失敗可定位時回來讀">可除錯的 bootstrap</a></li>
<li>最小系統缺什麼、怎麼驗證見<a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a></li>
<li>出問題時的判讀紀律見 <a href="/blog/linux/debug/" data-link-title="Linux 除錯與診斷" data-link-desc="遠端或本地除錯 Linux 時，一個現象看起來像 A 卻可能是 B，想建立一套先讀權威狀態再下判斷的紀律、按症狀分流到對的檢查與工具時回來讀">Linux 除錯與診斷</a></li>
<li>dotfile repo 怎麼同時服務 macOS 與 Linux 見<a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">一個 repo 管理跨平台環境</a></li>
</ul>
]]></content:encoded></item></channel></rss>