From d8d527616a8e879df2cd6f31cbd624169fe0de22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:52:59 +0200 Subject: [PATCH] Build cleanly as a git-submodule project reference - Warnings-as-errors only when the build starts inside this repository (NSparkStrictBuild), so a newer consumer SDK can't break consumer builds. - Skip MinVer when there is no git metadata (Docker contexts without .git). - NativeLibraryResolver: load libspark_frost from runtimes/{rid}/native when there is no deps.json entry (project references), else default probing. - A RID-specific build or publish copies only its own native library. --- Directory.Build.targets | 12 +++- docs/native-build.md | 12 ++++ src/NSpark/NSpark.csproj | 9 ++- src/NSpark/Signer/NativeLibraryResolver.cs | 70 +++++++++++++++++++ .../Signer/NativeLibraryResolverTests.cs | 33 +++++++++ 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/NSpark/Signer/NativeLibraryResolver.cs create mode 100644 tests/NSpark.UnitTests/Signer/NativeLibraryResolverTests.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 2565b7d..6b65ebf 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -6,13 +6,21 @@ are not yet set when Directory.Build.props runs. --> + + + true + + - true - true + true + true diff --git a/docs/native-build.md b/docs/native-build.md index cd2d06a..3117ccb 100644 --- a/docs/native-build.md +++ b/docs/native-build.md @@ -43,6 +43,18 @@ attestations to each library. Verify a committed tree with: ( cd src/NSpark/runtimes && shasum -a 256 -c SHA256SUMS.txt ) ``` +### Consuming NSpark from source (git submodule) + +A project that references `src/NSpark/NSpark.csproj` directly (instead of the NuGet package): + +- gets the `runtimes//native/` tree copied to its output (only the publish RID when + publishing with `-r `), and `NativeLibraryResolver` loads the library from there, since + no `deps.json` entry exists for it; +- builds NSpark without warnings-as-errors: strict analyzer gating applies only when the build is + started inside this repository (`NSparkStrictBuild`, overridable with `-p:NSparkStrictBuild=`); +- skips MinVer when the checkout has no git metadata (for example a Docker build context that + excludes `.git`). + ### Rebuilding locally ```bash diff --git a/src/NSpark/NSpark.csproj b/src/NSpark/NSpark.csproj index 2f3f6ef..04cacf3 100644 --- a/src/NSpark/NSpark.csproj +++ b/src/NSpark/NSpark.csproj @@ -50,6 +50,9 @@ + + true v alpha.0 @@ -75,17 +78,17 @@ Pack="true" PackagePath="runtimes/osx-arm64/native/" CopyToOutputDirectory="PreserveNewest" Link="runtimes/osx-arm64/native/libspark_frost.dylib" - Condition="Exists('runtimes/osx-arm64/native/libspark_frost.dylib')" /> + Condition="Exists('runtimes/osx-arm64/native/libspark_frost.dylib') and ('$(RuntimeIdentifier)' == '' or '$(RuntimeIdentifier)' == 'osx-arm64')" /> + Condition="Exists('runtimes/linux-x64/native/libspark_frost.so') and ('$(RuntimeIdentifier)' == '' or '$(RuntimeIdentifier)' == 'linux-x64')" /> + Condition="Exists('runtimes/linux-arm64/native/libspark_frost.so') and ('$(RuntimeIdentifier)' == '' or '$(RuntimeIdentifier)' == 'linux-arm64')" /> diff --git a/src/NSpark/Signer/NativeLibraryResolver.cs b/src/NSpark/Signer/NativeLibraryResolver.cs new file mode 100644 index 0000000..a7aa159 --- /dev/null +++ b/src/NSpark/Signer/NativeLibraryResolver.cs @@ -0,0 +1,70 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace NSpark.Signer; + +/// +/// Locates libspark_frost under runtimes/{rid}/native/ next to the application. +/// +/// +/// A NuGet consumer resolves the library through deps.json. A consumer that references +/// NSpark as a project (for example from a git submodule) gets the same runtimes/ tree +/// copied to its output but no deps.json entry for it, so default probing misses it. +/// This resolver covers that case and otherwise defers to default probing. +/// +internal static class NativeLibraryResolver +{ + internal const string LibraryName = "spark_frost"; + + [ModuleInitializer] + [SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", + Justification = "The resolver must be in place before the first P/Invoke into spark_frost, from any entry point.")] + internal static void Register() => + NativeLibrary.SetDllImportResolver(typeof(NativeLibraryResolver).Assembly, Resolve); + + private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (!string.Equals(libraryName, LibraryName, StringComparison.Ordinal)) + { + return IntPtr.Zero; + } + + foreach (var candidate in Candidates()) + { + if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out var handle)) + { + return handle; + } + } + + return IntPtr.Zero; + } + + internal static IEnumerable Candidates() + { + var fileName = OperatingSystem.IsWindows() ? "spark_frost.dll" + : OperatingSystem.IsMacOS() ? "libspark_frost.dylib" + : "libspark_frost.so"; + + // The runtime's RID first; a distro-built (non-portable) runtime reports e.g. + // "ubuntu.24.04-x64", so fall back to the portable RID for this OS and architecture. + foreach (var rid in new[] { RuntimeInformation.RuntimeIdentifier, PortableRid() }.Distinct(StringComparer.Ordinal)) + { + yield return Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", fileName); + } + } + + private static string PortableRid() + { + var os = OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsMacOS() ? "osx" : "linux"; + var arch = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => "arm64", + Architecture.X86 => "x86", + _ => "x64", + }; + return $"{os}-{arch}"; + } +} diff --git a/tests/NSpark.UnitTests/Signer/NativeLibraryResolverTests.cs b/tests/NSpark.UnitTests/Signer/NativeLibraryResolverTests.cs new file mode 100644 index 0000000..3acb141 --- /dev/null +++ b/tests/NSpark.UnitTests/Signer/NativeLibraryResolverTests.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; +using NSpark.Signer; + +namespace NSpark.UnitTests.Signer; + +/// +/// Tests for , which lets project-reference consumers load +/// libspark_frost from runtimes/{rid}/native/ without a deps.json entry. +/// +public class NativeLibraryResolverTests +{ + [Test] + public void Candidates_LookUnderTheRuntimesFolderNextToTheApp() + { + var candidates = NativeLibraryResolver.Candidates().ToList(); + + candidates.Should().NotBeEmpty(); + candidates.Should().OnlyContain(path => + path.StartsWith(Path.Combine(AppContext.BaseDirectory, "runtimes"), StringComparison.Ordinal) && + Path.GetFileName(Path.GetDirectoryName(path)) == "native"); + candidates.Should().Contain(path => path.Contains(RuntimeInformation.RuntimeIdentifier, StringComparison.Ordinal)); + } + + [Test] + public void Candidates_UseThePlatformLibraryFileName() + { + var expected = OperatingSystem.IsWindows() ? "spark_frost.dll" + : OperatingSystem.IsMacOS() ? "libspark_frost.dylib" + : "libspark_frost.so"; + + NativeLibraryResolver.Candidates().Should().OnlyContain(path => Path.GetFileName(path) == expected); + } +}