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

2022-08-31 08:08:51

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

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

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

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

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

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

有相当多的工具可用,ant(我们正在使用的构建系统)有一个任务将维护一个内部版本号,但目前还不清楚如何使用CVS,svn或类似工具与多个并发开发人员一起管理它。

[编辑]

下面出现了几个好的和有用的部分或特定的答案,所以我将总结其中的一些。在我看来,在这方面似乎没有一个强有力的“最佳实践”,而是一系列重叠的想法。在下面,找到我的摘要和一些由此产生的问题,人们可能会尝试回答这些问题作为后续问题。[堆栈溢出的新增功能...如果我做错了,请提供意见。

  • 如果您使用的是SVN,则特定结帐的版本控制会出现。内部版本号可以利用这一点来创建标识特定签出/修订的唯一内部版本号。[出于遗留原因,我们使用的 CVS 并不能提供这种级别的洞察力......手动干预标签可以让你走到一边。

  • 如果使用 maven 作为生成系统,则支持从 SCM 生成版本号,以及用于自动生成版本的发布模块。[由于各种原因,我们不能使用maven,但这可以帮助那些可以的人。[感谢马塞洛-莫拉莱斯]]

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

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

  • 断言:内部版本编号应该在像hudson这样的持续集成系统中定义/制定。[感谢马塞洛-莫拉莱斯]我们已经采纳了这个建议,但它确实打开了发布工程问题:发布是如何发生的?一个版本中是否有多个内部版本号?来自不同版本的构建编号之间是否存在有意义的关系?

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

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

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

  • 问题:内部版本号是否应该简短而甜蜜(即,单调地增加整数),以便很容易坚持使用文件名进行存档,易于在通信中引用等...或者它应该很长,充满用户名,日期戳,机器名称等?

  • 问:请详细说明内部版本号的分配如何适应更大的自动化发布过程。是的,专家爱好者,我们知道这已经完成了,但并不是所有人都喝过kool-aid......

我真的很想把它充实成一个完整的答案,至少对于我们的cvs/ant/hudson设置的具体例子,这样有人可以基于这个问题建立一个完整的策略。我将把任何能够对此特定情况进行从肉到坚果的描述的人标记为“答案”(包括 cvs 标记方案、相关的 CI 配置项,以及将内部版本号折叠到版本中的发布过程,以便可以通过编程方式访问它)。如果你想问/回答另一个特定的配置(比如,svn/maven/巡航控制),我会从这里链接到这个问题。--贾

[编辑2009年10月23日]我接受了得票最高的答案,因为我认为这是一个合理的解决方案,而其他几个答案也包括好主意。如果有人想破解其中一些与marty-lamb's合成,我会考虑接受另一个。我对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();
  }
}

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


答案 1

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

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


答案 2

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

推荐