天天看點

make和rpm的編譯、打包總結

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這個宏的值,比如:

make和rpm的編譯、打包總結

以下是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指令看下我們的項目結構:

make和rpm的編譯、打包總結

可以看到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

接着就可以在目前目錄下看到解壓出來的檔案了:

make和rpm的編譯、打包總結

可以看到除了spec檔案,還有很多的其它檔案也是需要的,将這些檔案都拷貝到~/rpmbuild/SPECS目錄下

(3)執行rpmbuild  -bb  ~/rpmbuild/SPECS/openstack-nova.spec指令後就可以建構rpm了(可以需要裝很多依賴包,根據報錯将其裝上就好了)

繼續閱讀