使用 Scala.js 和 Scala(jvm) 的 ScalaPB - 存在链接错误

ScalaPB with Scala.js and Scala(jvm) - There were linking errors

我的 sbt 中有多个子项目,一个是服务器(基于游戏框架),另一个是客户端ide(scala.js),第三个是两者之间的通信protobuf (scalapb) 的形式。

现在,这是我的 build.sbt:

lazy val generalSettings = Seq(
  organization := "tld.awesomeness",
  version := "0.0.1",
  scalaVersion := "2.12.1"
)

val CrossDependencies = new
  {
    val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % "test"
    val scalactic = "org.scalactic" %% "scalactic" % "3.0.1"
    val scalaTags = "com.lihaoyi" %% "scalatags" % "0.6.2"
  }

lazy val proto = (project in file("modules/proto"))
  .settings(generalSettings: _*)
  .settings(
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    ),
    // If you need scalapb/scalapb.proto or anything from google/protobuf/*.proto
    libraryDependencies ++= Seq(
      "com.trueaccord.scalapb" %% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf",
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion,
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf"
    )
  )

lazy val play = (project in file("modules/play"))
  .enablePlugins(PlayScala)
  .settings(generalSettings: _*)
  .settings(
    name := "play",
    libraryDependencies ++= Seq(
      CrossDependencies.scalaTest,
      CrossDependencies.scalactic,
      CrossDependencies.scalaTags,
      "com.typesafe.play" %% "play-json" % "2.6.0-M1"),
    scalaJSProjects := Seq(client),
    pipelineStages in Assets := Seq(scalaJSPipeline),
    compile in Compile := ((compile in Compile) dependsOn scalaJSPipeline).value
  )
  .aggregate(slick)
  .dependsOn(slick)
  .aggregate(flyway)
  .dependsOn(flyway)
  .aggregate(proto)
  .dependsOn(proto)

lazy val client = (project in file("modules/client"))
  .enablePlugins(ScalaJSPlugin, ScalaJSWeb)
  .settings(generalSettings: _*)
  .settings(
    name := "client",
    libraryDependencies += CrossDependencies.scalaTags,
    persistLauncher := true
  )
  .aggregate(proto)
  .dependsOn(proto)

// Loads the jvm project at sbt startup
onLoad in Global := (Command.process("project play", _: State)) compose (onLoad in Global).value

fork in run := true

这是 plugins.sbt:

// Scala.JS
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14")
addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.2")

// Play
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-SNAPSHOT")

// Proto
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.3" exclude ("com.trueaccord.scalapb", "protoc-bridge_2.10"))
libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin-shaded" % "0.5.47"

这是一个 proto 文件:

syntax = "proto3";

package tld.awesomeness.proto;

message Test {
    int32 id = 1;
    string email = 2;
}

编译后得到Test.class

现在在客户端我尝试:

private def doSend(ws: WebSocket): Unit =
{
    val msg = Test().withId(1337)
    val a: ArrayBuffer = new ArrayBuffer(msg.toByteArray.length)
    msg.toByteArray
    ws.send(a)
}

(当我通过它发送字符串时,websocket 本身工作得很好!)

现在我得到了这个巨大的堆栈跟踪:

[info] Fast optimizing /home/sorona/awesomeness/modules/client/target/scala-2.12/client-fastopt.js
[error] Referring to non-existent class tld.awesomeness.proto.Test.Test$
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent class tld.awesomeness.proto.Test.Test
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent method tld.awesomeness.proto.Test.Test.toByteArray()[scala.Byte
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent method tld.awesomeness.proto.Test.Test.withId(scala.Int)tld.awesomeness.proto.Test.Test
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent method tld.awesomeness.proto.Test.Test$.apply$default()java.lang.String
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent method tld.awesomeness.proto.Test.Test$.apply$default()scala.Int
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
[error] Referring to non-existent method tld.awesomeness.proto.Test.Test.<init>(scala.Int,java.lang.String)
[error]   called from tld.awesomeness.ScalaJSTest$.doSend(org.scalajs.dom.raw.WebSocket)scala.Unit
[error]   called from tld.awesomeness.ScalaJSTest$.tld$awesomeness$ScalaJSTest$$$anonfun$call(org.scalajs.dom.raw.Event,org.scalajs.dom.raw.WebSocket)org.scalajs.dom.raw.Event
[error]   called from tld.awesomeness.ScalaJSTest$.call()scala.Unit
[error]   called from tld.awesomeness.Main$.main()scala.Unit
[error]   called from scala.scalajs.js.JSApp.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.$$js$exported$meth$main()java.lang.Object
[error]   called from tld.awesomeness.Main$.main
[error]   exported to JavaScript with @JSExport
[error] involving instantiated classes:
[error]   tld.awesomeness.ScalaJSTest$
[error]   tld.awesomeness.Main$
java.lang.RuntimeException: There were linking errors
        at scala.sys.package$.error(package.scala:27)
        at org.scalajs.core.tools.linker.frontend.BaseLinker.linkInternal(BaseLinker.scala:133)
        at org.scalajs.core.tools.linker.frontend.BaseLinker.linkInternal(BaseLinker.scala:86)
        at org.scalajs.core.tools.linker.frontend.LinkerFrontend$$anonfun.apply(LinkerFrontend.scala:54)
        at org.scalajs.core.tools.linker.frontend.LinkerFrontend$$anonfun.apply(LinkerFrontend.scala:54)
        at org.scalajs.core.tools.logging.Logger$class.time(Logger.scala:28)
        at org.scalajs.sbtplugin.Loggers$SbtLoggerWrapper.time(Loggers.scala:7)
        at org.scalajs.core.tools.linker.frontend.LinkerFrontend.link(LinkerFrontend.scala:53)
        at org.scalajs.core.tools.linker.Linker$$anonfun$link.apply$mcV$sp(Linker.scala:50)
        at org.scalajs.core.tools.linker.Linker$$anonfun$link.apply(Linker.scala:49)
        at org.scalajs.core.tools.linker.Linker$$anonfun$link.apply(Linker.scala:49)
        at org.scalajs.core.tools.linker.Linker.guard(Linker.scala:67)
        at org.scalajs.core.tools.linker.Linker.link(Linker.scala:49)
        at org.scalajs.core.tools.linker.ClearableLinker$$anonfun$link.apply(ClearableLinker.scala:51)
        at org.scalajs.core.tools.linker.ClearableLinker$$anonfun$link.apply(ClearableLinker.scala:51)
        at org.scalajs.core.tools.linker.ClearableLinker.linkerOp(ClearableLinker.scala:62)
        at org.scalajs.core.tools.linker.ClearableLinker.link(ClearableLinker.scala:51)
        at org.scalajs.sbtplugin.ScalaJSPluginInternal$$anonfun$org$scalajs$sbtplugin$ScalaJSPluginInternal$$scalaJSStageSettings$$anonfun$apply$$anonfun$apply.apply(ScalaJSPluginInternal.scala:251)
        at org.scalajs.sbtplugin.ScalaJSPluginInternal$$anonfun$org$scalajs$sbtplugin$ScalaJSPluginInternal$$scalaJSStageSettings$$anonfun$apply$$anonfun$apply.apply(ScalaJSPluginInternal.scala:239)
        at sbt.FileFunction$$anonfun$cached.apply(Tracked.scala:253)
        at sbt.FileFunction$$anonfun$cached.apply(Tracked.scala:253)
        at sbt.FileFunction$$anonfun$cached$$anonfun$apply$$anonfun$apply.apply(Tracked.scala:267)
        at sbt.FileFunction$$anonfun$cached$$anonfun$apply$$anonfun$apply.apply(Tracked.scala:263)
        at sbt.Difference.apply(Tracked.scala:224)
        at sbt.Difference.apply(Tracked.scala:206)
        at sbt.FileFunction$$anonfun$cached$$anonfun$apply.apply(Tracked.scala:263)
        at sbt.FileFunction$$anonfun$cached$$anonfun$apply.apply(Tracked.scala:262)
        at sbt.Difference.apply(Tracked.scala:224)
        at sbt.Difference.apply(Tracked.scala:200)
        at sbt.FileFunction$$anonfun$cached.apply(Tracked.scala:262)
        at sbt.FileFunction$$anonfun$cached.apply(Tracked.scala:260)
        at org.scalajs.sbtplugin.ScalaJSPluginInternal$$anonfun$org$scalajs$sbtplugin$ScalaJSPluginInternal$$scalaJSStageSettings$$anonfun$apply.apply(ScalaJSPluginInternal.scala:256)
        at org.scalajs.sbtplugin.ScalaJSPluginInternal$$anonfun$org$scalajs$sbtplugin$ScalaJSPluginInternal$$scalaJSStageSettings$$anonfun$apply.apply(ScalaJSPluginInternal.scala:237)
        at scala.Function1$$anonfun$compose.apply(Function1.scala:47)
        at sbt.$tilde$greater$$anonfun$$u2219.apply(TypeFunctions.scala:40)
        at sbt.std.Transform$$anon.work(System.scala:63)
        at sbt.Execute$$anonfun$submit$$anonfun$apply.apply(Execute.scala:228)
        at sbt.Execute$$anonfun$submit$$anonfun$apply.apply(Execute.scala:228)
        at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
        at sbt.Execute.work(Execute.scala:237)
        at sbt.Execute$$anonfun$submit.apply(Execute.scala:228)
        at sbt.Execute$$anonfun$submit.apply(Execute.scala:228)
        at sbt.ConcurrentRestrictions$$anon$$anonfun.apply(ConcurrentRestrictions.scala:159)
        at sbt.CompletionService$$anon.call(CompletionService.scala:28)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)
