Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@
are not yet set when Directory.Build.props runs.
-->

<PropertyGroup Label="Strict builds only for NSpark itself">
<!-- When NSpark is built as a dependency of another repository (a git submodule
ProjectReference), the build starts outside this directory: a newer SDK's analyzer
warnings must not break the consumer's build. NSpark's own CI and local builds start
inside it and stay strict. Override with -p:NSparkStrictBuild=true|false. -->
<NSparkStrictBuild Condition="'$(NSparkStrictBuild)' == '' and $([MSBuild]::EnsureTrailingSlash('$(MSBuildStartupDirectory)').StartsWith('$(MSBuildThisFileDirectory)'))">true</NSparkStrictBuild>
</PropertyGroup>

<PropertyGroup Label="Warnings as errors gate">
<!-- Treat warnings as errors only for shipping packages. Test, sample,
and benchmark projects (IsPackable=false) get a looser ride so the
shape of test code doesn't have to mirror library conventions
(ConfigureAwait, CA1062, etc.). -->
<TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release' and '$(IsPackable)' != 'false'">true</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(IsPackable)' != 'false'">true</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(NSparkStrictBuild)' == 'true' and '$(Configuration)' == 'Release' and '$(IsPackable)' != 'false'">true</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(NSparkStrictBuild)' == 'true' and '$(ContinuousIntegrationBuild)' == 'true' and '$(IsPackable)' != 'false'">true</TreatWarningsAsErrors>
</PropertyGroup>

</Project>
12 changes: 12 additions & 0 deletions docs/native-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<rid>/native/` tree copied to its output (only the publish RID when
publishing with `-r <rid>`), 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
Expand Down
9 changes: 6 additions & 3 deletions src/NSpark/NSpark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
</PackageReference>
</ItemGroup>
<PropertyGroup>
<!-- Without git metadata (e.g. a Docker build context that excludes .git) MinVer can't
read tags; skip it instead of warning. -->
<MinVerSkip Condition="!Exists('$(MSBuildThisFileDirectory)../../.git')">true</MinVerSkip>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>alpha.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
Expand All @@ -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')" />
<None Include="runtimes/linux-x64/native/libspark_frost.so"
Pack="true" PackagePath="runtimes/linux-x64/native/"
CopyToOutputDirectory="PreserveNewest"
Link="runtimes/linux-x64/native/libspark_frost.so"
Condition="Exists('runtimes/linux-x64/native/libspark_frost.so')" />
Condition="Exists('runtimes/linux-x64/native/libspark_frost.so') and ('$(RuntimeIdentifier)' == '' or '$(RuntimeIdentifier)' == 'linux-x64')" />
<None Include="runtimes/linux-arm64/native/libspark_frost.so"
Pack="true" PackagePath="runtimes/linux-arm64/native/"
CopyToOutputDirectory="PreserveNewest"
Link="runtimes/linux-arm64/native/libspark_frost.so"
Condition="Exists('runtimes/linux-arm64/native/libspark_frost.so')" />
Condition="Exists('runtimes/linux-arm64/native/libspark_frost.so') and ('$(RuntimeIdentifier)' == '' or '$(RuntimeIdentifier)' == 'linux-arm64')" />
</ItemGroup>

<!-- Optional integrity manifest: SHA-256 of every shipped native binary. -->
Expand Down
70 changes: 70 additions & 0 deletions src/NSpark/Signer/NativeLibraryResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace NSpark.Signer;

/// <summary>
/// Locates <c>libspark_frost</c> under <c>runtimes/{rid}/native/</c> next to the application.
/// </summary>
/// <remarks>
/// A NuGet consumer resolves the library through <c>deps.json</c>. A consumer that references
/// NSpark as a project (for example from a git submodule) gets the same <c>runtimes/</c> tree
/// copied to its output but no <c>deps.json</c> entry for it, so default probing misses it.
/// This resolver covers that case and otherwise defers to default probing.
/// </remarks>
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<string> 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);
Comment thread
Jossec101 marked this conversation as resolved.
Dismissed
}
}

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}";
}
}
33 changes: 33 additions & 0 deletions tests/NSpark.UnitTests/Signer/NativeLibraryResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Runtime.InteropServices;
using NSpark.Signer;

namespace NSpark.UnitTests.Signer;

/// <summary>
/// Tests for <see cref="NativeLibraryResolver"/>, which lets project-reference consumers load
/// <c>libspark_frost</c> from <c>runtimes/{rid}/native/</c> without a deps.json entry.
/// </summary>
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);
}
}
Loading