Skip to content

consistently include proto's in built artifacts - #746

Merged
raboof merged 1 commit into
apache:mainfrom
raboof:consistently-include-protos
Jun 25, 2026
Merged

consistently include proto's in built artifacts#746
raboof merged 1 commit into
apache:mainfrom
raboof:consistently-include-protos

Conversation

@raboof

@raboof raboof commented Jun 22, 2026

Copy link
Copy Markdown
Member

Fixes #345

I observed the directory with the proto's was part of the unmanagedResourceDirectories. That might explain why they weren't included consistently: 'unmanaged' directories are for files that are expected to 'just exist' on disk, to there's no guard to make sure sbt reads that directory after it's been populated. This seemed to be caused by some code added in https://github.com/akka/akka-grpc/pull/149/changes which is intended to cover a fringe use case that probably doesn't really exist anymore, has this problem, and makes things complicated. This PRs simplifies things and makes sure the external proto directory is part of the managed classpath - but testing with google-cloud-pub-sub-grpc it seems to actually consistently not include the resources in the jar πŸ˜† . More detective work needed.

This perhaps begs the question: are we sure we want to include the proto's in the artifacts? The generated API's should be fully usable just by the generated code, right?

@raboof
raboof force-pushed the consistently-include-protos branch 2 times, most recently from 0378395 to 597d334 Compare June 24, 2026 10:21
@He-Pin

He-Pin commented Jun 24, 2026

Copy link
Copy Markdown
Member

Code Review

πŸ”΄ Removing PB.recompile config block likely breaks proto class generation

The removed inConfig(config)(Seq(...)) block (lines 139–161) did more than manage resource directories β€” it also configured PB.recompile / sources and PB.recompile / unmanagedSources, which control which proto files are discovered for compilation. Without this explicit configuration, sbt-protoc's defaults may not discover the project's own proto files from sourceDirectory / "proto", which likely explains the observed behavior of classes not being generated with google-cloud-pub-sub-grpc.

The PB.recompile / unmanagedSources and PB.recompile / sources settings are the ones that drive protoc's input discovery. The resource-related settings (PB.recompile / resources, PB.recompile / unmanagedResources) were only part of what that block did.

Suggestion: Keep the source-related settings (PB.recompile / includeFilter, PB.recompile / unmanagedSourceDirectories, PB.recompile / sources) and only remove the resource-related ones that are being replaced by the new packageBin / mappings approach. Or verify that sbt-protoc's defaults already cover this.


🟑 withoutDuplicates has O(n²) complexity

In PekkoGrpcPlugin.scala, the tail-recursive withoutDuplicates uses soFar :+ ((file, string)) which is O(n) per append on a Seq, making the overall loop O(nΒ²). For a small number of mappings this is fine, but it's easy to make it O(n):

@scala.annotation.tailrec
def withoutDuplicates(toAdd: Seq[(File, String)], seen: Set[String], acc: List[(File, String)])
    : Seq[(File, String)] = {
  toAdd.headOption match {
    case Some((file, path)) =>
      if (seen.contains(path)) withoutDuplicates(toAdd.tail, seen, acc)
      else withoutDuplicates(toAdd.tail, seen + path, (file, path) :: acc)
    case None => acc.reverse
  }
}
withoutDuplicates(mappingsToAdd, existingMappings.map(_._2).toSet, existingMappings.toList)

🟑 Parameter name string is unclear

In withoutDuplicates, the parameter name string for the second tuple element is not descriptive. path or relativePath would better convey that it's the target path within the JAR.


🟑 Scripted test assertion is fragile

In build.sbt:

assert(!(Compile / unmanagedResourceDirectories).value.mkString.contains("target/protobuf_external_src"))
assert((Compile / managedResourceDirectories).value.mkString.contains("target/protobuf_external_src"))

Using .mkString.contains(...) on a Seq[File] is fragile β€” it could match partial path segments or produce different results depending on path separators. A more robust approach:

val unmanagedDirs = (Compile / unmanagedResourceDirectories).value
assert(!unmanagedDirs.exists(_.getPath.endsWith("protobuf_external_src")),
  s"protobuf_external_src should not be in unmanagedResourceDirectories: $unmanagedDirs")

val managedDirs = (Compile / managedResourceDirectories).value
assert(managedDirs.exists(_.getPath.endsWith("protobuf_external_src")),
  s"protobuf_external_src should be in managedResourceDirectories: $managedDirs")

This gives clearer failure messages and avoids false positives from substring matching.


Summary

The core idea β€” moving proto files from unmanaged to managed classpath and using packageBin / mappings for explicit control β€” makes sense. The main concern is that removing the entire PB.recompile configuration block removes source discovery settings alongside the resource settings, which is likely the root cause of the broken class generation.

