COSCUP 2019 BSDTW x Cat System Workshop

FreeBSD Ports 和 FreeBSD 官方提供預先編譯好的套件,應該可以說是在 FreeBSD 上安 裝與管理軟體最常見也最簡便的方式。然而就如同許多作業系統或發行版本內建的套件管 理程式,一個設計作為系統管理用途的工具,對於開發或測試軟體本身的人來說總是有些 不方便。一來是開發與測試過程中需要經常修改原始碼,與套件管理程式將原始碼視為固 定不變輸入的假設並不相同;二來是開發與測試中的軟體通常不穩定,隨意安裝到系統上 可能會影響其他軟體或其他使用者。因此我們很常見到「穩定版本留在系統中、開發與測 試版本裝進家目錄」的作法。這樣的作法聽起來簡單,但對 C 和 C++ 這種與系統高度整 合卻沒有固定的編譯與安裝流程的語言來說就有些複雜了。使用者需要知道常用的編譯器 與連結器參數,也要知道常見用來自動化編譯與安裝的工具,例如 Autotools、CMake、 Meson,會使用哪些環境變數,又會如何使用這些環境變數。這個講題會以 GNOME 的 JHBuild 工具為例,介紹在家目錄中開發時常用的環境變數以及它們造成的效果與影響, 同時也提及 FreeBSD Ports 常用的變數與檔案,讓入門的使用者能認知到使用 FreeBSD Ports 和平時手動編譯的環境有什麼樣的不同,而不至於在精簡的 Makefile 中找不出也 猜不出每個變數的效果。

在 FreeBSD 安裝軟體

有些時候 FreeBSD Ports 無法滿足需求

網路上常常這樣教

./configure
make
make install

手動安裝:更改安裝路徑

./configure --prefix=/home/user/prefix
make
make install

手動安裝:找出安裝的檔案

./configure --prefix=/home/user/prefix
make
make DESTDIR=/home/user/dest install
<手動將檔案從 DESTDIR 移入 prefix>

手動安裝:執行安裝完成的後續指令

./configure --prefix=/home/user/prefix
make
make DESTDIR=/home/user/dest install
<手動將檔案從 DESTDIR 移入 prefix>
<手動執行安裝完成的後續指令>

手動安裝:還要考慮哪些事

CMake 與 Meson

cmake -G 'Unix Makefiles' -DCMAKE_INSTALL_PREFIX=/home/user/prefix .
make
make DESTDIR=/home/user/dest install
cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/home/user/prefix .
ninja
DESTDIR=/home/user/dest ninja install
mkdir _build && cd _build
meson --prefix=/home/user/prefix ..
ninja
DESTDIR=/home/user/dest ninja install

手動安裝和 Ports 的差異

man ports

man make

回來看 ports

打包流程

以 librsvg2 為例:有哪些檔案

以 librsvg2 為例:名稱、版本、來源

PORTNAME=       librsvg
PORTVERSION=    2.40.20
...
MASTER_SITES=   GNOME
...

以 librsvg2 為例:相依性

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

以 librsvg2 為例:USES

USES=           gettext gmake gnome libtool localbase pathfix pkgconfig tar:xz
USE_GNOME=      cairo gnomeprefix libgsf gdkpixbuf2 introspection:build \
                libxml2 pango

以 librsvg2 為例:plist 與 shell 指令

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 <bsd.port.mk>

以 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

JHBuild

建置時的參數

執行時的參數

參數與搜尋路徑

用環境變數傳參數

用命令列選項傳參數

如何傳命令列選項

Autotools 環境變數:C

CC          C compiler command
CFLAGS      C compiler flags
CPP         C preprocessor
CPPFLAGS    (Objective) C/C++ preprocessor flags,
            e.g. -I<include dir>
LDFLAGS     linker flags,
            e.g. -L<lib dir>
LIBS        libraries to pass to the linker,
            e.g. -l<library>

Autotools 環境變數:C++

CXX         C++ compiler command
CXXFLAGS    C++ compiler flags
CXXCPP      C++ preprocessor
CPPFLAGS    (Objective) C/C++ preprocessor flags,
            e.g. -I<include dir>
LDFLAGS     linker flags,
            e.g. -L<lib dir>
LIBS        libraries to pass to the linker,
            e.g. -l<library>

Autotools 如何使用變數

