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

 

以下是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了(可以需要裝很多依賴包,根據報錯將其裝上就好了)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM