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