1 make工具使用
1.1 makefile基本規則
Make工具最主要也是最基本的功能就是通過makefile檔案來描述源程式之間的互相關系并自動維護編譯工作。
Makefile的規則:
target ... : prerequisites ...
command
...
...
注意command如果不是在target那一行(一般都另起一行),則在command之前應先鍵入TAB符号,空格不行。
target是一個目标檔案,它可以是執行檔案,可以是Object File,也可以是一個标簽
target這一個或多個的目标檔案依賴于prerequisites中的檔案,其生成規則定義在command中。
prerequisites中如果有一個以上的檔案比target檔案要新的話,command所定義的指令就會被執行
是以利用這個特點,如果是一個大項目隻改了其中一個cpp檔案,就可以隻編譯其中的某一部分即可,大大節省了編譯時間。
makefile中的.PHONY目标的作用
使用.PHONY的兩個理由是:
(1)避免和同名檔案沖突
這個意思是比如目前makefile檔案的目錄下有跟目标target同名的目錄或檔案則會報錯,在.PHONY目标上顯示聲明可以避免沖突
(2)改善性能
舉個例子
clean:
rm *.o
由我們上面對makefile規則的了解,clean目标沒有依賴目标,是以當真的存在clean檔案時,則該clean檔案一直都認為是最新的,是以執行make clean并不會執行clean下方的指令,這時就可以使用.PHONY指明該目标,比如:
.PHONY: clean
這樣的話執行make clean指令,它将無視目标檔案是否存在,跳過隐含規則搜尋,直接執行clean下方的指令,是以這也就是它改善性能的原因,省略了隐含規則搜尋這步
1.2 舉例子
我們通過三個例子來講解,由淺入深。
(1)
//main.cpp
#include <stdio.h>
int main(int argc, char** argv) {
printf("app startup\n");
printf("app stop\n");
return 0;
}
Makefile可以這樣編寫:
main: main.o
g++ main.o -o main
main.o: main.cpp
g++ -c main.cpp -o main.o
clean:
rm -rf *.o main
當我們執行make指令時,make工具會執行到main目标,檢視到它的依賴main.o,沒有該檔案,是以要先生成main.o,main.o目标的依賴是main.cpp,該檔案存在,建立日期比main.o檔案新,是以執行指令g++ -c main.cpp -o main.o生成main.o,再執行指令g++ main.o -o main生成main執行檔案
clean是當執行make clean的時候會删除.o字尾檔案和main檔案,通常用來清理編譯生成的檔案
(2)
上面這個例子比較簡單,那我們寫個稍微比上面這個複雜一點的:
app.h檔案:
#ifndef APP_H
#define APP_H
class App{
public:
static App& getInstance();
bool start();
bool shutdown();
private:
App();
App(const App&);
App& operator=(const App&);
bool m_stopped;
};
#endif
app.cpp檔案:
#include "app.h"
#include <stdio.h>
#include <unistd.h>
App& App::getInstance() {
static App app;
return app;
}
App::App() {
m_stopped = false;
}
bool App::start() {
printf("app startup\n");
while (!m_stopped) {
printf("app run\n");
sleep(5);
}
return true;
}
bool App::shutdown() {
if (m_stopped == false) {
m_stopped = true;
}
return true;
}
main.cpp檔案:
//main.cpp
#include <stdio.h>
#include "app.h"
int main(int argc, char** argv) {
App& app = App::getInstance();
if(!app.start()) {
printf("app start fail\n");
}
app.shutdown();
return 0;
}
是以我們可以這樣寫makefile:
main: main.o app.o
g++ main.o app.o -o main
main.o:main.cpp
g++ -c main.cpp -o main.o
app.o:app.cpp
g++ -c app.cpp -o app.o
clean:
rm -rf *.o main
通過上一個例子解釋這個makefile很簡單,但我們要想如果每個cpp檔案都要這樣寫,或者每加一個cpp檔案都要這樣寫,豈不是很麻煩,是以其實是可以借鑒一些正則比對的思想,比如一個變量表示所有的cpp檔案,可寫出如下makefile:
CPP_SOURCES = $(wildcard *.cpp)
CPP_OBJS = $(patsubst %.cpp, %.o, $(CPP_SOURCES))
$(warning $(CPP_SOURCES))
$(warning $(CPP_OBJS))
default:compile
$(CPP_OBJS):%.o:%.cpp
$(warning $<)
$(warning $@)
g++ -c $< -o $@
compile: $(CPP_OBJS)
g++ $^ -o main
clean:
rm -f $(CPP_OBJS)
rm -f main
這裡解釋幾個關鍵點:
wildcard函數的作用是把所有字尾比對.cpp的檔案以空格隔開傳回給CPP_SOURCES變量儲存,可以看到用$(warning $(CPP_SOURCES))語句打出變量值為app.cpp main.cpp
patsubst函數的作用是進行替換,将$(CPP_SOURCES)的變量值每一項由xx.cpp替換為xx.o
指令中的"$<"和"$@"則是自動化變量,"$<"表示所有的依賴目标集(也就是"main.cpp app.cpp"),"$@"表示目标集(也就是"main.o cpp.o")
"$^"表示所有的依賴目标集,表示main.o app.o
但上面這些makefile還是有缺點的,比如隻支援cpp檔案,.h和.cpp檔案沒有分離,.o檔案全生成在目前目錄下,沒有支援第三方的庫檔案,包括include檔案和lib檔案
以下給出一個較完善的makefile檔案:
TARGET = main
OBJ_PATH = objs
CC = g++
CFLAGS = -Wall -Werror -g
LINKFLAGS =
#INCLUDES = -I include/myinclude -I include/otherinclude1 -I include/otherinclude2
INCLUDES = -I include
#SRCDIR =src/mysrcdir src/othersrc1 src/othersrc2
SRCDIR = src
#LIBS = -Llib -lcurl -Llib -lmysqlclient -Llib -llog4cpp
LIBS =
C_SRCDIR = $(SRCDIR)
C_SOURCES = $(foreach d,$(C_SRCDIR),$(wildcard $(d)/*.c) )
C_OBJS = $(patsubst %.c, $(OBJ_PATH)/%.o, $(C_SOURCES))
CPP_SRCDIR = $(SRCDIR)
CPP_SOURCES = $(foreach d,$(CPP_SRCDIR),$(wildcard $(d)/*.cpp) )
CPP_OBJS = $(patsubst %.cpp, $(OBJ_PATH)/%.o, $(CPP_SOURCES))
default:init compile
$(C_OBJS):$(OBJ_PATH)/%.o:%.c
$(CC) -c $(CFLAGS) $(INCLUDES) $< -o $@
$(CPP_OBJS):$(OBJ_PATH)/%.o:%.cpp
$(CC) -c $(CFLAGS) $(INCLUDES) $< -o $@
init:
$(foreach d,$(SRCDIR), mkdir -p $(OBJ_PATH)/$(d);)
compile:$(C_OBJS) $(CPP_OBJS)
$(CC) $^ -o $(TARGET) $(LINKFLAGS) $(LIBS)
clean:
rm -rf $(OBJ_PATH)
rm -f $(TARGET)
install: $(TARGET)
cp $(TARGET) $(PREFIX_BIN)
uninstall:
rm -f $(PREFIX_BIN)/$(TARGET)
rebuild: clean compile
當然makefile也不僅僅隻用到編譯上,任何想要做先後順序執行腳本的事情我們都可以利用make來幫我們做,比如這個是我們項目中的makefile的一部分:
aodh:
cp -f SPECS/aodh/openstack-aodh.spec ~/rpmbuild/SPECS/
cp -f SPECS/aodh/* ~/rpmbuild/SOURCES/
tar zcvf ~/rpmbuild/SOURCES/aodh-4.0.3.tar.gz aodh-4.0.3 --exclude=".svn"
rpmbuild -bb ~/rpmbuild/SPECS/openstack-aodh.spec
ceilometer:
cp -f SPECS/ceilometer/openstack-ceilometer.spec ~/rpmbuild/SPECS/
cp -f SPECS/ceilometer/* ~/rpmbuild/SOURCES/
tar zcvf ~/rpmbuild/SOURCES/ceilometer-8.1.4.tar.gz ceilometer-8.1.4 --exclude=".svn"
rpmbuild -bb ~/rpmbuild/SPECS/openstack-ceilometer.spec
all_services:aodh ceilometer
當我們執行make aodh,就可以很友善的幫我們自動執行aodh下的腳本,執行make all_services時,根據makefile的規則,它會讓aodh和ceilometer下的腳本都執行一次,這等同于我們的目标target是不存在的,是以每次都重新建構。
2 spec檔案文法和使用
2.1 spec檔案的基本知識
一般我們編譯一個rpm編寫spec檔案是必不可少的,同時rpmbuild需要的以下5個目錄也是必不可少的
BUILD:rpmbuild編譯軟體的目錄,同時源碼也會解壓到該目錄下
BUILDROOT:充當一個虛拟根目錄,将要安裝的檔案放置到該虛拟目錄下
SOURCES:放置源檔案的目錄
RPMS:用于存放編譯好的RPM的目錄
SRPMS:用以存放SOURCE RPM的目錄
SPECS:用以存放spec檔案
所有的預定義宏可在/usr/lib/rpm/macros檔案中找到
這個目錄下也還有其它定義的宏,比如systemd提供的spec檔案中的宏放在/usr/lib/rpm/macros.d/macros.systemd檔案中
也可以在shell下通過執行rpm –eval '%configure'指令來看configure這個宏的值,比如:
以下是spec的文法:
%{echo:message} :列印資訊到标準輸出,error是列印到标準錯誤,warn是列印警告資訊到标準錯誤
%global name value :定義一個全局宏
可以用%macro_name或者%{macro_name}來調用,也可以擴充到shell,如
%define today %(date)
%{?macro_to_text:expression}:如果macro_to_text存在,expand expression,如果不存在,則輸出為空;也可以逆着用:%{!?macro_to_text:expression}
%{?macro}:忽略表達式隻測試該macro是否存在,如果存在就用該宏的值,如果不存在,就不用,如:./configure %{?_with_ldap}
%undefine macro :取消給定的宏定義
if else語句:
%global VVV 5
%if 0%{?VVV}
%{echo:19999}
%else
%{echo:29999}
%endif
這段是表示VVV這個全局變量有沒有定義,如果有定義則輸出19999,否則輸出29999
if表達式裡還可以使用!和&&等符号
用#來注釋,如果注釋内容裡有%則需要%%轉義,否則會報錯
spec檔案的基本寫法:
Name: myapp #設定該包服務的名字
Version: 1.1.2 #設定rpm包的版本号
Release:1 #設定rpm包的修訂号
Group: System Environment/System #設定rpm包的分類,所有組列在檔案/usr/share/doc/rpm-version/GROUP,比如/usr/share/doc/rpm-4.11.3/GROUPS
Distribution: Red Hat Linux #列出這個包屬于那個發行版
Icon: file.xpm or file.gif #存儲在rpm包中的icon檔案
Vendor: Company #指定這個rpm包所屬的公司或組織
URL: #公司或組織的首頁
Packager: sam shen <email> #rpm包制作者的名字和email
License: LGPL #包的許可證
Copyright: BSD #包的版權
Summary: something descripe the package #rpm包的簡要資訊
ExcludeArch: sparc s390 #rpm包不能在該系統結構下建立
ExclusiveArch: i386 ia64 #rpm包隻能在給定的系統結構下建立
Excludeos:windows #rpm包不能在該作業系統下建立
Exclusiveos: linux #rpm包隻能在給定的作業系統下建立
Buildroot: /tmp/%{name}-%{version}-root #rpm包最終安裝的目錄,預設是/
Source0: telnet-client.tar.gz
Patch1:telnet-client-cvs.patch #更新檔檔案
Patch2:telnetd-0.17.diff
Requires:bash>=2.0 #該包需要包bash,且版本至少為2.0,還有很多比較符号如<,>,<=,>=,=
PreReq: capability >=version #capability包必須先安裝
Conflicts:bash>=2.0 #該包和所有不小于2.0的bash包有沖突
BuildRequires:
BuildPreReq:
BuildConflicts:
#這三個選項和上述三個類似,隻是他們的依賴性關系在建構包時就要滿足,而前三者是在安裝包時要滿足
Autoreq: 0 #禁用自動依賴
Prefix: /usr
#定義一個relocatable的包,當安裝或更新包時,所有在/usr目錄下的包都可以映射到其他目錄,當定義Prefix時,所有%files标志的檔案都要在Prefix定義的目錄下
%triggerin --package < version
#當package包安裝或更新時,或本包安裝更新且package已經安裝時,運作script
...script...
%triggerun --package
#當package包删除時,或本包删除且package已經安裝時,運作script
(這裡要注意的一點是這裡的本包并不等于package包,package是随意定義的其他包的名字)
%triggerpostun --package
#當package包解除安裝後,或本包删除且package已經安裝後,運作script
...script...
不過我在ceilometer項目中看到是這樣的寫法,是表示運作完後執行的段落:
%postun compute
%description: #rpm包的描述
%prep #定義準備編譯的指令 ,比如在項目中prep段落是執行%setup解壓源碼指令
%setup -c #在解壓之前建立子目錄
-q #在安靜模式下且最少輸出
-T #禁用自動化解壓包
-n name #設定子目錄名字為name
-D #在解壓之前禁止删除目錄
-a number #在改變目錄後,僅解壓給定數字的源碼,如-a 0 for source0
-b number #在改變目錄前,僅解壓給定數字的源碼,如-b 0 for source0
%patch -p0 #remove no slashes
%patch -p1 #remove one slashes
%patch #打更新檔0
%patch1 #打更新檔1
%build #編譯軟體
比如一般c++程式的:
./configure --prefix=$RPM_BUILD_ROOT/usr
make
一般python程式的:
%{__python2} setup.py build
%install #安裝軟體
比如:make install PREFIX=$RPM_BUILD_ROOT/usr
比如python裡的:%{__python2} setup.py install -O1 --skip-build --root %{buildroot}
install -d -m 755 %{buildroot}%{_sharedstatedir}/ceilometer
install可以在linux下用man install來看
install跟cp指令類似,但它可以控制檔案權限屬性,通常用于makefile中,基本使用格式:
install [OPTION]... [-T] SOURCE DEST
%clean #清除編譯和安裝時生成的臨時檔案
比如:rm -rf $RPM_BUILD_ROOT
%post #定義安裝之後執行的腳本
...script...
#rpm指令傳遞一個參數給這些腳本,1是第一次安裝,>=2是更新,0是删除最新版本,用到的變量為$1,$2,$0
%preun #定義解除安裝軟體之前執行的腳本
...script...
%postun #定義解除安裝軟體之後執行的腳本
%files #rpm包中要安裝的所有檔案清單
file1 #檔案中也可以包含通配符,如*
file2
directory #所有檔案都放在directory目錄下
%dir /etc/xtoolwait #包含一個空目錄/etc/xtoolwait 打進包裡
%doc /usr/X11R6/man/man1/xtoolwait.* #安裝該文檔
%doc README NEWS #安裝這些文檔到/usr/share/doc/ or /usr/doc
%docdir #定義存放文檔的目錄
%config /etc/yp.conf #标志該檔案是一個配置檔案
%config(noreplace) /etc/yp.conf
#該配置檔案不會覆寫已存在檔案(被修改)覆寫已存在檔案(沒被修改),建立新的檔案加上擴充字尾.rpmnew(被修改) ,比如我們不想更新後配置檔案被改了,就可以用上noreplace
%config(missingok) /etc/yp.conf #該檔案不是必須要的
%ghost /etc/yp.conf #該檔案不應該包含在包中
%attr(mode, user, group) filename #控制檔案的權限如%attr(0644,root,root) /etc/yp.conf,如果你不想指定值,可以用-
%config %attr(-,root,root) filename #設定檔案類型和權限
%defattr(-,root,root) #設定檔案的預設權限
%lang(en) %{_datadir}/locale/en/LC_MESSAGES/tcsh* #用特定的語言标志檔案
%verify(owner group size) filename #隻測試owner,group,size,預設測試所有
%verify(not owner) filename #不測試owner
#所有的認證如下:
#group:認證檔案的組
#maj:認證檔案的主裝置号
#md5:認證檔案的MD5
#min:認證檔案的輔裝置号
#mode:認證檔案的權限
#mtime:認證檔案最後修改時間
#owner:認證檔案的所有者
#size:認證檔案的大小
#symlink:認證符号連接配接
%verifyscript #check for an entry in a system
...script... #configuration file
這些verify用的少
%changelog
修改記錄,類似這樣
* Wed Mar 07 2018 RDO <[email protected]> 1:8.1.4-1
- Update to 8.1.4
如果在%package時用-n選項,那麼在%description時也要用,如:
%description -n my-telnet-server
如果在%package時用-n選項,那麼在%files時也要用
%package -n sub_package_name #定義一個子包,名字為sub_package_name
pushd、popd和dir對目錄棧進行操作
可以看成這些指令在維護一個目錄堆棧,堆棧的最上層一定是目前目錄,且隻有一個目錄時不可popd出了,可用dirs來看目前目錄棧情況,加上-c清空目錄棧,-v可看到目錄棧序号,pushd 目錄x,可将目錄x送入目錄堆棧頂層,于是目前目錄也會變成目錄x,當pushd沒有參數時,比如隻執行pushd,則會把頂部兩層目錄交換,popd是pop出一個頂層目錄出來,pushd +序号可以将這個目錄推到棧目錄頂部。
記住一點目前目錄路徑一定是棧目錄的頂部目錄路徑。
是以在spec中也可以通過pushd和popd來改變目前工作目錄
2.2 利用上面的知識制作一個簡單的rpm
為了示範spec檔案的靈活性,我們将c程式和python程式結合到一個spec檔案來編譯,但實際項目中肯定是要分成兩個spec檔案才是合理的。
該項目rpmbuild出來後會有兩個rpm,分别是rpm1和rpm2,rpm1是打包了c應用服務檔案,rpm2是打包了python的應用服務檔案
首先利用tree指令看下我們的項目結構:
可以看到test_project下有兩個目錄(c_program和python_program)和一個spec檔案,c_program檔案夾裡的内容就是我們上面make那裡講到的,python_program是使用python的打包部署工具setuptools來打包的,spec檔案是我們的主要關注點,我們将其内容列出:
Name: test_spec
Version: 1.0
Release: 1
Summary: pratise to make rpm
Group: System Environment/System
License: GPL
URL: https://www.cnblogs.com/luohaixian/
Source0: test_project.tar.gz
Source1: xxx
BuildArch: x86_64
BuildRequires: python-setuptools
%description
pratise to make rpm
rpm1 c program
rpm2 python program
# 定義一個子包rpm1
%package -n rpm1
Summary: make rpm1
Requires: gcc
%description -n rpm1
xxxxxx
# 定義一個子包rpm2
%package -n rpm2
Summary: make rpm2
%description -n rpm2
xxxxxx
# 解壓在Source0壓縮包
# 源碼檔案都應先放置到~/rpmbuild/SOURCES目錄下
%prep
%setup -q -n test_project
# 執行編譯
# 對于c_program的則利用它自己目錄下的makefile寫的編譯規則進行編譯
# 對于python_program的則利用它自己目錄下的setup.py檔案裡的setup函數進行編譯
# pushd在這裡起到了類似cd的功能
%build
pushd c_program
make
popd
pushd python_program
%{__python2} setup.py build
popd
# 拷貝或安裝編譯好的檔案到%{buildroot}目錄下,這個目錄我們可以看成是虛拟根目錄
# 對于c_program的我們隻需要安裝一個main可執行檔案到/usr/bin目錄下
# 對于python_program我們使用python setup.py install來将python子產品檔案放置到/usr/lib/python/site-packages/目錄下,注意這裡一定要先切換到python_program目錄下來執行
# 是以其實要裝的檔案都放到了虛拟根目錄%{buildroot}下,然後由%files來決定哪些檔案放置給哪個rpm
%install
mkdir -p %{buildroot}%{_bindir}
install -m 755 $RPM_BUILD_DIR/test_project/c_program/main %{buildroot}%{_bindir}/
pushd python_program
%{__python2} setup.py install --root=%{buildroot}
popd
# 定義rpm1安裝之後執行的腳本,比如可以做啟動服務等
%post -n rpm1
# 定義rpm2安裝之後執行的腳本,比如可以做啟動服務等
%post -n rpm2
# 定義rpm1包含的檔案或檔案夾
# 這裡是定義了rpm1隻包含一個main可執行檔案
%files -n rpm1
%{_bindir}/main
# 定義rpm2包含的檔案或檔案夾
# 這裡是定義了rpm2包含了所有比對%{python2_sitelib}/python_program*的檔案夾和目錄
%files -n rpm2
%{python2_sitelib}/python_program*
%changelog
* Fri Sep 09 2019 <email> 1.0
- create spec
test_project的github位址:https://github.com/luohaixiannz/test_project
要将這個項目編譯成兩個rpm可以遵從如下步驟:
(1)建立rpmbuild所需要使用的目錄,在~/目錄下建立rpmbuild目錄,然後再在rpmbuild目錄下建立BUILD、BUILDROOT、SOURCES、SPECS、RPMS和SRPMS這6個子目錄
(2)安裝依賴包,rpmdevtools、python-setuptools、gcc、gcc-c++(可能還有些其它依賴包沒說明,根據報錯資訊安裝缺少的依賴包)
(3)将該壓縮檔案拷貝到~/rpmbuild/SOURCES目錄下,将這個壓縮檔案裡的test_project.spec檔案拷貝到~/rpmbuild/SPECS目錄下
(4)執行rpmbuild -bb ~/rpmbuild/SPECS/test_project.spec
3 打包openstack的項目為rpm包
可以通過在redhat網站上( http://vault.centos.org/)下openstack服務的對應版本的srpm檔案,然後通過rpm2cpio指令結合cpio指令提取該srpm檔案裡的spec檔案為己所用(除了spec檔案,可能還包含了其它要用的檔案,比如systemctl服務要用的.service檔案),這樣就不用耗費很大的精力去自己編寫一個spec檔案了。
比如我從openstack官網上擷取了nova-15.0.0的項目源碼(也可以直接使用srpm下解壓出來的源碼),想将其通過編譯後打包成rpm,可通過如下步驟達到目的:
(1)從rethad網站上下srpm:wget http://vault.centos.org/7.4.1708/cloud/Source/openstack-ocata/openstack-nova-15.1.0-1.el7.src.rpm
(2)建立一個臨時目錄,比如test目錄,cd test,然後執行:
rpm2cpio ../openstack-nova-15.1.0-1.el7.src.rpm | cpio -idv
接着就可以在目前目錄下看到解壓出來的檔案了:
可以看到除了spec檔案,還有很多的其它檔案也是需要的,将這些檔案都拷貝到~/rpmbuild/SPECS目錄下
(3)執行rpmbuild -bb ~/rpmbuild/SPECS/openstack-nova.spec指令後就可以建構rpm了(可以需要裝很多依賴包,根據報錯将其裝上就好了)