Subversion中二进制文件的替代方案

23

我的一些同事坚信将构建产物提交到Subversion仓库是一个好主意。他们认为这样做可以轻松地在测试机器上安装和更新,只需 "svn up"!

我确信有很多反对这种不良做法的有力理由,但我能想到的都是一些无聊的理由,比如 "它占用更多的空间"。不做这件事的最佳和最有说服力的理由是什么?还有其他应该采取的方法吗?

如果这是针对Java代码的话,这会有所区别。所有的代码都是从Eclipse编译(没有自动化的PDE构建)。

当我说添加构建产物时,我的意思是提交会像这样:

"Added the new Whizbang feature"

 M src/foo/bar/Foo.java
 M bin/Foo.jar

每次代码更改都会生成相应的 jar 文件。


为什么会有负评?这是一个常见的问题 - 我并不是说这是一个好主意! - richq
1
哇!看起来“在Subversion中使用二进制文件”实际上不被认为是反模式了!这对我来说相当震惊。 - richq
不确定,但这常常会导致冲突吧?如果我自己构建JAR文件,然后进行更新,往往很难“合并”。也许将它们放在一个特殊目录中,那些构建自己的JAR文件的人就不会把它们放进去了。 - Frank
你不会合并二进制文件,只存储最新版本,并且不要试图将它们视为可合并的。你在处理所有其他二进制源时也会遇到相同的问题。 - gbjbaanb
合并,我猜这里是指当您在本地更改构建输出(通过编译内容),然后“svn up”时,您将合并您同事的更改到自己的更改中,并且几乎肯定会产生冲突。 - richq
有很多有力的反对这种不良做法的论点,但我所能想到的都是无力的。我慢慢意识到,如果你自己无法给出任何好的理由,那么即使它感觉不对,实际上可能也没什么问题,也许是那种感觉是错的。 - Karsten
15个回答

24
在我看来,代码仓库应该只包含源代码以及编译此源代码所需的第三方库(同时,第三方库在构建过程中可以通过某些依赖管理工具进行检索)。生成的二进制文件不应与源代码一起检入。
我认为在您的情况下问题出在没有适当的构建脚本。这就是为什么从源代码构建二进制文件需要一些工作,例如启动eclipse,导入项目,调整类路径等。
如果有构建脚本,获取二进制文件可以使用如下命令:
svn update; ant dist

我认为不将二进制文件与源代码一起提交的最重要原因是导致存储库大小增加。这样会引起以下问题:

  • 存储库变大,版本控制系统服务器上可能会出现空间不足的情况
  • 版本控制系统服务器和客户端之间的流量增多
  • 更新时间更长(想象一下你从互联网上进行 SVN 更新...)

另一个原因可能是:

  • 源代码很容易进行比较,所以版本控制系统的许多功能都有意义。但是二进制文件无法轻松比较...

此外,你上述的方法在我看来还带来了很多额外的开销。如果开发人员忘记更新相应的 jar 文件会怎么样呢?


你的论点提出得非常好。在 svn up;ant dist 这句话中,你直接点到了问题的要害。 - richq
3
我完全不同意“svn update; ant dist”这个评论。如果我将二进制文件经过昂贵的测试流程,那么我想要部署的就是确切的二进制文件,而不是类似它的东西。二进制文件确实需要被存档。也许不是在代码库里,但需要存档。 - Jim T
2
你的回答没有考虑到版本控制系统是一个时间机器。在5或10年后,没有任何保证源代码能够与当前的编译器完全兼容,甚至不能保证生成相同的测试二进制文件。 - Juliano
2
@Jim T:是的,我同意应该将二进制文件存档,但不要与源代码混在一起...我想通过我的帖子展示的是,如果你有一个适当的构建脚本,很容易就能够再现这些构件。 - Homes2001
1
@Juliano:但仅检查生成的二进制文件并不能解决这个问题。在这里,您需要将编译器和所有必需的产品一起提交以翻译您的源代码... - Homes2001
@Juliano:但是你打算对构建环境的虚拟机映像进行版本控制吗?那么虚拟机软件本身呢?等等。 - Wim Coenen

