From b66d332ed75e447e206201d4aec4f158e22c88e4 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Mon, 27 Jul 2026 11:48:49 -0500 Subject: [PATCH] refactor!: stop bundling lora_ffi natives in LoraDb.Client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LoraDb.Client packed the same five lora_ffi binaries as LoraDb.Client.Native (since ccefc93), duplicating that package wholesale and contradicting its own description ("Install alongside LoraDb.Client to enable the embedded transport"). It was also the only reason LoraDb.Client could not be plain MIT. The binaries now ship only in LoraDb.Client.Native, which keeps the BUSL-1.1 PackageLicenseFile treatment. LoraDb.Client returns to PackageLicenseExpression=MIT and drops PACKAGE-LICENSE.md, THIRD-PARTY-NOTICES.md and PackageRequireLicenseAcceptance — none of which apply to a package that ships no BSL content. It also drops from ~15 MB to ~71 KB. Nothing about the resolver changes. LoraDbNativeLoader.cs is still source-linked into LoraDb.Client (MIT source, not a binary reference) so its module initializer registers the DllImport hook when LoraDb.Client loads; the hook looks under runtimes/{rid}/native/, which is exactly where the LoraDb.Client.Native package places its assets. Dependency direction is unchanged and acyclic: LoraDb.Client.Native -> LoraDb.Client. BREAKING CHANGE: consumers who install only LoraDb.Client and use the embedded transport must now also install LoraDb.Client.Native. HTTP mode is unaffected. Such consumers previously got a bare DllNotFoundException; the bridge now rethrows one naming the companion package, but only when the default library name is in use — a caller who supplied their own name is not missing the package. Co-Authored-By: Claude Opus 5 (1M context) --- Directory.Build.props | 7 +-- LICENSE | 5 +- LoraDb.Client.Tests/NativeBridgeTests.cs | 52 +++++++++++++++++++ LoraDb.Client/LoraDb.Client.csproj | 32 ++++-------- .../Native/PInvokeLoraDbNativeBridge.cs | 41 +++++++++++++-- PACKAGE-LICENSE.md | 4 +- README.md | 29 ++++++++--- docs/USAGE.md | 21 ++++++-- 8 files changed, 148 insertions(+), 43 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5ec480c..7bcc892 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,10 @@ Harry Cordewener Harry Cordewener Copyright © Harry Cordewener 2026 - + https://github.com/HarryCordewener/loradb-dotnet-client https://github.com/HarryCordewener/loradb-dotnet-client git diff --git a/LICENSE b/LICENSE index 8c7fb6c..38e1889 100644 --- a/LICENSE +++ b/LICENSE @@ -7,8 +7,9 @@ liblora_ffi.dylib). Those are compiled from https://github.com/lora-db/lora, copyright LoraDB, Inc., and are licensed under the Business Source License 1.1. Their license text is reproduced verbatim in THIRD-PARTY-NOTICES.md. -Both published NuGet packages bundle those binaries; PACKAGE-LICENSE.md is the -license file shipped inside the packages. +Of the two published NuGet packages, only LoraDb.Client.Native bundles those +binaries; PACKAGE-LICENSE.md is the license file shipped inside that package. +LoraDb.Client ships managed code only and is published as plain MIT. ---------------------------------------------------------------------------- diff --git a/LoraDb.Client.Tests/NativeBridgeTests.cs b/LoraDb.Client.Tests/NativeBridgeTests.cs index be8c594..b200c70 100644 --- a/LoraDb.Client.Tests/NativeBridgeTests.cs +++ b/LoraDb.Client.Tests/NativeBridgeTests.cs @@ -54,6 +54,58 @@ public async Task Constructor_InvokesLibraryPathResolver() } } + [Test] + [NotInParallel("LibraryPathResolver")] + public async Task Constructor_PointsAtNativePackage_WhenDefaultLibraryIsMissing() + { + // A resolver that returns a path guarantees the load fails for a missing + // file regardless of what the OS search path happens to contain. + var missing = Path.Combine(Path.GetTempPath(), "loradb-absent-9f3c", "liblora_ffi.so"); + var original = PInvokeLoraDbNativeBridge.LibraryPathResolver; + try + { + PInvokeLoraDbNativeBridge.LibraryPathResolver = _ => missing; + + DllNotFoundException? caught = null; + try { _ = new PInvokeLoraDbNativeBridge("lora_ffi"); } + catch (DllNotFoundException ex) { caught = ex; } + + await Assert.That(caught).IsNotNull(); + await Assert.That(caught!.Message).Contains("LoraDb.Client.Native"); + await Assert.That(caught.Message).Contains(missing); + await Assert.That(caught.InnerException).IsNotNull(); + } + finally + { + PInvokeLoraDbNativeBridge.LibraryPathResolver = original; + } + } + + [Test] + [NotInParallel("LibraryPathResolver")] + public async Task Constructor_OmitsNativePackageHint_ForCustomLibraryName() + { + // A caller who supplied their own library name is not missing the + // companion package, so the hint would be misleading. + var original = PInvokeLoraDbNativeBridge.LibraryPathResolver; + try + { + PInvokeLoraDbNativeBridge.LibraryPathResolver = _ => null; + + DllNotFoundException? caught = null; + try { _ = new PInvokeLoraDbNativeBridge("custom_lora_build_9f3c"); } + catch (DllNotFoundException ex) { caught = ex; } + + await Assert.That(caught).IsNotNull(); + await Assert.That(caught!.Message).DoesNotContain("LoraDb.Client.Native"); + await Assert.That(caught.Message).Contains("custom_lora_build_9f3c"); + } + finally + { + PInvokeLoraDbNativeBridge.LibraryPathResolver = original; + } + } + [Test] public async Task LibraryPathResolver_IsNotNull_AfterModuleInitializer() { diff --git a/LoraDb.Client/LoraDb.Client.csproj b/LoraDb.Client/LoraDb.Client.csproj index 0d697d9..993f0b9 100644 --- a/LoraDb.Client/LoraDb.Client.csproj +++ b/LoraDb.Client/LoraDb.Client.csproj @@ -3,40 +3,26 @@ net10.0;netstandard2.1 LoraDb.Client - .NET client for LoraDB with HTTP and embedded Rust FFI transports. The managed client is MIT; this package also bundles lora_ffi native binaries from LoraDB, Inc. under Business Source License 1.1 — see PACKAGE-LICENSE.md and THIRD-PARTY-NOTICES.md in this package. - - PACKAGE-LICENSE.md - true + .NET client for LoraDB with HTTP and embedded Rust FFI transports. HTTP mode works with this package alone; embedded mode additionally needs the LoraDb.Client.Native package, which supplies the lora_ffi binaries. + + MIT - - + - - - - - - - - diff --git a/LoraDb.Client/Native/PInvokeLoraDbNativeBridge.cs b/LoraDb.Client/Native/PInvokeLoraDbNativeBridge.cs index 4aeb329..faf43b0 100644 --- a/LoraDb.Client/Native/PInvokeLoraDbNativeBridge.cs +++ b/LoraDb.Client/Native/PInvokeLoraDbNativeBridge.cs @@ -33,13 +33,16 @@ public void Dispose() #else /// /// Optional resolver invoked before . - /// Set by LoraDb.Client.Native's module initializer to locate - /// RID-specific binaries shipped inside that NuGet package. + /// Set by the LoraDbNativeLoader module initializer to locate the + /// RID-specific binaries shipped in the LoraDb.Client.Native package. /// Returns the full path to load, or to fall back /// to the OS default search. /// public static Func? LibraryPathResolver { get; set; } + /// Library name embedded mode loads unless overridden. + internal const string DefaultNativeLibraryName = "lora_ffi"; + private IntPtr _libraryHandle; private IntPtr _dbHandle; private readonly object _sync = new(); @@ -54,7 +57,7 @@ public void Dispose() private readonly DbSnapshotDelegate _dbLoadSnapshot; private readonly FreeStringDelegate _freeString; - public PInvokeLoraDbNativeBridge(string libraryName = "lora_ffi") + public PInvokeLoraDbNativeBridge(string libraryName = DefaultNativeLibraryName) : this(new LoraDbEmbeddedOpenOptions { NativeLibraryName = libraryName }) { } @@ -69,7 +72,15 @@ public PInvokeLoraDbNativeBridge(LoraDbEmbeddedOpenOptions openOptions) throw new ArgumentException("DatabaseName and WalDirectory are mutually exclusive in embedded mode.", nameof(openOptions)); var resolvedPath = LibraryPathResolver?.Invoke(openOptions.NativeLibraryName) ?? openOptions.NativeLibraryName; - _libraryHandle = NativeLibrary.Load(resolvedPath); + try + { + _libraryHandle = NativeLibrary.Load(resolvedPath); + } + catch (DllNotFoundException ex) + { + throw new DllNotFoundException( + BuildLibraryNotFoundMessage(openOptions.NativeLibraryName, resolvedPath), ex); + } _dbNew = Marshal.GetDelegateForFunctionPointer( NativeLibrary.GetExport(_libraryHandle, "lora_db_new")); @@ -306,6 +317,28 @@ private static (string Query, string? ParamsJson) ParseRequestJson(string reques return (query, paramsJson); } + /// + /// Builds an actionable message for a failed native load. The default + /// library name means the caller wanted embedded mode out of the box, and + /// the most common cause is that LoraDb.Client.Native — the package + /// that ships the binaries — was never installed. + /// + private static string BuildLibraryNotFoundMessage(string requestedName, string resolvedPath) + { + var detail = string.Equals(requestedName, resolvedPath, StringComparison.Ordinal) + ? $"Unable to load native library '{requestedName}'." + : $"Unable to load native library '{requestedName}' (resolved to '{resolvedPath}')."; + + if (!requestedName.Equals(DefaultNativeLibraryName, StringComparison.OrdinalIgnoreCase)) + return detail; + + return detail + + " Embedded mode needs the lora_ffi native binary, which the LoraDb.Client package does not ship." + + " Install the companion package (dotnet add package LoraDb.Client.Native)," + + " or set LoraDbEmbeddedOpenOptions.NativeLibraryName to the full path of your own build." + + " HTTP mode does not need it."; + } + private static (string? ErrorCode, string Message) ParseError(string raw) { var delimiterIndex = raw.IndexOf(':'); diff --git a/PACKAGE-LICENSE.md b/PACKAGE-LICENSE.md index 58311c1..01441ae 100644 --- a/PACKAGE-LICENSE.md +++ b/PACKAGE-LICENSE.md @@ -1,6 +1,8 @@ # License -This package contains code under two different licenses. +This file ships inside the `LoraDb.Client.Native` package, which contains code +under two different licenses. (The companion `LoraDb.Client` package ships +managed code only and is published as plain MIT.) ## MIT — managed code authored by this project diff --git a/README.md b/README.md index 4ad1403..1351ad2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,17 @@ A modern .NET client for [LoraDB](https://github.com/lora-db/lora) — a graph d dotnet add package LoraDb.Client ``` +That is everything HTTP mode needs. **Embedded mode additionally requires the +native binaries**, which ship in a separate package: + +```bash +dotnet add package LoraDb.Client.Native +``` + +`LoraDb.Client.Native` is kept separate because the `lora_ffi` binaries are +BUSL-1.1 licensed — see [License](#-license). Without it, embedded mode fails at +startup with a `DllNotFoundException`. + --- ## ⚡ Quick start @@ -70,6 +81,9 @@ await using var client = LoraDbClient.CreateHttp(new Uri("http://127.0.0.1:4747/ ### Embedded mode +> Requires the `LoraDb.Client.Native` package — or your own `lora_ffi` binary, +> pointed at via `LoraDbEmbeddedOpenOptions.NativeLibraryName`. + ```csharp // In-memory (ephemeral) await using var client = LoraDbClient.CreateEmbedded(); @@ -153,7 +167,7 @@ Two licenses apply, depending on which part you use. | Part | License | |---|---| | All C# source in this repo, and the managed assemblies in both packages | [MIT](LICENSE) — Copyright © 2026 Harry Cordewener | -| The bundled `lora_ffi` native libraries (`runtimes/*/native/`) | [Business Source License 1.1](THIRD-PARTY-NOTICES.md) — Copyright LoraDB, Inc. | +| The `lora_ffi` native libraries (`runtimes/*/native/`), bundled only in `LoraDb.Client.Native` | [Business Source License 1.1](THIRD-PARTY-NOTICES.md) — Copyright LoraDB, Inc. | The native libraries are compiled from [LoraDB](https://github.com/lora-db/lora), which is licensed under BUSL-1.1 (SPDX: `BUSL-1.1`), not an open source license. @@ -162,11 +176,12 @@ Additional Use Grant permits internal-business and non-production use but does not permit offering LoraDB as a database-as-a-service, hosted API, managed database platform, or substantially similar hosted service for third parties. -**Both `LoraDb.Client` and `LoraDb.Client.Native` bundle these binaries**, so -the BUSL-1.1 terms apply to either package. Each package ships -`PACKAGE-LICENSE.md` (the split) and `THIRD-PARTY-NOTICES.md` (the verbatim -BUSL-1.1 text). Read them before use. +**Only `LoraDb.Client.Native` bundles these binaries**, so the BUSL-1.1 terms +apply to that package. It ships `PACKAGE-LICENSE.md` (the split) and +`THIRD-PARTY-NOTICES.md` (the verbatim BUSL-1.1 text) — read them before use. +`LoraDb.Client` contains managed code only and is published as plain MIT; if you +never enable embedded mode, BUSL-1.1 does not enter your dependency graph. -> Versions 0.1.2 and earlier of both packages incorrectly declared `MIT` as the -> sole NuGet license expression. +> Versions 0.1.2 and earlier of `LoraDb.Client` also bundled the binaries, and +> both packages incorrectly declared `MIT` as the sole NuGet license expression. diff --git a/docs/USAGE.md b/docs/USAGE.md index 05aba36..e1b106b 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -22,7 +22,14 @@ await using var client = LoraDbClient.CreateHttp(endpoint, httpClientFactory); ### Embedded mode -Use embedded mode to execute queries through the native `lora_ffi` bridge: +Use embedded mode to execute queries through the native `lora_ffi` bridge. + +Embedded mode needs the `lora_ffi` native library at runtime; `LoraDb.Client` +does not ship it. Add the companion package: + +```bash +dotnet add package LoraDb.Client.Native +``` ```csharp using LoraDb.Client; @@ -30,6 +37,8 @@ using LoraDb.Client; await using var client = LoraDbClient.CreateEmbedded(); ``` +> `LoraDb.Client.Native` bundles BUSL-1.1 licensed binaries — see the repository +> `LICENSE` and the `THIRD-PARTY-NOTICES.md` shipped in that package. > Embedded mode is not supported on `netstandard2.1`. > Embedded mode currently supports only `rows` query format. @@ -138,9 +147,15 @@ Supported keys: ## 5) Embedded native library notes -By default, embedded mode loads library name `lora_ffi`. +By default, embedded mode loads library name `lora_ffi`. `LoraDb.Client` +registers a resolver that looks for the RID-specific binary under +`runtimes/{rid}/native/`, first next to the loaded assembly and then under +`AppContext.BaseDirectory` — that is where the `LoraDb.Client.Native` package +places the binaries. If neither is found the OS default search is used, and a +missing library surfaces as `DllNotFoundException`. -If needed, set a custom name via options: +If needed, set a custom name (or an absolute path to your own build) via +options: ```csharp services.AddLoraDb(new LoraDbClientOptions