Java 项目的构建和版本编号(ant,cvs,hudson)

Java 项目中系统构建编号和版本号管理的当前最佳实践是什么? 具体来说:

  • 如何在分布式开发环境中系统地管理构建编号

  • 如何在运行时应用程序的源代码/可用版本号中维护版本号

  • 如何正确地与源代码库集成

  • 如何更自动地管理版本号与存储库标记

  • 如何与持续构建基础设施集成

有相当多的工具可用,而 ant (我们正在使用的构建系统)有一个维护构建编号的任务,但是不清楚如何使用 CVS、 svn 或类似的多个并发开发人员来管理这个任务。

[编辑]

下面出现了一些好的、有帮助的部分或具体的答案,所以我将总结其中的一些。在我看来,在这个问题上并没有一个强有力的“最佳实践”,而是一系列重叠的想法。下面是我的总结和一些结果问题,大家可能会尝试作为后续回答。[ stackoverflow 新手... ... 如果我做错了,请提供评论。]

  • 如果您正在使用 SVN,那么特定签出的版本控制将随之而来。构建编号可以利用这一点来创建一个唯一的构建编号,用于标识特定的签出/修订。[我们使用 CVS 是出于传统的原因,它没有提供这种水平的洞察力... ... 使用标签进行手动干预可以让您在一定程度上达到目的。]

  • 如果您使用 maven 作为构建系统,则支持从 SCM 生成版本号,以及自动生成版本的发布模块。[由于各种原因,我们不能使用 maven,但这对那些能够使用它的人有所帮助。[感谢 Marcelo-Morales]

  • 如果使用 蚂蚁作为构建系统,以下任务描述可以帮助生成 Java。属性文件捕获生成信息,然后可以通过多种方式将这些信息折叠到生成中。[感谢 Marty-Lamb,我们将这个想法扩展到包括来自哈德逊的信息]。

  • Ant 和 maven (以及 Hudson 和巡航控制)提供了将构建数字放入。属性文件,或转换为。Txt/.Html 文件。这种“安全”是否足以防止它被有意或无意地篡改?在构建时将其编译成“版本控制”类是否更好?

  • 断言: 构建编号应该在像 Hudson这样的持续集成系统中定义/实施。[感谢 Marcelo-Morales]我们接受了这个建议,但它确实打开了发布工程学的问题: 一个发布是如何发生的?在一个版本中是否有多个构建编号?不同版本的构建编号之间是否存在有意义的关系?

  • 问: 构建编号背后的目标是什么?是否用于质量保证?怎么做到的?它主要被开发人员用来在开发过程中消除多个构建之间的歧义,还是更多地被 QA 用来确定最终用户得到了什么构建?如果目标是可重复性,那么在理论上,这就是版本号应该提供的——为什么没有呢?(请在下面的回答中回答这个问题,这将有助于阐明你所做的选择或建议... ...)

  • 问: 在手动构建中是否有编号的位置?这个问题是否如此严重,以至于每个人都应该使用 CI 解决方案?

  • 问: 构建编号是否应该签入到 SCM?如果目标是可靠且明确地识别特定的构建,那么如何处理各种可能会崩溃/重启的连续或手动构建系统。.

  • 问题: 一个构建编号是否应该简短(例如,单调递增的整数) ,以便容易粘贴到文件名中以便存档,容易在通信中引用,等等。.或者它应该是长和充分的用户名,日期戳,机器名称等?

  • 问题: 请提供关于如何将构建编号分配到更大的自动化发布过程中的详细信息。是的,玛文爱好者们,我们知道这已经是板上钉钉的事了,但是并不是我们所有人都已经喝下了酷爱饮料..。

我真的很想把这个问题充实成一个完整的答案,至少对于我们的 cvs/ant/Hudson 设置的具体例子来说是这样的,这样就有人可以根据这个问题构建一个完整的策略。对于这种特殊情况,任何能够给出详尽描述的人(包括 cvs 标记方案、相关的 CI 配置项,以及将构建号折叠到版本中以便通过编程可访问的发布过程) ,我都会标记为“答案”如果您想询问/回答另一个特定的配置(比如 svn/maven/巡航控制) ,我将从这里链接到这个问题。—— JA

[2009年10月23日编辑] 我接受了最受欢迎的答案,因为我认为这是一个合理的解决方案,而其他几个答案也包括好的想法。如果有人想尝试用 Marty-Lamb合成一些,我会考虑接受一个不同的。我对 Marty-Lamb 的唯一担心是它不能产生可靠的序列化构建编号——它依赖于构建器系统中的本地时钟来提供明确的构建编号,这并不好。

[编辑7月10日]

我们现在包含一个类,如下所示。这允许将版本号编译到最终的可执行文件中。不同形式的版本信息在日志数据、长期存档的输出产品中发出,并用于跟踪我们(有时是多年后)对输出产品的分析到特定的构建。

public final class AppVersion
{
// SVN should fill this out with the latest tag when it's checked out.


private static final String APP_SVNURL_RAW =
"$HeadURL: svn+ssh://user@host/svnroot/app/trunk/src/AppVersion.java $";
private static final String APP_SVN_REVISION_RAW = "$Revision: 325 $";


private static final Pattern SVNBRANCH_PAT =
Pattern.compile("(branches|trunk|releases)\\/([\\w\\.\\-]+)\\/.*");
private static final String APP_SVNTAIL =
APP_SVNURL_RAW.replaceFirst(".*\\/svnroot\\/app\\/", "");


private static final String APP_BRANCHTAG;
private static final String APP_BRANCHTAG_NAME;
private static final String APP_SVNREVISION =
APP_SVN_REVISION_RAW.replaceAll("\\$Revision:\\s*","").replaceAll("\\s*\\$", "");




static {
Matcher m = SVNBRANCH_PAT.matcher(APP_SVNTAIL);
if (!m.matches()) {
APP_BRANCHTAG = "[Broken SVN Info]";
APP_BRANCHTAG_NAME = "[Broken SVN Info]";
} else {
APP_BRANCHTAG = m.group(1);
if (APP_BRANCHTAG.equals("trunk")) {
// this isn't necessary in this SO example, but it
// is since we don't call it trunk in the real case
APP_BRANCHTAG_NAME = "trunk";
} else {
APP_BRANCHTAG_NAME = m.group(2);
}
}
}


public static String tagOrBranchName()
{ return APP_BRANCHTAG_NAME; }


/** Answers a formatter String descriptor for the app version.
* @return version string */
public static String longStringVersion()
{ return "app "+tagOrBranchName()+" ("+
tagOrBranchName()+", svn revision="+svnRevision()+")"; }


public static String shortStringVersion()
{ return tagOrBranchName(); }


public static String svnVersion()
{ return APP_SVNURL_RAW; }


public static String svnRevision()
{ return APP_SVNREVISION; }


public static String svnBranchId()
{ return APP_BRANCHTAG + "/" + APP_BRANCHTAG_NAME; }


public static final String banner()
{
StringBuilder sb = new StringBuilder();
sb.append("\n----------------------------------------------------------------");
sb.append("\nApplication -- ");
sb.append(longStringVersion());
sb.append("\n----------------------------------------------------------------\n");
return sb.toString();
}
}

如果这值得成为一个维基讨论,请留言。

93924 次浏览

我们通过 CruiseControl 运行构建(在这里插入您最喜欢的构建管理器) ,并执行主构建和测试。

然后,我们使用 Ant 和 楼宇编号增加版本号,并创建一个包含此信息以及构建日期和其他元数据的属性文件。

我们有一个类专门用于读取这些信息并将其提供给 GUI/日志等。

然后,我们将所有这些打包,并构建一个可部署的构建号和相应的构建。我们所有的服务器在启动时都会转储这个元信息。我们可以回过头来查看 CruiseControl 日志,并将构建号与日期和签入联系起来。

这是我的建议:

  • 我的构建脚本创建了一个构建编号(带有时间戳!)每次我开发这个应用的时候。这会产生太多的数字,但绝不会太少。如果我在代码中发生更改,那么构建编号将至少更改一次。

  • 我的版本号与每个版本(虽然不是在中间)。当我更新项目并得到一个新的构建编号(因为其他人发布了一个版本)时,我会覆盖我的本地版本并重新开始。这可能导致较低的构建编号,这就是为什么我包含了时间戳。

  • 当发布发生时,构建编号作为单个提交中的最后一项提交,并带有消息“ build1547”。之后,当它正式发布时,整棵树都会被标记。这样,构建文件总是包含所有标记,并且在标记和构建编号之间存在一个简单的1:1映射。

[编辑]我在我的项目中部署了一个 version. html,然后,我可以使用一个 scraper 来简单地收集一个精确的地图,这个地图安装在哪里。如果您正在使用 Tomcat 或类似的工具,请将构建编号和时间戳放在 Xmldescription元素中。记住: 当你可以让电脑帮你记忆的时候,千万不要记忆任何东西。

  • 构建编号应该与像 Hudson这样的持续集成服务器相关联。对不同的分支/团队/发行版使用不同的作业。
  • 为了在最终构建中保留版本号,我建议只对构建系统使用 Maven。它将创建一个。归档到最终的属性文件。Jar/.战争/。META-INF/maven/<project group>/<project id>/pom.properties台的随便什么节目。那个。属性文件将包含版本属性。
  • 因为我推荐的是 maven,所以我建议您检查一下 发布插件,以便在源代码库上准备版本,并保持版本同步。

软件:

  • SVN
  • 蚂蚁
  • 哈德森,为了持续整合
  • Svntask,一个用于查找 SVN 修订的 Ant 任务: http://code.google.com/p/svntask/

Hudson 有三个构建/作业: 持续、夜间和发布。

对于持续/夜间构建: 构建编号是 SVN 修订版,使用 svntask 找到。

对于发布版本/作业: 构建编号是 Ant 从 Properties 文件中读取的发布编号。还可以将属性文件与用于在运行时显示构建编号的版本一起分发。

Ant 构建脚本将构建编号放在构建过程中创建的 jar/war 文件的清单文件中。适用于所有构建。

针对发布版本的构建后操作,使用 Hudson 插件轻松完成: 使用构建编号标记 SVN。

好处:

  • 对于 jar/war 的开发版本,开发人员可以从 jar/war 中找到 SVN 修订版,并在 SVN 中查找相应的代码
  • 对于发行版,SVN 修订版是与包含发行版编号的 SVN 标记对应的版本。

希望这个能帮上忙。

对于我的几个项目,我捕获了 subversion 修订号、时间、运行构建的用户以及一些系统信息,并将它们填充到。属性文件,并在运行时读取该 jar。

蚂蚁代码是这样的:

<!-- software revision number -->
<property name="version" value="1.23"/>


<target name="buildinfo">
<tstamp>
<format property="builtat" pattern="MM/dd/yyyy hh:mm aa" timezone="America/New_York"/>
</tstamp>
<exec executable="svnversion" outputproperty="svnversion"/>
<exec executable="whoami" outputproperty="whoami"/>
<exec executable="uname" outputproperty="buildsystem"><arg value="-a"/></exec>


<propertyfile file="path/to/project.properties"
comment="This file is automatically generated - DO NOT EDIT">
<entry key="buildtime" value="${builtat}"/>
<entry key="build" value="${svnversion}"/>
<entry key="builder" value="${whoami}"/>
<entry key="version" value="${version}"/>
<entry key="system" value="${buildsystem}"/>
</propertyfile>
</target>

扩展它以包含您可能想要添加的任何信息都很简单。

我也在使用 Hudson,不过情况要简单得多:

我的 Ant 脚本中有一个目标,看起来像:

<target name="build-number">
<property environment="env" />
<echo append="false" file="${build.dir}/build-number.txt">Build: ${env.BUILD_TAG}, Id: ${env.BUILD_ID}, URL: ${env.HUDSON_URL}</echo>
</target>

每当我的作业运行时,Hudson 都会为我设置这些环境变量。

在我的情况下,这个项目是一个网络应用程序,我包括这个 build-number.txt文件在根文件夹的网络应用程序-我真的不在乎谁看到它。

在完成这项工作时,我们不会对源代码管理进行标记,因为我们已经设置了 Hudson 作业,以便在构建成功时用构建号/时间戳对其进行标记。

我的解决方案只涵盖了用于开发的增量构建编号,我们还没有在项目中进行足够深入的讨论,因为我们还没有涉及到发布编号。

你的 Xml

...
<property name="version" value="1.0"/>
...
<target name="jar" depends="compile">
<buildnumber file="build.num"/>
<manifest file="MANIFEST.MF">
...
<attribute name="Main-Class" value="MyClass"/>
<attribute name="Implementation-Version" value="${version}.${build.number}"/>
...
</manifest>
</target>
...

Your java code

String ver = MyClass.class.getPackage().getImplementationVersion();

您可能还想在 http://code.google.com/p/codebistro/wiki/BuildNumber的一个 jar 中查看 BuildNumber Maven 插件和 Ant 任务。我尽量简单明了。它是一个非常小的 jar 文件,只依赖于安装的命令行 Subversion。

我就是这么解决的:

  • 将源复制到生成目录
  • 然后应用 anttask“ versioninfo”
  • 编译修改后的源代码

下面是存储版本信息的 java 文件:

public class Settings {


public static final String VERSION = "$VERSION$";
public static final String DATE = "$DATE$";


}

这里是反任务“ version-info”:

    <!-- =================================
target: versioninfo
================================= -->
<target name="versioninfo"
depends="init"
description="gets version info from svn"
>


<!--
get svn info from the src folder
-->
<typedef resource="org/tigris/subversion/svnant/svnantlib.xml"
classpathref="ant.classpath"
/>
<svnSetting id="svn.setting"
javahl="false"
svnkit="true"
dateformatter="dd.MM.yyyy"
/>
<svn refid="svn.setting">
<info target="src" />
</svn>


<!--
if repository is a taged version use "v <tagname>"
else "rev <revisionnumber> (SVN)" as versionnumber
-->
<taskdef resource="net/sf/antcontrib/antcontrib.properties"
classpathref="ant.classpath"
/>
<propertyregex property="version"
input="${svn.info.url}"
regexp=".*/tags/(.*)/${ant.project.name}/src"
select="v \1"
defaultvalue="rev ${svn.info.lastRev} (SVN)"
override="true"
/>




<!--
replace date and version in the versionfile ()
-->
<replace file="build/${versionfile}">
<replacefilter token="$DATE$" value="${svn.info.lastDate}" />
<replacefilter token="$VERSION$" value="${version}" />
</replace>


</target>