16
首先,Subversion(以及现在所有其他的系统)不是源代码控制管理器(我一直认为SCM代表软件配置管理),而是版本控制系统。这意味着它们存储你在其中存储的东西的更改,它不一定是源代码,它可以是图像文件、位图资源、配置文件(文本或XML)以及各种各样的东西。只有一个原因不能将构建出的二进制文件视为此列表的一部分,那就是因为你可以重新构建它们。
然而,想一想为什么你需要在其中存储发布的二进制文件。
首先,它是帮助你的系统,而不是告诉你如何构建你的应用程序。让计算机为你工作,而不是反过来。如果存储二进制文件占用空间,那又怎样——你有数百GB的磁盘空间和超快速的网络。在里面存储二进制对象已经不再是个问题了(而十年前可能是一个问题——这也许是人们认为在SCM中存储二进制文件是个不好的做法的原因)。
其次,作为开发人员,你可能会习惯于使用该系统重新构建任何应用程序的任何版本,但其他人可能不会(例如QA、测试、支持)。这意味着你需要另一个系统来存储二进制文件,而实际上,你已经有这样一个系统了,它就是你的SCM!充分利用它。
第三,你假设你可以从源代码重新构建。显然,你在里面存储了所有的源代码,但你没有存储编译器、库、SDK以及所有其他依赖项所需的所有东西。当有人来问“你能否为我构建我们两年前发布的版本,客户遇到了那个版本的问题”时会发生什么。两年如今已经是一段漫长的时间,你甚至还有当时使用的同样的编译器吗?当你检查所有的源码时,会发现新更新的SDK与你的源码不兼容,并带来错误时会发生什么?你会清除你的开发机并重新安装所有的依赖关系来构建这个应用程序吗?你还记得所有的依赖关系是什么吗?!最后一个观点是最重要的,为了节省几千字节的磁盘空间,你可能会花费数天甚至数周的时间(而且Murphy定律还说你需要重新构建的任何应用程序都将是你曾经很高兴摆脱的最模糊、最难设置的依赖项)。因此,将二进制文件存储在SCM中,不必担心琐事。 PS.我们将所有二进制文件放在每个项目自己的“发布”目录中,然后当我们想要更新一台机器时,我们使用一个特殊的“安装”项目,其中仅包含svn:externals。您导出安装项目并完成,它将获取正确的内容并将其放入正确的目录结构中。

很好的论述,即使它与我的直觉相反。+1 - richq
4
第三个观点主张将SDK、编译器等放入源代码管理系统(SCM),而不仅仅是构建产物。如果您无法从两年前的源代码重新构建产品,则已经无法提供给客户修复,因为您无法使用更改后的源代码重新构建产品。 - imaginaryboy
是的,但通常你只需要再次提取二进制文件 - 可能是为了测试或重新发货。一旦到达那里,如果您无法重建它们以进行修复,则仍处于与未存储二进制文件相同的位置。 - gbjbaanb
1
也许二进制文件应该只添加到tags/release/x.y/binaries中。由于标签不应更改,因此二进制文件将很好地适合其中。 - Danijel

6

Hudson这样的持续集成服务器可以存档构建产物。虽然这并不能帮助你解决“为什么不”的问题,但至少它是一种替代方案。


+1 有趣。这可能比将二进制文件放在标签中更好,这是我在自己的答案中建议的解决方案。 - Wim Coenen

5

我确定有很多有力的反对意见针对这种不良做法。

你错误地认为将“build artifacts”提交到版本控制是一个不好的主意(除非你错误地阐述了你的问题)。事实并非如此。

在版本控制中保存你所谓的“build artifacts”是可以的,而且非常重要。你还应该保存编译器和其他用于将源文件转换为成品的工具。

