From ea9e55693db09cfb2fd118a5b113a030ac19a945 Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Sat, 25 Oct 2025 11:41:36 +0200 Subject: [PATCH 1/4] Fail integration test when native assets missing --- src/MLXSharp.Tests/ModelIntegrationTests.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/MLXSharp.Tests/ModelIntegrationTests.cs b/src/MLXSharp.Tests/ModelIntegrationTests.cs index 7cc4696..cd1d032 100644 --- a/src/MLXSharp.Tests/ModelIntegrationTests.cs +++ b/src/MLXSharp.Tests/ModelIntegrationTests.cs @@ -14,7 +14,7 @@ public sealed class ModelIntegrationTests public async Task NativeBackendAnswersSimpleMathAsync() { TestEnvironment.EnsureInitialized(); - EnsureAssetsOrSkip(); + EnsureAssets(); var options = CreateOptions(); using var backend = MlxNativeBackend.Create(options); @@ -59,18 +59,14 @@ private static MlxClientOptions CreateOptions() return options; } - private static void EnsureAssetsOrSkip() + private static void EnsureAssets() { var modelPath = Environment.GetEnvironmentVariable("MLXSHARP_MODEL_PATH"); - if (string.IsNullOrWhiteSpace(modelPath) || !System.IO.Directory.Exists(modelPath)) - { - Skip.If(true, "Native model bundle not found."); - } + Assert.False(string.IsNullOrWhiteSpace(modelPath), "Native model bundle path is not configured. Set MLXSHARP_MODEL_PATH to a valid directory."); + Assert.True(System.IO.Directory.Exists(modelPath), $"Native model bundle not found at '{modelPath}'."); var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); - if (string.IsNullOrWhiteSpace(library) || !System.IO.File.Exists(library)) - { - Skip.If(true, "Native libmlxsharp library not configured."); - } + Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the compiled native library."); + Assert.True(System.IO.File.Exists(library), $"Native libmlxsharp library not found at '{library}'."); } } From 7d019e0afb30aa440fa674761701abf0685effe8 Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Sat, 25 Oct 2025 12:07:16 +0200 Subject: [PATCH 2/4] Download official native binaries for tests --- .gitignore | 6 +- README.md | 2 +- src/MLXSharp.Tests/ModelIntegrationTests.cs | 2 +- src/MLXSharp.Tests/NativeBinaryManager.cs | 147 ++++++++++++++++++++ src/MLXSharp.Tests/TestEnvironment.cs | 11 ++ src/MLXSharp/MLXSharp.csproj | 4 + 6 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/MLXSharp.Tests/NativeBinaryManager.cs diff --git a/.gitignore b/.gitignore index eafd8ab..31ecd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -872,4 +872,8 @@ FodyWeavers.xsd # Local model files for testing Tests/**/LocalModels/ -Apps/**/LocalModels/ \ No newline at end of file +Apps/**/LocalModels/ +# Downloaded native artifacts +libs/native-libs/ +ManagedCode.MLXSharp.nupkg + diff --git a/README.md b/README.md index 996a79f..5473ad7 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ dotnet test `MLXSHARP_HF_MODEL_ID` is picked up by the Python smoke test; omit it to fall back to `mlx-community/Qwen1.5-0.5B-Chat-4bit`. -When running locally you can place prebuilt binaries under `libs/native-osx-arm64` (and/or `libs/native-libs`) and a corresponding model bundle under `model/`. The test harness auto-discovers these folders and configures `MLXSHARP_LIBRARY`, `MLXSHARP_MODEL_PATH`, and `MLXSHARP_TOKENIZER_PATH` so you can iterate completely offline. +When running locally you can place prebuilt binaries under `libs/native-osx-arm64` (and/or `libs/native-libs`) and a corresponding model bundle under `model/`. The test harness auto-discovers these folders and configures `MLXSHARP_LIBRARY`, `MLXSHARP_MODEL_PATH`, and `MLXSHARP_TOKENIZER_PATH` so you can iterate completely offline. If no native binary is present the tests now download the latest signed `ManagedCode.MLXSharp` NuGet package and stage its `runtimes/{rid}/native/libmlxsharp.{dylib|so}` assets under `libs/native-libs/` automatically. The integration suite invokes `python -m mlx_lm.generate` with deterministic settings (temperature `0`, seed `42`) and asserts that the generated response for prompts like “Скільки буде 2+2?” contains the correct answer. Test output includes the raw generation transcript so you can verify the model behaviour directly from the CI logs. diff --git a/src/MLXSharp.Tests/ModelIntegrationTests.cs b/src/MLXSharp.Tests/ModelIntegrationTests.cs index cd1d032..9778307 100644 --- a/src/MLXSharp.Tests/ModelIntegrationTests.cs +++ b/src/MLXSharp.Tests/ModelIntegrationTests.cs @@ -66,7 +66,7 @@ private static void EnsureAssets() Assert.True(System.IO.Directory.Exists(modelPath), $"Native model bundle not found at '{modelPath}'."); var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); - Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the compiled native library."); + Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the compiled native library or rely on the official ManagedCode.MLXSharp package that the test harness can download automatically."); Assert.True(System.IO.File.Exists(library), $"Native libmlxsharp library not found at '{library}'."); } } diff --git a/src/MLXSharp.Tests/NativeBinaryManager.cs b/src/MLXSharp.Tests/NativeBinaryManager.cs new file mode 100644 index 0000000..7ebce2b --- /dev/null +++ b/src/MLXSharp.Tests/NativeBinaryManager.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text.Json; + +namespace MLXSharp.Tests; + +internal static class NativeBinaryManager +{ + private const string PackageId = "managedcode.mlxsharp"; + private const string BaseUrl = "https://api.nuget.org/v3-flatcontainer"; + + private static readonly object s_sync = new(); + private static bool s_attempted; + private static string? s_cachedPath; + private static string? s_lastError; + + public static bool TryEnsureNativeLibrary(string repoRoot, out string? libraryPath, out string? error) + { + if (!OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) + { + libraryPath = null; + error = "Official native binaries are only published for macOS and Linux."; + return false; + } + + lock (s_sync) + { + if (!string.IsNullOrEmpty(s_cachedPath) && File.Exists(s_cachedPath)) + { + libraryPath = s_cachedPath; + error = null; + return true; + } + + if (s_attempted) + { + libraryPath = s_cachedPath; + error = s_lastError; + return libraryPath is not null; + } + + s_attempted = true; + + try + { + var path = DownloadOfficialBinary(repoRoot); + s_cachedPath = path; + s_lastError = null; + libraryPath = path; + error = null; + return true; + } + catch (Exception ex) + { + s_cachedPath = null; + s_lastError = ex.Message; + libraryPath = null; + error = s_lastError; + return false; + } + } + } + + private static string DownloadOfficialBinary(string repoRoot) + { + var rid = GetRuntimeIdentifier(); + var fileName = OperatingSystem.IsMacOS() ? "libmlxsharp.dylib" : "libmlxsharp.so"; + var nativeDirectory = Path.Combine(repoRoot, "libs", "native-libs", rid); + Directory.CreateDirectory(nativeDirectory); + + var destination = Path.Combine(nativeDirectory, fileName); + if (File.Exists(destination)) + { + return destination; + } + + using var client = new HttpClient(); + var version = ResolvePackageVersion(client); + var packageUrl = $"{BaseUrl}/{PackageId}/{version}/{PackageId}.{version}.nupkg"; + + using var packageStream = client.GetStreamAsync(packageUrl).GetAwaiter().GetResult(); + var tempFile = Path.GetTempFileName(); + try + { + using (var fileStream = File.OpenWrite(tempFile)) + { + packageStream.CopyTo(fileStream); + } + + using var archive = ZipFile.OpenRead(tempFile); + var entryPath = $"runtimes/{rid}/native/{fileName}"; + var entry = archive.GetEntry(entryPath) ?? + throw new InvalidOperationException($"The official package does not contain {entryPath}."); + + entry.ExtractToFile(destination, overwrite: true); + return destination; + } + finally + { + try + { + File.Delete(tempFile); + } + catch + { + // ignore cleanup errors + } + } + } + + private static string ResolvePackageVersion(HttpClient client) + { + var overrideVersion = Environment.GetEnvironmentVariable("MLXSHARP_OFFICIAL_NATIVE_VERSION"); + if (!string.IsNullOrWhiteSpace(overrideVersion)) + { + return overrideVersion.Trim(); + } + + var indexUrl = $"{BaseUrl}/{PackageId}/index.json"; + using var stream = client.GetStreamAsync(indexUrl).GetAwaiter().GetResult(); + using var document = JsonDocument.Parse(stream); + if (!document.RootElement.TryGetProperty("versions", out var versions) || versions.GetArrayLength() == 0) + { + throw new InvalidOperationException("Unable to determine the latest ManagedCode.MLXSharp package version."); + } + + return versions[versions.GetArrayLength() - 1].GetString() + ?? throw new InvalidOperationException("ManagedCode.MLXSharp package version entry was null."); + } + + private static string GetRuntimeIdentifier() + { + if (OperatingSystem.IsMacOS()) + { + return "osx-arm64"; + } + + if (OperatingSystem.IsLinux()) + { + return "linux-x64"; + } + + throw new PlatformNotSupportedException("Unsupported platform for native MLXSharp binaries."); + } +} diff --git a/src/MLXSharp.Tests/TestEnvironment.cs b/src/MLXSharp.Tests/TestEnvironment.cs index 55fcdad..288e0fa 100644 --- a/src/MLXSharp.Tests/TestEnvironment.cs +++ b/src/MLXSharp.Tests/TestEnvironment.cs @@ -25,6 +25,17 @@ public static void EnsureInitialized() private static void ConfigureNativeLibrary(string repoRoot) { + if (NativeBinaryManager.TryEnsureNativeLibrary(repoRoot, out var officialLibrary, out var downloadError) && officialLibrary is not null) + { + ApplyNativeLibrary(officialLibrary); + return; + } + + if (!string.IsNullOrWhiteSpace(downloadError)) + { + Console.Error.WriteLine($"Failed to download official MLXSharp native library: {downloadError}"); + } + var existing = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); if (!string.IsNullOrWhiteSpace(existing) && File.Exists(existing)) { diff --git a/src/MLXSharp/MLXSharp.csproj b/src/MLXSharp/MLXSharp.csproj index d258dff..dd58fe0 100644 --- a/src/MLXSharp/MLXSharp.csproj +++ b/src/MLXSharp/MLXSharp.csproj @@ -16,6 +16,9 @@ + <_RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../..')) + $([System.IO.Path]::Combine('$(_RepoRoot)','libs','native-libs')) + $([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','osx-arm64','libmlxsharp.dylib')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','libmlxsharp.dylib')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','osx-arm64','native','libmlxsharp.dylib')) false @@ -23,6 +26,7 @@ $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','macos','extern','mlx','mlx','backend','metal','kernels','mlx.metallib')) $([System.IO.Path]::Combine('$(MLXSharpMacNativeDestinationDir)','mlx.metallib')) + $([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','linux-x64','libmlxsharp.so')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','linux','libmlxsharp.so')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','linux-x64','native','libmlxsharp.so')) false From e47378c42f94a451591a4952469d633ed026f4e6 Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Sat, 25 Oct 2025 12:07:20 +0200 Subject: [PATCH 3/4] Download native MLXSharp release assets in CI --- .github/workflows/ci.yml | 118 ++++++++++------ README.md | 6 +- src/MLXSharp.Tests/ModelIntegrationTests.cs | 2 +- src/MLXSharp.Tests/NativeBinaryManager.cs | 147 -------------------- src/MLXSharp.Tests/TestEnvironment.cs | 11 -- 5 files changed, 79 insertions(+), 205 deletions(-) delete mode 100644 src/MLXSharp.Tests/NativeBinaryManager.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c65e7..6fa5317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,71 +25,103 @@ jobs: - name: Build managed projects run: dotnet build --configuration Release --no-restore - native-linux: + native-assets: needs: dotnet-build runs-on: ubuntu-latest + env: + NATIVE_RELEASE_REPO: ${{ vars.MLXSHARP_NATIVE_REPO || 'ManagedCode/MLXSharp' }} + NATIVE_RELEASE_TAG: ${{ vars.MLXSHARP_NATIVE_TAG || '' }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install build dependencies + - name: Install tooling run: | sudo apt-get update - sudo apt-get install -y build-essential cmake libopenblas-dev liblapack-dev liblapacke-dev + sudo apt-get install -y jq unzip - - name: Configure native build - run: cmake -S native -B native/build/linux -DCMAKE_BUILD_TYPE=Release + - name: Download official native binaries + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail - - name: Build native library - run: cmake --build native/build/linux --target mlxsharp --config Release + repo="${NATIVE_RELEASE_REPO}" + if [ -z "$repo" ]; then + echo "::error::NATIVE_RELEASE_REPO must be provided" >&2 + exit 1 + fi - - name: Package native artifact - run: | - mkdir -p artifacts/native/linux-x64 - cp native/build/linux/libmlxsharp.so artifacts/native/linux-x64/ + if [ -n "${NATIVE_RELEASE_TAG}" ]; then + release_api="https://api.github.com/repos/${repo}/releases/tags/${NATIVE_RELEASE_TAG}" + else + release_api="https://api.github.com/repos/${repo}/releases/latest" + fi - - name: Upload native artifact - uses: actions/upload-artifact@v4 - with: - name: native-linux-x64 - path: artifacts/native/linux-x64/libmlxsharp.so + echo "Fetching release metadata from ${release_api}" + response=$(curl -fsSL -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${GITHUB_TOKEN}" "$release_api") + tag=$(echo "$response" | jq -r '.tag_name // ""') - native-macos: - needs: dotnet-build - runs-on: macos-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive + if [ -z "$tag" ]; then + echo "::error::Unable to resolve release tag from ${release_api}" >&2 + exit 1 + fi - - name: Install CMake - run: brew install cmake + echo "Using native release ${repo}@${tag}" - - name: Configure native build - run: cmake -S native -B native/build/macos -DCMAKE_BUILD_TYPE=Release + nupkg_asset=$(echo "$response" | jq -r '.assets[] | select(.name | startswith("ManagedCode.MLXSharp.")) | select(.name | endswith(".nupkg")) | .name' | head -n1) + if [ -z "$nupkg_asset" ] || [ "$nupkg_asset" = "null" ]; then + echo "::error::No ManagedCode.MLXSharp.*.nupkg asset found in release ${tag}" >&2 + exit 1 + fi - - name: Build native library - run: cmake --build native/build/macos --target mlxsharp --config Release + asset_url=$(echo "$response" | jq -r --arg name "$nupkg_asset" '.assets[] | select(.name == $name) | .url') + if [ -z "$asset_url" ] || [ "$asset_url" = "null" ]; then + echo "::error::Failed to resolve download URL for ${nupkg_asset}" >&2 + exit 1 + fi - - name: Package native artifact - run: | - mkdir -p artifacts/native/osx-arm64 - cp native/build/macos/libmlxsharp.dylib artifacts/native/osx-arm64/ - cp native/build/macos/extern/mlx/mlx/backend/metal/kernels/mlx.metallib artifacts/native/osx-arm64/ + mkdir -p work artifacts/native/osx-arm64 artifacts/native/linux-x64 - - name: Upload native artifact + echo "Downloading ${nupkg_asset}" + curl -fsSL -H "Accept: application/octet-stream" -H "Authorization: Bearer ${GITHUB_TOKEN}" "$asset_url" -o work/native.nupkg + + echo "Extracting native runtimes" + unzip -q work/native.nupkg 'runtimes/osx-arm64/native/*' -d work/extract + unzip -q work/native.nupkg 'runtimes/linux-x64/native/*' -d work/extract + + shopt -s nullglob + mac_files=(work/extract/runtimes/osx-arm64/native/*) + linux_files=(work/extract/runtimes/linux-x64/native/*) + + if [ ${#mac_files[@]} -eq 0 ]; then + echo "::error::macOS native assets are missing from ${nupkg_asset}" >&2 + exit 1 + fi + + if [ ${#linux_files[@]} -eq 0 ]; then + echo "::error::Linux native assets are missing from ${nupkg_asset}" >&2 + exit 1 + fi + + cp work/extract/runtimes/osx-arm64/native/* artifacts/native/osx-arm64/ + cp work/extract/runtimes/linux-x64/native/* artifacts/native/linux-x64/ + + echo "Staged native artifacts:" + ls -R artifacts/native + + - name: Upload macOS native artifact uses: actions/upload-artifact@v4 with: name: native-osx-arm64 path: artifacts/native/osx-arm64 + - name: Upload Linux native artifact + uses: actions/upload-artifact@v4 + with: + name: native-linux-x64 + path: artifacts/native/linux-x64 + package-test: needs: - - native-linux - - native-macos + - native-assets runs-on: macos-latest steps: - name: Checkout repository diff --git a/README.md b/README.md index 5473ad7..e5f4103 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ The CMake project vendored from MLX builds MLX and the shim in one go. macOS bui ## CI overview 1. `dotnet-build` (Ubuntu): restores the solution and compiles managed projects. -2. `native-linux` / `native-macos`: compile `libmlxsharp.so` and `libmlxsharp.dylib` in parallel. -3. `package-test` (macOS): downloads both native artifacts, stages them into `src/MLXSharp/runtimes/{rid}/native`, rebuilds, runs the integration tests, and produces NuGet packages. +2. `native-assets` (Ubuntu): downloads the signed native binaries published with the latest MLXSharp release and uploads them as workflow artifacts. +3. `package-test` (macOS): pulls down the staged native artifacts, copies them into `src/MLXSharp/runtimes/{rid}/native`, rebuilds, runs the integration tests, and produces NuGet packages. ## Testing The managed integration tests still piggy-back on `mlx_lm` until the native runner is feature-complete. Bring your own HuggingFace bundle (any MLX-compatible repo) and point `MLXSHARP_MODEL_PATH` to it before running: @@ -117,7 +117,7 @@ dotnet test `MLXSHARP_HF_MODEL_ID` is picked up by the Python smoke test; omit it to fall back to `mlx-community/Qwen1.5-0.5B-Chat-4bit`. -When running locally you can place prebuilt binaries under `libs/native-osx-arm64` (and/or `libs/native-libs`) and a corresponding model bundle under `model/`. The test harness auto-discovers these folders and configures `MLXSHARP_LIBRARY`, `MLXSHARP_MODEL_PATH`, and `MLXSHARP_TOKENIZER_PATH` so you can iterate completely offline. If no native binary is present the tests now download the latest signed `ManagedCode.MLXSharp` NuGet package and stage its `runtimes/{rid}/native/libmlxsharp.{dylib|so}` assets under `libs/native-libs/` automatically. +When running locally you can place prebuilt binaries under `libs/native-osx-arm64` (and/or `libs/native-libs`) and a corresponding model bundle under `model/`. The test harness auto-discovers these folders and configures `MLXSHARP_LIBRARY`, `MLXSHARP_MODEL_PATH`, and `MLXSHARP_TOKENIZER_PATH` so you can iterate completely offline. The integration suite invokes `python -m mlx_lm.generate` with deterministic settings (temperature `0`, seed `42`) and asserts that the generated response for prompts like “Скільки буде 2+2?” contains the correct answer. Test output includes the raw generation transcript so you can verify the model behaviour directly from the CI logs. diff --git a/src/MLXSharp.Tests/ModelIntegrationTests.cs b/src/MLXSharp.Tests/ModelIntegrationTests.cs index 9778307..98c8f8f 100644 --- a/src/MLXSharp.Tests/ModelIntegrationTests.cs +++ b/src/MLXSharp.Tests/ModelIntegrationTests.cs @@ -66,7 +66,7 @@ private static void EnsureAssets() Assert.True(System.IO.Directory.Exists(modelPath), $"Native model bundle not found at '{modelPath}'."); var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); - Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the compiled native library or rely on the official ManagedCode.MLXSharp package that the test harness can download automatically."); + Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the staged native library that ships with the official MLXSharp release."); Assert.True(System.IO.File.Exists(library), $"Native libmlxsharp library not found at '{library}'."); } } diff --git a/src/MLXSharp.Tests/NativeBinaryManager.cs b/src/MLXSharp.Tests/NativeBinaryManager.cs deleted file mode 100644 index 7ebce2b..0000000 --- a/src/MLXSharp.Tests/NativeBinaryManager.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Net.Http; -using System.Text.Json; - -namespace MLXSharp.Tests; - -internal static class NativeBinaryManager -{ - private const string PackageId = "managedcode.mlxsharp"; - private const string BaseUrl = "https://api.nuget.org/v3-flatcontainer"; - - private static readonly object s_sync = new(); - private static bool s_attempted; - private static string? s_cachedPath; - private static string? s_lastError; - - public static bool TryEnsureNativeLibrary(string repoRoot, out string? libraryPath, out string? error) - { - if (!OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) - { - libraryPath = null; - error = "Official native binaries are only published for macOS and Linux."; - return false; - } - - lock (s_sync) - { - if (!string.IsNullOrEmpty(s_cachedPath) && File.Exists(s_cachedPath)) - { - libraryPath = s_cachedPath; - error = null; - return true; - } - - if (s_attempted) - { - libraryPath = s_cachedPath; - error = s_lastError; - return libraryPath is not null; - } - - s_attempted = true; - - try - { - var path = DownloadOfficialBinary(repoRoot); - s_cachedPath = path; - s_lastError = null; - libraryPath = path; - error = null; - return true; - } - catch (Exception ex) - { - s_cachedPath = null; - s_lastError = ex.Message; - libraryPath = null; - error = s_lastError; - return false; - } - } - } - - private static string DownloadOfficialBinary(string repoRoot) - { - var rid = GetRuntimeIdentifier(); - var fileName = OperatingSystem.IsMacOS() ? "libmlxsharp.dylib" : "libmlxsharp.so"; - var nativeDirectory = Path.Combine(repoRoot, "libs", "native-libs", rid); - Directory.CreateDirectory(nativeDirectory); - - var destination = Path.Combine(nativeDirectory, fileName); - if (File.Exists(destination)) - { - return destination; - } - - using var client = new HttpClient(); - var version = ResolvePackageVersion(client); - var packageUrl = $"{BaseUrl}/{PackageId}/{version}/{PackageId}.{version}.nupkg"; - - using var packageStream = client.GetStreamAsync(packageUrl).GetAwaiter().GetResult(); - var tempFile = Path.GetTempFileName(); - try - { - using (var fileStream = File.OpenWrite(tempFile)) - { - packageStream.CopyTo(fileStream); - } - - using var archive = ZipFile.OpenRead(tempFile); - var entryPath = $"runtimes/{rid}/native/{fileName}"; - var entry = archive.GetEntry(entryPath) ?? - throw new InvalidOperationException($"The official package does not contain {entryPath}."); - - entry.ExtractToFile(destination, overwrite: true); - return destination; - } - finally - { - try - { - File.Delete(tempFile); - } - catch - { - // ignore cleanup errors - } - } - } - - private static string ResolvePackageVersion(HttpClient client) - { - var overrideVersion = Environment.GetEnvironmentVariable("MLXSHARP_OFFICIAL_NATIVE_VERSION"); - if (!string.IsNullOrWhiteSpace(overrideVersion)) - { - return overrideVersion.Trim(); - } - - var indexUrl = $"{BaseUrl}/{PackageId}/index.json"; - using var stream = client.GetStreamAsync(indexUrl).GetAwaiter().GetResult(); - using var document = JsonDocument.Parse(stream); - if (!document.RootElement.TryGetProperty("versions", out var versions) || versions.GetArrayLength() == 0) - { - throw new InvalidOperationException("Unable to determine the latest ManagedCode.MLXSharp package version."); - } - - return versions[versions.GetArrayLength() - 1].GetString() - ?? throw new InvalidOperationException("ManagedCode.MLXSharp package version entry was null."); - } - - private static string GetRuntimeIdentifier() - { - if (OperatingSystem.IsMacOS()) - { - return "osx-arm64"; - } - - if (OperatingSystem.IsLinux()) - { - return "linux-x64"; - } - - throw new PlatformNotSupportedException("Unsupported platform for native MLXSharp binaries."); - } -} diff --git a/src/MLXSharp.Tests/TestEnvironment.cs b/src/MLXSharp.Tests/TestEnvironment.cs index 288e0fa..55fcdad 100644 --- a/src/MLXSharp.Tests/TestEnvironment.cs +++ b/src/MLXSharp.Tests/TestEnvironment.cs @@ -25,17 +25,6 @@ public static void EnsureInitialized() private static void ConfigureNativeLibrary(string repoRoot) { - if (NativeBinaryManager.TryEnsureNativeLibrary(repoRoot, out var officialLibrary, out var downloadError) && officialLibrary is not null) - { - ApplyNativeLibrary(officialLibrary); - return; - } - - if (!string.IsNullOrWhiteSpace(downloadError)) - { - Console.Error.WriteLine($"Failed to download official MLXSharp native library: {downloadError}"); - } - var existing = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); if (!string.IsNullOrWhiteSpace(existing) && File.Exists(existing)) { From 292d363f200c09875261c0f7782bfb8d90dbfe21 Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Sat, 25 Oct 2025 12:18:50 +0200 Subject: [PATCH 4/4] Fix native library path conditions --- src/MLXSharp/MLXSharp.csproj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/MLXSharp/MLXSharp.csproj b/src/MLXSharp/MLXSharp.csproj index dd58fe0..8a2a84e 100644 --- a/src/MLXSharp/MLXSharp.csproj +++ b/src/MLXSharp/MLXSharp.csproj @@ -18,15 +18,19 @@ <_RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../..')) $([System.IO.Path]::Combine('$(_RepoRoot)','libs','native-libs')) - $([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','osx-arm64','libmlxsharp.dylib')) + <_MLXSharpMacNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'libmlxsharp.dylib')) + <_MLXSharpMacMetallibFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'mlx.metallib')) + <_MLXSharpLinuxNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'linux-x64', 'libmlxsharp.so')) + $(_MLXSharpMacNativeFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','libmlxsharp.dylib')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','osx-arm64','native','libmlxsharp.dylib')) false $([System.IO.Path]::GetDirectoryName('$(MLXSharpMacNativeDestination)')) + $(_MLXSharpMacMetallibFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','macos','extern','mlx','mlx','backend','metal','kernels','mlx.metallib')) $([System.IO.Path]::Combine('$(MLXSharpMacNativeDestinationDir)','mlx.metallib')) - $([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','linux-x64','libmlxsharp.so')) + $(_MLXSharpLinuxNativeFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','linux','libmlxsharp.so')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','linux-x64','native','libmlxsharp.so')) false