本页介绍了构建系统的定义、用途、使用原因,以及在组织开始扩大规模时,为什么编译器和构建脚本不是最佳选择。本教程适用于缺乏构建系统经验的开发者。
什么是构建系统?
从根本上讲,所有构建系统都有一个简单明了的用途:将工程师编写的源代码转换为可供机器读取的可执行二进制文件。构建系统不仅适用于由人编写的代码;它们还允许机器自动创建 build,无论是用于测试还是用于发布到生产环境。在拥有数千名工程师的组织中,大多数 build 通常由系统自动触发,而不是由工程师直接触发。
我不能直接使用编译器吗?
对构建系统的需求可能不会立即显现出来。大多数工程师在学习编码时不会使用构建系统:大多数人开始时会直接从命令行调用 gcc
或 javac
等工具,或在集成开发环境 (IDE) 中调用等效工具。只要所有源代码都位于同一目录中,如下所示的命令就能正常运行:
javac *.java
这会指示 Java 编译器将当前目录中的每个 Java 源文件转换为二进制类文件。在最简单的情况下,只需这样做即可。
不过,一旦代码扩展,复杂问题就开始了。javac
非常智能,足以在当前目录的子目录中查找要导入的代码。但它无法找到存储在文件系统其他部分(可能是多个项目共享的库)中的代码。它也只知道如何构建 Java 代码。大型系统通常涉及使用各种编程语言编写的不同部分,这些部分之间存在复杂的依赖关系网,这意味着任何单一语言的编译器都无法构建整个系统。
一旦处理了来自多种语言或多个编译单元的代码,构建代码就不再是一步到位的过程,现在,您必须评估代码所依赖的组件,并以正确的顺序构建这些部分,并且可能为每个部分使用不同的工具集。如果任何依赖项发生变化,您必须重复此过程,以免依赖于过时的二进制文件。对于中等大小的代码库,此过程很快就会变得繁琐且容易出错。
此外,编译器也不知道如何处理外部依赖项,例如 Java 中的第三方 JAR
文件。如果没有构建系统,您可以通过以下方式进行管理:从互联网下载依赖项,将其粘贴到硬盘上的 lib
文件夹中,并将编译器配置为从该目录读取库。随着时间的推移,将难以维护这些外部依赖项的更新、版本和来源。
Shell 脚本怎么样?
假设您的业余项目一开始足够简单,您只需使用编译器即可构建它,但您开始遇到之前所述的一些问题。您可能仍然认为自己不需要构建系统,并且可以使用一些简单的 shell 脚本来自动执行繁琐的部分,这些脚本会按照正确的顺序处理构建事宜。这样做有一段时间了,但很快就会开始遇到更多问题:
这会很繁琐。随着系统变得越来越复杂,您在构建脚本上花费的时间与编写实际代码的时间差不多。调试 shell 脚本很麻烦,因为越来越多的黑客会层层叠加。
速度很慢。为确保您不会意外依赖于过时库,您可以让 build 脚本在每次运行时按顺序构建每个依赖项。您考虑添加一些逻辑来检测哪些部分需要重新构建,但这对于脚本来说听起来非常复杂且容易出错。或者,您考虑指定每次需要重新构建哪些部分,但又回到了原点。
好消息:现在可以发布了!最好先弄清楚您需要传递给 jar 命令的所有参数,以便生成最终 build。并记住如何上传该文件并将其推送到中央代码库。并构建和推送文档更新,以及向用户发送通知。嗯,可能需要使用其他脚本...
糟糕!您的硬盘崩溃了,现在您需要重新创建整个系统。您很聪明,将所有源文件都纳入了版本控制,但下载的库呢?您能否重新找到它们,并确保它们与首次下载时是相同的版本?您的脚本可能依赖于在特定位置安装特定工具 - 您能否恢复相同的环境,以便脚本再次正常运行?您很久以前为使编译器正常运行而后忘记的那些环境变量如何?
尽管存在一些问题,但您的项目已经非常成功,您可以开始招聘更多工程师。现在,您已经意识到,之前出现的问题并非灾难性的,因为每当有新开发者加入您的团队时,您都需要经历同样痛苦的引导流程。尽管您已尽最大努力,但每个用户的系统之间仍会存在细微差异。通常,某个用户的机器上可正常运行的代码在另一个用户的机器上无法正常运行,而且每次都需要花费数小时来调试工具路径或库版本,才能找出差异所在。
您决定需要自动化构建系统。从理论上讲,只需购买一台新电脑并将其设置为使用 cron 每天晚上运行构建脚本,即可轻松实现此目的。您仍然需要经历痛苦的设置流程,但现在您无法像人脑那样检测和解决小问题。现在,每天早上,您都会看到昨晚的构建失败了,因为昨天有一位开发者进行了一项更改,这项更改在他们的系统上有效,但在自动化构建系统上无效。每次都是简单的修复,但问题出现的频率非常高,以至于您每天都要花费大量时间来发现和应用这些简单的修复。
随着项目的增长,构建速度会越来越慢。某天,在等待 build 完成时,您悲哀地看着正在休假的同事闲置的桌面,并希望能有办法利用所有浪费的计算能力。
您遇到了一个典型的规模问题。对于单个开发者,如果他只需要处理最多几百行代码,并且最多只需要一两周的时间(这可能就是刚刚从大学毕业的初级开发者的全部经验),那么只需使用编译器即可。脚本或许可以帮您更进一步。但是,当您需要在多个开发者及其机器之间进行协调时,即使是完美的构建脚本也是不够的,因为要解释这些机器之间的细微差异就会变得非常困难。此时,这种简单的方法就行不通了,您需要投资于真正的构建系统。