为什么要使用构建系统?

本页介绍了什么是构建系统、构建系统的作用、为什么要使用构建系统,以及为什么随着组织规模的扩大,编译器和构建脚本不再是最佳选择。本页面向对构建系统经验不多的开发者。

什么是构建系统?

从根本上讲,所有构建系统都有一个简单的目的:将工程师编写的源代码转换为机器可读取的可执行二进制文件。构建系统不仅适用于人工编写的代码,还允许机器自动创建 build,无论是用于测试还是发布到生产环境。在拥有数千名工程师的组织中,大多数 build 通常由机器自动触发,而不是由工程师直接触发。

我不能只使用编译器吗?

您可能不会立即意识到需要构建系统。大多数工程师在学习编码时都不会使用构建系统:大多数人都是从命令行直接调用 gccjavac 等工具,或者在集成开发环境 (IDE) 中调用等效工具。只要所有源代码都在同一目录中,这样的命令就可以正常运行:

javac *.java

这会指示 Java 编译器获取当前目录中的每个 Java 源文件,并将其转换为二进制类文件。在最简单的情况下,您只需要这样做。

但是,一旦代码扩展,问题就会开始出现。javac 非常智能,可以查找当前目录的子目录以查找要导入的代码。但它无法找到存储在文件系统的其他部分(可能是多个项目共享的库)中的代码。 它也只知道如何构建 Java 代码。大型系统通常涉及用各种编程语言编写的不同部分,这些部分之间存在复杂的依赖关系,这意味着没有一种语言的编译器可以构建整个系统。

一旦您处理来自多种语言或多个编译单元的代码,构建代码就不再是一步完成的过程。现在,您必须评估代码所依赖的内容,并按正确的顺序构建这些部分,可能需要为每个部分使用不同的工具集。如果任何依赖项发生更改,您必须重复此过程,以避免依赖过时的二进制文件。对于即使是中等大小的代码库,此过程也会很快变得繁琐且容易出错。

编译器也不知道如何处理外部依赖项,例如 Java 中的第三方 JAR 文件。如果没有构建系统,您可以通过从互联网下载依赖项、将其放入硬盘上的 lib 文件夹中,并配置编译器以从该目录读取库来管理此问题。随着时间的推移,很难维护这些外部依赖项的更新、版本和来源。

那么 Shell 脚本呢?

假设您的业余项目一开始很简单,您可以使用编译器构建它,但您开始遇到前面描述的一些问题。也许您仍然认为不需要构建系统,并且可以使用一些简单的 Shell 脚本来自动执行繁琐的部分,这些脚本负责按正确的顺序构建内容。这在一段时间内很有帮助,但很快您就会遇到更多问题:

  • 它变得很繁琐。随着系统的日益复杂,您开始花费几乎与编写实际代码一样多的时间来处理构建脚本。调试 Shell 脚本非常痛苦,因为越来越多的黑客攻击层出不穷。

  • 它很慢。为了确保您没有意外依赖过时的库,您让构建脚本在每次运行时按顺序构建每个依赖项。您考虑添加一些逻辑来检测哪些部分需要重建,但对于脚本来说,这听起来非常复杂且容易出错。或者,您考虑每次指定哪些部分需要重建,但这样您又回到了原点。

  • 好消息:该发布版本即将发布!最好弄清楚您需要传递给 jar 命令的所有实参,以进行最终构建。并记住如何上传它并将其推送到中央代码库。并构建和推送文档更新,并向用户发送通知。嗯,也许这需要另一个脚本...

  • 灾难!您的硬盘崩溃了,现在您需要重新创建整个系统。您很聪明,将所有源文件都保留在版本控制中,但您下载的那些库呢?您能再次找到它们并确保它们与您首次下载时的版本相同吗?您的脚本可能依赖于在特定位置安装的特定工具,您能恢复相同的环境以使脚本再次运行吗?您很久以前设置的所有环境变量呢?您设置这些变量是为了让编译器正常运行,但后来却忘记了它们。

  • 尽管存在这些问题,但您的项目非常成功,您可以开始聘用更多工程师。现在您意识到,不需要发生灾难,之前的问题也会出现:每当有新开发者加入您的团队时,您都需要经历同样痛苦的引导过程。尽管您尽了最大的努力,但每个人的系统仍然存在细微的差异。通常,在一个人机器上运行正常的内容在另一个人的机器上无法正常运行,每次都需要花费几个小时来调试工具路径或库版本,才能找出差异所在。

  • 您决定需要自动执行构建系统。从理论上讲,这就像购买一台新计算机并将其设置为每晚使用 cron 运行构建脚本一样简单。您仍然需要经历痛苦的设置过程,但现在您无法获得人脑能够检测和解决小问题的优势。现在,每天早上您上班时,都会看到昨晚的 build 失败了,因为昨天一位开发者做出的更改在其系统上运行正常,但在自动构建系统上无法正常运行。每次都是简单的修复,但这种情况经常发生,以至于您每天都要花费大量时间来发现和应用这些简单的修复。

  • 随着项目的增长,build 变得越来越慢。有一天,在等待 build 完成时,您悲伤地凝视着正在休假的同事的空闲桌面,并希望有一种方法可以利用所有浪费的计算能力。

您遇到了一个典型的规模问题。对于最多处理几百行代码的单个开发者来说,最多一两周的时间(这可能是刚从大学毕业的初级开发者迄今为止的全部经验),编译器就足够了。脚本或许可以帮助您更进一步。但是,一旦您需要协调多个开发者及其机器,即使是完美的构建脚本也不够,因为很难考虑到这些机器的细微差异。此时,这种简单的方法就会失效,您需要投资于真正的构建系统。