本页将介绍基于任务的构建系统、这些系统的工作原理以及一些 基于任务的系统可能会出现的复杂问题。在 Shell 脚本之后 基于任务的构建系统是构建领域的下一个逻辑发展。
了解基于任务的构建系统
在基于任务的构建系统中,基本工作单元是任务。每个 任务是一个脚本,可执行任何种类的逻辑,而任务指定 它们作为依赖项必须在它们之前运行。正在使用的大多数主要构建系统 例如 Ant、Maven、Gradle、Grunt 和 Rake,它们都是基于任务的。而不是 shell 脚本,大多数现代构建系统都需要工程师创建 build 文件 说明了如何执行构建。
从 Ant 手册:
<project name="MyProject" default="dist" basedir=".">
<description>
simple example build file
</description>
<!-- set global properties for this build -->
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init"
description="compile the source">
<!-- Compile the Java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>
<target name="dist" depends="compile"
description="generate the distribution">
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>
<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
</target>
<target name="clean"
description="clean up">
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
构建文件使用 XML 编写,并定义了一些关于构建的简单元数据
以及任务列表(XML 中的 <target>
标记)。(Ant 使用
target 表示任务,并使用任务一词来指代
命令。)每个任务都会执行由 Ant 定义的一系列可能的命令,
其中包括创建和删除目录、运行 javac
以及
创建 JAR 文件。这组命令可通过用户提供
涵盖各种逻辑的插件每个任务还可以定义
依赖项。这些依赖关系形成一个无环图,
如图 1 所示。
图 1. 显示依赖关系的无环图
用户通过向 Ant 的命令行工具提供任务来执行构建。例如:
当用户输入 ant dist
时,Ant 会执行以下步骤:
- 在当前目录中加载名为
build.xml
的文件,并将其解析为 创建如图 1 所示的图表结构。 - 查找在命令行中提供的名为
dist
的任务,然后 发现它依赖于名为compile
的任务。 - 查找名为
compile
的任务,并发现它有一个依赖项 名为init
的任务。 - 查找名为
init
的任务,并发现它没有依赖项。 - 执行
init
任务中定义的命令。 - 执行
compile
任务中定义的命令,前提是所有这些命令 任务依赖项已运行。 - 执行
dist
任务中定义的命令,前提是所有这些命令 任务依赖项已运行。
最后,运行 dist
任务时由 Ant 执行的代码是等效的
复制到以下 Shell 脚本:
./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*
删除语法后,buildfile 和 build 脚本实际上
没有太大区别但我们通过这样做获益良多。我们可以
在其他目录中创建新的 buildfile 并将它们关联在一起。我们可以轻松
以任意且复杂的方式添加依赖于现有任务的新任务。周三
只需将单个任务的名称传递给 ant
命令行工具,它
决定需要运行的所有内容
Ant 是一种最早于 2000 年发布的旧软件。其他工具 在接下来的几年里,Maven 和 Gradle 对 Ant 进行了改进,基本上 取而代之的是,通过添加功能来自动管理外部 并采用更简洁的语法,无需任何 XML。但这些新技术的性质 系统则保持不变:这些系统允许工程师以 原则化的模块化任务,并提供用于执行这些任务的工具 以及管理它们之间的依赖关系
基于任务的构建系统的黑暗面
从本质上讲,这些工具可让工程师将任何脚本定义为任务, 功能极其强大,能够让您做任何能想到的事情, 。但这种能力也存在缺点,基于任务的构建系统 使其构建脚本变得越来越复杂。通过 这类系统的问题在于,它们实际上最终会输送出太多电量, 对系统的需求不足。因为系统不知道 脚本的作用,性能会受到影响,因为必须非常保守 如何安排和执行构建步骤。而且系统无法 确认每个脚本都执行了它应有的操作,因此脚本往往 并最终成为另一个需要调试的问题。
难以并行执行构建步骤
现代开发工作站功能非常强大, 能够并行执行多个构建步骤。但基于任务的系统 通常无法并行执行任务,即使看起来应该 。假设任务 A 依赖于任务 B 和 C。因为任务 B 和任务 C 不相互依赖,同时运行它们是否安全? 系统是否能够更快地完成任务 A?有可能,如果他们不碰任何东西 资源。但也可能不是 - 可能两者使用相同的文件来跟踪 同时运行这些容器会导致冲突。我们没有 但系统通常知道这些冲突, (会导致罕见但非常难以调试的构建问题),或者 将整个构建限制为在单个进程的单个线程上运行。 这可能是强大的开发机器的巨大浪费, 排除了将 build 分发到多台机器的可能性。
难以执行增量构建
良好的构建系统可让工程师执行可靠的增量构建,例如: 一项小小的更改并不需要从头构建整个代码库 。如果构建系统运行缓慢且无法 基于上述原因并行执行构建步骤。但遗憾的是 基于任务的构建系统在这方面也有困难。任务可以执行任何操作 因此一般无法检查这些活动是否已经完成。多项任务 只需获取一组源文件并运行编译器 即可创建一组 二进制文件;因此如果底层源文件存在 没有任何变化但如果没有额外信息,系统便无法判断 任务下载的文件可能已更改 写入一个时间戳,该时间戳在每次运行时可能都有所不同。为了保证 正确性,系统通常必须在每次构建期间重新运行每个任务。部分 构建系统可通过让工程师指定 需要重新运行任务的条件。有时这样做是可行的 这往往比表面上看起来要复杂得多。例如,在语言中 就像允许其他文件直接包含文件的 C++ 一样, 无法确定必须监视更改的整组文件 而无需解析输入源。工程师最终往往会选择走捷径 这些快捷方式可能会导致罕见且令人沮丧的问题, 即便是不应被重复使用的情况。如果这种情况经常发生,工程师会 养成在每次构建前都进行清理的习惯,以获得全新状态, 完全违背了在首次测试阶段进行增量构建的目的。 位置。弄清楚何时需要重新运行某个任务非常微妙, 让机器比人类更好地处理工作。
难以维护和调试脚本
最后,基于任务的构建系统施加的构建脚本通常只是 也很难处理虽然它们通常不会受到审查,但构建脚本 就像是正在构建的系统代码,很容易将 bug 隐藏起来。 下面列举了一些使用 基于任务的构建系统:
- 任务 A 依赖任务 B 来生成特定文件作为输出。所有者 的任务 B 没有意识到其他任务依赖于该任务,因此将其更改为 在其他位置生成输出直到有人 尝试运行任务 A,但发现它失败了。
- 任务 A 依赖于任务 B,而任务 B 依赖于任务 C,而任务 C 会生成 输出特定文件作为任务 A 所需的输出。任务 B 的所有者 确定不再需要继续依赖任务 C, A 失败,即使任务 B 根本不关心任务 C!
- 新任务的开发者不小心对 例如工具的位置或 特定环境变量任务可以在其机器上运行,但失败了 就会出现这种错误
- 任务包含不确定性组件,例如下载文件 或者向 build 添加时间戳。现在,人们可以 每次运行构建时产生的结果可能都不同,这意味着 工程师并非总能重现并修复彼此的故障 自动编译系统上发生的故障或故障
- 具有多个依赖项的任务可能会创建竞态条件。如果任务 A 都依赖于任务 B 和任务 C,而任务 B 和 C 都修改同一个 任务 A 会得到不同的结果,具体取决于任务 B 和 C 中的哪一项 先完成。
没有通用的方法来解决这些性能、正确性或 基于任务的框架中的可维护性问题。再见 因为工程师可以编写在构建期间运行的任意代码, 但没有足够的信息始终能够快速运行构建作业 正确。要解决这个问题,我们需要节省一些能源, 把它放回系统手里,重新概念 而不是运行中的任务,而是生成工件。
这种方法催生了基于工件的构建系统,如 Blaze 和 Bazel