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了(可以需要装很多依赖包,根据报错将其装上就好了)

 

posted @ 2019-09-08 02:02  luohaixian  阅读(9933)  评论(0编辑  收藏  举报