[error] (client/compile:fastOptJS) There were linking errors

我的 ide 找到了一切,但显然我做错了什么。我已经看过 https://github.com/thesamet/scalapbjs-test 但无济于事。

该行出现问题 val msg = Test().withId(1337)

编辑: 评论后我更改了 build.sbt

lazy val proto = (crossProject in file("modules/proto"))
  .settings(generalSettings: _*)
  .settings(
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    )).
  jvmSettings(
    libraryDependencies += "com.trueaccord.scalapb" %% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf",
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    )
  ).
  jsSettings(
    libraryDependencies ++= Seq(
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion,
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf"
    ),
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    )
  )

lazy val protoJs = proto.js
lazy val protoJVM = proto.jvm

lazy val play = (project in file("modules/play"))
  .enablePlugins(PlayScala)
  .settings(generalSettings: _*)
  .settings(
    name := "play",
    libraryDependencies ++= Seq(
      CrossDependencies.scalaTest,
      CrossDependencies.scalactic,
      CrossDependencies.scalaTags,
      "com.typesafe.play" %% "play-json" % "2.6.0-M1"),
    scalaJSProjects := Seq(client),
    pipelineStages in Assets := Seq(scalaJSPipeline),
    compile in Compile := ((compile in Compile) dependsOn scalaJSPipeline).value
  )
  .aggregate(slick)
  .dependsOn(slick)
  .aggregate(flyway)
  .dependsOn(flyway)
  .aggregate(protoJVM)
  .dependsOn(protoJVM)

lazy val client = (project in file("modules/client"))
  .enablePlugins(ScalaJSPlugin, ScalaJSWeb)
  .settings(generalSettings: _*)
  .settings(
    name := "client",
    libraryDependencies += CrossDependencies.scalaTags,
    persistLauncher := true
  )
  .aggregate(protoJs)
  .dependsOn(protoJs)

现在 playclient 都无法解析原型 class :(

(我也知道冗余 PB.targets in Compile...,我只是认为共享可能在那里不起作用,所以我再次将它添加到两个不同的设置中)

对于纯 CrossProject,您需要指定 ScalaPB 应该查找原型文件的实际路径(它猜测的值是错误的)。这是一个最小的例子:

lazy val proto = (crossProject.crossType(CrossType.Pure) in file("proto"))
  .settings(
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    ),
    // The trick is in this line:
    PB.protoSources in Compile := Seq(file("proto/src/main/protobuf")),
    libraryDependencies ++= Seq(
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion,
      "com.trueaccord.scalapb" %%% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf"
    )
  )