Skip to content

Handle missing native exports when running tests #55

Handle missing native exports when running tests

Handle missing native exports when running tests #55

Workflow file for this run

name: Build and Release
on:
push:
branches: [ main ]
pull_request:
jobs:
dotnet-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build managed projects
run: dotnet build --configuration Release --no-restore
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: Install tooling
run: |
sudo apt-get update
sudo apt-get install -y jq unzip
- name: Download official native binaries
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo="${NATIVE_RELEASE_REPO}"
if [ -z "$repo" ]; then
echo "::error::NATIVE_RELEASE_REPO must be provided" >&2
exit 1
fi
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
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 // ""')
if [ -z "$tag" ]; then
echo "::error::Unable to resolve release tag from ${release_api}" >&2
exit 1
fi
echo "Using native release ${repo}@${tag}"
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
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
mkdir -p work artifacts/native/osx-arm64 artifacts/native/linux-x64
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-assets
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Show .NET info
run: dotnet --info
- name: Restore dependencies
run: dotnet restore
- name: Build C# projects (initial validation)
run: dotnet build --configuration Release --no-restore
- name: Setup Python for HuggingFace
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install huggingface_hub mlx mlx-lm
- name: Download test model from HuggingFace
run: |
mkdir -p models
hf download mlx-community/Qwen1.5-0.5B-Chat-4bit --local-dir models/Qwen1.5-0.5B-Chat-4bit --local-dir-use-symlinks False
echo "Model files:"
ls -la models/Qwen1.5-0.5B-Chat-4bit/
- name: Download macOS native library
uses: actions/download-artifact@v4
with:
name: native-osx-arm64
path: artifacts/native/osx-arm64
- name: Download Linux native library
uses: actions/download-artifact@v4
with:
name: native-linux-x64
path: artifacts/native/linux-x64
- name: Ensure macOS metallib is available
run: |
set -euo pipefail
metallib_path="artifacts/native/osx-arm64/mlx.metallib"
if [ -f "${metallib_path}" ]; then
echo "Found mlx.metallib in downloaded native artifact."
exit 0
fi
echo "::warning::mlx.metallib missing from native artifact; attempting to source from installed mlx package"
python - <<'PY'
import pathlib
import shutil
import sys
try:
import mlx # type: ignore
except ImportError:
print("::error::The 'mlx' Python package is not installed; cannot locate mlx.metallib.")
sys.exit(1)
kernels_dir = pathlib.Path(mlx.__file__).resolve().parent / "backend" / "metal" / "kernels"
if not kernels_dir.exists():
print(f"::error::Could not find MLX metal kernels directory at {kernels_dir}")
sys.exit(1)
preferred = kernels_dir / "mlx.metallib"
if preferred.exists():
src = preferred
else:
candidates = sorted(kernels_dir.glob("*.metallib"))
if not candidates:
print(f"::error::No metallib files were found under {kernels_dir}")
sys.exit(1)
src = candidates[0]
print(f"::warning::Defaulting to metallib file {src.name}")
dest = pathlib.Path("artifacts/native/osx-arm64/mlx.metallib").resolve()
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
print(f"Copied mlx.metallib from {src} to {dest}")
PY
- name: Stage native libraries in project
run: |
mkdir -p src/MLXSharp/runtimes/osx-arm64/native
cp artifacts/native/osx-arm64/libmlxsharp.dylib src/MLXSharp/runtimes/osx-arm64/native/
cp artifacts/native/osx-arm64/mlx.metallib src/MLXSharp/runtimes/osx-arm64/native/
mkdir -p src/MLXSharp/runtimes/linux-x64/native
cp artifacts/native/linux-x64/libmlxsharp.so src/MLXSharp/runtimes/linux-x64/native/
- name: Build C# projects with native libraries
run: dotnet build --configuration Release --no-restore
- name: Copy native library to test output
run: |
TEST_OUTPUT="src/MLXSharp.Tests/bin/Release/net9.0"
mkdir -p "$TEST_OUTPUT/runtimes/osx-arm64/native"
cp src/MLXSharp/runtimes/osx-arm64/native/libmlxsharp.dylib "$TEST_OUTPUT/runtimes/osx-arm64/native/"
cp src/MLXSharp/runtimes/osx-arm64/native/mlx.metallib "$TEST_OUTPUT/runtimes/osx-arm64/native/"
ls -la "$TEST_OUTPUT/runtimes/osx-arm64/native/"
- name: Run tests
run: |
dotnet test \
--configuration Release \
--no-build \
--logger "trx;LogFileName=TestResults.trx" \
--logger "console;verbosity=detailed" \
--results-directory artifacts/test-results
env:
MLXSHARP_MODEL_PATH: ${{ github.workspace }}/models/Qwen1.5-0.5B-Chat-4bit
- name: Prepare artifact folders
run: |
mkdir -p artifacts/test-results
mkdir -p artifacts/packages
mkdir -p artifacts/native
cp artifacts/native/osx-arm64/libmlxsharp.dylib artifacts/native/
cp artifacts/native/osx-arm64/mlx.metallib artifacts/native/
cp artifacts/native/linux-x64/libmlxsharp.so artifacts/native/
- name: Pack MLXSharp library
run: dotnet pack src/MLXSharp/MLXSharp.csproj --configuration Release --output artifacts/packages -p:MLXSharpMacNativeBinary=$GITHUB_WORKSPACE/artifacts/native/osx-arm64/libmlxsharp.dylib -p:MLXSharpMacMetallibBinary=$GITHUB_WORKSPACE/artifacts/native/osx-arm64/mlx.metallib -p:MLXSharpLinuxNativeBinary=$GITHUB_WORKSPACE/artifacts/native/linux-x64/libmlxsharp.so
- name: Pack MLXSharp.SemanticKernel library
run: dotnet pack src/MLXSharp.SemanticKernel/MLXSharp.SemanticKernel.csproj --configuration Release --output artifacts/packages -p:MLXSharpMacNativeBinary=$GITHUB_WORKSPACE/artifacts/native/osx-arm64/libmlxsharp.dylib -p:MLXSharpMacMetallibBinary=$GITHUB_WORKSPACE/artifacts/native/osx-arm64/mlx.metallib -p:MLXSharpLinuxNativeBinary=$GITHUB_WORKSPACE/artifacts/native/linux-x64/libmlxsharp.so -p:MLXSharpSkipLinuxNativeValidation=true
- name: Verify package contains native libraries
run: |
echo "Checking package contents..."
shopt -s nullglob
packages=(artifacts/packages/*.nupkg)
if [ ${#packages[@]} -eq 0 ]; then
echo "✗ ERROR: No packages were produced"
exit 1
fi
missing=0
for package in "${packages[@]}"; do
echo "Inspecting ${package}"
filename=$(basename "${package}")
case "${filename}" in
MLXSharp.SemanticKernel.*.nupkg)
echo " ↷ Skipping native check for ${filename}"
;;
MLXSharp.*.nupkg)
package_missing=0
if unzip -l "${package}" | grep -q "runtimes/osx-arm64/native/libmlxsharp.dylib"; then
echo " ✓ macOS library present"
else
echo " ✗ macOS library missing"
package_missing=1
fi
if unzip -l "${package}" | grep -q "runtimes/osx-arm64/native/mlx.metallib"; then
echo " ✓ macOS metallib present"
else
echo " ✗ macOS metallib missing"
package_missing=1
fi
if unzip -l "${package}" | grep -q "runtimes/linux-x64/native/libmlxsharp.so"; then
echo " ✓ Linux library present"
else
echo " ✗ Linux library missing"
package_missing=1
fi
if [ ${package_missing} -ne 0 ]; then
unzip -l "${package}"
missing=1
fi
;;
*)
echo " ↷ Skipping native check for ${filename}"
;;
esac
done
if [ $missing -ne 0 ]; then
exit 1
fi
- name: Upload native artifact
uses: actions/upload-artifact@v4
with:
name: native-libs
path: artifacts/native
- name: Upload packages artifact
uses: actions/upload-artifact@v4
with:
name: packages
path: artifacts/packages
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: artifacts/test-results
- name: Publish test results summary
if: always()
run: |
python - <<'PY'
import os
import xml.etree.ElementTree as ET
trx_path = "artifacts/test-results/TestResults.trx"
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if not os.path.exists(trx_path) or not summary_path:
print("No test results found to summarize.")
raise SystemExit(0)
ns = {"trx": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}
root = ET.parse(trx_path).getroot()
counters = root.find(".//trx:ResultSummary/trx:Counters", ns)
with open(summary_path, "a", encoding="utf-8") as summary:
summary.write("### Test Results\n\n")
if counters is not None:
metrics = {
"Total": counters.attrib.get("total", "0"),
"Executed": counters.attrib.get("executed", "0"),
"Passed": counters.attrib.get("passed", "0"),
"Failed": counters.attrib.get("failed", "0"),
"Errors": counters.attrib.get("error", "0"),
"Timeouts": counters.attrib.get("timeout", "0"),
"Aborted": counters.attrib.get("aborted", "0"),
"Inconclusive": counters.attrib.get("inconclusive", "0"),
"Skipped": counters.attrib.get("notExecuted", "0")
}
for label, value in metrics.items():
if value and value != "0":
summary.write(f"- {label}: {value}\n")
else:
summary.write("- No counters were present in the TRX log.\n")
failed_results = root.findall(".//trx:UnitTestResult[@outcome!='Passed']", ns)
if failed_results:
summary.write("\nFailed Tests:\n")
for result in failed_results:
test_name = result.attrib.get("testName", "(unknown test)")
outcome = result.attrib.get("outcome", "Unknown")
summary.write(f"- {test_name} ({outcome})\n")
summary.write("\n")
PY
release:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: package-test
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
# - name: Determine MLX version
# id: mlx_version
# run: |
# git submodule update --init --recursive
# git -C extern/mlx fetch --tags
#
# if MLX_TAG=$(git -C extern/mlx describe --tags --abbrev=0 2>/dev/null); then
# echo "Detected MLX tag: ${MLX_TAG}"
# else
# echo "::warning::Unable to determine MLX tag; falling back to commit hash"
# MLX_TAG="unknown"
# fi
#
# MLX_COMMIT=$(git -C extern/mlx rev-parse --short HEAD)
#
# echo "tag=${MLX_TAG}" >> "$GITHUB_OUTPUT"
# echo "commit=${MLX_COMMIT}" >> "$GITHUB_OUTPUT"
#
# - name: Extract version from Directory.Build.props
# id: version
# run: |
# VERSION=$(grep -oP '<Version>\K[^<]+' Directory.Build.props)
# echo "version=$VERSION" >> $GITHUB_OUTPUT
# echo "Extracted version: $VERSION"
#
# - name: Check if tag exists
# id: check_tag
# run: |
# if git ls-remote --tags origin | grep -q "refs/tags/v${{ steps.version.outputs.version }}"; then
# echo "exists=true" >> $GITHUB_OUTPUT
# echo "Tag v${{ steps.version.outputs.version }} already exists"
# else
# echo "exists=false" >> $GITHUB_OUTPUT
# echo "Tag v${{ steps.version.outputs.version }} does not exist"
# fi
#
# - name: Download package artifacts
# if: steps.check_tag.outputs.exists == 'false'
# uses: actions/download-artifact@v4
# with:
# name: packages
# path: artifacts/packages
#
# - name: Publish to NuGet
# if: steps.check_tag.outputs.exists == 'false'
# run: |
# for package in artifacts/packages/*.nupkg; do
# echo "Publishing $package..."
# dotnet nuget push "$package" \
# --api-key ${{ secrets.NUGET_API_KEY }} \
# --source https://api.nuget.org/v3/index.json \
# --skip-duplicate || true
# done
#
# - name: Create Git tag
# if: steps.check_tag.outputs.exists == 'false'
# run: |
# git config user.name "github-actions[bot]"
# git config user.email "github-actions[bot]@users.noreply.github.com"
# git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}"
# git push origin "v${{ steps.version.outputs.version }}"
#
# - name: Generate release notes
# if: steps.check_tag.outputs.exists == 'false'
# id: release_notes
# env:
# RELEASE_VERSION: ${{ steps.version.outputs.version }}
# MLX_TAG: ${{ steps.mlx_version.outputs.tag }}
# MLX_COMMIT: ${{ steps.mlx_version.outputs.commit }}
# run: |
# PREVIOUS_TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "")
# if [ -z "$PREVIOUS_TAG" ]; then
# COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse)
# else
# COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"- %s (%h)" --reverse)
# fi
#
# echo "## What's Changed" > release_notes.md
# echo "" >> release_notes.md
# echo "$COMMITS" >> release_notes.md
# echo "" >> release_notes.md
# echo "## Upstream MLX" >> release_notes.md
# echo "- Version: ${MLX_TAG}" >> release_notes.md
# echo "- Commit: ${MLX_COMMIT}" >> release_notes.md
# echo "" >> release_notes.md
# echo "## NuGet Packages" >> release_notes.md
# echo "- [MLXSharp v${RELEASE_VERSION}](https://www.nuget.org/packages/MLXSharp/${RELEASE_VERSION})" >> release_notes.md
# echo "- [MLXSharp.SemanticKernel v${RELEASE_VERSION}](https://www.nuget.org/packages/MLXSharp.SemanticKernel/${RELEASE_VERSION})" >> release_notes.md
#
# cat release_notes.md
#
# - name: Create GitHub Release
# if: steps.check_tag.outputs.exists == 'false'
# uses: softprops/action-gh-release@v1
# with:
# tag_name: v${{ steps.version.outputs.version }}
# name: Release v${{ steps.version.outputs.version }}
# body_path: release_notes.md
# files: artifacts/packages/*
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}