软件构建系统
本文是《深入理解软件构造系统--原理与最佳实践》一书的学习笔记。
前言
sorter : main.c sort.c files.c tree.c merge.c
cc -o sorter main.c sort.c files.c tree.c merge.c
上述makefile
存在的问题:
源代码列出了两次
每次构造程序时,都会重新编译所有源文件,我们希望的是只编译修改过的源文件
没有指明源文件之间的依赖关系
构建系统遇见的典型问题:
错误的依赖关系导致编译失败
错误的依赖关系产生错误的软件,但是却未报错
编译速度太慢
花费大量时间去理解构建系统,只为了新增几个文件
开发大型软件产品的两种不同的方式:
单体构造: 在一次构建过程中,整个代码库只编译成一个
exe
组件构造: 把源代码划分多个层级,分别单独编译,最后把各个编译好的中间文件合并成
exe
第1章 构造系统概述
构造系统:
把源代码转换为
exe
Web
应用打包、混淆文档生成
源代码自动分析、静态分析工具
执行单元测试
编译型语言构造模型:
解释型语言构建模型:
Web应用程序构建模型:
单元测试构建模型:
静态分析构建模型:
文档构建模型:
源树与目标树:尽管可以将目标文件与源文件存放在同一目录,但这种方式带来了混乱,更好的做法是另外创建一个目录树,用于存放编译所产生的目标文件或可执行程序。下面是windows
一个小型程序的源树与目标树:
软件打包与安装方法:
归档文件,如
tar.gz
打包管理工具,如
rpm
与deb
包定制图形用户界面安装工具
一个例子:
# SCons 构建系统配置文件
Program("stock", ["ticker.c", "currency.c"])
评价一个构造系统:
易用性
正确性
性能
可伸缩性
第2章 基于Make的构造系统
如上图,所有的.o
都依赖同名的.c
(这一规则正是Make
内置的),所有.o
都依赖numbers.h
(需要自己定义),所以makefile
参考如下:
SRCS = add.c calc.c mult.c sub.c
OBJS = $(SRCS:.c=.o)
BIN = calculator
CC = gcc
CFLAGS = -g
INSTALL_ROOT = /usr/local
$(BIN) : $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$(OBJS) : numbers.h
.PHONY=clean
clean:
rm -f $(OBJS) $(BIN)
.PHONY=install
install:
cp $(BIN) $(INSTALL_ROOT)/bin
大多数软件开发人员不需要面对上述文件的复杂性与专业性,他们只需要知道在什么地方填入什么内容就可以,框架化是一个非常好的做法:
BIN = calculator # 填入目标可执行文件
HEADERS = numbers.h # 填入头文件
SRCS = add.c calc.c mult.c sub.c # 填入源文件
include framework.mk
由专业人员实现framework.mk
框架:
OBJS = $(SRCS:.c=.o)
CC = gcc
INSTALL_ROOT=/usr/local
ifdef DEBUG
CFLAGS = -o -g
else
CFLAGS = -o
endif
$(BIN) : $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$(OBJS) : $(HEADERS)
.PHONY=clean
clean:
rm -f $(OBJS) $(BIN)
.PHONY=install
install:
cp $(BIN) $(INSTALL_ROOT)/bin
第3章 程序的运行时视图
概念:
可执行程序
程序库
配置文件和数据文件
分布式程序
可执行文件:
解释型语言:
程序库:
创建新程序库
链接到现有程序库
分布式程序:
第4章 文件类型与编译工具
构造工具: 较高层次,控制编译次序和流程
编译工具: 将一种文件转换为另一种文件
GCC工具链:
C预处理器
C编译器
汇编器
链接器
第5章 多个平台多个版本
平台: Windows、Linux
控制编译平台:
ifeq($(HOST),Linux)
CC := gcc
CFLAGS = -g -o
endif
ifeq($(HOST),Windows)
CC = cl.exe
CFLAGS = /O2 /Zi
endif
细粒度控制源代码条件编译:
char *get_user_name()
{
#ifdef linux
struct passwd *pwd = getpwuid(getuid());
return pwd->pw_name;
#endif
#ifdef WIN32
static char name[100];
DWORD size = sizeof(name);
GetUserName(name, &size);
return name;
#endif
}
版本: 家庭版、专业版、语言
int compute_costs()
{
int total_costs = 0;
#ifdef EDITION_PROF
total_costs += capital_cost_allowance();
#else
total_costs += basic_costs();
#endif
}
语言版本可以划分多个语言包,在编译时调整、在安装时调整、在使用时调整。
第6章 构造工具 Make
构建场景:
源代码放在单个目录中
源代码放在多个目录中
定义新的编译工具,必须有某种方式来告诉构造系统,如何使用新的工具,以及如何推测源文件的所有依赖关系
支持多个构造变量(多个版本、多种CPU、操作系统类型)
清理构造树
对不正确的构造结果进行调试
Make
提供了完整的语言,来描述构造过程:
文件依赖:一种基于规则的语法
Shell命令
字符串处理
文件模式匹配:
%.o : %c
commands;
单个target
多条依赖:
chunk.o : chunk.c
gcc -c chunk.c
chunk.o : chunk.h list.h data.h
多个target
依赖单文件:
add.o calc.o mult.o sub.o : common.h
第7章 构造工具 Ant
第8章 构造工具 SCons
第9章 构造工具 CMake
CMake提供一种高层次语言描述构造过程,使用生成器将这种语言转换成原生构造工具(Make)的描述语言,从而对开发人员隐藏了原生语言的一切复杂性。
每个编译平台(Linux、Mac、Windows)都有对应的生成器,从而实现了跨平台。
支持特性:
字符串操作
列表操作
文件操作
数学表达式
配置数据文件
测试可执行程序
打包与安装
平台无关的shell命令
第10章 Eclipse IDE
第11章 依赖关系
依赖关系的建立是增量式编译的前提。
为了建立依赖关系,构造工具必须:
判断所有文件之间的依赖关系。为整个程序创建依赖关系图。
依据依赖关系图,判断某文件更新后,需要重新生成哪些文件
按逻辑有序的步骤执行具体编译步骤
依赖关系图:
修改cat.c
后:
下图中,dog.c
与cat.c
都使用animals.h
文件中定义的数据结构,如果dog.obj
与cat.obj
到animals.h
的依赖关系缺失的话,更新animals.h
后,dog.obj
与cat.obj
并不会随着更新,当里面的数据结构被使用时,可能会带来运行时错误。
多余的依赖关系将会导致大量不必要的重新编译:
无效的依赖关系,导致构建出错:
循环依赖,导致大量执行命令,并产生无用数据:
判断文件更新:
方法1:先对比文件的时间戳是否变化,如果有则再对比内容(md5校验),有一项优化是可以不计入注释
方法2:由构建系统为文件加标记
方法3:询问版本控制系统
对animals.h
的修改导致文件从左至右依次更新:
第12章 优化我们的项目
调试器通过编译进入源代码中的额外数据进行调试。
性能分析测试。
代码分支覆盖分析支持。
源代码文档化。
单元测试。
静态分析。