zio / zio-protoquill

Quill for Scala 3
Apache License 2.0
204 stars 48 forks source link

Protoquill transitive internal dependency breaks sbt-assembly #480

Open Nexus6 opened 1 month ago

Nexus6 commented 1 month ago

Scala Version: 3.4.2 SBT Version: 1.10.1 Protoquill Version: 4.8.5 Module: quill-sql Database: postgresql

Expected behavior

Expect to be able to use sbt-assembly to package a stand-alone executable jar file containing protoquill 4.8.5.

Actual behavior

sbt-assembly errors-out because protoquill 4.8.5 has a transitive dependency on quill-engine 4.8.4 and the two jars contain classes that are different, but have the same fully-qualified names. Two examples among many from the console output when I run "sbt assembly":

[error] Deduplicate found different file contents in the following: [error] Jar name = quill-engine_3-4.8.4.jar, jar org = io.getquill, entry target = io/getquill/context/ProtoStreamContext.class [error] Jar name = quill-sql_3-4.8.5.jar, jar org = io.getquill, entry target = io/getquill/context/ProtoStreamContext.class [error] Deduplicate found different file contents in the following: [error] Jar name = quill-engine_3-4.8.4.jar, jar org = io.getquill, entry target = io/getquill/context/ExecutionInfo.class [error] Jar name = quill-sql_3-4.8.5.jar, jar org = io.getquill, entry target = io/getquill/context/ExecutionInfo.class

Apart from the "assembly" step, the project builds and runs ("sbt run") just fine. Running a trivial query that uses protoquill 4.8.5 from the project also works w/no problems. There seem to be no compile-time or runtime issues.

Steps to reproduce the behavior

Include protoquill in your build.sbt as: "io.getquill" %% "quill-jdbc" % "4.8.5" Add the sbt-assembly plugin to your plugins.sbt:

addSbtPlugin ("com.eed3si9n" % "sbt-assembly" % "2.2.0")

then run "sbt assembly"

Workaround

N/A @getquill/maintainers

mev42 commented 6 days ago
assembly / assemblyMergeStrategy := {
  case path if path.endsWith("io/getquill/context/ExecutionInfo$.class")         => MergeStrategy.last
  case path if path.endsWith("io/getquill/util/TraceConfig.class")               => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/AstSplicing.class")            => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionInfo.class")          => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionInfo.tasty")          => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ProtoStreamContext.class")     => MergeStrategy.last
  case path if path.endsWith("io/getquill/util/TraceConfig$.class")              => MergeStrategy.last
  case path if path.endsWith("io/getquill/util/TraceConfig.tasty")               => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType.tasty")          => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType$Dynamic$.class") => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType$Unknown$.class") => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ProtoStreamContext.tasty")     => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType$.class")         => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType.class")          => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/AstSplicing.tasty")            => MergeStrategy.last
  case path if path.endsWith("io/getquill/context/ExecutionType$Static$.class")  => MergeStrategy.last
  case x =>
    val oldStrategy = (assembly / assemblyMergeStrategy).value
    oldStrategy(x)
}
Nexus6 commented 5 days ago

This might be a temporary work-around, but seems awfully fragile, because

  1. It's dependent on the classpath ordering between the protoquill and quill-engine libraries. Even if MergeStrategy.last picks the correct class most of the time, the approach would fail if the classpath was reordered (intentionally or not) in such a way that the strategy grabs the quill-engine version of the class rather than the protoquill version.
  2. If any of the enumerated classes are renamed, or if classes are added to protoquill and/or quill-engine such that there are new conflicts, users will need to manually update their assemblyMergeStrategy.
  3. Mixing together classes from these two different builds of quill seems like asking for trouble. As the sbt-assembly docs put it, "Merge conflict of *.class files indicate pathological classpath". Wouldn't it be best to fix the root problem?

The first potential fixes that occur to me are:

  1. Move all protoquill classes to their own (new) package. Maybe something like net.protoquill.* ? This would of course require that users update their .sbt files and their import statements. Not sure how big a deal that would be. It would also require updates to the protoquill user docs to reflect the new package naming.
  2. Remove the transitive dependency by packaging quill-engine code into the protoquill library and exclude classes from the former, when there's a conflict, as part of the protoquill build/release process.

I could be wrong but it seems to me that protoquill is probably dependent on classpath ordering at the moment just to work at all. If so this seems like something that needs to be fixed even if 'playing nice' with sbt-assembly isn't deemed a high priority.