Symptom

After #168 set duplicatesStrategy = DuplicatesStrategy.INCLUDE so the shadow plugin’s mergeServiceFiles() transformer could see duplicate service files, ./gradlew :server:proguard started failing with:

1
2
3
java.io.IOException: Can't write [/home/ben/Code/krill/server/build/libs/server-min.jar]
  (Can't read [/home/ben/Code/krill/server/build/libs/server-all.jar]
   (Duplicate jar entry [io/netty/handler/codec/AsciiHeadersEncoder$1.class]))

server-all.jar was readable by Java’s stricter consumers but ProGuard’s zip reader rejected the duplicate entries — the deb couldn’t be obfuscated or shipped.

Root cause

INCLUDE keeps every duplicate of every path, not just service files. AWS SDK pins io.netty:netty-codec:4.1.x while ktor and other deps pull io.netty:netty-codec-base:4.2.x; netty’s 4.1 → 4.2 module split renamed netty-codec to netty-codec-base but the two artifacts still ship a non-trivial overlap of class FQCNs (io/netty/handler/codec/AsciiHeadersEncoder$1.class and many more). With INCLUDE, both copies of every shared class survive into the fat jar — legal-ish ZIP, but ProGuard chokes. EXCLUDE was the only setting that produced a clean jar, but it dropped the gRPC service files #168 was protecting.

Fix

Keep duplicatesStrategy = DuplicatesStrategy.EXCLUDE (so the jar stays ProGuard-readable), drop mergeServiceFiles(), and re-merge the gRPC service files manually in the existing doLast after shadowJar finishes. The merge reads every META-INF/services/io.grpc.* entry from each jar in jvmRuntimeClasspath, dedupes the lines into a LinkedHashSet, and rewrites the output via a jar: FileSystem mount. The post-build class-presence assertion (#164) and service-file-content assertion (#168) both run after the merge, so a future regression in either packaging axis still fails CI.

Verified: :server:shadowJar produces server-all.jar with both gRPC NameResolverProviders listed; :server:proguard succeeds and server-min.jar retains the merged service files.

Prevention

Three packaging regressions on the same gRPC bootstrap path now have build-time guards: classes pruned by minimize (#164), service files dropped by duplicatesStrategy (#168), and ProGuard rejecting INCLUDE’s duplicates (#171). The class-FQCN overlap between netty 4.1 and 4.2 artifacts is the underlying root cause that ruled out the documented shadow mergeServiceFiles() approach — if a future dep upgrade resolves the netty conflict to a single major version, switching back to mergeServiceFiles() + INCLUDE would be cleaner. Until then, the manual merge is the only safe option.