Skip to content

Commit 7d019e0

Browse files
committed
Download official native binaries for tests
1 parent ea9e556 commit 7d019e0

File tree

6 files changed

+169
-3
lines changed

6 files changed

+169
-3
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,4 +872,8 @@ FodyWeavers.xsd
872872

873873
# Local model files for testing
874874
Tests/**/LocalModels/
875-
Apps/**/LocalModels/
875+
Apps/**/LocalModels/
876+
# Downloaded native artifacts
877+
libs/native-libs/
878+
ManagedCode.MLXSharp.nupkg
879+

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ dotnet test
117117

118118
`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`.
119119

120-
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.
120+
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.
121121

122122
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.
123123

src/MLXSharp.Tests/ModelIntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static void EnsureAssets()
6666
Assert.True(System.IO.Directory.Exists(modelPath), $"Native model bundle not found at '{modelPath}'.");
6767

6868
var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY");
69-
Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the compiled native library.");
69+
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.");
7070
Assert.True(System.IO.File.Exists(library), $"Native libmlxsharp library not found at '{library}'.");
7171
}
7272
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Net.Http;
5+
using System.Text.Json;
6+
7+
namespace MLXSharp.Tests;
8+
9+
internal static class NativeBinaryManager
10+
{
11+
private const string PackageId = "managedcode.mlxsharp";
12+
private const string BaseUrl = "https://api.nuget.org/v3-flatcontainer";
13+
14+
private static readonly object s_sync = new();
15+
private static bool s_attempted;
16+
private static string? s_cachedPath;
17+
private static string? s_lastError;
18+
19+
public static bool TryEnsureNativeLibrary(string repoRoot, out string? libraryPath, out string? error)
20+
{
21+
if (!OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
22+
{
23+
libraryPath = null;
24+
error = "Official native binaries are only published for macOS and Linux.";
25+
return false;
26+
}
27+
28+
lock (s_sync)
29+
{
30+
if (!string.IsNullOrEmpty(s_cachedPath) && File.Exists(s_cachedPath))
31+
{
32+
libraryPath = s_cachedPath;
33+
error = null;
34+
return true;
35+
}
36+
37+
if (s_attempted)
38+
{
39+
libraryPath = s_cachedPath;
40+
error = s_lastError;
41+
return libraryPath is not null;
42+
}
43+
44+
s_attempted = true;
45+
46+
try
47+
{
48+
var path = DownloadOfficialBinary(repoRoot);
49+
s_cachedPath = path;
50+
s_lastError = null;
51+
libraryPath = path;
52+
error = null;
53+
return true;
54+
}
55+
catch (Exception ex)
56+
{
57+
s_cachedPath = null;
58+
s_lastError = ex.Message;
59+
libraryPath = null;
60+
error = s_lastError;
61+
return false;
62+
}
63+
}
64+
}
65+
66+
private static string DownloadOfficialBinary(string repoRoot)
67+
{
68+
var rid = GetRuntimeIdentifier();
69+
var fileName = OperatingSystem.IsMacOS() ? "libmlxsharp.dylib" : "libmlxsharp.so";
70+
var nativeDirectory = Path.Combine(repoRoot, "libs", "native-libs", rid);
71+
Directory.CreateDirectory(nativeDirectory);
72+
73+
var destination = Path.Combine(nativeDirectory, fileName);
74+
if (File.Exists(destination))
75+
{
76+
return destination;
77+
}
78+
79+
using var client = new HttpClient();
80+
var version = ResolvePackageVersion(client);
81+
var packageUrl = $"{BaseUrl}/{PackageId}/{version}/{PackageId}.{version}.nupkg";
82+
83+
using var packageStream = client.GetStreamAsync(packageUrl).GetAwaiter().GetResult();
84+
var tempFile = Path.GetTempFileName();
85+
try
86+
{
87+
using (var fileStream = File.OpenWrite(tempFile))
88+
{
89+
packageStream.CopyTo(fileStream);
90+
}
91+
92+
using var archive = ZipFile.OpenRead(tempFile);
93+
var entryPath = $"runtimes/{rid}/native/{fileName}";
94+
var entry = archive.GetEntry(entryPath) ??
95+
throw new InvalidOperationException($"The official package does not contain {entryPath}.");
96+
97+
entry.ExtractToFile(destination, overwrite: true);
98+
return destination;
99+
}
100+
finally
101+
{
102+
try
103+
{
104+
File.Delete(tempFile);
105+
}
106+
catch
107+
{
108+
// ignore cleanup errors
109+
}
110+
}
111+
}
112+
113+
private static string ResolvePackageVersion(HttpClient client)
114+
{
115+
var overrideVersion = Environment.GetEnvironmentVariable("MLXSHARP_OFFICIAL_NATIVE_VERSION");
116+
if (!string.IsNullOrWhiteSpace(overrideVersion))
117+
{
118+
return overrideVersion.Trim();
119+
}
120+
121+
var indexUrl = $"{BaseUrl}/{PackageId}/index.json";
122+
using var stream = client.GetStreamAsync(indexUrl).GetAwaiter().GetResult();
123+
using var document = JsonDocument.Parse(stream);
124+
if (!document.RootElement.TryGetProperty("versions", out var versions) || versions.GetArrayLength() == 0)
125+
{
126+
throw new InvalidOperationException("Unable to determine the latest ManagedCode.MLXSharp package version.");
127+
}
128+
129+
return versions[versions.GetArrayLength() - 1].GetString()
130+
?? throw new InvalidOperationException("ManagedCode.MLXSharp package version entry was null.");
131+
}
132+
133+
private static string GetRuntimeIdentifier()
134+
{
135+
if (OperatingSystem.IsMacOS())
136+
{
137+
return "osx-arm64";
138+
}
139+
140+
if (OperatingSystem.IsLinux())
141+
{
142+
return "linux-x64";
143+
}
144+
145+
throw new PlatformNotSupportedException("Unsupported platform for native MLXSharp binaries.");
146+
}
147+
}

src/MLXSharp.Tests/TestEnvironment.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ public static void EnsureInitialized()
2525

2626
private static void ConfigureNativeLibrary(string repoRoot)
2727
{
28+
if (NativeBinaryManager.TryEnsureNativeLibrary(repoRoot, out var officialLibrary, out var downloadError) && officialLibrary is not null)
29+
{
30+
ApplyNativeLibrary(officialLibrary);
31+
return;
32+
}
33+
34+
if (!string.IsNullOrWhiteSpace(downloadError))
35+
{
36+
Console.Error.WriteLine($"Failed to download official MLXSharp native library: {downloadError}");
37+
}
38+
2839
var existing = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY");
2940
if (!string.IsNullOrWhiteSpace(existing) && File.Exists(existing))
3041
{

src/MLXSharp/MLXSharp.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
</ItemGroup>
1717

1818
<PropertyGroup>
19+
<_RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../..'))</_RepoRoot>
20+
<MLXSharpNativeLibsDir Condition="'$(MLXSharpNativeLibsDir)' == ''">$([System.IO.Path]::Combine('$(_RepoRoot)','libs','native-libs'))</MLXSharpNativeLibsDir>
21+
<MLXSharpMacNativeBinary Condition="'$(MLXSharpMacNativeBinary)' == '' and Exists('$([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','osx-arm64','libmlxsharp.dylib'))')">$([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','osx-arm64','libmlxsharp.dylib'))</MLXSharpMacNativeBinary>
1922
<MLXSharpMacNativeBinary Condition="'$(MLXSharpMacNativeBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','libmlxsharp.dylib'))</MLXSharpMacNativeBinary>
2023
<MLXSharpMacNativeDestination>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','osx-arm64','native','libmlxsharp.dylib'))</MLXSharpMacNativeDestination>
2124
<MLXSharpSkipMacNativeValidation Condition="'$(MLXSharpSkipMacNativeValidation)' == ''">false</MLXSharpSkipMacNativeValidation>
2225
<MLXSharpMacNativeDestinationDir>$([System.IO.Path]::GetDirectoryName('$(MLXSharpMacNativeDestination)'))</MLXSharpMacNativeDestinationDir>
2326
<MLXSharpMacMetallibBinary Condition="'$(MLXSharpMacMetallibBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','macos','extern','mlx','mlx','backend','metal','kernels','mlx.metallib'))</MLXSharpMacMetallibBinary>
2427
<MLXSharpMacMetallibDestination Condition="'$(MLXSharpMacMetallibDestination)' == ''">$([System.IO.Path]::Combine('$(MLXSharpMacNativeDestinationDir)','mlx.metallib'))</MLXSharpMacMetallibDestination>
2528

29+
<MLXSharpLinuxNativeBinary Condition="'$(MLXSharpLinuxNativeBinary)' == '' and Exists('$([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','linux-x64','libmlxsharp.so'))')">$([System.IO.Path]::Combine('$(MLXSharpNativeLibsDir)','linux-x64','libmlxsharp.so'))</MLXSharpLinuxNativeBinary>
2630
<MLXSharpLinuxNativeBinary Condition="'$(MLXSharpLinuxNativeBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','linux','libmlxsharp.so'))</MLXSharpLinuxNativeBinary>
2731
<MLXSharpLinuxNativeDestination>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','linux-x64','native','libmlxsharp.so'))</MLXSharpLinuxNativeDestination>
2832
<MLXSharpSkipLinuxNativeValidation Condition="'$(MLXSharpSkipLinuxNativeValidation)' == ''">false</MLXSharpSkipLinuxNativeValidation>

0 commit comments

Comments
 (0)