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

Java项目中系统构build编号和版本号pipe理的最佳实践是什么? 特别:

  • 如何在分布式开发环境中系统地pipe理版本号

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

  • 如何正确整合源代码库

  • 如何更自动地pipe理版本号与版本库标签

  • 如何与持续build设基础设施整合

有相当多的工具可用,ant(我们正在使用的构build系统)有一个任务将维护内部版本号,但是不清楚如何使用CVS,svn或类似的并发开发人员来pipe理这个工作。

[编辑]

下面出现了一些好的和有帮助的部分或特定的答案,所以我将总结其中的一些。 这听起来似乎没有一个强有力的“最佳实践”,而是一堆重叠的想法。 下面,find我的总结和一些人们可能试图回答作为后续问题的结果问题。 [新来stackoverflow …请提供意见,如果我做错了。]

  • 如果您使用的是SVN,那么特定的结帐版本会随之而来。 内部编号可以利用这个来创build一个唯一的内部编号来标识特定的检出/修改。 [CVS,我们正在使用的遗留原因,并没有提供相当的这种水平的洞察力…手动干预与标签让你分开的方式。]

  • 如果您使用maven作为您的构build系统,则支持从SCM生成版本号以及用于自动生成版本的发布模块。 [我们不能使用maven,由于各种原因,但这有助于那些谁可以。 [感谢marcelo-morales ]]

  • 如果您使用ant作为构build系统,以下任务描述可以帮助您生成一个捕获构build信息的Java .properties文件,然后可以通过多种方式将其折叠到您的构build中。 [我们扩大了这个想法,包括哈德森派生的信息,感谢烈士羊肉 ]。

  • Ant和maven(以及hudson和巡航控件)为将内部版本号获取到.properties文件或者在.txt / .html文件中提供了简单的方法。 这是否“安全”足以防止有意或无意地被篡改? 在编译时将它编译成“版本控制”类更好吗?

  • 声明:构build编号应该在像哈德森这样的持续集成系统中定义/制定。 [感谢marcelo-morales ]我们已经采取了这个build议,但它确实打开了发布工程的问题:发布如何发生? 发行版中是否有多个buildnumbers? 不同版本的buildnumbers之间是否有意义的关系?

  • 问题:内部编号背后的目标是什么? 是否用于质量检查? 怎么样? 主要是由开发人员在开发过程中为多个构build之间消除歧义,或者更多地使用QA来确定最终用户构build的是什么? 如果目标是可重复的,理论上这是一个发行版本号应该提供的 – 为什么不呢? (请回答下面的答案,这将有助于说明你所做的/build议的select…)

  • 问题:在手动构build中是否有构build数字的地方? 这是否有问题,每个人都应该使用CI解决scheme?

  • 问题:应该build立数字签入SCM? 如果目标是可靠和明确地识别特定的构build,如何应对各种连续或手动构build系统,可能会崩溃/重新启动/等…

  • 问题:一个内部版本号应该简短而又甜美(即单调递增的整数),以便易于使用文件名进行存档,易于在通信中引用等等,或者是否应该是很长且充满用户名的,date,机器名称等?

  • 问题:请提供有关如何将内部版本号分配到较大的自动发布stream程中的详细信息。 是的,maven爱好者,我们知道这已经完成了,但是我们并不是所有人都已经喝醉了…

我真的希望把这个完整的回答,至less对于我们的cvs / ant / hudson设置的具体例子来说,有人可以根据这个问题来构build一个完整的策略。 我将标记为“答案”的任何人可以给这个特定情况(包括cvs标签scheme,相关的CIconfiguration项目,以及将版本号码折叠到发行版中的发行过程,从而以编程方式)如果你想问/为另一个特定的configuration(例如,svn / maven / cruise control)回答,我会链接到这里的问题。 –JA

[编辑09年10月23日]我接受了最高票的答案,因为我认为这是一个合理的解决scheme,而其他几个答案也包括好点子。 如果有人想要用烈士羊肉来合成其中的一些,我会考虑接受一个不同的。 我与烈士 – 羔羊的唯一关系是,它不会产生可靠的连续编号 – 它取决于build造者系统中的本地时钟,以提供明确的编号,这并不是很好。

[编辑Jul 10]

我们现在包括一个像下面这样的类。 这允许版本号被编译到最终的可执行文件中。 版本信息的不同forms在日志数据,长期归档输出产品中发出,并用于追踪我们(有时在几年后)对输出产品的分析到特定版本。

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(); } } 

留下评论,如果这应该成为一个维基讨论。

