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


134

Java项目中系统内部版本号和版本号管理的当前最佳实践是什么?特别:

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

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

  • 如何与源存储库正确集成

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

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

有很多可用的工具,而ant(我们正在使用的构建系统)的任务将维护一个构建号,但是目前尚不清楚如何使用CVS,svn或类似的工具与多个并发开发人员一起管理该数目。 。

[编辑]

下面出现了几个很好的有用的部分或特定答案,因此我将总结其中的一些。在我看来,这并不是真正的“最佳实践”,而是一些相互重叠的想法。在下面,找到我的摘要以及随之而来的人们可能会尝试回答的一些问题。[stackoverflow的新功能...如果我做错了,请提供评论。]

  • 如果您使用的是SVN,则特定的结帐版本会随之增加。内部编号可以利用此编号来创建唯一的内部编号,以标识特定的结帐/修订。[出于遗留原因,我们使用的CVS并不能提供如此深入的了解...使用标签进行手动干预会使您有所作为。]

  • 如果将maven用作构建系统,则支持从SCM生成版本号,以及用于自动生成发行版的发行版模块。[出于多种原因,我们不能使用Maven,但这可以帮助那些可以使用Maven的人。[感谢marcelo-morales ]]

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

  • Ant和Maven(以及Hudson和Cruise Control)提供了将内部版本号获取到.properties文件或.txt / .html文件的简便方法。这个“安全”足以防止它被有意或无意地篡改吗?在构建时将其编译为“版本”类更好吗?

  • 断言:应在诸如hudson的连续集成系统中定义/制定内部版本号。[感谢marcelo-morales ]我们采纳了这个建议,但是它确实解决了发布工程问题:发布是如何发生的?版本中是否有多个内部版本号?不同发行版的内部版本号之间是否存在有意义的关系?

  • 问题:内部编号背后的目标是什么?是否用于质量检查?怎么样?是开发人员主要使用它在开发过程中消除多个版本之间的歧义,还是让质量检查人员确定最终用户获得了什么版本?如果目标是可复制性,那么从理论上讲,这就是发行版本号应提供的内容-为什么不提供?(请在下面的回答中作为一部分回答,这将有助于阐明您所做的/建议的选择...)

  • 问题:手动构建中是否存在内部版本号?这是否有问题,以至每个人都应该使用CI解决方案?

  • 问题:是否应将内部版本号签入SCM?如果目标是可靠,明确地确定特定的构建,则如何应对可能会崩溃/重新启动等的各种连续或手动构建系统。

  • 问题:内部版本号短而甜美(即,单调递增的整数),以便它易于粘贴到文件名中进行存档,易于在通信中引用等……还是应该长而又充满用户名,日期戳,机器名称等?

  • 问题:请提供有关内部编号分配如何适合较大的自动发布过程的详细信息。是的,Maven爱好者,我们知道这已经完成了,但是并不是我们所有人都已经喝了苦艾酒。

我真的很想将其充实为一个完整的答案,至少对于我们的cvs / ant / hudson设置的具体示例而言,这样,有人可以根据此问题制定出完整的策略。我将标记为“答案”的任何人都可以为此特定情况提供详尽的描述(包括cvs标记方案,相关的CI配置项以及将内部版本号折叠到版本中的发布过程,以便以编程方式进行如果您要询问/回答其他特定配置(例如svn / maven / cruise控件),我将在此处链接到该问题。--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();
  }
}

如果这值得成为Wiki讨论,请发表评论。


2
对于将来的读者,请注意,您建议的代码中的修订号是文件的,而不是存储库的全局修订。有关更多信息,请参见:subversion.apache.org/faq.html#version-value-in-source
maayank 2011年

2
我想知道是否有人使用gradle和/或时有类似的简单方法git
bnjmn 2014年

Answers:


63

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

将其扩展为包含您可能想要添加的任何信息很简单。


12
对于跨平台版本,请使用它代替上面的whoami属性:<entry key =“ builder” value =“ $ {user.name}” />
Ed Brannin 2010年

