Symptom

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.

Root cause

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.

Fix

Prevention

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.