Scala conditional compilation

前端 未结 2 873
走了就别回头了
走了就别回头了 2021-01-28 11:12

I\'m writing a Scala program and I want it to work with two version of a big library.

This big library\'s version 2 changes the API very slightly (only

2条回答
  •  北恋
    北恋 (楼主)
    2021-01-28 11:34

    1. C++ preprocessors can be used with Java/Scala if you run cpp before javac or scalac (also there is Manifold).


    2. If you really want to have conditional compilation in Scala you can use macro annotation (expanding at compile time)

    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 (if LIB_VERSION=2)

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

    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. Alternatively you can use Scalameta for code generation (at the time before compile time)

    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)
    

    in/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

    (after sbt out/compile if 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?

提交回复
热议问题