-1用于具有平台依赖性的解决方案。将所有可用蚂蚁属性放入文件的一种好方法是:`<属性名称=“ antprops.file” location =“ $ {build.temp.project.dir} /used_ant.properties” /> <echoproperties destfile =“ $ {antprops.file}“ /> <!-对文件进行排序,仅适用于ant 1.7.0及更高版本!-> <concat> <union> <sort> <tokens> <file file =” $ {antprops .file}“” /> <linetokenizer includedelims =“ true” /> </ tokens> </ sort> </ union> </ concat>`
raudi

这样之后您是否再次提交?还是在提交您的实际修订之前完成?
查尔斯伍德

6
如何为git仓库做到这一点?
TechCrunch 2014年

46

您的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();

6
+1用于使用Java已经支持的诸如此类的属性。
Ed Brannin 2010年

2
-1,用于在build.xml中而不是在单独的属性文件中拥有内部版本号
raudi

6
  • 内部版本号应与hudson等连续集成服务器关联。对不同的分支机构/团队/分布使用不同的工作。
  • 为了将版本号保留在最终版本中,我建议仅对构建系统使用maven。它将创建一个.properties文件,该文件存档到最终的.jar / .war / .whatever-ar中META-INF/maven/<project group>/<project id>/pom.properties。.properties文件将包含version属性。
  • 由于我建议使用Maven,因此我敦促您检查发布插件以准备源存储库上的发布并保持版本同步。

6

软件:

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

哈德森有3个版本/职位:连续,每晚和发布。

对于连续/夜间构建:内部版本号是SVN修订版,可使用svntask找到。

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

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

使用Hudson插件:带有版本号的SVN标签,可以轻松完成Release版本的构建后操作。

优点:

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

希望这可以帮助。


您是否具有Release build / job,将build / release number属性文件检回到SVN中?
马特b

对于连续构建,内部版本号是SVN修订版号。因此,无需检查任何内容。对于版本内部版本,是用于获取内部版本号的已检入文件。通常,发布工程师或其他任何人都希望在发布之前很好地更新此文件。
罗利

4

我也在使用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都会为我设置这些环境变量。

就我而言,该项目是一个Web应用程序,我将此build-number.txt文件包含在该Web应用程序的根文件夹中-我并不在乎谁会看到它。

当完成此操作时,我们不标记源代码控制,因为我们已经设置了Hudson作业,以在构建成功时使用构建号/时间戳对其进行标记。

我的解决方案仅涵盖用于开发的增量内部版本号,而在涉及版本号的项目中,我们还远远不够。


`env.SVN_URL_1 env.SVN_REVISION_1 env.SVN_URL_2 env.SVN_REVISION_2等:另外(在当前版本的詹金斯至少),你可以检查出的属性
raudi


3

这就是我解决的方法:

  • 源被复制到构建目录
  • 然后应用anttask“ versioninfo”
  • 编译修改后的源

这是存储版本信息的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>

2

这是我的2美分:

  • 每次我构建应用程序时,我的构建脚本都会创建一个内部版本号(带有时间戳!)。这会产生太多的数字,但永远不会太少。如果我更改了代码,内部版本号将至少更改一次。

  • 我在每个发行版中都对内部版本号进行了版本化(尽管不是介于两者之间)。当我更新项目并获得新的内部版本号(因为其他人发布了版本)时,我覆盖了本地版本并重新开始。这可能导致内部版本号降低,这就是为什么我添加了时间戳记的原因。

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

[编辑]我在项目中部署了一个version.html,然后,我可以使用刮板简单地收集准确的地图,该地图安装在何处。如果您使用的是Tomcat或类似的产品,则将内部版本号和时间戳记放在web.xmldescription元素中。切记:当您可以拥有一台计算机为您服务时,切记不要记住任何事情。


是的,这也是我们所拥有的。当您必须记住在这里或那里已部署了哪个难以置信的长时间构建时,使用起来很痛苦。太多的数字也是
一件

@Varkhan:为什么?只需将版本放在HTML
抓取工具

1

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

然后,我们使用Ant和BuildNumber递增版本号,并使用此信息以及构建日期和其他元数据创建属性文件。

我们有一堂课专门致力于阅读并提供给GUI /日志等。

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


提示:如果您正在使用CC和Ant的BuildNumber,则可以使用PropertyFileLabelIncrementer使CC标签与内部版本号保持同步。
杰弗里·弗雷德里克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.