From 549d4737ea93e8367355f8b7a7915841621eeb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Silveira?= Date: Mon, 31 Aug 2026 11:12:38 +0200 Subject: [PATCH 1/3] Download the native libraries during restore and stage them outside obj A clean clone needed two builds. The first downloaded the libraries and copied none of them; a second build then picked them up. Three separate defects produced that. The download guard was the existence of obj/runtimes//native. It recorded no version, so raising the DuckDB release left every machine that had built once on the old engine, and a checkout targeting v1.5.5 kept running v1.2.0 and failed 127 tests with EntryPointNotFoundException. MakeDir also ran before DownloadFile, so a download that failed left an empty directory that satisfied the guard and every later build skipped in silence. The libraries were staged under obj, which the SDK lists in DefaultItemExcludes. Every item glob over them matched nothing and reported nothing, so the files reached no output directory and a cold dotnet pack shipped DuckDB.NET.Bindings.Full with no native library in it. The download now runs during restore, which is the only phase that precedes evaluation, where item globs expand. The hook is a NuGet target rather than the public Restore target, because Restore only runs on the project the command names and a solution build would skip every project inside it. It carries no compatibility guarantee, so if a later SDK renames it the symptom is a missing library on a clean clone. Staging moves to artifacts/natives///native. That is outside obj, so globs see it, and the version in the path makes the directory the cache key: a bump lands in a fresh folder and cannot inherit the previous engine. Incrementality follows the SDK cache file convention, where a value rather than a source file drives the decision, as CreateGeneratedAssemblyInfoInputsCacheFile does in Microsoft.NET.GenerateAssemblyInfo.targets. The download cache file is written last, so a failure leaves it older than the inputs and the next build retries. The fetch also completes before the installed payload is cleared, so a download that fails over a dead network leaves the working engine in place. Bindings declares the libraries as Content in the project body. Content flows across a ProjectReference, so the test, benchmark and sample projects each get the engine from the reference they already had and their own globs are gone. Copying is unconditional because every project that runs needs the engine beside its assembly; only packing stays gated on BuildType. DuckDbVersion moves to Directory.Build.props and drives the artifact URL, the staging path and both packages' release notes, which each carried the version as hand typed prose. The release and nightly download targets differed in one step and are now one target. DuckDbArtifactRoot still wins when set, which the nightly CI job does through the environment. --- Directory.Build.props | 7 +- DuckDB.NET.Benchmarks/Benchmarks.csproj | 10 -- DuckDB.NET.Bindings/Bindings.csproj | 38 ++++-- .../DownloadNativeLibs.targets | 114 +++++++++++------- DuckDB.NET.Data/Data.csproj | 2 +- DuckDB.NET.Test/Test.csproj | 12 -- 6 files changed, 107 insertions(+), 76 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d00d035c..a6a75b3a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -39,7 +39,12 @@ - ManagedOnly + ManagedOnly + 1.5.5 + + $(MSBuildThisFileDirectory)artifacts\natives\$(DuckDbVersion)\ This package does not include a copy of the native DuckDB library. diff --git a/DuckDB.NET.Benchmarks/Benchmarks.csproj b/DuckDB.NET.Benchmarks/Benchmarks.csproj index 2de0564f..8ced6dcd 100644 --- a/DuckDB.NET.Benchmarks/Benchmarks.csproj +++ b/DuckDB.NET.Benchmarks/Benchmarks.csproj @@ -20,14 +20,4 @@ - - - - false - PreserveNewest - runtimes\%(RecursiveDir)\%(FileName)%(Extension) - - - diff --git a/DuckDB.NET.Bindings/Bindings.csproj b/DuckDB.NET.Bindings/Bindings.csproj index 3ba02773..d99b9617 100644 --- a/DuckDB.NET.Bindings/Bindings.csproj +++ b/DuckDB.NET.Bindings/Bindings.csproj @@ -3,7 +3,7 @@ DuckDB Bindings for C#. -- Updated to DuckDB v1.5.5 +- Updated to DuckDB v$(DuckDbVersion) New features: - Added bindings for the Arrow C Data Interface (duckdb_to_arrow_schema, duckdb_data_chunk_to_arrow, duckdb_result_get_arrow_options) @@ -11,7 +11,7 @@ New features: DuckDB.NET.Native win-x64;win-arm64;linux-x64;linux-arm64;osx - https://github.com/duckdb/duckdb/releases/download/v1.5.5 + https://github.com/duckdb/duckdb/releases/download/v$(DuckDbVersion) True ..\keyPair.snk true @@ -23,7 +23,16 @@ New features: - + + + + <_PackDuckDbNatives Condition="'$(BuildType)' == 'Full' ">true + <_PackDuckDbNatives Condition="'$(_PackDuckDbNatives)' == ''">false + + + <_NativeLib Include="win-x64" LibUrl="$(DuckDbArtifactRoot)/libduckdb-windows-amd64.zip" /> <_NativeLib Include="win-arm64" LibUrl="$(DuckDbArtifactRoot)/libduckdb-windows-arm64.zip" /> @@ -36,20 +45,29 @@ New features: <_NativeLib Include="linux-x64" LibUrl="$(DuckDbArtifactRoot)/duckdb-binaries-linux-amd64.zip" InnerZipName="libduckdb-linux-amd64.zip" Condition="$([MSBuild]::IsOSPlatform('Linux'))" /> <_NativeLib Include="osx" LibUrl="$(DuckDbArtifactRoot)/duckdb-binaries-osx.zip" InnerZipName="libduckdb-osx-universal.zip" Condition="$([MSBuild]::IsOSPlatform('OSX'))" /> - + - + - - + + + false PreserveNewest - true + $(_PackDuckDbNatives) \runtimes - runtimes\%(RecursiveDir)\%(FileName)%(Extension) - + runtimes\%(RecursiveDir)%(FileName)%(Extension) + + + + diff --git a/DuckDB.NET.Bindings/DownloadNativeLibs.targets b/DuckDB.NET.Bindings/DownloadNativeLibs.targets index 0a73e20c..0c89ee23 100644 --- a/DuckDB.NET.Bindings/DownloadNativeLibs.targets +++ b/DuckDB.NET.Bindings/DownloadNativeLibs.targets @@ -1,47 +1,77 @@ - - - - - - $(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native.zip - - - - - - + + + + $(DuckDbNativeRoot)$(Rid) + $(RidDir)\native + $(RidDir)\native.zip + $(RidDir)\temp + $(RidDir)\duckdb.inputs.cache + $(RidDir)\duckdb.download.cache + + + + + + + + + + + + - - - - $(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native.zip - $(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\temp - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDB.NET.Data/Data.csproj b/DuckDB.NET.Data/Data.csproj index 15b11e3c..5096979a 100644 --- a/DuckDB.NET.Data/Data.csproj +++ b/DuckDB.NET.Data/Data.csproj @@ -3,7 +3,7 @@ DuckDB ADO.NET Provider for C#. -- Updated to DuckDB v1.5.5 +- Updated to DuckDB v$(DuckDbVersion) New features: - Apache Arrow result streaming via DuckDBCommand.ExecuteArrowStream and ExecuteArrowBatchesAsync (#26) diff --git a/DuckDB.NET.Test/Test.csproj b/DuckDB.NET.Test/Test.csproj index 37ced068..9eb1d118 100644 --- a/DuckDB.NET.Test/Test.csproj +++ b/DuckDB.NET.Test/Test.csproj @@ -26,16 +26,4 @@ - - - - - - false - PreserveNewest - \runtimes - runtimes\%(RecursiveDir)\%(FileName)%(Extension) - - - From 355483b23a01b30f013679a7bd72faf853a20ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Silveira?= Date: Mon, 31 Aug 2026 11:12:38 +0200 Subject: [PATCH 2/3] Resolve the native library from the assembly that declares the imports A NuGet package writes a runtimeTargets entry into deps.json and the host then probes runtimes/{rid}/native on its own. A project reference produces no such entry, so every project in this repository loaded the library itself: the test project through a module initializer, the benchmarks through a second copy of the same code, and the samples by compiling the test project's file through a Compile Include that reached across projects. DuckDBNativeLibrary registers a DllImportResolver for the Bindings assembly, which is where DuckDbLibrary and its imports live. One copy of the runtime identifier logic now serves every consumer, in this repository and downstream. The resolver returns zero for a name it does not own and for a file it cannot find, so default probing still applies and a library already present on the system still loads. It passes a relative path with DllImportSearchPath.AssemblyDirectory, which keeps it working under single file publish where Assembly.Location is empty, and the four argument overload applies the platform naming convention so one name matches libduckdb.dylib, libduckdb.so and duckdb.dll. --- DuckDB.NET.Benchmarks/NativeLibraryLoader.cs | 48 ------------ DuckDB.NET.Bindings/DuckDBNativeLibrary.cs | 74 +++++++++++++++++++ .../NativeMethods/NativeMethods.All.cs | 2 +- DuckDB.NET.Samples/Program.cs | 7 -- DuckDB.NET.Samples/Samples.csproj | 4 - .../Helpers/NativeLibraryHelper.cs | 45 ----------- DuckDB.NET.Test/ModuleInit.cs | 2 - 7 files changed, 75 insertions(+), 107 deletions(-) delete mode 100644 DuckDB.NET.Benchmarks/NativeLibraryLoader.cs create mode 100644 DuckDB.NET.Bindings/DuckDBNativeLibrary.cs delete mode 100644 DuckDB.NET.Test/Helpers/NativeLibraryHelper.cs diff --git a/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs b/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs deleted file mode 100644 index 429de245..00000000 --- a/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace DuckDB.NET.Benchmarks; - -/// -/// Loads the platform-native DuckDB library for the benchmark process. -/// -internal static class NativeLibraryLoader -{ - [ModuleInitializer] - public static void Init() - { - if (GetRid() is not { } rid) - { - return; - } - - _ = NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "duckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _) || - NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "libduckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _); - } - - private static string? GetRid() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Environment.Is64BitProcess ? "win-x64" : "win-x86"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return RuntimeInformation.ProcessArchitecture switch - { - Architecture.X64 => "linux-x64", - Architecture.Arm64 => "linux-arm64", - _ => null, - }; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "osx"; - } - - return null; - } -} diff --git a/DuckDB.NET.Bindings/DuckDBNativeLibrary.cs b/DuckDB.NET.Bindings/DuckDBNativeLibrary.cs new file mode 100644 index 00000000..9fa0a3a9 --- /dev/null +++ b/DuckDB.NET.Bindings/DuckDBNativeLibrary.cs @@ -0,0 +1,74 @@ +using System.IO; +using System.Reflection; + +namespace DuckDB.NET.Native; + +/// +/// Resolves the native DuckDB library from the runtimes folder beside this assembly. +/// +/// +/// A NuGet package writes a runtimeTargets entry into deps.json and the host probes +/// runtimes/{rid}/native on its own. A project reference produces no such entry, so without this +/// resolver every project that consumes DuckDB.NET by reference has to load the library itself. +/// +internal static class DuckDBNativeLibrary +{ + // CA2255 warns against ModuleInitializer in a library. Registering a DllImport resolver is the + // case the rule exempts: the resolver has to be in place before the first P/Invoke, and those + // live on nested types whose static constructors the outer type cannot hook. +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void Register() => + NativeLibrary.SetDllImportResolver(typeof(DuckDBNativeLibrary).Assembly, Resolve); + + private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != NativeMethods.DuckDbLibrary || GetRuntimeIdentifier() is not { } rid) + { + return IntPtr.Zero; + } + + // The four-argument overload applies the platform naming convention, so "duckdb" matches + // libduckdb.dylib, libduckdb.so and duckdb.dll. The two-argument one needs an exact path. + // Passing a relative path keeps this working under single-file publish, where + // Assembly.Location is empty. + foreach (var candidate in (string[])["duckdb", "libduckdb"]) + { + var path = Path.Combine("runtimes", rid, "native", candidate); + + if (NativeLibrary.TryLoad(path, assembly, DllImportSearchPath.AssemblyDirectory, out var handle)) + { + return handle; + } + } + + // Zero hands the name back to the default probing, so a library already on the system or + // resolved from a NuGet package still loads. + return IntPtr.Zero; + } + + /// + /// These identifiers name the folders the build stages, not the full RID graph. DuckDB ships one + /// universal macOS binary, so that folder is "osx" rather than osx-x64 and osx-arm64. + /// + private static string? GetRuntimeIdentifier() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Environment.Is64BitProcess ? "win-x64" : "win-x86"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "linux-x64", + Architecture.Arm64 => "linux-arm64", + _ => null, + }; + } + + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" : null; + } +} diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.All.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.All.cs index 51183c92..aa935918 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.All.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.All.cs @@ -2,7 +2,7 @@ public static partial class NativeMethods { - private const string DuckDbLibrary = "duckdb"; + internal const string DuckDbLibrary = "duckdb"; //Grouped according to https://duckdb.org/docs/archive/0.8.1/api/c/api } \ No newline at end of file diff --git a/DuckDB.NET.Samples/Program.cs b/DuckDB.NET.Samples/Program.cs index f575c49c..0d1f6297 100644 --- a/DuckDB.NET.Samples/Program.cs +++ b/DuckDB.NET.Samples/Program.cs @@ -1,7 +1,6 @@ using Dapper; using DuckDB.NET.Data; using DuckDB.NET.Native; -using DuckDB.NET.Test.Helpers; using System; using System.Data.Common; using System.Diagnostics; @@ -15,12 +14,6 @@ class Program { static void Main(string[] args) { - if (!NativeLibraryHelper.TryLoad()) - { - Console.Error.WriteLine("native assembly not found"); - return; - } - DapperSample(); AdoNetSamples(); diff --git a/DuckDB.NET.Samples/Samples.csproj b/DuckDB.NET.Samples/Samples.csproj index c22e1f3e..ad364db8 100644 --- a/DuckDB.NET.Samples/Samples.csproj +++ b/DuckDB.NET.Samples/Samples.csproj @@ -9,10 +9,6 @@ Full - - - - diff --git a/DuckDB.NET.Test/Helpers/NativeLibraryHelper.cs b/DuckDB.NET.Test/Helpers/NativeLibraryHelper.cs deleted file mode 100644 index 80cb8411..00000000 --- a/DuckDB.NET.Test/Helpers/NativeLibraryHelper.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace DuckDB.NET.Test.Helpers; - -static class NativeLibraryHelper -{ - public static bool TryLoad() - { - if (GetRid() is not { } rid) - { - return false; - } - - return NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "duckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _) || - NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "libduckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _); - } - - private static string GetRid() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Environment.Is64BitProcess ? "win-x64" : "win-x86"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return RuntimeInformation.ProcessArchitecture switch - { - Architecture.X64 => "linux-x64", - Architecture.Arm64 => "linux-arm64", - _ => null, - }; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "osx"; - } - - return null; - } -} \ No newline at end of file diff --git a/DuckDB.NET.Test/ModuleInit.cs b/DuckDB.NET.Test/ModuleInit.cs index a9b77c3e..230f8901 100644 --- a/DuckDB.NET.Test/ModuleInit.cs +++ b/DuckDB.NET.Test/ModuleInit.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using DuckDB.NET.Test.Helpers; #nullable enable namespace DuckDB.NET.Test; @@ -8,7 +7,6 @@ public static class ModuleInit [ModuleInitializer] public static void Init() { - NativeLibraryHelper.TryLoad(); AssertionOptions.AssertEquivalencyUsing(options => options.Using(new DateTimeOffsetTimeComparer())); } From 75dedb3d8ecf463319bf01af012e25f49826cef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Silveira?= Date: Mon, 31 Aug 2026 11:54:09 +0200 Subject: [PATCH 3/3] Document how to change the DuckDB version Raising the version used to touch three files and nothing checked that they agreed. Commit fd45dbb updated the artifact URL and left both packages' release notes on the previous version. It is one property now, so say where it is. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index ea5f8b02..181d1832 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,18 @@ using var duckDBConnection = new DuckDBConnection("DataSource=md:{your_database} If you want to build DuckDB extensions with C#, see [Giorgi/DuckDB.ExtensionKit](https://github.com/Giorgi/DuckDB.ExtensionKit). +## Updating the DuckDB version + +Set `DuckDbVersion` in `Directory.Build.props`: + +```xml +1.5.5 +``` + +That property drives the download URL, the staging path, the download cache, and the release notes in both packages. Build once and the new engine downloads. + +Each version is staged in its own folder under `artifacts/natives/`, so a previous version stays on disk and switching back downloads nothing. To try a version without editing the file, pass `-p:DuckDbVersion=1.4.1` to `dotnet build`. + ## Known Issues When debugging your project that uses DuckDB.NET library, you may get the following error: **System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt**. The error happens due to debugger interaction with the native memory. For a workaround check out [Debugger Options mess up debugging session during Marshalling