Skip to content

Commit 319917b

Browse files
committed
feat: add native Windows support (server + windows/amd64 llama-cpp backend)
Native Windows support end to end: LocalAI releases a windows/amd64 server binary and the llama-cpp backend is built natively for windows/amd64 under MSYS2 UCRT64 and packaged as an OCI image tar that LocalAI installs and runs as a native process - no docker daemon or WSL required on the host. Server side: - goreleaser: add windows (amd64/arm64) to the release targets - Makefile: download the win64 protoc zip and rename protoc.exe to protoc, resolve code-gen plugins via --plugin instead of PATH, force SHELL=sh and name the binary local-ai.exe on Windows, ignore protoc.exe - build-test.yaml: add a native windows-latest build gate that installs GNU make via choco, adds Git for Windows' usr/bin to PATH and builds with CGO_ENABLED=0 - pkg/downloader: close the write handle before removing or renaming the partial so Windows file locks do not break resume and error paths; guard the POSIX-permission and symlink tests on non-Windows - tests: Windows guards and path fixes for core/gallery, video_internal, loader and the testcontainers database setup Backend side: - scripts/build/llama-cpp-windows.sh: builds gRPC from source (pinned v1.59.0, with mingw-w64 fixes for c-ares, boringssl and zlib), then the three llama.cpp variants (cpu-all with GGML_CPU_ALL_VARIANTS + Vulkan, rpc, and the AVX-off fallback), bundles the mingw runtime DLLs and ships an OCI tar via local-ai util create-oci-image. Re-runnable and auto-dispatchs into MSYS2 when launched from Git for Windows' bash. - backend/cpp/llama-cpp/run.ps1: PowerShell launcher (mirrors run.sh) that pkg/model/process.go starts on Windows. - backend/index.yaml: windows/amd64 backend entry and variants. - pkg/system/capabilities.go: windows engine preference rules so the gallery picks the native build on Windows hosts. - .github/backend-matrix.yml + backend_build_windows.yml: windows matrix entries and a reusable windows build workflow; backend.yml and backend_pr.yml wire the windows backend jobs (build on PR, publish on master). - docs: getting-started/windows.md plus related page updates. JOBS in the build script honors an override so memory-limited hosts can build with reduced parallelism. Signed-off-by: Lionel Colaso <lionelcolaso@outlook.com>
1 parent f129a76 commit 319917b

35 files changed

Lines changed: 1793 additions & 87 deletions

.github/backend-matrix.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# CUDA / ROCm / SYCL / Vulkan variants).
1212
# - macOS -> the `includeDarwin:` matrix (Apple Silicon / arm64; Metal where
1313
# the engine supports it, otherwise a native arm64 CPU build).
14+
# - Windows -> the `includeWindows:` matrix (x86_64 / amd64; native builds
15+
# under MSYS2, no WSL/Docker — see backend_build_windows.yml).
1416
#
1517
# New backends must target EVERY OS they can build for, not just Linux. A backend
1618
# listed only under `include:` is silently unavailable on macOS even when its code
@@ -24,6 +26,12 @@
2426
# `metal:` capability + `metal-<backend>` image entries, a `run.sh` Darwin/DYLD
2527
# branch for C/C++ backends, and the inferBackendPathDarwin case in
2628
# scripts/lib/backend-filter.mjs so the path filter actually builds it).
29+
#
30+
# Windows builds are bespoke for now: every entry builds via a per-backend make
31+
# target + MSYS2 build script (see scripts/build/llama-cpp-windows.sh), the
32+
# index.yaml `windows:` capability + `windows-<backend>` image entries, a
33+
# run-windows launcher next to run.sh, and the inferBackendPathWindows case in
34+
# scripts/lib/backend-filter.mjs.
2735

2836
# Linux matrix (consumed by backend-jobs).
2937
include:
@@ -6540,3 +6548,11 @@ includeDarwin:
65406548
- backend: "ds4"
65416549
tag-suffix: "-metal-darwin-arm64-ds4"
65426550
lang: "go"
6551+
6552+
# Windows matrix (consumed by backend-jobs-windows).
6553+
# Native windows/amd64 builds under MSYS2 — no WSL, no Docker. Each entry builds
6554+
# via a bespoke make target + scripts/build/<backend>-windows.sh.
6555+
includeWindows:
6556+
- backend: "llama-cpp"
6557+
tag-suffix: "-windows-amd64-llama-cpp"
6558+
lang: "go"

.github/workflows/backend.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ jobs:
3434
outputs:
3535
matrix-multiarch: ${{ steps.set-matrix.outputs['matrix-multiarch'] }}
3636
matrix-darwin: ${{ steps.set-matrix.outputs['matrix-darwin'] }}
37+
matrix-windows: ${{ steps.set-matrix.outputs['matrix-windows'] }}
3738
merge-matrix-multiarch: ${{ steps.set-matrix.outputs['merge-matrix-multiarch'] }}
3839
has-backends-multiarch: ${{ steps.set-matrix.outputs['has-backends-multiarch'] }}
3940
has-backends-darwin: ${{ steps.set-matrix.outputs['has-backends-darwin'] }}
41+
has-backends-windows: ${{ steps.set-matrix.outputs['has-backends-windows'] }}
4042
has-merges-multiarch: ${{ steps.set-matrix.outputs['has-merges-multiarch'] }}
4143
# Single-arch backends are sharded across SINGLEARCH_SHARDS matrix jobs to
4244
# stay under GitHub's 256-jobs-per-matrix limit (see changed-backends.js).
@@ -368,3 +370,23 @@ jobs:
368370
strategy:
369371
fail-fast: false
370372
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix-darwin) }}
373+
374+
backend-jobs-windows:
375+
needs: generate-matrix
376+
if: needs.generate-matrix.outputs.has-backends-windows == 'true'
377+
uses: ./.github/workflows/backend_build_windows.yml
378+
with:
379+
backend: ${{ matrix.backend }}
380+
build-type: ${{ matrix.build-type }}
381+
go-version: "1.25.x"
382+
tag-suffix: ${{ matrix.tag-suffix }}
383+
lang: ${{ matrix.lang || 'go' }}
384+
runs-on: "windows-latest"
385+
secrets:
386+
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
387+
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
388+
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
389+
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
390+
strategy:
391+
fail-fast: false
392+
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix-windows) }}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
---
2+
name: 'build windows backend container images (reusable)'
3+
4+
on:
5+
workflow_call:
6+
inputs:
7+
backend:
8+
description: 'Backend to build'
9+
required: true
10+
type: string
11+
build-type:
12+
description: 'Build type'
13+
default: ''
14+
type: string
15+
lang:
16+
description: 'Programming language (e.g. go)'
17+
default: 'python'
18+
type: string
19+
go-version:
20+
description: 'Go version to use'
21+
default: '1.25.x'
22+
type: string
23+
tag-suffix:
24+
description: 'Tag suffix for the built image'
25+
required: true
26+
type: string
27+
runs-on:
28+
description: 'Runner to use'
29+
default: 'windows-latest'
30+
type: string
31+
secrets:
32+
dockerUsername:
33+
required: false
34+
dockerPassword:
35+
required: false
36+
quayUsername:
37+
required: true
38+
quayPassword:
39+
required: true
40+
41+
jobs:
42+
windows-backend-build:
43+
runs-on: ${{ inputs.runs-on }}
44+
strategy:
45+
matrix:
46+
go-version: ['${{ inputs.go-version }}']
47+
env:
48+
# Every CMake variant below (gRPC + the three llama.cpp variants) compiles
49+
# the same source trees with overlapping flags; ccache dedupes them.
50+
# CCACHE_DIR is set in a run step (below), not here: a job-level env value
51+
# is used verbatim, and MSYS2's $HOME must be expanded at runtime so it
52+
# matches the ~/.cache/ccache path handed to actions/cache.
53+
CMAKE_ARGS: "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
54+
steps:
55+
- name: Clone
56+
uses: actions/checkout@v7
57+
with:
58+
submodules: true
59+
60+
- name: Setup Go ${{ matrix.go-version }}
61+
uses: actions/setup-go@v5
62+
with:
63+
go-version: ${{ matrix.go-version }}
64+
# Caches ~/go/pkg/mod and %LOCALAPPDATA%\go-build keyed on go.sum.
65+
cache: true
66+
67+
- name: Display Go version
68+
run: go version
69+
70+
# ---- MSYS2 toolchain ----
71+
# Native windows builds need a mingw gcc toolchain; windows-latest ships
72+
# none by default (build-test-windows works only because LocalAI itself
73+
# builds with CGO_ENABLED=0). UCRT64 matches the /ucrt64/bin DLL bundling
74+
# in scripts/build/llama-cpp-windows.sh. `perl` is required by gRPC's
75+
# third_party/openssl build (openssl's Configure is a perl script).
76+
# `install` is a folded scalar (>-): every line folds into the pacman
77+
# command, so no `#` comments may live inside it - they would become
78+
# literal package names ("target not found: Vulkan:"). Vulkan packages:
79+
# headers + loader (ucrt64) for FindVulkan, and the mingw64 shaderc
80+
# build for glslc (no ucrt64 shaderc exists; glslc is a standalone tool
81+
# so the prefix split is irrelevant). The loader's vulkan-1.dll is
82+
# bundled next to the image's DLLs; ggml-vulkan loads it dynamically, so
83+
# it is never a hard import. spirv-headers provides the cmake config
84+
# ggml-vulkan requires (find_package(SPIRV-Headers CONFIG REQUIRED)).
85+
- name: Set up MSYS2
86+
uses: msys2/setup-msys2@v2
87+
with:
88+
msystem: UCRT64
89+
update: false
90+
install: >-
91+
git
92+
make
93+
cmake
94+
mingw-w64-ucrt-x86_64-cmake
95+
ninja
96+
pkg-config
97+
perl
98+
patch
99+
unzip
100+
curl
101+
mingw-w64-ucrt-x86_64-gcc
102+
mingw-w64-ucrt-x86_64-gcc-libs
103+
mingw-w64-ucrt-x86_64-binutils
104+
mingw-w64-ucrt-x86_64-ccache
105+
mingw-w64-ucrt-x86_64-vulkan-headers
106+
mingw-w64-ucrt-x86_64-vulkan-loader
107+
mingw-w64-ucrt-x86_64-spirv-headers
108+
mingw-w64-x86_64-shaderc
109+
110+
# Run steps that need bash/msys2 tools (make, ccache, grep) declare
111+
# `shell: msys2 {0}` individually instead of a job-level defaults block:
112+
# the msys2 shell only exists once the Set up MSYS2 step above has run.
113+
- name: Set CCACHE_DIR
114+
shell: msys2 {0}
115+
run: echo "CCACHE_DIR=$HOME/.cache/ccache" >> "$GITHUB_ENV"
116+
117+
- name: Display toolchain versions
118+
shell: msys2 {0}
119+
run: |
120+
gcc --version | head -1
121+
cmake --version | head -1
122+
make --version | head -1
123+
ccache --version | head -1
124+
125+
# ---- ccache for llama.cpp CMake builds ----
126+
# Same shape as the Darwin workflow: key on the pinned LLAMA_VERSION so a
127+
# pin bump invalidates cleanly; restore-keys fall back to the latest entry
128+
# for the same pin so unchanged TUs stay warm.
129+
- name: Compute llama.cpp version
130+
if: inputs.backend == 'llama-cpp'
131+
id: llama-version
132+
shell: msys2 {0}
133+
run: |
134+
version=$(grep '^LLAMA_VERSION' backend/cpp/llama-cpp/Makefile | head -1 | cut -d= -f2 | cut -d'?' -f1 | tr -d ' ')
135+
echo "version=${version}" >> "$GITHUB_OUTPUT"
136+
137+
- name: Restore ccache
138+
if: inputs.backend == 'llama-cpp'
139+
id: ccache-cache
140+
uses: actions/cache/restore@v6
141+
with:
142+
path: ~/.cache/ccache
143+
key: ccache-llama-windows-amd64-${{ steps.llama-version.outputs.version }}-${{ github.run_id }}
144+
restore-keys: |
145+
ccache-llama-windows-amd64-${{ steps.llama-version.outputs.version }}-
146+
147+
# Only llama-cpp has a windows build path today - the matrix's
148+
# includeWindows section lists exactly this backend. Fail loudly rather
149+
# than upload an empty tar if a future entry dispatches here without a
150+
# build step of its own. Keep in sync with WINDOWS_BESPOKE_BUILDERS in
151+
# scripts/lib/backend-filter.mjs.
152+
- name: Check backend is supported
153+
if: inputs.backend != 'llama-cpp'
154+
run: |
155+
echo "::error::no windows build path for backend '${{ inputs.backend }}'"
156+
exit 1
157+
158+
# The msys2 shell below resets PATH, so the Go toolchain setup-go put on
159+
# the runner PATH is invisible to it (and setup-go only exports GOROOT
160+
# for Go < 1.9). Resolve the install dir with the default shell, where
161+
# `go` is reachable, and hand it to the script to prepend.
162+
- name: Resolve Go toolchain path
163+
id: go-toolchain
164+
if: inputs.backend == 'llama-cpp'
165+
shell: bash
166+
run: echo "root=$(go env GOROOT)" >> "$GITHUB_OUTPUT"
167+
168+
- name: Build ${{ inputs.backend }} (llama-cpp)
169+
if: inputs.backend == 'llama-cpp'
170+
shell: msys2 {0}
171+
env:
172+
GO_TOOLCHAIN_ROOT: ${{ steps.go-toolchain.outputs.root }}
173+
run: |
174+
make backends/llama-cpp-windows
175+
176+
- name: ccache stats
177+
if: inputs.backend == 'llama-cpp'
178+
shell: msys2 {0}
179+
run: ccache -s
180+
181+
- name: Save ccache
182+
if: inputs.backend == 'llama-cpp' && github.event_name != 'pull_request'
183+
uses: actions/cache/save@v6
184+
with:
185+
path: ~/.cache/ccache
186+
key: ccache-llama-windows-amd64-${{ steps.llama-version.outputs.version }}-${{ github.run_id }}
187+
188+
- name: Upload ${{ inputs.backend }}.tar
189+
uses: actions/upload-artifact@v7
190+
with:
191+
name: ${{ inputs.backend }}-tar
192+
path: backend-images/${{ inputs.backend }}.tar
193+
194+
windows-backend-publish:
195+
needs: windows-backend-build
196+
if: github.event_name != 'pull_request'
197+
runs-on: ubuntu-latest
198+
steps:
199+
- name: Download ${{ inputs.backend }}.tar
200+
uses: actions/download-artifact@v8
201+
with:
202+
name: ${{ inputs.backend }}-tar
203+
path: .
204+
205+
- name: Install crane
206+
run: |
207+
curl -L https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar -xz
208+
sudo mv crane /usr/local/bin/
209+
210+
- name: Log in to DockerHub
211+
run: |
212+
echo "${{ secrets.dockerPassword }}" | crane auth login docker.io -u "${{ secrets.dockerUsername }}" --password-stdin
213+
214+
- name: Log in to quay.io
215+
run: |
216+
echo "${{ secrets.quayPassword }}" | crane auth login quay.io -u "${{ secrets.quayUsername }}" --password-stdin
217+
218+
- name: Docker meta
219+
id: meta
220+
uses: docker/metadata-action@v6
221+
with:
222+
images: |
223+
localai/localai-backends
224+
tags: |
225+
type=ref,event=branch
226+
type=semver,pattern={{raw}}
227+
type=sha
228+
flavor: |
229+
latest=auto
230+
suffix=${{ inputs.tag-suffix }},onlatest=true
231+
232+
- name: Docker meta
233+
id: quaymeta
234+
uses: docker/metadata-action@v6
235+
with:
236+
images: |
237+
quay.io/go-skynet/local-ai-backends
238+
tags: |
239+
type=ref,event=branch
240+
type=semver,pattern={{raw}}
241+
type=sha
242+
flavor: |
243+
latest=auto
244+
suffix=${{ inputs.tag-suffix }},onlatest=true
245+
246+
- name: Push Docker image (DockerHub)
247+
run: |
248+
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do
249+
crane push ${{ inputs.backend }}.tar $tag
250+
done
251+
252+
- name: Push Docker image (Quay)
253+
run: |
254+
for tag in $(echo "${{ steps.quaymeta.outputs.tags }}" | tr ',' '\n'); do
255+
crane push ${{ inputs.backend }}.tar $tag
256+
done

.github/workflows/backend_pr.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ jobs:
1313
outputs:
1414
matrix-multiarch: ${{ steps.set-matrix.outputs['matrix-multiarch'] }}
1515
matrix-darwin: ${{ steps.set-matrix.outputs['matrix-darwin'] }}
16+
matrix-windows: ${{ steps.set-matrix.outputs['matrix-windows'] }}
1617
merge-matrix-multiarch: ${{ steps.set-matrix.outputs['merge-matrix-multiarch'] }}
1718
has-backends-multiarch: ${{ steps.set-matrix.outputs['has-backends-multiarch'] }}
1819
has-backends-darwin: ${{ steps.set-matrix.outputs['has-backends-darwin'] }}
20+
has-backends-windows: ${{ steps.set-matrix.outputs['has-backends-windows'] }}
1921
has-merges-multiarch: ${{ steps.set-matrix.outputs['has-merges-multiarch'] }}
2022
# Single-arch backends are sharded across SINGLEARCH_SHARDS matrix jobs to
2123
# stay under GitHub's 256-jobs-per-matrix limit (see changed-backends.js).
@@ -292,3 +294,21 @@ jobs:
292294
strategy:
293295
fail-fast: true
294296
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix-darwin) }}
297+
298+
backend-jobs-windows:
299+
needs: generate-matrix
300+
uses: ./.github/workflows/backend_build_windows.yml
301+
if: needs.generate-matrix.outputs.has-backends-windows == 'true'
302+
with:
303+
backend: ${{ matrix.backend }}
304+
build-type: ${{ matrix.build-type }}
305+
go-version: "1.25.x"
306+
tag-suffix: ${{ matrix.tag-suffix }}
307+
lang: ${{ matrix.lang || 'go' }}
308+
runs-on: "windows-latest"
309+
secrets:
310+
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
311+
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
312+
strategy:
313+
fail-fast: true
314+
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix-windows) }}

.github/workflows/build-test.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,35 @@ jobs:
5353
- name: Run GoReleaser
5454
run: |
5555
make ${{ github.event_name == 'pull_request' && 'dev-dist-single' || 'dev-dist' }}
56+
# Windows ships no GNU toolchain on the default runner: make is not
57+
# installed there and Git for Windows does not ship it (it bundles sh, uname,
58+
# unzip, grep, awk, sed only), so it is installed via Chocolatey below. The
59+
# Makefile finds Git for Windows' sh itself and runs recipes through it (see
60+
# the SHELL setup at the top of the Makefile), so the explicit usr/bin PATH
61+
# step below is belt-and-suspenders for environments where make must locate
62+
# sh before the makefile is read. The server builds with CGO_ENABLED=0, so no
63+
# mingw/gcc is needed; the Makefile downloads protoc and installs the Go
64+
# protobuf plugins itself (see the protoc/protogen-go targets).
65+
build-test-windows:
66+
runs-on: windows-latest
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@v7
70+
with:
71+
fetch-depth: 0
72+
- name: Set up Go
73+
uses: actions/setup-go@v5
74+
with:
75+
go-version: 1.25
76+
- name: Add Git usr/bin to PATH
77+
run: |
78+
echo "$env:ProgramFiles\Git\usr\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
79+
- name: Install GNU Make
80+
run: choco install make -y
81+
- name: Build LocalAI (CGO disabled, mirrors release config)
82+
env:
83+
CGO_ENABLED: '0'
84+
run: make build
5685
launcher-build-darwin:
5786
runs-on: macos-latest
5887
steps:

0 commit comments

Comments
 (0)