目录

《深入理解软件构造系统原理与最佳实践》

本文是《深入理解软件构造系统--原理与最佳实践》一书的学习笔记。

前言

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应用程序构建模型:

Web应用程序构建模型

单元测试构建模型:

单元测试构建模型

静态分析构建模型:

静态分析

文档构建模型:

文档构建模型

源树与目标树:尽管可以将目标文件与源文件存放在同一目录,但这种方式带来了混乱,更好的做法是另外创建一个目录树,用于存放编译所产生的目标文件或可执行程序。下面是windows一个小型程序的源树与目标树:

源树与目标树

软件打包与安装方法:

  • 归档文件,如tar.gz

  • 打包管理工具,如rpmdeb

  • 定制图形用户界面安装工具

编译型语言构建方法

一个例子:

深度截图_选择区域_20190624015957.png

# 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章 程序的运行时视图

概念:

  • 可执行程序

  • 程序库

  • 配置文件和数据文件

  • 分布式程序

可执行文件:

可执行文件

解释型语言:

解释型语言

程序库:

  • 创建新程序库

  • 链接到现有程序库

静态链接与动态链接

分布式程序:

深度截图_选择区域_20190624110709.png

第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)都有对应的生成器,从而实现了跨平台。

CMake

支持特性:

  • 字符串操作

  • 列表操作

  • 文件操作

  • 数学表达式

  • 配置数据文件

  • 测试可执行程序

  • 打包与安装

  • 平台无关的shell命令

第10章 Eclipse IDE

第11章 依赖关系

依赖关系的建立是增量式编译的前提。

为了建立依赖关系,构造工具必须:

  • 判断所有文件之间的依赖关系。为整个程序创建依赖关系图。

  • 依据依赖关系图,判断某文件更新后,需要重新生成哪些文件

  • 按逻辑有序的步骤执行具体编译步骤

依赖关系图:

依赖关系图

修改cat.c后:

修改文件后

下图中,dog.ccat.c都使用animals.h文件中定义的数据结构,如果dog.objcat.objanimals.h的依赖关系缺失的话,更新animals.h后,dog.objcat.obj并不会随着更新,当里面的数据结构被使用时,可能会带来运行时错误。

依赖关系缺失

多余的依赖关系将会导致大量不必要的重新编译:

多余的依赖关系

无效的依赖关系,导致构建出错:

无效的依赖关系

循环依赖,导致大量执行命令,并产生无用数据:

循环依赖

判断文件更新:

  • 方法1:先对比文件的时间戳是否变化,如果有则再对比内容(md5校验),有一项优化是可以不计入注释

  • 方法2:由构建系统为文件加标记

  • 方法3:询问版本控制系统

animals.h的修改导致文件从左至右依次更新:

按逻辑有序的步骤执行具体编译步骤

第12章 优化我们的项目

调试器通过编译进入源代码中的额外数据进行调试。

性能分析测试。

代码分支覆盖分析支持。

源代码文档化。

单元测试。

静态分析。

第13章 软件打包与安装

第14章 版本管理

第15章 构造机器

第16章 工具管理

第17章 降低最终用户面对的复杂度

第18章 管理构造规模

第19章 更快的构造