$(CC) $(project_CPPFLAGS)     $(CPPFLAGS)
      $(project_CFLAGS)       $(CFLAGS)
      $(project_LDFLAGS)      $(LDFLAGS)
      -o <輸出檔> <輸入檔> ...
      $(project_LIBADD/LDADD) $(LIBS)

其他 build system

輸出陣列 = shlex.split(輸入字串)
輸出字串 = ' '.join([shlex.quote(x) for x in 輸入陣列])

找函式庫

PKG_CONFIG_PATH='«prefix»/lib/pkgconfig:«prefix»/share/pkgconfig'
CMAKE_PREFIX_PATH='«prefix»:/usr/local'

標頭檔路徑:參數

標頭檔路徑:來源

標頭檔路徑:順序

# 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'

建置時期函式庫路徑:參數

建置時期函式庫路徑:執行

建置時期函式庫路徑:來源

建置時期函式庫路徑:順序

# Ports 和 JHBuild 都是這樣用。
LDFLAGS='-L«prefix»/lib -L/usr/local/lib'

建置時期函式庫路徑:pkg-config

建置時期函式庫路徑:-L 順序問題

  1. 選項一:用 LDFLAGS 蓋過去。
    • LDFLAGS 擺放的位置為什麼可以剛好蓋過去?
  2. 選項二:用 .so 檔絕對路徑代替 -L-l
    • 比較可靠,但是幾乎所有的 .pc 檔都是用 -L-l
    • GLib:如果 -l 指定的函式庫在 -L 找不到怎麼辦?
    • 這代表著 build system 要模仿 ld 自己找到 .so 檔。
    • OpenBSD:系統上只有 libc.so.92.5 沒有 libc.so 怎麼辦?

建置時期函式庫路徑:LDFLAGS 位置

$(CC) ... $(project_LDFLAGS) $(LDFLAGS)
      ... $(project_LIBADD/LDADD) $(LIBS)
$(CC) ... $(內部參數與函式庫) $(LDFLAGS)
      ... $(外部參數與函式庫)

建置時期函式庫路徑:LDFLAGS 舉例

cc ... -L../libhello -lhello           # 內部函式庫
   ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
   ... -L«prefix»/lib -lharfbuzz       # 外部函式庫
   ... -L/usr/local/lib -lfreetype     # 外部函式庫
cc ... -L../libhello -lhello           # 內部函式庫
   ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
   ... -L/usr/local/lib -lfreetype     # 外部函式庫
   ... -L«prefix»/lib -lharfbuzz       # 外部函式庫

建置時期函式庫路徑:相依函式庫介紹

$ 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]

建置時期函式庫路徑:相依函式庫搜尋

建置時期函式庫路徑:相依函式庫重點

Libtool

Libtool 的 .la 檔案

Libtool 的 .la 檔案如何製造問題

cc ... -L«prefix»/lib -L/usr/local/lib # LDFLAGS
   ... -L«prefix»/lib -lpango-1.0 ...  # Pango
   ... -L/usr/local/lib -lgmp          # GMP

Libtool 的 .la 如果放了 -L

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'

Libtool 的 .la 如果放了 -rpath

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'

正常情況的 ld --verbose

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

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 檔案?

執行檔搜尋路徑

PATH="«prefix»/bin:«prefix»/sbin:${PATH}"

執行時期函式庫路徑

LD_LIBRARY_PATH="«prefix»/lib:${LD_LIBRARY_PATH}"

LD_LIBRARY_PATH 與搜尋順序

DT_RPATH 與 DT_RUNPATH

事後操作 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

執行未安裝的檔案

Libtool 的 wrapper script

以 GEGL 為例操作 Libtool

$ ./bin/gegl --help
usage: .../bin/.libs/gegl [options] <file | -- [op [op] ..]>
...

$ 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

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

XDG_CONFIG_DIRS='«prefix»/etc/xdg:/usr/local/etc/xdg:/etc/xdg'
XDG_DATA_DIRS='«prefix»/share:/usr/local/share:/usr/share'

D-Bus

man 與 info

MANPATH="«prefix»/share/man:/usr/local/man:$(manpath)"
INFOPATH='«prefix»/share/info:/usr/local/share/info'

其他 JHBuild 設定的相關變數

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'

安裝完成的後續指令

用安裝的檔案決定要執行的指令

# 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"

總結

聯絡