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是隨意定義的其他包的名字)
...script...
%triggerpostun --package
#當package包卸載后,或本包刪除且package已經安裝后,運行script
...script...
不過我在ceilometer項目中看到是這樣的寫法,是表示運行完后執行的段落:
%postun compute
%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 #定義卸載軟件之后執行的腳本
...script...
%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 <dev@lists.rdoproject.org> 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了(可以需要裝很多依賴包,根據報錯將其裝上就好了)