从现在起五年后,你肯定会使用不同的编译器和不同的构建环境,这些环境可能无法编译今天的版本,出于任何原因。对一个旧版本进行修复一个小bug可能会变成噩梦,需要将旧软件移植到当前编译器和构建工具中,只是为了重新编译一个有一行更改的源文件。

因此,你没有理由害怕将“build artifacts”存储在版本控制中。你可以将它们分开存储。

我建议将它们分开,例如:

 ProjectName
 |--- /trunk
 |    |--- /build
 |    |    |--- /bin        <-- compilers go here
 |    |    |--- /lib        <-- libraries (*.dll, *.jar) go here
 |    |    '--- /object     <-- object files (*.class, *.jar) go here
 |    '--- /source          <-- sources (*.java) go here
 |         |--- package1    <-- sources (*.java) go here
 |         |--- package2    <-- sources (*.java) go here

你需要配置你的IDE或者构建脚本,将目标文件放在/ProjectName/trunk/build/object中(可能需要重新创建.../source下的目录结构)。
这样,你可以让用户选择检出/ProjectName/trunk以获取完整的构建环境,或者检出/ProjectName/trunk/source以获取应用程序的源代码。
在../build/bin和../build/lib中,你必须放置编译最终产品所使用的编译器和库,也就是用于向用户提供软件的工具。5年或10年后,你还可以在那里找到它们,以备不时之需。

我的意思是指从构建中产生的输出,而不是输入(无论是编译器还是第三方库)。 - richq
啊,甚至不是众所周知的有票输出,而是日常的“单个错误修复或功能”输出。 - richq
是的,保留构建的输出与输入一样重要。至少在某些里程碑上,如果您想节省一些空间。 - Juliano
我认为将构建环境以某种方式存储起来可能是个好主意,也许可以在一个单独的代码库中进行存储,因为仅存储编译器和库是不够的。想象一下,你使用了像3.3这样的旧版本gcc进行编译,并只保留了编译器。尝试在最新的Linux发行版上运行它 :-) - hochl

5
如果您知道原因,将构建工件提交到子版本库可能是个好主意。这对于发布管理目的来说是个好主意,更具体地说,它适用于:
1. 打包问题
如果一个构建工件不仅仅是exe(或dll等),而且还包括:
- 一些配置文件 - 一些用于启动/停止/重启工件的脚本 - 一些用于更新数据库的sql语句 - 一些源代码(压缩成文件)以方便调试 - 一些文档(javadoc压缩成文件)
那么将构建工件和所有相关文件存储在版本控制系统中是个好主意。(因为这不再只是“重新构建”工件的问题,还涉及到“检索”所有这些额外的文件,使该工件运行)
2. 部署问题
假设您需要在不同的环境(测试、预发布、生产)中部署多个工件。如果:
- 您生成了许多构建工件 - 这些工件相当耗时,无法从头开始重新创建
那么将这些工件存储在版本控制系统中是个好主意,以避免重新创建它们。您只需从一个环境查询到另一个环境即可。
但是您需要记住:
1. 您不能将每个构建工件都存储在版本控制系统中:所有用于持续集成目的的中间构建都不应存储在版本控制系统中(否则您将得到一个巨大的仓库,其中包含许多无用的二进制文件版本)。只有适用于预发布和生产目的的版本需要被引用。对于中间构建,您需要一个外部仓库(maven或共享目录),以便快速发布/测试这些构建。
2. 您不应该将它们存储在同一个Subversion版本库中,因为您的开发提交(修订号)比您的重要构建(那些被认为是适合预发布和生产部署的版本)更频繁。这意味着存储在第二个版本库中的工件必须具有标签(或属性)的命名约定,以便轻松检索它们所构建的开发的修订号。

4

根据我的经验,将Jars存储在SVN中可能会导致混乱。
我认为最好将Jar文件保存在像Nexus这样的Maven仓库中。
这也有优点,您可以使用像Maven或Ivy这样的依赖管理工具。


