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);
+ }
+}