RPM 打包︰由一竅不通到動手濫用 (三)

讓你進一步了解 .spec 檔內變量和巨集的玩法

Koala Yeung
Linux 入門筆記
11 min readApr 17, 2020

--

上一篇文章談到,如何將示範的 tick5 程式變成 systemd 長駐服務,這次我們談談 rpm 的變量和巨集 (macro) 功能。

讓我們重新看看先前的 tick5.spec 檔案︰

其實 SPEC 檔中的數值、文字,我們有可能想在打包時才決定的,比如︰

  • 版本號應該可以在打包時才決定吧?
  • 安裝程式的位置。雖然說 /usr 是正常套件的安裝地,但也許我想安裝在 /usr/local 的啊。
  • 安裝 tick5.service 的地點,我也有機會修改的啊。有些分發版本傾向讓你安裝在 /etc/lib/sysmted/system ,也有可能放在其他資料匣。
  • tick5.service 裏的 “come on, James” 我不喜歡,我想每次打包都能重新決定啊。

那該怎麼做呢?用變量吧。

在 SPEC 中使用變量和巨集

先前一直談,SPEC 支援變量和巨集功能,這裏首先老實告訴你,是假的。事實上,SPEC 只支援巨集,所有「變量」其實都是巨集

先別震驚,慢慢讀下去你便會明白。

巨集的定義及使用

最簡單的巨集定義︰

%define ​version 0.1

語法上是這個意思︰

%define <name> <body>

在 SPEC 檔內,定義語句以下的任何一行,可以用 %{<name>}%<name> 去呼叫巨集。我們可以稍為修改一下先前 SPEC 中的表頭︰

%define version 0.1
%define release 1
Name: tick5
Summary: Print a message every 5s
Version %{version}
Release: %release
License: MIT

這樣,%{version} 會被解讀為 “0.1” 而 %release 會被解讀為 “1”,

大家應該都留意到——這只是簡單的文字取代啊!的確,最簡單的巨集只是一些重覆被使用的文字,所以巨集在 SPEC 的世界是同時用來做變量和 script 使用。

可以如果你細想一下,這是合理的設計。SPEC 檔原本就只是用來定義 bash 執行稿和檔案清單,而 bash 執行稿又不過是文字而已。一切都是文字而已。

巨集只是處理「如何重覆使用文字」啊。

預設巨集

現在細心的朋友可能留意到——我們 SPEC 檔內的 %{buildroot} 不就是巨集嘛!其實 rpm 工具有一堆內置的巨集的。當然我們可以看文檔啦,但更有趣的方法,是看源碼。

大家可以用你最熟悉的文字編輯器,打開這條路徑︰

  • /usr/lib/rpm/macros

內容大約如下︰

#/*! \page config_macros Default configuration: /usr/lib/rpm/macros
# \verbatim
#
# This is a global RPM configuration file. All changes made here
# will be lost when the rpm package is upgraded. Any per-system
# configuration should be added to /etc/rpm/macros, while per-user
# configuration should be added to ~/.rpmmacros.
#
#=================================================================
# ---- A macro that expands to nothing.
#
%nil %{!?nil}
#=================================================================
# ---- filesystem macros.
#
%_usr /usr
%_usrsrc %{_usr}/src
%_var /var
#=================================================================
# ---- Generally useful path macros.
#
%__7zip /usr/bin/7za
%__awk mawk
%__bzip2 /bin/bzip2
%__cat /bin/cat
%__chgrp /bin/chgrp
%__chmod /bin/chmod
%__chown /bin/chown
%__cp /bin/cp
%__cpio /bin/cpio
%__file /usr/bin/file
%__gpg /usr/bin/gpg
%__grep /bin/grep
...
...
...

其實很可能還有這些︰

  • /usr/lib/rpm/macros.perl
  • /usr/lib/rpm/macros.php
  • /usr/lib/rpm/macros.python

