Production deb installs of krill (1.0.889) silently broke all GPIO on a
Pi 5: /header returned [], configured pins never actuated hardware,
watchInput listeners never fired, and reads always reported OFF. A
source-built server (./gradlew :server:jvmRun) on the same hardware worked
fine. The journal carried java.util.ServiceConfigurationError: io.grpc.ManagedChannelProvider:
Provider io.grpc.netty.shaded.io.grpc.netty.NettyChannelProvider not found,
followed by a stream of swallowed UninitializedPropertyAccessExceptions
from every ServerPiManager method.
shadowJar’s minimize block was excluding io.netty:* (because Netty loads
classes dynamically) but not io.grpc:*. gRPC discovers
ManagedChannelProvider implementations through META-INF/services/… —
shadowJar’s static-reachability pass can’t see those references and pruned
the provider classes (NettyChannelProvider, UdsNettyChannelProvider)
from the fat jar. The service registration file then pointed at classes
that no longer existed, so Pi4jClient()’s constructor threw
ServiceConfigurationError. That exception bypassed ServerPiManager.init’s
try-catch (which only wrapped the subsequent ping()), the lateinit var
client was never assigned, and every public method dereferenced it inside
its own try-catch — each catch quietly returned a benign default
(emptyList(), DigitalState.OFF, no-op), masking the failure.
exclude(dependency("io.grpc:.*:.*")) to shadowJar’s minimize block.doLast build-time assertion that fails the build if the gRPC
ServiceLoader provider classes are absent from server-all.jar, so the
next minimize tweak that drops them is loud rather than silently shipping
a broken deb.ServerPiManager: wrap the entire client init
(constructor + ping) in one try-catch so a ctor failure leaves the lateinit
cleanly unset, and add an isClientReady short-circuit at the top of
every public method that dereferences client. Failures now log
"krill-pi4j client unavailable" once per call site instead of an opaque
lateinit property has not been initialized.The build-time assertion in server/build.gradle.kts is the regression test —
any future minimize-related change that drops NettyChannelProvider /
UdsNettyChannelProvider from the fat jar fails CI. The class of failure —
“ServiceLoader providers vanish under static-reachability minimization” —
applies to any library that uses META-INF/services/; the existing comments
on io.netty, H2, and Exposed call this out, and the new comment on
io.grpc reinforces the rule. New runtime dependencies should be checked
for META-INF/services/ entries before they’re added without a minimize
exclude.