Handle missing native exports when running tests #55
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |