1 make tools
1.1 makefile basic rules
Make the most important tool is the most basic function is to describe the relationship between the source and the makefile automatically maintained by the compiler work.
Makefile rules:
target ... : prerequisites ...
command
...
...
Note command if not target the line (usually a separate line), type the command should be preceded by the symbol TAB, space does not work.
target is a target file, it can be executable file, which can be Object File, may also be a label
target one or more target files in the file depends on the prerequisites, which is defined in command generation rules.
prerequisites if there is more than one file is newer than the target file, then the command will be defined command execution
So take advantage of this feature, if it is a large project only changed one cpp file, you can compile only a part of which can be significant savings in compilation time.
The role of .PHONY target in the makefile
Two reasons to use .PHONY are:
(1) avoid the same name and file conflicts
This means that there is a directory such as the current makefile file with the same name as the target target directory or file will error, displaying declaration avoids conflict on .PHONY target
(2) improve performance
for example
clean:
rm * .o
By our understanding makefile rules on the face, clean goals are not dependent on target, so when there is really a clean file, the clean file has been considered to be up to date, so execute the command make clean and does not perform under the clean, then you can use .PHONY specify the target, such as:
.PHONY: clean
In this case execute make clean command, it ignores whether the target file exists, skip the implicit rule search, execute commands directly beneath the clean, so this is why it improves performance, omit this step implicit rule search
Example 1.2 Sub
Let's explain through three examples, progressive approach.
(1)
//main.cpp #include <stdio.h> int main(int argc, char** argv) { printf("app startup\n"); printf("app stop\n"); return 0; }
Makefile can write:
main: main.o g++ main.o -o main main.o: main.cpp g++ -c main.cpp -o main.o clean: rm -rf *.o main
When we make command, the tool will make the implementation of the main goal, to see to it depends main.o, not the file, so the first generation main.o, rely main.o goal is main.cpp, the file exists , file creation date is newer than main.o, so execute the command g ++ -c main.cpp -o main.o generate main.o, and then execute the command g ++ main.o -o main generating main executable file
When the execution is clean make clean will delete files and .o suffix main file, usually used to clean up the files compiled
(2)
The above example is relatively simple, then we write a little more than a point above the complex:
app.h file:
#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 file:
#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 file:
//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; }
So we can write 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
An example of this interpretation by the makefile is very simple, but if we are to each cpp file to be written, or each plus a cpp file to be written, would not be too much trouble, so they are actually thinking can learn from some of the regular match , such as a variable represents all the cpp files, you can write the following 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
Here to explain several key points:
The role of wildcard function is to match all suffixes .cpp file to return to the CPP_SOURCES variable separated by a space saving, can be seen with the $ (warning $ (CPP_SOURCES)) statement played the variable value app.cpp main.cpp
Patsubst action function is to be replaced, the $ (CPP_SOURCES) the value of each variable is replaced by the xx.cpp xx.o
Command "$ <" and "$ @" is the automatic variable, "$ <" means that all depends on the target set (that is, "main.cpp app.cpp"), "$ @" represents the target set (that is, "main.o cpp.o")
"$" Represents all dependent targets set, expressed main.o app.o
But above these makefile still have shortcomings, for example, only supports cpp files, .h and .cpp files are not isolated, .o file full generated under the current directory, without the support of third-party libraries, including the include file and lib files
The following gives a more complete makefile file:
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 <[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命令看下我们的项目结构:
可以看到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了(可以需要装很多依赖包,根据报错将其装上就好了)