如何在一个库中支持多个 Scala 版本

Posted

技术标签:

【中文标题】如何在一个库中支持多个 Scala 版本【英文标题】:How to support multiple Scala versions in a library 【发布时间】:2012-12-02 01:46:00 【问题描述】:

我有一个相当普通的Scala project 目前正在使用 Maven 构建。我想同时支持 Scala 2.9.x 和即将推出的 2.10,它们不兼容二进制或源代码。如有必要,我愿意接受转换为 SBT,但我遇到了一些挑战。

我对这个项目的要求是:

单一源树(无分支)。我相信尝试为每个 Scala 版本支持多个并发“主”分支将是错过分支之间错误修复的最快方法。

版本特定的源目录。由于 Scala 版本不兼容源代码,因此我需要能够为特定版本的源代码指定辅助源目录。

版本特定的源 jar。最终用户应该能够为他们的 Scala 版本下载正确的源 jar,以及正确的版本特定源。

集成部署。我目前使用 Maven 发布插件将新版本部署到 Sonatype OSS 存储库,并希望有一个类似的简单发布工作流。

最终用户 Maven 支持。我的最终用户通常是 Maven 用户,因此准确反映依赖关系的功能性 POM 至关重要。

阴影 jar 支持。我需要能够生成一个 JAR,其中包含我的依赖项的子集并从已发布的 POM 中删除阴影依赖项。

我尝试过的事情:

Maven 配置文件。我创建了一组 Maven 配置文件来控制用于构建的 Scala 版本,使用 Maven build-helper 插件来选择版本特定的源代码树。在发布之前一直运行良好;

使用分类器来限定版本效果不好,因为源 jar 还需要自定义分类器('source-2.9.2' 等),并且大多数 IDE 工具不知道如何定位他们。

我尝试使用 Maven 属性将 SBT 样式的 _$scala.version 后缀添加到工件名称,但 Maven 不喜欢工件名称中的属性。

SBT。一旦你可以理解它,它就会很好地工作(尽管有大量文档,但这不是一项小任务)。缺点是似乎没有与 Maven 阴影插件等效的插件。我看过:

Proguard。该插件未针对 SBT 0.12.x 进行更新,并且不会从源代码构建,因为它依赖于另一个已更改 groupIds 的 SBT 插件,并且在旧名称下没有 0.12.x 版本。我还没有弄清楚如何指示 SBT 忽略/替换插件依赖项。

一个罐子。这使用自定义类加载从嵌入式 jar 中运行 Main 类,这不是预期的结果;我希望我的项目的类文件与我的阴影依赖项中的(可能重命名的)类文件一起放在 jar 中。

SBT 程序集插件。这可以在一定程度上起作用,但 POM 文件似乎包含我试图遮蔽的依赖项,这对我的最终用户没有帮助。

我接受可能没有解决方案可以满足我对 Scala 的要求,和/或我可能需要编写自己的 Maven 或 Scala 插件来实现目标。但如果可以的话,我想找到一个现有的解决方案。

更新

我已经接近接受@Jon-Ander 的优秀answer,但我还有一个优秀的作品,那就是统一的发布流程。我的build.sbt is on GitHub 的当前状态。 (我将在稍后的答案中复制它以供后代使用)。

sbt-release 插件不支持多版本构建(即,+ release 的行为不像人们所希望的那样),这是有道理的,因为发布标记的过程并不真正需要跨版本。但我希望流程的两个部分是多版本的:测试和发布。

我希望发生的事情类似于两阶段 maven-release-plugin 过程。第一阶段将执行更新 Git 和运行测试的管理工作,在这种情况下,这意味着运行 + test 以便测试所有版本、标记、更新到快照,然后将结果推送到上游。

第二阶段将检查标记版本和+ publish,这将重新运行测试并将标记版本推送到 Sonatype 存储库。

我怀疑我可以编写releaseProcess 值来执行这些操作,但我不确定我是否可以在build.sbt 中支持多个releaseProcess 值。它可能可以与一些额外的范围一起使用,但是 SBT 的那部分对我来说仍然是奇怪的。

我目前所做的是将releaseProcess 更改为不发布。然后我必须手动检查标记的版本并在事后运行+ publish,这接近我想要的但确实妥协,特别是因为测试仅在发布过程中在当前的 scala 版本上运行。我可以接受一个不像 maven 插件那样的两阶段流程,但确实实现了多版本测试和发布。

任何可以帮助我完成最后一英里的额外反馈将不胜感激。

【问题讨论】:

