如何在 FreeBSD 家目錄下安裝軟體
===============================
:author: 藍挺瑋 (Ting-Wei Lan)
:email: lantw44@gmail.com
:backend: slidy
:icons:
++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++
:text_terminal: black-background lime
COSCUP 2019 BSDTW x Cat System Workshop
[small]##
FreeBSD Ports 和 FreeBSD 官方提供預先編譯好的套件,應該可以說是在 FreeBSD 上安
裝與管理軟體最常見也最簡便的方式。然而就如同許多作業系統或發行版本內建的套件管
理程式,一個設計作為系統管理用途的工具,對於開發或測試軟體本身的人來說總是有些
不方便。一來是開發與測試過程中需要經常修改原始碼,與套件管理程式將原始碼視為固
定不變輸入的假設並不相同;二來是開發與測試中的軟體通常不穩定,隨意安裝到系統上
可能會影響其他軟體或其他使用者。因此我們很常見到「穩定版本留在系統中、開發與測
試版本裝進家目錄」的作法。這樣的作法聽起來簡單,但對 C 和 C++ 這種與系統高度整
合卻沒有固定的編譯與安裝流程的語言來說就有些複雜了。使用者需要知道常用的編譯器
與連結器參數,也要知道常見用來自動化編譯與安裝的工具,例如 Autotools、CMake、
Meson,會使用哪些環境變數,又會如何使用這些環境變數。這個講題會以 GNOME 的
JHBuild 工具為例,介紹在家目錄中開發時常用的環境變數以及它們造成的效果與影響,
同時也提及 FreeBSD Ports 常用的變數與檔案,讓入門的使用者能認知到使用 FreeBSD
Ports 和平時手動編譯的環境有什麼樣的不同,而不至於在精簡的 Makefile 中找不出也
猜不出每個變數的效果。##
在 FreeBSD 安裝軟體
-------------------
[role="incremental"]
- 使用 pkg 安裝預先編譯好的軟體
* `pkg add`
* `pkg install`
- 使用 FreeBSD Ports 自行編譯軟體
* `make install`
* `portmaster`
* `portupgrade`
- 套件管理程式 pkg 記錄軟體安裝資訊
* 安裝、查詢、升級、刪除都很簡單方便。
* 滿足大多數使用者與系統管理者的需求。
有些時候 FreeBSD Ports 無法滿足需求
-----------------------------------
[role="incremental"]
- 可能沒有你要的軟體或版本
* 自己製作 port 或是修改現有的 port 都需要不少時間。
* 即使 `svn log` 有舊版本也不見得能直接拿來使用。
- 可能無法滿足軟體開發需求
* 開發過程中通常會使用版本控制系統,但是 ports 只需要單一版本,原始碼完即丟,
使用沒有版本控制功能的 tar 或 zip 檔反而驗證方便、節省空間。
* 開發過程中通常會很頻繁地安裝、刪除、切換版本,這在個人使用的電腦上可能還好,
在多人共用的主機上則很容易影響到其他正在使用相同軟體的使用者。
網路上常常這樣教
----------------
[source,sh]
-------------------------------------------------------------------------------
./configure
make
make install
-------------------------------------------------------------------------------
[role="incremental"]
- 可是 `configure` 和 ports 預設都安裝到 `/usr/local`
* 這樣不行,檔案衝突會對管理上造成很大的麻煩。
* `make install` 只會無條件覆蓋檔案。
* `make uninstall` 也只會無條件刪除檔案。
* Makefile 不認識你的系統,也不會幫你管理檔案。
手動安裝:更改安裝路徑
----------------------
[source,sh]
-------------------------------------------------------------------------------
./configure --prefix=/home/user/prefix
make
make install
-------------------------------------------------------------------------------
[role="incremental"]
- 這樣安裝路徑就改到家目錄下了
* 解決檔案衝突問題,也不影響其他使用者。
* 這個路徑通常稱作 prefix,所有檔案預設都安裝在這個目錄下,但使用者也可以要求
將某些檔案移往別處。
* `/usr` 和 `/usr/local` 也都是 prefix,因此這裡指定的 `/home/user/prefix`
是環境中的第三個 prefix。
手動安裝:找出安裝的檔案
------------------------
[source,sh]
-------------------------------------------------------------------------------
./configure --prefix=/home/user/prefix
make
make DESTDIR=/home/user/dest install
<手動將檔案從 DESTDIR 移入 prefix>
-------------------------------------------------------------------------------
[role="incremental"]
- 利用 `DESTDIR` 變數更改 `make install` 時使用的目錄
* 大部分專案都支援此功能,設定後 Makefile 將不會更動 prefix
下任何檔案或目錄。
* 注意 `DESTDIR` 不是 prefix, `DESTDIR` 是為了打包和記錄檔案所用的暫時目錄,
prefix 才是最終安裝的位置。
* 無論如何檔案最終都要移入 prefix 才能使用。
手動安裝:執行安裝完成的後續指令
--------------------------------
[source,sh]
-------------------------------------------------------------------------------
./configure --prefix=/home/user/prefix
make
make DESTDIR=/home/user/dest install
<手動將檔案從 DESTDIR 移入 prefix>
<手動執行安裝完成的後續指令>
-------------------------------------------------------------------------------
[role="incremental"]
- 設定 `DESTDIR` 通常會使 Makefile 跳過某些指令的執行
* 某些程式需要事先建立索引或快取才能正常運作。
* `install-info` 、 `glib-compile-schemas` 、 +
`fc-cache` 、 `update-desktop-database` 、 +
`update-mime-database` ……
手動安裝:還要考慮哪些事
------------------------
[role="incremental"]
- 相依性問題
* 我們要安裝的軟體可能需要 ports 裡沒有的函式庫。
* 即使相依的套件 ports 都有,版本也不一定符合需求。
* 真實狀況:某專案可能需要其他專案幾個小時前才剛 commit 的新功能。
- 編譯與執行的方式
* 除了 Autotools 以外還有 CMake 和 Meson。
* 除了 make 以外還有 ninja。
* 環境變數: `ld-elf.so` 要能找到函式庫、 `man` 要能找到 man pages、
`dbus-daemon` 要能找到服務……
CMake 與 Meson
--------------
[source,sh]
-------------------------------------------------------------------------------
cmake -G 'Unix Makefiles' -DCMAKE_INSTALL_PREFIX=/home/user/prefix .
make
make DESTDIR=/home/user/dest install
-------------------------------------------------------------------------------
[source,sh]
-------------------------------------------------------------------------------
cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/home/user/prefix .
ninja
DESTDIR=/home/user/dest ninja install
-------------------------------------------------------------------------------
[source,sh]
-------------------------------------------------------------------------------
mkdir _build && cd _build
meson --prefix=/home/user/prefix ..
ninja
DESTDIR=/home/user/dest ninja install
-------------------------------------------------------------------------------
手動安裝和 Ports 的差異
-----------------------
[role="incremental"]
- Ports 安裝成功可是手動安裝失敗
* 環境變數問題。
* `configure` 和 `make` 的選項與變數問題。
* Ports 使用的原始碼可能有修改過,甚至可能不是從官網下載,而是維護者自己修改
過後的版本。
- Ports 安裝目錄和手動安裝不同
* 以上三種狀況都可能是原因。
* 通常是因為 ports 的規範和上游的預設值不同,例如:
** `lib/pkgconfig` -> `libdata/pkgconfig`
** `share/man` -> `man`
** `var/lib` -> `var/db`
man ports
---------
- 本身位置
* `PORTSDIR: /usr/ports`
- 安裝路徑
* `LOCALBASE: /usr/local` +
相依的東西要去哪裡找。
* `PREFIX: /usr/local` +
要安裝到哪個 prefix。
- 下載與生成檔案
* `DISTDIR: /usr/ports/distfiles` +
下載的檔案放這裡。
* `PACKAGES: /usr/ports/packages` +
`make package` 生成的套件放這裡。
man make
--------
- `make [variable=value] [target ...]`
* 命令列上設定的變數值可覆蓋 Makefile 裡的定義。
- 使用與測試 ports 時常用的選項
* `-C <目錄>` 先 `cd` 到指定目錄再開始執行。
* `-V <變數>` 印出變數值。
- 你應該要知道的 BSD Makefile 語法
* `.include ` +
去系統放共用 Makefile 的目錄 `/usr/share/mk` 找 `bsd.port.mk` 並把它引入。
回來看 ports
------------
- 我們知道每個 port 都放在 `<分類>/<名稱>` 這樣的目錄下
* 但是並非每個 `/usr/ports` 底下的目錄都是 `<分類>` 。
* 重點是 ports 本身的程式碼放在哪裡。
- `make -C /usr/ports -V IGNOREDIR`
* [{text_terminal}]+Mk Templates Tools distfiles packages pkg Keywords+
* 我們知道 `distfiles` 和 `packages` 不是重點。
* 重要的東西大多在 `Mk` ,原因請看 `/usr/share/mk/bsd.port.mk` 。
打包流程
--------
- 套件管理程式打包軟體一般有這些步驟
* `extract` 解開下載來的原始碼。
* `patch` 適當修改原始碼。
* `configure` -> `./configure --prefix=«prefix»`
* `build` -> `make`
* `stage` -> `make DESTDIR=<暫時目錄> install`
* `package` 把暫時目錄裡的東西打包成套件。
以 librsvg2 為例:有哪些檔案
----------------------------
- `ls /usr/ports/graphics/librsvg2`
* [{text_terminal}]+distinfo Makefile pkg-descr pkg-plist+
* `distinfo` 和 `pkg-descr` 基本上不用解釋。
* `Makefile` 和 `pkg-plist` 比較需要認真看。
- `ls /usr/ports/devel/glib20`
* [{text_terminal}]+distinfo files Makefile pkg-descr pkg-plist+
* 多出了 `files` ,裡面大多是放 patch。
* `files` 底下 `patch-` 開頭的檔案都會在 `make patch` 時拿去修改原始碼。
* 但這不是唯一可能修改原始碼的地方!
以 librsvg2 為例:名稱、版本、來源
----------------------------------
[source,makefile]
-------------------------------------------------------------------------------
PORTNAME= librsvg
PORTVERSION= 2.40.20
...
MASTER_SITES= GNOME
...
-------------------------------------------------------------------------------
- 許多 GNU/Linux 發行版打包套件都是在寫 shell script。
但是在 ports 的 Makefile 裡,很多時候你只會看到設定變數值。
- 許多常用、共用的 script 片段都收集到 `Mk` 之類的地方去了。
- `MASTER_SITES` 指定原始碼從哪裡下載。
- `GNOME` 顯然不是個完整網址而只是個簡寫,詳情請看
`Mk/bsd.port.mk` 和 `Mk/bsd.sites.mk` 。
以 librsvg2 為例:相依性
------------------------
[source,makefile]
-------------------------------------------------------------------------------
BUILD_DEPENDS= valac:lang/vala
LIB_DEPENDS= libfreetype.so:print/freetype2 \
libfontconfig.so:x11-fonts/fontconfig \
libpng.so:graphics/png \
libcroco-0.6.so:textproc/libcroco
-------------------------------------------------------------------------------
- `*_DEPENDS` 指定相依性,可在 `Mk/bsd.port.mk` 找到。
- 詳細用法請看 FreeBSD Porter's Handbook。
- 這裡我們只需要知道,當前面的東西找不到時,ports 會嘗試安裝後面指定的 port
來滿足相依性。
- 用於滿足相依性的 port 不一定要是冒號後面指定的那個。
以 librsvg2 為例:USES
----------------------
[source,makefile]
-------------------------------------------------------------------------------
USES= gettext gmake gnome libtool localbase pathfix pkgconfig tar:xz
USE_GNOME= cairo gnomeprefix libgsf gdkpixbuf2 introspection:build \
libxml2 pango
-------------------------------------------------------------------------------
- `USES` 引入 `Mk/Uses` 底下對應名稱的 `.mk` 檔案。
- `USES` 引入的檔案什麼事都可能做,例如:
* `gmake` 和 `localbase` 增加相依套件、修改環境變數。
* `gnome` 在 plist 加入檔案與安裝時要執行的指令。
* `libtool` 和 `pathfix` 甚至用 `sed` 自動修改原始碼!
- 參數太多的 `USES` 可能會另外用其他變數來接收值,像是這裡的 `USE_GNOME` 。
以 librsvg2 為例:plist 與 shell 指令
-------------------------------------
[source,makefile]
-------------------------------------------------------------------------------
PLIST_SUB+= PORTVERSION=${PORTVERSION}
post-patch:
@${REINPLACE_CMD} -e 's|GTK3_REQUIRED=3.[0-9][0-9].[0-9]|GTK3_REQUIRED=9.90.0|g' \
${WRKSRC}/configure
.include
-------------------------------------------------------------------------------
- 名字有 `PLIST` 的變數大概可以猜到跟 `pkg-plist` 有關。
- `USES` 可以用 `sed` 改檔案, `Makefile` 自己當然也可以。
- 整個 `Makefile` 檔案中至少會有一行用來引入其他檔案。
以 librsvg2 為例:plist 與 pkg 關鍵字
-------------------------------------
-------------------------------------------------------------------------------
...
share/gir-1.0/Rsvg-2.0.gir
share/thumbnailers/librsvg.thumbnailer
share/vala/vapi/librsvg-2.0.vapi
@postexec %D/bin/gdk-pixbuf-query-loaders > /dev/null 2>&1 && %D/bin/gdk-pixbuf-query-loaders > %D/lib/gdk-pixbuf-2.0/%%GTK2_VERSION%%/loaders.cache 2>/dev/null || /usr/bin/true
@postunexec %D/bin/gdk-pixbuf-query-loaders > /dev/null 2>&1 && %D/bin/gdk-pixbuf-query-loaders > %D/lib/gdk-pixbuf-2.0/%%GTK2_VERSION%%/loaders.cache 2>/dev/null || /usr/bin/true
-------------------------------------------------------------------------------
- 從外觀上看來 `pkg-plist` 就是個列出套件裡有哪些檔案。
- 可能包含像是 `%%OSREL%%` 的變數來減少重複的內容或是選擇性加入某些檔案。
- 可能包含像是 `@postexec` 的關鍵字用來指示 `pkg` 打包過程中的特殊事項,像是
在安裝或移除時要執行的指令。
JHBuild
-------
[role="incremental"]
- 曾經是 GNOME 用來測試某個版本是否可以釋出的工具
* 目前這個地位已經被 BuildStream 取代。
* JHBuild 仍然作為開發工具繼續存在,但維護狀況已經不像以往那麼好。
- 根據設定自動下載、編譯、安裝軟體至指定的 prefix
* 自動處理許多手動安裝軟體的麻煩事。
* 幫你記錄安裝進 prefix 的檔案,讓你可以升級或刪除。
* 幫你設定好編譯、連結、執行所需的環境變數。
* 幫你執行安裝完成後該執行的指令。
建置時的參數
------------
[role="incremental"]
- 建置過程會用到什麼程式
* 編譯器 `cc` 。
* 連結器 `ld` ,通常由編譯器間接執行。
* 產生靜態函式庫需要 `ar` 和 `ranlib` 。
* 找函式庫可能需要 `pkg-config` 或 `cmake` 。
- 什麼程式需要知道 prefix
* `cc` 需要知道放在 `/usr/include` 以外的標頭檔。
* `ld` 需要知道放在 `/usr/lib` 和 `/lib` 以外的函式庫。
* `ar` 和 `ranlib` 不需要知道,靜態函式庫沒有相依性。
* `pkg-config` 和 `cmake` 需要找 `.pc` 和 `.cmake` 檔。
執行時的參數
------------
[role="incremental"]
- 基本需求:要能找到 prefix 裡的執行檔與函式庫
* `PATH` 用來找執行檔。
* `LD_LIBRARY_PATH` 用來找函式庫。
- Base 與 ports 的程式需要知道 prefix
* 例如 `man` 需要 `MANPATH` 、 `info` 需要 `INFOPATH` 。
- Prefix 裡的程式需要知道 base 與 ports
* 例如 `gobject-introspection` 需要 `GI_TYPELIB_PATH` 。
- 還好現在很多程式支援 XDG base directory specification
* 只要 `XDG_CONFIG_DIRS` 和 `XDG_DATA_DIRS` 設定好就能滿足很多程式的需求了。
參數與搜尋路徑
--------------
- 傳參數給程式
* 命令列選項。
* 環境變數。
- 參數類型
* 單一數值。
* 多個數值,通常是有順序的。
- 搜尋路徑
* 有順序的多數值參數。
* 越前面的越重要。前面已經找到,後面就用不到。
用環境變數傳參數
----------------
- 環境變數
* 通常一個變數只會出現一次。
* 使用者不該讓一個變數出現多次。
- 單一數值
* `DISPLAY=:1` -> `:1`
- 多個數值
* `PATH=/a:/b/c:/d` -> `['/a', '/b/c', '/d']`
用命令列選項傳參數
------------------
- 命令列選項
* 通常一個選項可以出現多次。
- 單一數值
* 通常後面蓋過前面,越後面的越重要。
* `-g3 -g2 -g1` -> `-g1`
* `-Werror -Wno-error` -> `-Wno-error`
- 多個數值
* 通常按照順序由前到後組成陣列,越前面的越重要。
* `-I/a -I/b -I/c` -> `['/a', '/b', '/c']`
如何傳命令列選項
----------------
- 建置過程中的命令列選項通常無法直接控制
* 執行指令的通常是 `make` 或 `ninja` 而不是你自己。
* 通常你需要告訴 build system 想要使用的選項,由它在產生編譯指令時自動加入。
Autotools 環境變數:C
---------------------
- 找個專案執行 `./configure --help`
-------------------------------------------------------------------------------
CC C compiler command
CFLAGS C compiler flags
CPP C preprocessor
CPPFLAGS (Objective) C/C++ preprocessor flags,
e.g. -I
LDFLAGS linker flags,
e.g. -L
LIBS libraries to pass to the linker,
e.g. -l
-------------------------------------------------------------------------------
Autotools 環境變數:C++
-----------------------
- C++ 專案
-------------------------------------------------------------------------------
CXX C++ compiler command
CXXFLAGS C++ compiler flags
CXXCPP C++ preprocessor
CPPFLAGS (Objective) C/C++ preprocessor flags,
e.g. -I
LDFLAGS linker flags,
e.g. -L
LIBS libraries to pass to the linker,
e.g. -l
-------------------------------------------------------------------------------
Autotools 如何使用變數
----------------------
- 全部塞進 `Makefile`
[source,makefile]
-------------------------------------------------------------------------------
$(CC) $(project_CPPFLAGS) $(CPPFLAGS)
$(project_CFLAGS) $(CFLAGS)
$(project_LDFLAGS) $(LDFLAGS)
-o <輸出檔> <輸入檔> ...
$(project_LIBADD/LDADD) $(LIBS)
-------------------------------------------------------------------------------
- `make` 填入變數值組出 shell 指令
* 通常將變數視為以 shell 語法編碼的字串陣列。
- 順序
* 專案變數放在使用者變數的前面。
其他 build system
-----------------
- 許多 build system 看這些變數
* 可能不是透過 `make` 變數來使用。
* CMake 不看 `CPPFLAGS` 。
- 有些 build system 不會直接拿變數值去組 shell 指令
* 可能自己用類似 shell 語法的方式解讀。
[source,python]
-------------------------------------------------------------------------------
輸出陣列 = shlex.split(輸入字串)
輸出字串 = ' '.join([shlex.quote(x) for x in 輸入陣列])
-------------------------------------------------------------------------------
找函式庫
--------
- `pkg-config` 需要 `PKG_CONFIG_PATH`
* 許多函式庫會提供 `.pc` 檔供 `pkg-config` 使用。
- `cmake` 需要 `CMAKE_PREFIX_PATH`
* 使用 CMake 的專案常會提供 `Find<專案名稱>.cmake` 檔案。
詳情請看 `man cmake-packages` 。
* `CMAKE_PREFIX_PATH` 環境變數 ≠ `CMAKE_PREFIX_PATH` 變數。
- 環境設定參考
[source,sh]
-------------------------------------------------------------------------------
PKG_CONFIG_PATH='«prefix»/lib/pkgconfig:«prefix»/share/pkgconfig'
CMAKE_PREFIX_PATH='«prefix»:/usr/local'
-------------------------------------------------------------------------------
標頭檔路徑:參數
----------------
- `man gcc9`
* Clang 的 man page 內容太少。
- 大概可以分這幾級
. `-I` 選項和 `CPATH` 環境變數。
. `-isystem` 選項和 `C_INCLUDE_PATH` 環境變數。
. 預設路徑 `/usr/include` 和編譯器內建路徑。
. `-idirafter` 選項。
- 同級的選項較環境變數優先
* 同級的選項和環境變數中若有共同的路徑時,決定順序時以環境變數為準。
標頭檔路徑:來源
----------------
- `pkg-config` 和類似功能的 script
* `pkg-config --cflags` 回傳 `CPPFLAGS` 。
* 通常是用 `-I` 選項指定路徑,擁有最高優先權。
- 例如
* `atk` -> `-I«prefix»/include/atk-1.0`
* `gtk+-3.0` -> `-I«prefix»/include/gtk-3.0 ...`
- 其他函式庫
* 通常是直接用 `«prefix»/include` 。
* Ports 的 `USES=localbase` 是用 `-isystem` 選項。
* JHBuild 是用 `C_INCLUDE_PATH` 、 `CPLUS_INCLUDE_PATH` 等環境變數。
標頭檔路徑:順序
----------------
- `pkg-config` 回傳的 `-I` 選項沒有順序
* 標頭檔通常不會直接放在 `«prefix»/include` 下,順序不同通常不會造成問題。
* 如果真的出現 `-I«prefix»/include` 造成衝突,也許就只能用 `CPATH` 調整了。
- 環境設定參考
[source,sh]
-------------------------------------------------------------------------------
# Ports 風格,注意 CMake 不支援 CPPFLAGS。
CPPFLAGS='-isystem «prefix»/include -isystem /usr/local/include'
# JHBuild 風格,直接用環境變數告知編譯器路徑。
C_INCLUDE_PATH='«prefix»/include:/usr/local/include'
CPLUS_INCLUDE_PATH='«prefix»/include:/usr/local/include'
OBJC_INCLUDE_PATH='«prefix»/include:/usr/local/include'
-------------------------------------------------------------------------------
建置時期函式庫路徑:參數
------------------------
- `man ld`
* LLVM lld 還是 GNU Binutils(ld.bfd、ld.gold)?
- 搜尋順序
. `-L` 選項。
. `-Y` 選項,LLVM lld 不支援。
建置時期函式庫路徑:執行
------------------------
- 通常不會直接執行 `ld`
* 而是由 `cc` 幫忙補足參數並執行。
- `cc -print-prog-name=ld`
* [{text_terminal}]+/usr/bin/ld+
- `cc -B/usr/local/bin -print-prog-name=ld`
* [{text_terminal}]+/usr/local/bin/ld+
* 你可以自己選擇想要使用哪一個 `ld` 。
- 請 `cc` 幫忙傳參數給 `ld` 時記得加 `-Wl,`
* 除了常用的 `-L` 和 `-l` 外幾乎都需要。
* `ld --verbose` -> `cc -Wl,--verbose`
建置時期函式庫路徑:來源
------------------------
- `pkg-config` 和類似功能的 script
* `pkg-config --libs` 回傳 `LIBADD/LDADD` 。
* 通常是 `-L` 選項指定路徑、 `-l` 選項指定函式庫名稱。
- 例如
* `atk` -> `-L«prefix»/lib -latk-1.0`
* `gtk+-3.0` -> `-L«prefix»/lib -lgtk-3 ...`
- 其他函式庫
* 通常是直接用 `«prefix»/lib` 。
* Ports 的 `USES=localbase` 和 JHBuild 都用 `-L` 選項。
* `-Y` 選項似乎相當罕見,應該不是設計來這樣使用的。
建置時期函式庫路徑:順序
------------------------
- `pkg-config` 回傳的 `-L` 沒有順序
* 但是 `-L` 的順序卻是相當重要!
* 函式庫大多直接放在 `«prefix»/lib` 下,順序錯誤很容易因為版本不符而出現
undefined reference 錯誤。
- 環境設定參考
[source,sh]
-------------------------------------------------------------------------------
# Ports 和 JHBuild 都是這樣用。
LDFLAGS='-L«prefix»/lib -L/usr/local/lib'
-------------------------------------------------------------------------------
建置時期函式庫路徑:pkg-config
------------------------------
- `pkg-config --libs harfbuzz libpng`
* [{text_terminal}]+-L«prefix»/lib -lharfbuzz -L/usr/local/lib -lpng16 -lz+
- `pkg-config --libs libpng harfbuzz`
* [{text_terminal}]+-L/usr/local/lib -lpng16 -lz -L«prefix»/lib -lharfbuzz+
- 通常我們會希望 prefix 裡的優先使用
* 上面:正確,先找 prefix 再找 /usr/local。
* 下面:順序相反,可能找到錯誤的 `harfbuzz` 。
* 難道能否建置成功要靠運氣?
建置時期函式庫路徑:-L 順序問題
-------------------------------
. 選項一:用 `LDFLAGS` 蓋過去。
* `LDFLAGS` 擺放的位置為什麼可以剛好蓋過去?
. 選項二:用 `.so` 檔絕對路徑代替 `-L` 和 `-l` 。
* 比較可靠,但是幾乎所有的 `.pc` 檔都是用 `-L` 和 `-l` 。
* GLib:如果 `-l` 指定的函式庫在 `-L` 找不到怎麼辦?
* 這代表著 build system 要模仿 `ld` 自己找到 `.so` 檔。
* OpenBSD:系統上只有 `libc.so.92.5` 沒有 `libc.so` 怎麼辦?
建置時期函式庫路徑:LDFLAGS 位置
--------------------------------
[source,makefile]
-------------------------------------------------------------------------------
$(CC) ... $(project_LDFLAGS) $(LDFLAGS)
... $(project_LIBADD/LDADD) $(LIBS)
-------------------------------------------------------------------------------
[source,makefile]
-------------------------------------------------------------------------------
$(CC) ... $(內部參數與函式庫) $(LDFLAGS)
... $(外部參數與函式庫)
-------------------------------------------------------------------------------
- 內部:目前正在編譯的這個專案(未安裝)
* `LDFLAGS` 能蓋過普通參數,但不能蓋過搜尋路徑。
* 未安裝至 prefix 的函式庫必須比已安裝的更先被找到。
- 外部: `pkg-config` 從系統上找到的函式庫(已安裝)
* `LDFLAGS` 的 `-L` 蓋過 `pkg-config` 回傳的 `-L` 。
建置時期函式庫路徑:LDFLAGS 舉例
--------------------------------
- 假設我們正在編譯 `hello` 專案,它需要 prefix 的 `harfbuzz` 和 ports 的
`freetype` 。
[source,sh]
-------------------------------------------------------------------------------
cc ... -L../libhello -lhello # 內部函式庫
... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lharfbuzz # 外部函式庫
... -L/usr/local/lib -lfreetype # 外部函式庫
-------------------------------------------------------------------------------
[source,sh]
-------------------------------------------------------------------------------
cc ... -L../libhello -lhello # 內部函式庫
... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L/usr/local/lib -lfreetype # 外部函式庫
... -L«prefix»/lib -lharfbuzz # 外部函式庫
-------------------------------------------------------------------------------
建置時期函式庫路徑:相依函式庫介紹
----------------------------------
- `pkg-config` 回傳的 `-l` = 專案需要的外部函式庫?
* `pkg-config` 回傳的 `-l` ⊆ 專案需要的外部函式庫。
- `readelf -d` 看看 `NEEDED`
* ELF 檔可以指定相依的函式庫。
-------------------------------------------------------------------------------
$ readelf -d /usr/local/lib/libfreetype.so
Tag Type Name/Value
0x... NEEDED Shared library: [libbz2.so.4]
0x... NEEDED Shared library: [libpng16.so.16]
0x... NEEDED Shared library: [libz.so.6]
0x... NEEDED Shared library: [libc.so.7]
-------------------------------------------------------------------------------
- `ld` 去哪裡找這些函式庫?
建置時期函式庫路徑:相依函式庫搜尋
----------------------------------
- 再看一次 `man ld` (GNU Binutils)
. `-rpath-link` 選項。
. `-rpath` 選項。
. `LD_RUN_PATH` 環境變數。(ELF, native linker) +
只在沒用 `-rpath-link` 和 `-rpath` 時使用。
. `-L` 選項。(SunOS)
. `LD_LIBRARY_PATH` 環境變數。(native linker)
. ELF 檔的 `DT_RUNPATH` 和 `DT_RPATH` 。(native linker)
. 預設路徑 `/lib` 和 `/usr/lib` 。
. 檔案 `/etc/ld.so.conf` 中的路徑。
- 大概沒有多少人會真的把這規則記起來。
建置時期函式庫路徑:相依函式庫重點
----------------------------------
- 重點
* 直接依賴和間接依賴的函式庫搜尋規則不同。
* 直接依賴的用 `-L` 搜尋。
* 間接依賴的有 `-rpath*` 系列和 `LD_LIBRARY_PATH` 。
- 狀況
* 直接依賴的你可以用絕對路徑代替 `-L` 和 `-l` 以避免 `-L` 順序問題,
但間接依賴的不行。
* 通常 build system 會保持這兩種搜尋路徑一致。
* 看到 undefined reference 卻又不知道 `ld` 去哪裡找了不合適的 `.so` 檔來用時,
就把 `--verbose` 加下去吧。
Libtool
-------
- Autotools (Autoconf + Automake + Libtool) 的一部分
* Automake 的 `LIBRARIES` 只支援靜態函式庫。
* Libtool 提供的 `LTLIBRARIES` 支援動態函式庫。
- Libtool 是個上萬行的超大型 shell script
* Libtool 隨著 Autotools 進入許多專案的原始碼。
* 上游修了 bug,可是在專案中的那份沒有跟著更新。
* 沒更新怎麼辦?自己執行 `autoreconf -fi` 來更新。
* `autoreconf` 很慢?看看 `USES=libtool` 做了什麼。
- Libtool 2.4.5 以前的版本在 FreeBSD 上有重大 bug
* 如果 `libglib-2.0.so.3792` 無論大小更新,只要更新後面的數字就改變,
`SONAME` 也跟著變,會如何?
Libtool 的 .la 檔案
-------------------
- Libtool 會產生副檔名是 `.la` 和 `.lo` 的檔案供內部使用
* Libtool 還會把 `.la` 檔案安裝到系統上。
- Libtool 常常看到 `.la` 就以爲是內部函式庫
* 內部函式庫和外部函式庫搞混會發生什麼事?
- Libtool 的 `.la` 安裝到系統上有何意義
* 最初的用途應該是記錄相依性,但現在還需要嗎?
* 動態連結:ELF 檔案本身就有記錄相依性。
* 靜態連結: `pkg-config --static` 。
- 現實狀況:大家都在打包過程中把 `.la` 砍掉
* Ports 和 JHBuild 也不例外,請看 `USES=libtool` 。
Libtool 的 .la 檔案如何製造問題
-------------------------------
- 關鍵:Libtool 看到 `.la` 就覺得是內部函式庫。
- 假設有個程式用到 `pango` 和 `gmp` ,並且設定好環境, `pango` 來自 prefix,
`gmp` 來自 ports。
[source,sh]
-------------------------------------------------------------------------------
cc ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lpango-1.0 ... # Pango
... -L/usr/local/lib -lgmp # GMP
-------------------------------------------------------------------------------
- 看起來一切正常,那 `libgmp.la` 存在會造成什麼問題?
Libtool 的 .la 如果放了 -L
--------------------------
[source,sh]
-------------------------------------------------------------------------------
cc ... -L/usr/local/lib -lgmp ... # GMP
... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lpango-1.0 ... # Pango
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
main.o: In function `main':
main.c: undefined reference to `pango_font_family_is_variable'
main.c: undefined reference to `pango_font_metrics_get_height'
main.c: undefined reference to `pango_font_family_is_variable'
main.c: undefined reference to `pango_font_get_hb_font'
-------------------------------------------------------------------------------
- `.la` 讓 `-L` 出現在錯誤的地方,改變函式庫搜尋順序。
Libtool 的 .la 如果放了 -rpath
------------------------------
[source,sh]
-------------------------------------------------------------------------------
cc ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lpango-1.0 ... # Pango
... /usr/local/lib/libgmp.so # GMP
... -Wl,-rpath=/usr/local/lib
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
.../libpango-1.0.so: undefined reference to `fribidi_get_par_embedding_levels_ex'
.../libpango-1.0.so: undefined reference to `fribidi_get_bracket'
-------------------------------------------------------------------------------
- `.la` 使用 `-rpath` 結果讓改變了搜尋相依函式庫的路徑。
* Prefix 裡的 `pango` 需要 prefix 裡的 `fribidi` ,可是出現 `-rpath` 造成 `ld`
去 /usr/local/lib 找 `fribidi` 。
正常情況的 ld --verbose
-----------------------
[source,sh]
-------------------------------------------------------------------------------
cc ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lpango-1.0 ... # Pango
... -L/usr/local/lib -lgmp # GMP
... -Wl,--verbose
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
attempt to open «prefix»/lib/libpango-1.0.so succeeded
-lpango-1.0 («prefix»/lib/libpango-1.0.so)
attempt to open «prefix»/lib/libgmp.so failed
attempt to open «prefix»/lib/libgmp.a failed
attempt to open /usr/local/lib/libgmp.so succeeded
-lgmp (/usr/local/lib/libgmp.so)
...
libfribidi.so.0 needed by «prefix»/lib/libpango-1.0.so
found libfribidi.so.0 at «prefix»/lib/libfribidi.so.0
-------------------------------------------------------------------------------
錯誤使用 -rpath 時的 ld --verbose
---------------------------------
[source,sh]
-------------------------------------------------------------------------------
cc ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
... -L«prefix»/lib -lpango-1.0 ... # Pango
... /usr/local/lib/libgmp.so # GMP
... -Wl,-rpath=/usr/local/lib
... -Wl,--verbose
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
attempt to open «prefix»/lib/libpango-1.0.so succeeded
-lpango-1.0 («prefix»/lib/libpango-1.0.so)
attempt to open /usr/local/lib/libgmp.so succeeded
/usr/local/lib/libgmp.so
...
libfribidi.so.0 needed by «prefix»/lib/libpango-1.0.so
found libfribidi.so.0 at /usr/local/lib/libfribidi.so.0
-------------------------------------------------------------------------------
無條件刪除 .la 檔案?
---------------------
- 很遺憾,不行。有些程式會在執行時開啟 `.la` 檔案的。
- 通常是因為這類程式使用 Libtool 的 `libltdl` 函式庫。
- 為了這類需求,ports 提供 `USES=libtool:keepla` 。
* 這並不代表用 `USES=libtool:keepla` 的 ports 真的都需要 `.la` 檔案,
很多時候只是沒有人去測試而已。
執行檔搜尋路徑
--------------
[source,sh]
-------------------------------------------------------------------------------
PATH="«prefix»/bin:«prefix»/sbin:${PATH}"
-------------------------------------------------------------------------------
- `PATH` 沒有什麼特別的,按照自己的習慣去設定就行了。
執行時期函式庫路徑
------------------
[source,sh]
-------------------------------------------------------------------------------
LD_LIBRARY_PATH="«prefix»/lib:${LD_LIBRARY_PATH}"
-------------------------------------------------------------------------------
- `LD_LIBRARY_PATH` 用來指示 runtime linker (rtld) 應該優先搜尋的函式庫
路徑,但是可執行檔本身也能指定搜尋路徑。
LD_LIBRARY_PATH 與搜尋順序
--------------------------
- `man ld.so` 或是 `man rtld`
. ELF 檔的 `DT_RPATH` 。(deprecated)
. `LD_LIBRARY_PATH` 環境變數。
. ELF 檔的 `DT_RUNPATH` 。
. `ldconfig` 生成的 hints 檔案,通常是由 `/etc/rc.d/ldconfig` 在開機過程中從
`/etc/rc.conf` 的設定和 `/usr/local/libdata/ldconfig` 中的檔案生成。
. 預設路徑 `/lib` 和 `/usr/lib` 。
- FreeBSD 還有支援其他功能類似的環境變數,例如 `LD_LIBRARY_PATH_RPATH` 和
`LD_LIBRARY_PATH_FDS` 。其他檔案像是 `/etc/libmap.conf` 也會影響函式庫載入。
DT_RPATH 與 DT_RUNPATH
----------------------
- `DT_RPATH` 不能被 `LD_LIBRARY_PATH` 覆蓋。
* 由 `ld --disable-new-dtags` 遇到 `-rpath` 產生。
- `DT_RUNPATH` 可以被 `LD_LIBRARY_PATH` 覆蓋。
* 由 `ld --enable-new-dtags` 遇到 `-rpath` 產生。
- FreeBSD 預設情況下會用 `DT_RUNPATH`。
* 執行 `cc -v` 可以看到 `cc` 在預設情況下會傳 `--enable-new-dtags` 給 `ld` 。
- 常用來指定內部使用的函式庫的路徑。
* 但是 build system 也常拿去做其他用途。
事後操作 ELF 檔中的路徑
-----------------------
-------------------------------------------------------------------------------
$ readelf -d «prefix»/bin/gnome-shell
Tag Type Name/Value
0x... NEEDED Shared library: [libgnome-shell.so]
0x... NEEDED Shared library: [libmutter-clutter-5.so.0]
0x... NEEDED Shared library: [libmutter-cogl-pango-5.so.0]
...
0x... RUNPATH Library runpath: [«prefix»/lib/mutter-5:«prefix»/lib/gnome-shell]
...
$ chrpath «prefix»/bin/gnome-shell
«prefix»/bin/gnome-shell: RUNPATH=«prefix»/lib/mutter-5:«prefix»/lib/gnome-shell
$ patchelf --print-rpath «prefix»/bin/gnome-shell
«prefix»/lib/mutter-5:«prefix»/lib/gnome-shell
-------------------------------------------------------------------------------
執行未安裝的檔案
----------------
- 如果不執行 `make install` ,要如何執行編譯出來的程式?
* 有些程式可能根本不會安裝到系統上,例如程式碼產生器、文件產生器、單元測試。
- 問題:如何讓未安裝的程式使用同樣也未安裝的函式庫?
* `LD_LIBRARY_PATH` 會讓 rtld 優先搜尋你的 prefix。
* 假設你在開發 `cairo` ,想要執行專案中的 `cairo-test-suite` 測試程式,你會
希望它用的是剛才編譯出來的那個 `libcairo.so.2` 而不是去 prefix 找。
Libtool 的 wrapper script
-------------------------
- Libtool 不會把編譯好的檔案放在 `Makefile.am` 指定的位置
* 而是擺個 shell script 在原本的地方幫你設定環境。
* 直接操作檔案請用 `libtool --mode=execute` 。
- 如果系統預設用 `DT_RPATH`
* 連結時可能已經加上 `-rpath` 讓你可以直接執行。
- 如果系統預設用 `DT_RUNPATH`
* 自動產生的 wrapper script 應該會幫你設定好 `LD_LIBRARY_PATH` 再去執行程式。
以 GEGL 為例操作 Libtool
------------------------
-------------------------------------------------------------------------------
$ ./bin/gegl --help
usage: .../bin/.libs/gegl [options]
...
$ file ./bin/gegl
./bin/gegl: POSIX shell script, ASCII text executable
$ ldd ./bin/gegl
ldd: ./bin/gegl: not a dynamic executable
$ libtool --mode=execute ldd ./bin/gegl
.../bin/.libs/gegl:
libgegl-0.4.so.0 => .../gegl/.libs/libgegl-0.4.so.0 (0x80082a000)
libgmodule-2.0.so.0 => «prefix»/lib/libgmodule-2.0.so.0 (0x800af0000)
libjson-glib-1.0.so.0 => «prefix»/lib/libjson-glib-1.0.so.0 (0x800cf3000)
...
-------------------------------------------------------------------------------
CMake 和 Meson 沒有 wrapper script
----------------------------------
- CMake 和 Meson 都假設 `-rpath` 可以解決所有事
* 但是 `LD_LIBRARY_PATH` 會把 `DT_RUNPATH` 蓋掉。
* 於是 rtld 因為找到錯誤的函式庫而 undefined symbol 或 version not defined。
- 改用 `DT_RPATH` 並不是個解法
* `DT_RPATH` 已經被標記為 deprecated 非常多年了。
Meson 與 DT_RUNPATH 的失敗案例
------------------------------
-------------------------------------------------------------------------------
$ jhbuild buildone colord
...
[107/215] Generating AppleRGB.icc with a custom command.
FAILED: data/profiles/AppleRGB.icc
.../client/cd-create-profile --output=data/profiles/AppleRGB.icc data/profiles/AppleRGB.iccprofile.xml
.../client/cd-create-profile: Undefined symbol "cd_icc_set_created"
ninja: build stopped: subcommand failed.
$ jhbuild buildone babl
...
[186/189] Generating index.html.tmp with a meson_exe.py custom command.
FAILED: docs/index.html.tmp
«prefix»/bin/meson --internal exe .../meson-private/meson_exe_env_1e391eda23a81f355345fa1aadd0bf6fc89c918d.dat
«prefix»/lib/libbabl-0.1.so.0: version V0_1_0 required by .../tools/babl-html-dump not defined
ninja: build stopped: subcommand failed.
-------------------------------------------------------------------------------
XDG Base Directory Specification
--------------------------------
[source,sh]
-------------------------------------------------------------------------------
XDG_CONFIG_DIRS='«prefix»/etc/xdg:/usr/local/etc/xdg:/etc/xdg'
XDG_DATA_DIRS='«prefix»/share:/usr/local/share:/usr/share'
-------------------------------------------------------------------------------
- 這份文件定義許多環境變數,這裡只列出與 prefix 相關的。
D-Bus
-----
- `dbus-daemon` 根據 XDG base directory specification 找服務
* 服務搜尋路徑可用 `XDG_DATA_DIRS` 環境變數控制。
* `dbus-daemon` 通常是由桌面環境用 `dbus-launch` 啟動的,可能會需要在啟動桌面
前就先設定好環境變數。
- D-Bus Activation
* 有些透過 D-Bus 使用的服務平時並不會執行,而是在被使用時才由 `dbus-daemon`
主動執行指定程式。
* `dbus-update-activation-environment` 可以設定 `dbus-daemon` 啟動服務時的
環境變數。
man 與 info
-----------
[source,sh]
-------------------------------------------------------------------------------
MANPATH="«prefix»/share/man:/usr/local/man:$(manpath)"
INFOPATH='«prefix»/share/info:/usr/local/share/info'
-------------------------------------------------------------------------------
- FreeBSD `man` 只要看到 `MANPATH` 環境變數就會忽略其他的搜尋路徑,所以要手動
把 `manpath` 列出的路徑加上去。
- GNU `info` 沒有這樣的現象,因此不需自己找出預設路徑。
其他 JHBuild 設定的相關變數
---------------------------
[source,sh]
-------------------------------------------------------------------------------
ACLOCAL_PATH='«prefix»/share/aclocal:/usr/local/share/aclocal'
GI_TYPELIB_PATH='«prefix»/lib/girepository-1.0:/usr/local/lib/girepository-1.0'
GST_PLUGIN_PATH_1_0='«prefix»/lib/gstreamer-1.0:/usr/local/lib/gstreamer-1.0'
PERL5LIB='«prefix»/lib/perl5:/usr/local/lib/perl5'
PYTHONPATH='«prefix»/share/jhbuild/sitecustomize'
XCURSOR_PATH='«prefix»/share/icons:/usr/local/share/icons'
-------------------------------------------------------------------------------
- `PYTHONPATH` 是這之中比較特別的,因為這個變數會同時被 Python 2 和 Python 3
使用。
* JHBuild 選擇用 `sitecustomize` 模組來動態設定 `sys.path` 。
* 但是這樣的作法會讓 `venv` 不太正常,使用 `venv` 前應自行拿掉 `PYTHONPATH` 。
安裝完成的後續指令
------------------
- 記得使用了 `DESTDIR` 就要自己處理這些事。
* 可以參考 Ports 的 `/usr/ports/Keywords` 目錄。
* 可以參考 JHBuild 的 `triggers` 目錄。
用安裝的檔案決定要執行的指令
----------------------------
[source,sh]
-------------------------------------------------------------------------------
# IfExecutable: update-desktop-database
# REMatch: ^share/applications/.*\.desktop
update-desktop-database -q
# IfExecutable: update-mime-database
# REMatch: /mime/packages/.*\.xml
update-mime-database "$JHBUILD_PREFIX/share/mime"
# IfExecutable: install-info
# REMatch: ^share/info/.*\.info
rm -f "$JHBUILD_PREFIX/share/info/dir-new"
for info in "$JHBUILD_PREFIX/share/info/"*.info; do
install-info "$info" "$JHBUILD_PREFIX/share/info/dir-new"
done
mv "$JHBUILD_PREFIX/share/info/dir-new" "$JHBUILD_PREFIX/share/info/dir"
-------------------------------------------------------------------------------
總結
----
- 除非必要,不要手刻 Makefile 當 build system。
- 除非必要,不要重新發明 build system。
- 不要覺得正確編譯和執行 C 程式是很簡單的事。
- 不要只看網路文章就隨便執行 `sudo make install` 。
- 記得有 `DESTDIR` 和安裝後續指令這件事。
- 建置時的環境變數: `CPPFLAGS` 、 `LDFLAGS` 、
`PKG_CONFIG_PATH` 、 `CMAKE_PREFIX_PATH` 。
- 執行時的環境變數: `PATH` 、 `LD_LIBRARY_PATH` 、
`XDG_CONFIG_DIRS` 、 `XDG_DATA_DIRS` 。
- `LD_LIBRARY_PATH` 和 `-rpath` 可能造成不可靠的結果。
- Libtool < 2.4.5 容易造成 `DT_SONAME` 問題,使用前應先檢查。
- Libtool 的 `.la` 檔裝到系統上通常只會製造問題,記得刪除。
聯絡
----
- freenode IRC +
暱稱 lantw44 +
頻道 #freebsd-gnome +
頻道 #bsdchat
- JHBuild on FreeBSD +
https://wiki.gnome.org/Projects/Jhbuild/FreeBSD
- GNOME +
https://gitlab.gnome.org/lantw
- GitHub +
https://github.com/lantw44
// vim: set ft=asciidoc et ts=2 sts=2 sw=2: