rs-flint 0.1.11

RustScript-native AI inference with Torch, llama.cpp, and stable-diffusion.cpp
name: Release

on:
  push:
    tags:
      - "*"
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag to upload files to"
        required: true
        type: string

permissions:
  contents: write

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

env:
  CARGO_TERM_COLOR: always
  CARGO_TARGET_DIR: target
  LIBTORCH_VERSION: "2.12.1"

jobs:
  build-runner:
    name: Build Flint runner (${{ matrix.platform }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            platform: linux-x86_64
            exe_suffix: ""
            libtorch_device: cpu
          # CUDA 13.1 integrates with Visual Studio 2022, available on this image.
          - os: windows-2022
            platform: windows-x86_64
            exe_suffix: ".exe"
            libtorch_device: cu130
          - os: macos-14
            platform: macos-arm64
            exe_suffix: ""
            libtorch_device: cpu

    steps:
      - name: Checkout Flint
        uses: actions/checkout@v4
        with:
          path: flint

      - name: Checkout RustScript / pd-vm
        uses: actions/checkout@v4
        with:
          repository: rustscript-lang/rustscript
          path: rustscript

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Patch Koharu release dependencies
        working-directory: flint
        run: python .github/scripts/patch-koharu-diffusion.py

      - name: Prefetch platform LibTorch
        working-directory: flint
        shell: python
        env:
          RUNNER_OS: ${{ runner.os }}
        run: |
          import os
          import pathlib
          import shutil
          import tempfile
          import urllib.request
          import zipfile

          version = os.environ["LIBTORCH_VERSION"]
          runner_os = os.environ["RUNNER_OS"]
          if runner_os == "Linux":
              device = "cpu"
              archive_name = f"libtorch-shared-with-deps-{version}%2Bcpu.zip"
              required = (
                  "libgomp.so.1",
                  "libc10.so",
                  "libshm.so",
                  "libtorch_global_deps.so",
                  "libtorch_cpu.so",
                  "libtorch.so",
              )
          elif runner_os == "macOS":
              # Apple Silicon LibTorch includes the MPS backend implemented on Metal.
              device = "cpu"
              archive_name = f"libtorch-macos-arm64-{version}.zip"
              required = (
                  "libomp.dylib",
                  "libc10.dylib",
                  "libshm.dylib",
                  "libtorch_global_deps.dylib",
                  "libtorch_cpu.dylib",
                  "libtorch.dylib",
              )
          elif runner_os == "Windows":
              # GitHub's Windows runner has no CUDA driver. Prefetch CUDA LibTorch
              # explicitly so the released archive can use CUDA on a user's machine.
              device = "cu130"
              archive_name = f"libtorch-win-shared-with-deps-{version}%2B{device}.zip"
              required = (
                  "libiomp5md.dll",
                  "libiompstubs5md.dll",
                  "zlibwapi.dll",
                  "uv.dll",
                  "c10.dll",
                  "c10_cuda.dll",
                  "caffe2_nvrtc.dll",
                  "torch_global_deps.dll",
                  "torch_cpu.dll",
                  "torch_cuda.dll",
                  "shm.dll",
                  "torch.dll",
              )
          else:
              raise RuntimeError(f"unsupported runner OS: {runner_os}")
          url = f"https://download.pytorch.org/libtorch/{device}/{archive_name}"
          target_dir = pathlib.Path(os.environ.get("CARGO_TARGET_DIR", "target"))
          package_dir = target_dir / "store" / "libtorch" / version / device
          lib_dir = package_dir / "libtorch" / "lib"

          if all((lib_dir / name).is_file() for name in required):
              raise SystemExit(0)

          package_dir.parent.mkdir(parents=True, exist_ok=True)
          staging_dir = package_dir.with_name(f"{package_dir.name}.tmp")
          shutil.rmtree(staging_dir, ignore_errors=True)

          archive_path = None
          try:
              with tempfile.NamedTemporaryFile(
                  dir=package_dir.parent, suffix=".zip", delete=False
              ) as archive:
                  archive_path = pathlib.Path(archive.name)
                  request = urllib.request.Request(
                      url, headers={"User-Agent": "flint-release-ci"}
                  )
                  with urllib.request.urlopen(request, timeout=300) as response:
                      shutil.copyfileobj(response, archive, length=1024 * 1024)

              with zipfile.ZipFile(archive_path) as zip_file:
                  damaged = zip_file.testzip()
                  if damaged is not None:
                      raise RuntimeError(f"damaged LibTorch member: {damaged}")
                  zip_file.extractall(staging_dir)

              if not all(
                  (staging_dir / "libtorch" / "lib" / name).is_file()
                  for name in required
              ):
                  raise RuntimeError("LibTorch archive is missing required libraries")

              shutil.rmtree(package_dir, ignore_errors=True)
              staging_dir.replace(package_dir)
          finally:
              if archive_path is not None:
                  archive_path.unlink(missing_ok=True)
              shutil.rmtree(staging_dir, ignore_errors=True)

      - name: Install CUDA Toolkit
        if: runner.os == 'Windows'
        uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76
        with:
          cuda: "13.1.0"

      - name: Build release runner
        working-directory: flint
        env:
          FLINT_LIBTORCH_DEVICE: ${{ matrix.libtorch_device }}
        run: cargo build --release --locked --bin flint

      - name: Package runner
        id: package
        shell: python
        env:
          PLATFORM: ${{ matrix.platform }}
          EXE_SUFFIX: ${{ matrix.exe_suffix }}
          RUNNER_OS: ${{ runner.os }}
          LIBTORCH_VERSION: ${{ env.LIBTORCH_VERSION }}
        run: |
          import os
          import pathlib
          import shutil
          import zipfile

          root = pathlib.Path("flint")
          cargo_target_dir = pathlib.Path(os.environ.get("CARGO_TARGET_DIR", "target"))
          if not cargo_target_dir.is_absolute():
              cargo_target_dir = root / cargo_target_dir
          release_dir = cargo_target_dir / "release"
          package_name = f"flint-{os.environ['PLATFORM']}"
          package_dir = root / "dist" / package_name
          archive = root / "dist" / f"{package_name}.zip"
          exe_suffix = os.environ.get("EXE_SUFFIX", "")

          if package_dir.exists():
              shutil.rmtree(package_dir)
          package_dir.mkdir(parents=True, exist_ok=True)

          binary = release_dir / f"flint{exe_suffix}"
          if not binary.exists():
              raise FileNotFoundError(f"missing built runner binary: {binary}")
          shutil.copy2(binary, package_dir / binary.name)

          native_library_patterns = {
              "Windows": "koharu*.dll",
              "macOS": "libkoharu*.dylib",
              "Linux": "libkoharu*.so",
          }
          pattern = native_library_patterns[os.environ["RUNNER_OS"]]
          for source in release_dir.glob(pattern):
              shutil.copy2(source, package_dir / source.name)

          for extra in ("README.md", "LICENSE"):
              source = root / extra
              if source.exists():
                  shutil.copy2(source, package_dir / extra)

          platform = os.environ["PLATFORM"]
          if platform == "windows-x86_64":
              required = (
                  "flint.exe",
                  "koharu-torch.dll",
              )
          elif platform == "macos-arm64":
              required = (
                  "flint",
                  "libkoharu-torch.dylib",
              )
          else:
              required = ("flint", "libkoharu-torch.so")
          missing = [name for name in required if not (package_dir / name).is_file()]
          if missing:
              raise RuntimeError(f"release package is missing: {', '.join(missing)}")

          all_files = sorted(path for path in package_dir.rglob("*") if path.is_file())
          with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as zf:
              for path in all_files:
                  zf.write(path, path.relative_to(package_dir.parent))

          with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
              output.write(f"archive={archive.as_posix()}\n")

      - name: Upload release asset
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
          files: ${{ steps.package.outputs.archive }}
          fail_on_unmatched_files: true