对于我的几个项目,我捕获了Subversion版本号,时间,运行构build的用户以及一些系统信息,将它们填充到包含在应用程序jar中的.properties文件中,并在运行时读取该jar。

ant代码如下所示:

 <!-- 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> 

包括任何你想要添加的信息都很简单。

你的build.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> ... 

你的java代码

 String ver = MyClass.class.getPackage().getImplementationVersion(); 
  • 内部版本号应该与像哈德森这样的持续集成服务器相关联。 为不同的分支/团队/分布使用不同的工作。
  • 为了保持最终版本中的版本号,我build议只使用maven来构build系统。 它将创build一个.properties文件,存档到META-INF/maven/<project group>/<project id>/pom.properties文件的最终.jar / .war / .whatever-ar META-INF/maven/<project group>/<project id>/pom.properties 。 .properties文件将包含版本属性。
  • 由于我推荐的maven,我会敦促你检查发布插件准备在源代码库的版本,并保持版本同步。

软件:

哈德森有三个build设/工作:连续,夜间和释放。

对于连续/夜间构build:内部版本号是使用svntaskfind的SVN版本。

对于版本构build/作业:版本号是Ant从属性文件中读取的版本号。 属性文件也可以与发行版一起分发,以便在运行时显示内部版本号。

Ant构build脚本将构build编号放入构build期间创build的jar / war文件的清单文件中。 适用于所有版本。

使用Hudson插件轻松完成发布版本的构build后操作:使用内部版本号标记SVN。

优点:

  • 对于jar / war的开发版本,开发人员可以从jar / war中findSVN修订版,并在SVN中查找相应的代码
  • 对于发布版本,SVN版本是与SVN标签相对应的版本号。

希望这可以帮助。

我也使用哈德森,虽然更简单的情况:

我的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每当我的作业运行时为我设置这些环境variables。

在我的情况下,这个项目是一个build-number.txt应用程序,我将这个build-number.txt文件包含在webapp的根文件夹中 – 我不在乎谁看见它。

当完成这个工作时,我们不会标记源代码pipe理,因为当构build成功时,我们已经将Hudson作业设置为使用构build编号/时间戳进行标记。

我的解决scheme只涵盖了增量版本的开发数量,我们还没有得到足够的项目,我们正在涵盖发行版本号。

您也可以在http://code.google.com/p/codebistro/wiki/BuildNumber中的一个jar中查看BuildNumber Maven插件和Ant任务。 我试图使它简单明了。 这是一个非常小的jar文件,只取决于安装的命令行Subversion。

这是我的2美分:

  • 每次构build应用程序时,我的构build脚本都会创build构build编号(包含时间戳!)。 这会造成太多的数字,但不会太less。 如果我在代码中有更改,内部版本号将至less更改一次。

  • 我版本的版本号与每个版本(虽然不介于)之间。 当我更新项目,并得到一个新的版本号(因为别人做了一个版本),我覆盖我的本地版本,并重新开始。 这可能会导致更低的内部版本号,这就是为什么包含时间戳的原因。

  • 发行版本时,内部版本号将作为单个提交中的最后一项提交,并带有消息“build 1547”。 之后,当它正式发布时,整棵树被标记。 这样,构build文件始终具有所有标记,并且标记和内部版本号之间存在简单的1:1映射。

[编辑]我用我的项目部署一个version.html,然后,我可以使用一个刮板来简单地收集一个准确的地图什么是安装在哪里。 如果您使用的是Tomcat或类似的工具,请将内部版本号和时间戳放入web.xml的description元素中。 记住:当你有一台电脑为你做的时候,千万不要记住任何东西。

这是我如何解决这个问题:

  • 源被复制到构build目录
  • 那么应用“antinfo”版本信息
  • 编译修改的源代码

这里是存储版本信息的java文件:

 public class Settings { public static final String VERSION = "$VERSION$"; public static final String DATE = "$DATE$"; } 

这里是anttask“versioninfo”:

  <!-- ================================= 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> 

我们通过CruiseControl运行我们的构build(在这里插入你最喜欢的构buildpipe理器),并执行主构build和testing。

然后,我们使用Ant和BuildNumber来增加版本号,并用这个信息创build一个属性文件加上构builddate和其他元数据。

我们有一个专门阅读这个课程,并提供给GUI /日志等。

然后,我们将所有这些打包起来,并构build一个可部署的捆绑版本号和相应的版本。 我们所有的服务器在启动时转储这个元信息。 我们可以通过CruiseControl日志返回,并将内部版本号与date和签入绑定。