我质疑你的前两个要点。版本特定的源目录是一种手动维护(因此容易出错)内部分支的形式。我的建议是:不要那样做,而是使用 VCS 来简化分支和合并(git 是显而易见的,但不是唯一的选择)。然后你会发现谜题的其余部分变得更容易解决。 我同意使用正确的 VCS 可以使分支和合并的过程更容易,但并不能消除人们忘记切换分支以测试更改的人为问题。我可以设想一个为我执行此操作的构建脚本,如有必要,我会将其视为一个选项。不过,抽象地说,我喜欢为不同的功能状态保留分支,而不是为了平台支持。 这是“必要的”,因为它是迄今为止最简单、最可靠的处理方式,并且只需要一点设置时间(如果您使用的是 SVN 和需要切换到 Git 或类似的东西,但仍然值得)。 它最初只需要一点设置,但它对开发有持续的影响。 每个更改必须首先在一个分支中实现,在那里测试,合并到另一个分支,再次测试,合并回来,回归等等。在抽象的情况下,这种方法是好的和纯粹的,但在白天 -今天的发展过程是昂贵的。我并不是说这种方法最终不会“更好”,尽管有税,但现在测试单个提交的所有变体的单命令构建具有更大的吸引力。 我为 shapeless 所做的方法是修复主线上的错误,然后合并回 2.9.x 分支。因为差异只是由于 Scala 版本控制,所以合并通常是微不足道的。在从分支发布之前,可以很容易地自动化测试是否已完成合并。 【参考方案1】:

在单个源代码树中的 sbt 中很好地支持了大部分内容

通常不需要版本特定的源目录。 Scala 程序往往是源代码兼容的——事实上经常如此 crossbuilding (http://www.scala-sbt.org/release/docs/Detailed-Topics/Cross-Build) 在 sbt 中拥有一流的支持。

如果您确实需要特定于版本的代码,您可以添加额外的源文件夹。 把它放在你的 build.sbt 文件中,除了常规的“src/main/scala”之外,当你交叉构建时,将添加“src/main/scala-[scalaVersion]”作为每个版本的源目录。 (还有一个插件可用于在版本之间生成垫片,但我没有尝试过 - https://github.com/sbt/sbt-scalashim)

unmanagedSourceDirectories in Compile <+= (sourceDirectory in Compile, scalaVersion) (s,v) => s / ("scala-"+v) 

版本特定的源 jars - 见交叉构建,开箱即用

集成部署 - https://github.com/sbt/sbt-release(也有很棒的 git 集成)

Maven 最终用户 - http://www.scala-sbt.org/release/docs/Detailed-Topics/Publishing.html

阴影 - 我已经使用了这个https://github.com/sbt/sbt-assembly,它可以很好地满足我的需求。 你的组装插件问题可以通过重写生成的 pom.xml 来解决。 这是一个删除 joda-time 的示例。

pomPostProcess := 
    import xml.transform._
    new RuleTransformer(new RewriteRule
        override def transform(node:xml.Node) = 
            if((node \ "groupId").text == "joda-time") xml.NodeSeq.Empty else node
        
    )

完整的 build.sbt 供参考

scalaVersion := "2.9.2"

crossScalaVersions := Seq("2.9.2", "2.10.0-RC5")

unmanagedSourceDirectories in Compile <+= (sourceDirectory in Compile, scalaVersion) (s,v) => s / ("scala-"+v) 

libraryDependencies += "joda-time" % "joda-time" % "1.6.2"

libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m"

pomPostProcess := 
    import xml.transform._
    new RuleTransformer(new RewriteRule
        override def transform(node:xml.Node) = 
            if((node \ "groupId").text == "joda-time") xml.NodeSeq.Empty else node
        
    )

【讨论】:

这是一个很好的提示,包含我没有遇到过的资源,我将尽快实施这些建议。关于源兼容性:尽管 2.10.0 反射是“实验性的”,但 2.9 反射在此过程中被打破(scala.reflect.generic 从 2.9.1 开始被弃用,但没有提供过渡期)。 IIRC 这种情况在 2.8 -> 2.9 过渡的不同领域都是正确的,我知道 2.8 与源不兼容。这不是一个深奥的问题,它需要一流的解决方案。 对,如果您打算使用 scala 反射,那么您当然需要针对每个版本的代码。请记住,尽管您仍然可以使用常规的 java 反射来避免这个问题(并且会教您很多关于 scala 编译到的内容)【参考方案2】:

我以 SBT 为例做了类似的事情: https://github.com/seanparsons/scalaz/commit/21298eb4af80f107181bfd09eaaa51c9b56bdc28

SBT 使得所有设置都可以根据其他设置来确定,这意味着大多数其他事情应该“正常工作”。

至于 pom.xml 方面,我只能建议在 SBT 邮件列表中提问,如果您不能这样做,我会感到惊讶。

【讨论】:

【参考方案3】:

我的博文http://www.day-to-day-stuff.blogspot.nl/2013/04/fixing-code-and-binary.html 包含了一个更细粒度的附加不同源目录的解决方案示例;每个主要 S 一个。它还解释了如何创建非特定代码可以使用的特定于 scala-version 的代码。

2016-11-08 更新:Sbt 现在支持此功能:http://www.scala-sbt.org/0.13/docs/sbt-0.13-Tech-Previews.html#Cross-version+support+for+Scala+sources

【讨论】:

以上是关于如何在一个库中支持多个 Scala 版本的主要内容,如果未能解决你的问题,请参考以下文章

如何在单个存储库中维护多个 iOS 应用程序?

如何从特定项目的交叉构建的sbt多项目中删除scala版本

如何强制 Scala 使用不同的库版本?

Scala 风格:一个文件中有多个类?

如何在 Scala 中使用 Spark SQL 返回多个 JSON 对象

热点Scala 2.12将只支持Java 8