pact-foundation / pact-net

.NET version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
https://pact.io
MIT License
842 stars 231 forks source link

Add linux-musl-x64 runtime support #497

Open botondbotos opened 5 months ago

botondbotos commented 5 months ago

Possible solution for https://github.com/pact-foundation/pact-net/issues/496

adamrodger commented 5 months ago

The CI additionally would need updating to test the new target

YOU54F commented 5 months ago

Nice,

I had a go at this myself as part of a mission to bring alpine/musl support to the Pact ecosystem - see linked issue

https://github.com/pact-foundation/pact-net/compare/master...YOU54F:pact-net:feat/musl

added a gh test in the branch, tried to test arm64 under qemu but no 🎲

https://github.com/pact-foundation/pact-net/compare/master...YOU54F:pact-net:feat/trimmed_binaries

There still doesn't seem to be a nice way to detect musl via .NET

I used a file system lookup for the musl shared lib, which varies on the arch of the target, and I've only tested against alpine based images.

From looking at a few different languages, most tend to ldd <executable> like /bin/sh and grep for musl, unless its a compiled language and compilation is set by flags (like rust). I couldn't find a way in the .NET sln to run a command, so went for a musl shared lib look up instead.

For rust based executables, (not the pact_ffi shared libs), we are beginning to roll out the building of them statically with musl, rather than glibc, so we can support a single executable for all linux users.

Unfortunatly, we cannot do that with the ffi libs in languages that read on the shared, not static libs (PHP, .NET, Ruby)

adamrodger commented 5 months ago

Yep that was the same problem I had when I originally raised a PR for this and had to abandon it @YOU54F.

There doesn't seem to be a reliable way to determine libc vs musl at NuGet restore time to make sure the correct shared library is unpacked, and if you get the wrong one it just won't work with no real workaround. That would be a disaster for existing users, hence why we need extensive CI for that situation.

It's got to be totally reliable and it can't break existing libc distros, and that's where the problem lies.

adamrodger commented 5 months ago

It may be more pragmatic on Alpine to install something like https://github.com/Stantheman/gcompat to provide glibc compatibility so that the existing shared library will work, although I've not tested that.

botondbotos commented 5 months ago

It may be more pragmatic on Alpine to install something like https://github.com/Stantheman/gcompat to provide glibc compatibility so that the existing shared library will work, although I've not tested that.

Unfortunately, gcompat doesn't seem to work: https://github.com/botondbotos/pact-net-glibc-vs-musl/blob/main/Dockerfile.gcompat I managed to make it run by patching libpact_ffi.so with https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v0.4.19/libpact_ffi-linux-x86_64-musl.so.gz

botondbotos commented 5 months ago

Refactored the project to use runtime specific native packages as shown at https://github.com/Mizux/dotnet-native. Managed to run the tests under samples/OrdersApi:

./pack.sh
docker build -f Dockerfile.alpine -t pact-net-alpine .
docker build -f Dockerfile.debian -t pact-net-debian .

The runtimes folder seems to contain all native libraries.

On Alpine

docker run --rm  pact-net-alpine tree samples/OrdersApi/Consumer.Tests/bin/Debug/net8.0/runtimes

Output:

samples/OrdersApi/Consumer.Tests/bin/Debug/net8.0/runtimes
├── linux-musl-x64
│   └── native
│       └── libpact_ffi.so
├── linux-x64
│   └── native
│       └── libpact_ffi.so
├── osx-arm64
│   └── native
│       └── libpact_ffi.dylib
├── osx-x64
│   └── native
│       └── libpact_ffi.dylib
├── win
│   └── lib
│       ├── net6.0
│       │   ├── System.Diagnostics.EventLog.Messages.dll
│       │   └── System.Diagnostics.EventLog.dll
│       └── netstandard2.0
│           └── System.Security.Cryptography.ProtectedData.dll
└── win-x64
    └── native
        └── pact_ffi.dll

On Debian

docker run --rm  pact-net-debian tree samples/OrdersApi/Consumer.Tests/bin/Debug/net8.0/runtimes

Output:

samples/OrdersApi/Consumer.Tests/bin/Debug/net8.0/runtimes
|-- linux-musl-x64
|   `-- native
|       `-- libpact_ffi.so
|-- linux-x64
|   `-- native
|       `-- libpact_ffi.so
|-- osx-arm64
|   `-- native
|       `-- libpact_ffi.dylib
|-- osx-x64
|   `-- native
|       `-- libpact_ffi.dylib
|-- win
|   `-- lib
|       |-- net6.0
|       |   |-- System.Diagnostics.EventLog.Messages.dll
|       |   `-- System.Diagnostics.EventLog.dll
|       `-- netstandard2.0
|           `-- System.Security.Cryptography.ProtectedData.dll
`-- win-x64
    `-- native
        `-- pact_ffi.dll

Is this approach worth pursuing?

adamrodger commented 5 months ago

PactNet used to have architecture specific packages but a key design goal of the major changes in 4.x was to move away from this approach.

In practice it's very awkward to use for consumers - for example if you run the tests locally on Windows/Mac but your CI runs in Linux (as is very common) then you've got to reference multiple native packages and add conditions to your dependencies, which is a bad devex.

I don't see the cost of reintroducing all the complexity both within PactNet and especially for our users being worth adding musl support. At the end of the day this is a test library, so it's much less of a burden to run your tests in a glibc env instead, until such a time that detecting running on musl is more robust and can use the existing packaging mechanism.

It may be worth you creating an issue on the .Net team to add a feature to make detecting musl easier. It's definitely not an easy task to do really robustly though. Like even just detecting certain heuristics is hard because of things like cross compilation envs.

botondbotos commented 5 months ago

In practice it's very awkward to use for consumers - for example if you run the tests locally on Windows/Mac but your CI runs in Linux (as is very common) then you've got to reference multiple native packages and add conditions to your dependencies, which is a bad devex.

There's only need to reference a single package, PactNet: https://github.com/pact-foundation/pact-net/pull/497/files#diff-bc4f87702468a93737635b96e0dc7fc5ec46a8fab44ebc9f249a17091de6fd60R16. The PactNet package depends on all the other packages containing the native pact ffi libraries. The packages containing the native libraries should never be referenced directly. This packaging model helps generate the correct deps.json so that the correct assembly can be picked up by the runtime. This is demonstrated by the Dockerfile.alpine and Dockerfile.debian containers.

... At the end of the day this is a test library, so it's much less of a burden to run your tests in a glibc env instead ...

Some teams prefer to run tests on the exact same platform with exactly the same dependencies as what gets deployed to production. Exceptions could be made, and contract tests could be run on a sperate platform, but that adds extra complexity to the CI pipeline.