如果你是 RHEL、CentOS 或者 Fedora 的用戶,還有︰

  • /usr/lib/rpm/macros.d/*

(Debian 或者 Ubuntu 的這個資料匣似乎甚麼都沒有)

用巨集語法,重構 tick5.spec

表頭的 5 行 %define 應該一看就懂,之後引用的地方應該都看得明白,有幾個地方用了 rpm 自帶的巨集,這裏解釋一下︰

  • %{_prefix}
    就是程式安裝的地點,預設是 /usr
  • %{__mkdir}​ , %{__cp} , %{__cat}
    是這些工具的系統路徑。

其實如前面講過, rpm 自帶的巨集實在很多,有興趣可以詳細鑽研一下前面提過的巨集檔案路徑。

打包時重新定義巨集

如果你查看 rpmbuild 的 man 說明,可以找到 rpmbuild 有這樣一個選項︰

--define=’MACRO EXPR’ — Defines MACRO with value EXPR

既然前面已經講過甚麼是變數(巨集),大家應該可以想到,只要用這個選項,其實可以在打包時修改所有變數(巨集)。

想想,如果版本編號等資料給死死的寫在 SPEC 檔裏頭,每次打包一個新版本的 rpm 包裝都得修改 SPEC,該有多不方便?如果使用 -D 或者 --define 選項,則可以為打包時提供彈性。

只要原本的 %define 語句改寫,只有在巨集未經定義的情況下,才對該名稱的巨集進行定義︰

%{!?name: %define name tick5}
%{!?version: %define version 0.0}
%{!?release: %define release 1}
%{!?systemdinstalldir: %define systemdinstalldir /etc/systemd/system}
%{!?message: %define message come on, James}

好玩一點,反正我們的 tick5 只是一間簡單的 bash 文字稿,可以索性連檔案內容都放到 SPEC 的 %install 內產生︰

# create the executable from scratch in buildroot.
%{__mkdir} -p %{buildroot}%{_prefix}/bin
%{__cat} <<EOF> %{buildroot}/usr/bin/%{name}
#!/bin/bash
# say something, with timestamp, every 5 seconds
while echo "\$@"; do
sleep %{sleep}s
done
EOF

所以最後重構成 tick.spec 這個樣子︰

所以你就可以隨時在執行 rpmbuild 時指定幾乎所有打包參數︰

rpmbuild -bb ./tick.spec \
--define '_prefix /usr/local' \
--define 'version 1.1' \
--define 'release 0' \
--define 'sleep 10' \
--define 'message Hi, Daisy'

如無意外你的 rpm 檔案會被建立到︰

~/rpmbuild/RPMS/x86_64/tick10–1.1–0.x86_64.rpm

這安裝後會產生 tick10 服務,執行檔案會被安裝到 /usr/local/bin/tick10,重覆的頻率由 5 秒一次變成 10 秒一次,背景重覆的訊息也由 Come on, James 變成 Hi, Daisy

小難題

寫到這裏,給大家建議一些小難題,大家不妨自己動手玩玩︰

  1. 給不熟悉「語意化版本」(Semantic versioning) 的朋友︰版本號(即 versionrelease)會影響你是否能成功運行 rpm -Uvh tick5-<version string>.rpm 安裝指令,到底這個版本號有甚麼規則?怎樣編寫 rpmbuild 指令的 --define 選項,才能打包出一個 rpm 檔,去升級先前安裝的 tick5 版本 1.2.0 ?
  2. 之前為 tick5 寫的 systemd 服務,並不會在安裝後自動運行,試試查閱文檔,把 tick.spec 修改,讓 rpm 變成一安裝就自動運行 systemctl start tick5 指令。
  3. 承接第 (2),當你為先前安裝好的 tick5 進行升級時,若果 tick5.service 已經在運行,可能會失敗。試試查閱文檔,看看到底要怎樣修改 .spec,才能讓 rpm 在升級前把舊的 tick5.service 停下,讓升級能順利進行。

實踐,是驗證所學的唯一標準,請大家一定要動手做做看啊!

還可以更進一步嗎?

抱持住「更快、更高、更強」的精神(?),我們其實還能用巨集玩出更多花樣來。比方說,如果你查看官方文檔,定義巨集的語法是︰

%define <name>[(opts)] <body>

所以說巨集還能接受參數,然後在引用巨集才指定,就是同一個巨集在不同的場景也可以玩出花樣來︰

%define mymacro() (echo -n "My arg is %1" ; sleep %1 ; echo done.) 

而前面提過,一些 Linux 分發版有為不同程式語言預製一些打包用預設巨集,比如 Fedora 就有 PythonRubyPHPPerlNode.jsGo 甚至還有 Rust 的打包指南,裏面提及各種語言專用的巨集及使用方法,大家不妨試試打包你自己的專案成 rpm 檔。

甚至如果你要玩,還可以自己寫巨集,打包成 rpm,讓其他人安裝到 /usr/lib/rpm/macros.d/* 裏頭,之類之類。

筆者寫到這裏,深深感到「能力越大,濫用就越大」這句話背後的智慧…… 🤔 (謎之聲︰不就是你自己隨便寫的嘛)

後語

寫到這裏希望大家都能享受在濫用工具中學習的樂趣。還有各位在深山隱藏的各方高人,希望能不吝嗇分享一下濫用的主意,還有現實世界使用的經驗。

原本是 2018 年開始寫的系列,這第三部份自 2018 沉郵在草稿中,讓筆者在工作中忙一忙,不覺就已經 2020,深深感到有屁快快放的道理,希望之後有系列文章能快越快結。

還有少不了一句,喜歡本文的話,別忘了在下面按「Like」和「Clap」支持啊 🙂

一些建議廷伸閱讀

  1. Fedora 官方 Wiki 文檔︰如何建立一個 RPM 包裝檔(正體中文)
    https://fedoraproject.org/wiki/How_to_create_an_RPM_package/zh-tw
  2. 鳥哥的 Linux 私房菜︰軟體安裝 RPM, SRPM 與 YUM
    http://linux.vbird.org/linux_basic/0520rpm_and_srpm.php

--

--

Koala Yeung
Linux 入門筆記

An FOSS enthusiast. Lives in Hong Kong. Writes Go, Javascript, PHP and occasionally fictions.