Earlier it would depend on the task ordering between the download task
and the resource discovery task, this should make it deterministic
@raboof
raboof force-pushed the consistently-include-protos branch from 597d334 to 5383e21 Compare June 24, 2026 10:36
@raboof

raboof commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

I observed the directory with the proto's was part of the unmanagedResourceDirectories. That might explain why they weren't included consistently: 'unmanaged' directories are for files that are expected to 'just exist' on disk, to there's no guard to make sure sbt reads that directory after it's been populated. This seemed to be caused by some code added in https://github.com/akka/akka-grpc/pull/149/changes which is intended to cover a fringe use case that probably doesn't really exist anymore, has this problem, and makes things complicated. This PRs simplifies things and makes sure the external proto directory is part of the managed classpath - but testing with google-cloud-pub-sub-grpc it seems to actually consistently not include the resources in the jar πŸ˜† . More detective work needed.

We have to add them to the mappings instead of populating managedResourceDirectories to make them show up.

The removed inConfig(config)(Seq(...)) block (lines 139–161) did more than manage resource directories β€” it also configured PB.recompile / sources and PB.recompile / unmanagedSources, which control which proto files are discovered for compilation. Without this explicit configuration, sbt-protoc's defaults may not discover the project's own proto files from sourceDirectory / "proto"

If that were true shouldn't the scripted tests have caught this?

which likely explains the observed behavior of classes not being generated with google-cloud-pub-sub-grpc.

That doesn't make any sense - the behavior of proto's not being included in the jar with google-cloud-pub-sub-grpc was observed without these changes

For a small number of mappings this is fine, but it's easy to make it O(n):

I doubt projects will have so many .proto files that this would be noticable against the load of compiling those proto files, but indeed easy enough to move to front-appending to a list, done

In withoutDuplicates, the parameter name string for the second tuple element is not descriptive. path or relativePath would better convey that it's the target path within the JAR.

On the other hand this makes it clearer this parameter is the string-typed representation of the target path. I'll change it when other changes are needed in that area.

@raboof
raboof marked this pull request as ready for review June 24, 2026 10:47
@raboof raboof changed the title WIP: consistently include proto's in built artifacts consistently include proto's in built artifacts Jun 24, 2026
//#languages-both

// Make sure proto's reliably make it into the artifact:
TaskKey[Unit]("checkJar") := {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to have this

val binary = (Compile / packageBin).value
IO.withTemporaryDirectory { dir =>
val files = IO.unzip(binary, dir, "*.proto")
assert(files.contains(dir / "google/protobuf/duration.proto"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.

Comment on lines +80 to +81
IO.withTemporaryDirectory { dir =>
val files = IO.unzip(binary, dir, "*.proto")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not **.proto?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@He-Pin

He-Pin commented Jun 24, 2026

Copy link
Copy Markdown
Member

Thanks for the clarifications β€” you're right on the main points:

  1. PB.recompile block removal: Fair point about the scripted tests β€” if source discovery were broken, they'd catch it. I also misread the original issue: the problem was proto resources not being included in the JAR, not class generation failure. Withdrawing the πŸ”΄ concern.

  2. O(nΒ²): Thanks for the fix.

  3. Parameter name: Fair enough, string does convey the type. Minor nit, no need to change on its own.

The approach of using packageBin / mappings for explicit control over JAR contents is the right sbt idiom β€” more deterministic than relying on unmanaged resource directory scanning timing.

One remaining minor note: the scripted test assertion using .mkString.contains(...) could be made more robust with .exists(_.getPath.endsWith(...)) for clearer failure messages, but that's a nit.

Overall the direction looks good. Looking forward to the final version once the google-cloud-pub-sub-grpc case is sorted out.

if (seen.contains(string)) {
withoutDuplicates(soFar, seen, toAdd.tail)
} else {
withoutDuplicates((file, string) :: soFar, seen + string, toAdd.tail)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just a mutable hashSet?

@@ -1 +1,2 @@
> checkJar

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkJarContainsProtos ?

@He-Pin He-Pin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I reviewed the current head 5383e21 and found no must-fix issues.\n\nVerified locally:\n- git diff --check\n- sbt "sbt-plugin / scripted gen-scala-server/00-interop"\n- sbt "sbt-plugin / scripted gen-scala-server/03-test-config"

@raboof
raboof merged commit 800ec10 into apache:main Jun 25, 2026
14 checks passed
@pjfanning pjfanning added this to the 2.0.0-M3 milestone Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proto's from protobuf-src are not consistently included in the published artifact

3 participants