【问题标题】:Scala conditional compilationScala条件编译
【发布时间】:2020-05-14 11:46:40
【问题描述】:

我正在编写一个 Scala 程序,我希望它与一个大库的两个版本一起工作。

这个大库的第 2 版对 API 进行了非常轻微的(只有一个类构造函数签名有一个额外的参数)。

// Lib v1
class APIClass(a: String, b:Integer){
...
}

// Lib v2
class APIClass(a: String, b: Integer, c: String){
...
}


// And my code extends APIClass.. And I have no #IFDEF

class MyClass() extends APIClass("x", 1){ //  <--  would be APIClass("x", 1, "y") in library v2
  ...
}

我真的不想分支我的代码。因为那时我需要维护两个分支,明天需要维护 3,4,.. 分支以进行微小的 API 更改 :(

理想情况下,我们应该在 Scala 中有一个简单的预处理器,但这个想法很久以前就被 Scala 社区拒绝了。

我真的无法理解的是:Scalameta 在这种情况下可以帮助模拟预处理器吗? IE。有条件地解析两个源文件以 - 比如说 - 在编译时已知的环境变量?

如果不是,您将如何解决这个现实生活中的问题?

【问题讨论】:

    标签: scala preprocessor scala-macros scala-compiler scalameta


    【解决方案1】:

    1.如果您在javacscalac 之前运行cpp(还有Manifold),则可以将C++ 预处理器与Java/Scala 一起使用。


    2.如果你真的想在 Scala 中进行条件编译,你可以使用macro annotation(在编译时扩展)

    macros/src/main/scala/extendsAPIClass.scala

    import scala.annotation.{StaticAnnotation, compileTimeOnly}
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    @compileTimeOnly("enable macro paradise")
    class extendsAPIClass extends StaticAnnotation {
      def macroTransform(annottees: Any*): Any = macro ExtendsAPIClassMacro.impl
    }
    
    object ExtendsAPIClassMacro {
      def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
        import c.universe._
        annottees match {
          case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail => 
            def updateParents(parents: Seq[Tree], args: Seq[Tree]) = 
              q"""${tq"APIClass"}(..$args)""" +: parents.filter { case tq"scala.AnyRef" => false; case _ => true }
    
            val parents1 = sys.env.get("LIB_VERSION") match {
              case Some("1") => updateParents(parents, Seq(q""" "x" """, q"1"))
              case Some("2") => updateParents(parents, Seq(q""" "x" """, q"1", q""" "y" """))
              case None      => parents
            }
    
            q"""
              $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents1 { $self => ..$stats }
              ..$tail
            """
        }
      }
    }
    

    core/src/main/scala/MyClass.scala(如果LIB_VERSION=2

    @extendsAPIClass
    class MyClass
    
    //Warning:scalac: {
    //  class MyClass extends APIClass("x", 1, "y") {
    //    def <init>() = {
    //      super.<init>();
    //      ()
    //    }
    //  };
    //  ()
    //}
    

    build.sbt

    ThisBuild / name := "macrosdemo"
    
    lazy val commonSettings = Seq(
      scalaVersion := "2.13.2",
      organization := "com.example",
      version := "1.0.0",
      scalacOptions ++= Seq(
        "-Ymacro-debug-lite",
        "-Ymacro-annotations",
      ),
    )
    
    lazy val macros: Project = (project in file("macros")).settings(
      commonSettings,
      libraryDependencies ++= Seq(
        scalaOrganization.value % "scala-reflect" % scalaVersion.value,
      )
    )
    
    lazy val core: Project = (project in file("core")).aggregate(macros).dependsOn(macros).settings(
      commonSettings,
      )
    )
    

    3. 或者,您可以使用Scalameta 进行代码生成(在编译时间之前)

    build.sbt

    ThisBuild / name := "scalametacodegendemo"
    
    lazy val commonSettings = Seq(
      scalaVersion := "2.13.2",
      organization := "com.example",
      version := "1.0.0",
    )
    
    lazy val common = project
      .settings(
        commonSettings,
      )
    
    lazy val in = project
      .dependsOn(common)
      .settings(
        commonSettings,
      )
    
    lazy val out = project
      .dependsOn(common)
      .settings(
        sourceGenerators in Compile += Def.task {
          Generator.gen(
            inputDir = sourceDirectory.in(in, Compile).value,
            outputDir = sourceManaged.in(Compile).value
          )
        }.taskValue,
        commonSettings,
      )
    

    project/build.sbt

    libraryDependencies += "org.scalameta" %% "scalameta" % "4.3.10"
    

    project/Generator.scala

    import sbt._
    
    object Generator {
      def gen(inputDir: File, outputDir: File): Seq[File] = {
        val finder: PathFinder = inputDir ** "*.scala"
    
        for(inputFile <- finder.get) yield {
          val inputStr = IO.read(inputFile)
          val outputFile = outputDir / inputFile.toURI.toString.stripPrefix(inputDir.toURI.toString)
          val outputStr = Transformer.transform(inputStr)
          IO.write(outputFile, outputStr)
          outputFile
        }
      }
    }
    

    project/Transformer.scala

    import scala.meta._
    
    object Transformer {
      def transform(input: String): String = {
        val (v1on, v2on) = sys.env.get("LIB_VERSION") match {
          case Some("1") => (true, false)
          case Some("2") => (false, true)
          case None      => (false, false)
        }
        var v1 = false
        var v2 = false
        input.tokenize.get.filter(_.text match {
          case "// Lib v1" =>
            v1 = true
            false
          case "// End Lib v1" =>
            v1 = false
            false
          case "// Lib v2" =>
            v2 = true
            false
          case "// End Lib v2" =>
            v2 = false
            false
          case _ => (v1on && v1) || (v2on && v2) || (!v1 && !v2)
        }).mkString("")
      }
    }
    

    common/src/main/scala/com/api/APIClass.scala

    package com.api
    
    class APIClass(a: String, b: Integer, c: String)
    

    在/src/main/scala/com/example/MyClass.scala

    package com.example
    
    import com.api.APIClass
    
    // Lib v1
    class MyClass extends APIClass("x", 1)
    // End Lib v1
    
    // Lib v2
    class MyClass extends APIClass("x", 1, "y")
    // End Lib v2
    

    out/target/scala-2.13/src_managed/main/scala/com/example/MyClass.scala

    (在sbt out/compile 之后,如果LIB_VERSION=2

    package com.example
    
    import com.api.APIClass
    
    class MyClass extends APIClass("x", 1, "y")
    

    Macro annotation to override toString of Scala function

    How to merge multiple imports in scala?

    【讨论】:

      【解决方案2】:

      我看到一些选项,但如果它们是“条件编译”则没有

      • 您可以在构建中创建 2 个模块 - 它们将有一个共享源目录,并且每个模块都有一个特定于它的代码的源目录。然后您将发布整个库的 2 个版本
      • 创建 3 个模块 - 一个带有您的库和一个抽象类/特征,它将与之通信/通过,另外 2 个带有特定于版本的特征实现

      问题是 - 如果您针对 v1 和用户提供的 v2 构建代码怎么办?还是相反?你发出了字节码,但 JVM 期待别的东西,它都崩溃了。

      几乎每次您遇到此类兼容性破坏性更改时,库要么拒绝更新,要么拒绝分叉。不是因为你不能生成 2 个版本 - 你会。问题出在下游——您的用户将如何处理这种情况。如果您正在编写应用程序,则可以提交其中之一。如果您正在编写库并且不想将用户锁定在您的选择中......您必须为每个选择发布单独的版本。

      理论上,您可以创建一个项目,其中包含 2 个模块,它们共享相同的代码并使用不同的分支,例如使用 Scala 宏或 Scalameta 的 C++ 中的 #ifdef 宏 - 但如果您想使用 IDE 或发布源代码,那将是一场灾难您的用户可以在 IDE 中使用。没有来源可看。无法跳转到定义的来源。充其量是反汇编的字节码。

      因此,从长远来看,您只需为不匹配的版本提供单独的源目录的解决方案更容易阅读、编写和维护。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-03-10
        • 1970-01-01
        • 2013-02-26
        • 2013-05-30
        • 2010-11-15
        • 2023-03-05
        • 2013-07-22
        • 1970-01-01
        相关资源
        最近更新 更多