4

二进制文件,尤其是自己的二进制文件,但也包括第三方的,不应该存储在像 SVN 这样的源代码控制工具中。

理想情况下,你应该有一个构建脚本来构建自己的二进制文件(可以使用许多优秀的自动构建工具之一,这些工具可以直接从 SVN 检查源代码并进行自动化)。

对于第三方二进制文件,您需要像 Maven2 这样的依赖管理工具。然后,您可以设置一个本地 Maven 存储库来处理所有第三方二进制文件(或者只依赖公共的)。本地存储库还可以管理您自己的二进制文件。


4

把二进制文件放在代码仓库的主干或分支上绝对是累赘。除了像你提到的那样占用空间,它还会导致源代码与二进制文件之间的不一致性。当你提到修订版本1234时,你不希望疑惑这是否意味着“来自修订版本1234的源代码构建”还是“修订版本1234中的二进制文件”。避免不一致性的相同规则也适用于自动生成的代码。你不应该将可以由构建生成的内容进行版本控制。

另一方面,我更或多或少同意将二进制文件放在标签中。这样其他项目就可以通过svn:externals使用其他项目的二进制文件,而无需构建所有这些依赖项。它还使测试人员可以轻松地在不需要完整构建环境的情况下在不同的标签之间切换。

要将二进制文件放在标签中,您可以使用以下过程:

  1. 检出一个干净的工作副本
  2. 运行构建脚本并评估任何测试结果
  3. 如果构建成功,请将二进制文件 svn add
  4. 不要提交到主干或分支,直接从您的工作副本进行标记,如此操作:svn copy myWorkingCopyFolder myTagURL
  5. 丢弃工作副本以避免意外提交二进制文件到主干或分支

我们有一个tagbuild脚本来半自动化步骤3和4。


2

一个好的理由是快速在新机器上运行可执行文件。特别是如果构建环境需要花费一段时间来设置。(加载编译器、第三方库和工具等)


1

检查重要二进制文件违反了源代码/SVN的使用原则,即源代码控制中的文件应具有“差异”的有意义属性。

今天的源文件与昨天的源文件有着有意义的不同之处;差异将产生一组变化,这些变化对人类读者有意义。今天办公室前面的照片与昨天的相同位置的照片没有任何意义的差别。

因为像图像这样的东西没有差异的概念,所以你为什么要将它们存储在一个存在于记录和存储文件之间“差异”的系统中呢?

基于版本的存储是关于存储文件更改历史的。 (例如) JPEG 文件中没有有意义的更改历史。这些文件在目录中同样可以完美地进行存储。

更实际的是,将大型文件 - 构建输出文件 - 存储在 SVN 中会使检出速度变慢。滥用 SVN 作为广义二进制存储库的可能性是存在的。一开始似乎没什么问题——因为没有太多的二进制文件。当然,随着时间的推移,文件数量也会增加。我见过需要几个小时来检出的模块。

最好将大型关联的二进制文件(和输出文件)存储在一个目录结构中,并从构建过程中引用它们。

因为您仍然希望将所有正确的源代码存储在一个地方 - 而不是“获取源代码,然后从文件服务器x下载图像”,有可能这些图像早已被删除。 - gbjbaanb
图片仍然可以进行差异化处理,只是不能使用文本文件的工具 - 例如,您想要区分两个JPEG文件?将它们都检查出来并并排查看即可。很容易。哦,而且提交时的注释应该会有很大帮助。 - gbjbaanb
您需要保留与二进制文件相关的控制权。保留控制权并不要求这些文件被放置在SVN中。 - user82238
是的,JPEG文件有一个有意义的区别 - 但不是作为diff。基于版本的存储是关于存储文件更改历史记录的。在JPEG文件的数据中没有有意义的更改历史记录。这样的文件同样可以完美地存储在目录中。 - user82238

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接