diff --git a/.github/workflows/launcher-appimage.yaml b/.github/workflows/launcher-appimage.yaml new file mode 100644 index 000000000000..a6891325efa6 --- /dev/null +++ b/.github/workflows/launcher-appimage.yaml @@ -0,0 +1,73 @@ +name: Launcher AppImage + +on: + workflow_call: + inputs: + publish: + type: boolean + default: false + workflow_dispatch: + pull_request: + paths: + - 'cmd/launcher/**' + - 'pkg/**' + - 'go.mod' + - 'go.sum' + - 'Makefile' + - 'contrib/appimage/**' + - '.github/workflows/launcher-appimage.yaml' + - '.github/workflows/release.yaml' + +permissions: + contents: read + +jobs: + appimage: + # Native CGO build; glibc 2.35 is the supported minimum. + runs-on: ubuntu-22.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Install Fyne and validation dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential file pkg-config libgl1-mesa-dev xorg-dev libxkbcommon-dev desktop-file-utils xvfb xauth + - name: Check packaging contracts + run: make test-launcher-appimage + - name: Build native amd64 AppImage + run: make build-launcher-appimage APPIMAGE_ARCH=amd64 + - name: Extract and smoke without FUSE + run: bash contrib/appimage/smoke.sh + - uses: actions/upload-artifact@v7 + with: + name: launcher-appimage-x86_64 + path: | + dist/LocalAI-Launcher-x86_64.AppImage + dist/LocalAI-Launcher-x86_64.AppImage.sha256 + if-no-files-found: error + + publish: + needs: appimage + if: inputs.publish && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + # Download only: never execute repository code with the write token. + - uses: actions/download-artifact@v4 + with: + name: launcher-appimage-x86_64 + path: dist + - name: Attach to existing version release + env: + GH_TOKEN: ${{ github.token }} + # The caller waits for GoReleaser. Never create or replace its release. + run: | + gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" + gh release upload "$GITHUB_REF_NAME" dist/LocalAI-Launcher-x86_64.AppImage dist/LocalAI-Launcher-x86_64.AppImage.sha256 --clobber --repo "$GITHUB_REPOSITORY" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b1b585c4b7a3..a7d0517f9370 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.23 + go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: @@ -39,7 +39,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.23 + go-version-file: go.mod - name: Import signing certificate env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} @@ -69,7 +69,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.23 + go-version-file: go.mod - name: Build launcher for Linux run: | sudo apt-get update @@ -79,3 +79,11 @@ jobs: uses: softprops/action-gh-release@v3 with: files: ./local-ai-launcher-linux.tar.xz + + launcher-appimage: + needs: goreleaser + permissions: + contents: write + uses: ./.github/workflows/launcher-appimage.yaml + with: + publish: true diff --git a/Makefile b/Makefile index 7c05fda6dd3d..d4264379ef54 100644 --- a/Makefile +++ b/Makefile @@ -1701,3 +1701,11 @@ release-launcher-darwin: notarize-launcher-darwin build-launcher-linux: cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux --app-version $(LAUNCHER_APP_VERSION) && mv LocalAI.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz + +# Thin native launcher only; retains the first-run LocalAI runtime downloader. +.PHONY: build-launcher-appimage test-launcher-appimage +build-launcher-appimage: + APPIMAGE_ARCH=$(or $(APPIMAGE_ARCH),amd64) bash contrib/appimage/build.sh + +test-launcher-appimage: + python3 -m unittest discover -s contrib/appimage -p 'test_*.py' -v diff --git a/contrib/appimage/AppRun b/contrib/appimage/AppRun new file mode 100755 index 000000000000..984cb462e56b --- /dev/null +++ b/contrib/appimage/AppRun @@ -0,0 +1,7 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +set -eu +# Resolve the image without changing the caller's working directory or library +# search path: both are inherited by the separately downloaded LocalAI server. +HERE=$(dirname "$(readlink -f "$0")") +exec "$HERE/usr/bin/local-ai-launcher" "$@" diff --git a/contrib/appimage/README.md b/contrib/appimage/README.md new file mode 100644 index 000000000000..d6898771bee8 --- /dev/null +++ b/contrib/appimage/README.md @@ -0,0 +1,69 @@ +# Linux launcher AppImage + +This packages only the Go/Fyne launcher. It does not bundle the LocalAI server, +models, a worker interface, or an AppImage updater. The existing runtime +downloader remains responsible for installing LocalAI on first use. + +## Build + +Use native **Linux amd64 (x86_64)**, Go from `go.mod`, GCC, GNU Make, curl, +`file`, pkg-config and coreutils. On Ubuntu 22.04 the development packages are: + +```sh +sudo apt-get install build-essential file pkg-config libgl1-mesa-dev xorg-dev libxkbcommon-dev +make test-launcher-appimage +make build-launcher-appimage APPIMAGE_ARCH=amd64 +``` + +Output: `dist/LocalAI-Launcher-x86_64.AppImage` and its `.sha256` file. +Only amd64 is supported, matching the existing Linux launcher release job. +ARM64 requires a native CGO builder and separate runtime/tool pins; it is not +silently cross-compiled or labelled as supported. + +The release builder is Ubuntu 22.04 (glibc 2.35). Fyne uses CGO, OpenGL and X11; +this is **not a static or fully self-contained executable**. We intentionally +use host graphics libraries rather than bundle drivers or glibc, and never set +`LD_LIBRARY_PATH`: the downloaded server must not inherit launcher libraries. +Ubuntu 22.04+ desktop systems are the compatibility target. Other distributions +need glibc >= 2.35, libGL, libX11, libXcursor, libXrandr, libXinerama, libXi and +libXxf86vm plus their dependencies. Wayland needs XWayland. A working tray is +recommended because the existing launcher starts in the tray. The separately +downloaded server/backends have their own system requirements. + +`build.sh` pins appimagetool 1.9.1 and type-2 runtime 20251108 by release and +SHA-256. Both hashes come from the upstream release asset digests. Cached files +are verified on every build. To update, review the upstream release and replace +the URL and hash together. The build extracts appimagetool explicitly, so CI +needs no FUSE. No linuxdeploy or unpinned Fyne packaging tool is used. + +AppDir contents and timestamps are controlled, Go paths/build IDs are stripped, +and `SOURCE_DATE_EPOCH` defaults to the source commit timestamp. Repeatable +builds require the same Go/compiler/system packages; this does not promise +bit-identical output across arbitrary distributions. `dist/appimage/ldd.txt` +records the actual dynamic dependency tree of each build. + +## Verification and release + +```sh +make test-launcher-appimage +# After building; requires desktop-file-utils, xvfb and xauth: +bash contrib/appimage/smoke.sh +``` + +The smoke extracts the real image, validates metadata/dependencies, then runs +its AppRun under Xvfb with an isolated home and server auto-start disabled. It +requires the existing “No LocalAI installation found” initialization milestone, +created model/backend directories, and survival until the 15-second timeout. +Initialization errors, early exits and forced kills after a 5-second termination +grace fail the check. This is a **limited startup smoke**, not proof of a working +UI event loop: the tray-only launcher exposes no UI heartbeat. It does not test +tray interaction, rendering, a model or GPU workload. Logs are in +`dist/appimage/smoke/`. + +The reusable workflow builds and smokes on relevant pull requests or manual +dispatch with read-only repository permissions and no persisted checkout +credentials. Version tags call it after GoReleaser succeeds. A separate job, +gated on both a version-tag push and `publish`, downloads the tested artifact +and uploads it to that existing release with write permission. That job does +not check out or execute repository code. Existing tar.xz, +DMG and server artifacts remain unchanged. diff --git a/contrib/appimage/build.sh b/contrib/appimage/build.sh new file mode 100755 index 000000000000..b2d793acc472 --- /dev/null +++ b/contrib/appimage/build.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +cd "$root" +# Match the existing Linux launcher release (ubuntu x86_64). CGO makes this a +# native build, not a Go-only cross compile. +arch=${APPIMAGE_ARCH:-amd64} +if [[ "$arch" != amd64 ]]; then + echo 'AppImage supports only amd64; use a native x86_64 Linux builder.' >&2 + exit 1 +fi +[[ $(uname -s) == Linux && $(uname -m) == x86_64 ]] || { + echo 'AppImage requires a native x86_64 Linux builder.' >&2; exit 1; +} +# appimagetool needs file even after the Go compilation succeeds. +command -v file >/dev/null 2>&1 || { + echo 'Missing file command required by appimagetool; install the file package.' >&2; exit 1; +} +pkg-config --exists gl x11 xcursor xrandr xinerama xi xxf86vm || { + echo 'Missing Fyne development libraries; see contrib/appimage/README.md.' >&2; exit 1; +} +build="$root/dist/appimage" +tools="$build/tools" +appdir="$build/AppDir" +mkdir -p "$tools" "$build/tmp" +export TMPDIR="$build/tmp" +export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct)} +fetch() { + local url=$1 file=$2 sha=$3 + if [[ ! -f "$file" ]]; then + curl --fail --location --retry 3 "$url" -o "$file.part" + mv "$file.part" "$file" + fi + printf '%s %s\n' "$sha" "$file" | sha256sum --check --status || { + echo "Checksum mismatch: $file (remove it before retrying)" >&2; exit 1; + } + chmod +x "$file" +} +fetch https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage \ + "$tools/appimagetool" ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 +fetch https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-x86_64 \ + "$tools/runtime-x86_64" 2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d +rm -rf "$appdir" "$tools/squashfs-root" +mkdir -p "$appdir/usr/bin" +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath -buildvcs=false \ + -ldflags='-s -w -buildid=' -o "$appdir/usr/bin/local-ai-launcher" ./cmd/launcher +# Do not ship graphics drivers, glibc, or inject a library path into the server. +ldd "$appdir/usr/bin/local-ai-launcher" | tee "$build/ldd.txt" +if grep -q 'not found' "$build/ldd.txt"; then exit 1; fi +install -m755 contrib/appimage/AppRun "$appdir/AppRun" +install -m644 contrib/appimage/local-ai-launcher.desktop "$appdir/" +install -m644 cmd/launcher/logo.png "$appdir/local-ai-launcher.png" +ln -s local-ai-launcher.png "$appdir/.DirIcon" +install -Dm644 LICENSE "$appdir/usr/share/licenses/local-ai-launcher/LICENSE" +find "$appdir" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} + +# Explicit extraction avoids FUSE even on unprivileged CI builders. +(cd "$tools" && ./appimagetool --appimage-extract >/dev/null) +ARCH=x86_64 "$tools/squashfs-root/AppRun" --runtime-file "$tools/runtime-x86_64" \ + "$appdir" "$root/dist/LocalAI-Launcher-x86_64.AppImage" +(cd dist && sha256sum LocalAI-Launcher-x86_64.AppImage > LocalAI-Launcher-x86_64.AppImage.sha256) diff --git a/contrib/appimage/local-ai-launcher.desktop b/contrib/appimage/local-ai-launcher.desktop new file mode 100644 index 000000000000..36073e3d38ff --- /dev/null +++ b/contrib/appimage/local-ai-launcher.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=LocalAI Launcher +Comment=Download and manage your local LocalAI server +Exec=local-ai-launcher +Icon=local-ai-launcher +Terminal=false +Categories=Development;Utility; diff --git a/contrib/appimage/smoke.sh b/contrib/appimage/smoke.sh new file mode 100755 index 000000000000..ef16e176729d --- /dev/null +++ b/contrib/appimage/smoke.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +image="$root/dist/LocalAI-Launcher-x86_64.AppImage" +work="$root/dist/appimage/smoke" +rm -rf "$work" +mkdir -p "$work/home" "$work/tmp" +export TMPDIR="$work/tmp" +(cd "$work" && "$image" --appimage-extract >/dev/null) +app="$work/squashfs-root" +test -x "$app/AppRun" +test -f "$app/local-ai-launcher.desktop" +test -f "$app/local-ai-launcher.png" +test ! -e "$app/usr/bin/local-ai" +desktop-file-validate "$app/local-ai-launcher.desktop" +ldd "$app/usr/bin/local-ai-launcher" > "$work/ldd.txt" +if grep -q 'not found' "$work/ldd.txt"; then + cat "$work/ldd.txt" >&2 + exit 1 +fi +# Prevent Initialize from downloading or starting the server during the smoke. +printf '%s\n' '{"auto_start_server":false,"show_welcome":false}' > "$work/config.json" +mkdir -p "$work/home/.localai" +cp "$work/config.json" "$work/home/.localai/launcher.json" +set +e +HOME="$work/home" XDG_CONFIG_HOME="$work/home/.config" LIBGL_ALWAYS_SOFTWARE=1 \ + timeout --kill-after=5s 15s xvfb-run -a "$app/AppRun" > "$work/launch.log" 2>&1 +status=$? +set -e +# This is a bounded startup check, not a UI event-loop heartbeat. +# Early exit (even code 0) and forced SIGKILL are failures. +[[ $status == 124 ]] +if grep -Ei 'panic:|fatal error:|failed to initialize GLFW|Failed to initialize launcher:|APIUnavailable' "$work/launch.log"; then + exit 1 +fi +# On a fresh home this milestone follows config loading and startup setup. +# Survival alone could accept a process stalled before initialization. +grep -Fq 'No LocalAI installation found' "$work/launch.log" +test -d "$work/home/.localai/models" +test -d "$work/home/.localai/backends" +test ! -e "$work/home/.localai/bin/local-ai" diff --git a/contrib/appimage/test_packaging.py b/contrib/appimage/test_packaging.py new file mode 100644 index 000000000000..9c4c32390c32 --- /dev/null +++ b/contrib/appimage/test_packaging.py @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: MIT +import os +import re +import shutil +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +PACK = ROOT / 'contrib/appimage' + + +class PackagingTests(unittest.TestCase): + def test_apprun_preserves_context_and_arguments(self): + with tempfile.TemporaryDirectory(dir=ROOT) as tmp: + app = Path(tmp) / 'space AppDir' + (app / 'usr/bin').mkdir(parents=True) + (app / 'AppRun').write_bytes((PACK / 'AppRun').read_bytes()) + binary = app / 'usr/bin/local-ai-launcher' + binary.write_text('#!/bin/sh\nprintf "%s\\n" "$PWD" "$HOME" "$LD_LIBRARY_PATH" "$@"\n') + binary.chmod(0o755) + env = dict(os.environ, HOME=tmp, LD_LIBRARY_PATH='/caller/libs') + result = subprocess.check_output(['sh', str(app / 'AppRun'), 'two words', '*'], + cwd=tmp, env=env, text=True) + self.assertEqual(result.splitlines(), [tmp, tmp, '/caller/libs', 'two words', '*']) + + def test_unsupported_architecture_fails_before_build(self): + result = subprocess.run(['bash', str(PACK / 'build.sh')], + env=dict(os.environ, APPIMAGE_ARCH='arm64'), capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('only amd64', result.stderr) + + def test_missing_file_fails_before_build(self): + with tempfile.TemporaryDirectory(dir=ROOT) as tmp: + # Expose only the utilities needed before the dependency preflight. + for command in ('dirname', 'uname'): + Path(tmp, command).symlink_to(shutil.which(command)) + result = subprocess.run([shutil.which('bash'), str(PACK / 'build.sh')], + env=dict(os.environ, PATH=tmp, APPIMAGE_ARCH='amd64'), + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Missing file command required by appimagetool', result.stderr) + for path in (ROOT / '.github/workflows/launcher-appimage.yaml', PACK / 'README.md'): + install = next(line for line in path.read_text().splitlines() + if 'apt-get install' in line) + self.assertIn('file', install.split()) + + def test_release_contract(self): + release = (ROOT / '.github/workflows/release.yaml').read_text() + workflow = (ROOT / '.github/workflows/launcher-appimage.yaml').read_text() + self.assertIn('needs: goreleaser', release) + self.assertIn('publish: true', release) + self.assertIn('local-ai-launcher-linux.tar.xz', release) + self.assertIn('dist/LocalAI.dmg', release) + self.assertNotIn('go-version:', release) + self.assertIn('go-version-file: go.mod', workflow) + self.assertIn('ubuntu-22.04', workflow) + self.assertIn("inputs.publish && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')", workflow) + self.assertIn('gh release view', workflow) + self.assertIn('smoke.sh', workflow) + + def test_publish_permission_isolation(self): + workflow = (ROOT / '.github/workflows/launcher-appimage.yaml').read_text() + header, jobs = workflow.split('jobs:\n', 1) + self.assertIn('permissions:\n contents: read', header) + sections = re.split(r'^ ([a-z]+):\n', jobs, flags=re.MULTILINE) + jobs = dict(zip(sections[1::2], sections[2::2])) + self.assertEqual(set(jobs), {'appimage', 'publish'}) + build, publish = jobs['appimage'], jobs['publish'] + self.assertIn(' permissions:\n contents: read', build) + self.assertNotIn('contents: write', build) + self.assertNotIn('GH_TOKEN', build) + self.assertIn('persist-credentials: false', build) + self.assertIn('actions/upload-artifact@', build) + self.assertIn(' needs: appimage\n', publish) + self.assertIn(" if: inputs.publish && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')\n", publish) + self.assertIn(' permissions:\n contents: write', publish) + self.assertIn('actions/download-artifact@', publish) + self.assertIn('name: launcher-appimage-x86_64', build) + self.assertIn('name: launcher-appimage-x86_64', publish) + self.assertNotIn('checkout@', publish) + self.assertNotIn('git fetch', publish) + self.assertNotIn('gh release create', publish) + self.assertLess(publish.index('gh release view'), publish.index('gh release upload')) + # The only shell commands in the privileged job inspect/upload releases. + commands = publish.split(' run: |\n', 1)[1].strip().splitlines() + self.assertEqual(len(commands), 2) + self.assertTrue(all(line.strip().startswith('gh release ') for line in commands)) + + def test_smoke_startup_contract(self): + # Run the actual smoke script with fake extraction and external tools. + # No GUI, AppImage build, network or host dist directory is needed. + with tempfile.TemporaryDirectory(dir=ROOT) as tmp: + root = Path(tmp) + pack = root / 'contrib/appimage' + pack.mkdir(parents=True) + smoke = pack / 'smoke.sh' + smoke.write_bytes((PACK / 'smoke.sh').read_bytes()) + tools = root / 'tools' + tools.mkdir() + dist = root / 'dist' + dist.mkdir() + + def executable(path, body): + path.write_text('#!/bin/sh\nset -eu\n' + body) + path.chmod(0o755) + + executable(dist / 'LocalAI-Launcher-x86_64.AppImage', + 'test "$1" = --appimage-extract\n' + 'mkdir -p squashfs-root/usr/bin\n' + 'touch squashfs-root/local-ai-launcher.desktop squashfs-root/local-ai-launcher.png\n' + 'touch squashfs-root/AppRun squashfs-root/usr/bin/local-ai-launcher\n' + 'chmod +x squashfs-root/AppRun\n') + executable(tools / 'desktop-file-validate', 'exit 0\n') + executable(tools / 'ldd', 'echo "${SMOKE_LDD:-libc.so => /lib/libc.so}"\n') + executable(tools / 'timeout', + 'test "$1" = --kill-after=5s\n' + 'test "$2" = 15s\n' + 'test "$3" = xvfb-run\n' + 'mkdir -p "$HOME/.localai/models" "$HOME/.localai/backends"\n' + 'printf "%s\\n" "$SMOKE_LOG"\n' + 'exit "$SMOKE_STATUS"\n') + ready = 'No LocalAI installation found' + cases = [ + ('startup', ready, 124, True), + ('silent hang', '', 124, False), + ('incomplete initialization', 'Initializing launcher...', 124, False), + ('initialization failure', ready + '\nFailed to initialize launcher: bad config', 124, False), + ('panic', ready + '\npanic: broken', 124, False), + ('GLFW failure', ready + '\nfailed to initialize GLFW', 124, False), + ('early success', ready, 0, False), + ('early failure', ready, 1, False), + ('forced kill', ready, 137, False), + ('missing dependency', ready, 124, False), + ] + for name, log, status, success in cases: + with self.subTest(name=name): + env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'], + SMOKE_LOG=log, SMOKE_STATUS=str(status)) + if name == 'missing dependency': + env['SMOKE_LDD'] = 'libGL.so => not found' + result = subprocess.run(['bash', str(smoke)], env=env, + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode == 0, success, result.stdout + result.stderr) + + def test_pinned_tools_and_no_library_injection(self): + script = (PACK / 'build.sh').read_text() + self.assertIn('sha256sum --check', script) + self.assertIn('--appimage-extract', script) + self.assertIn('--runtime-file', script) + self.assertNotIn('/continuous/', script) + self.assertNotIn('LD_LIBRARY_PATH=', (PACK / 'AppRun').read_text()) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/content/getting-started/linux.md b/docs/content/getting-started/linux.md index 22a670388f55..ae3bf16a6051 100644 --- a/docs/content/getting-started/linux.md +++ b/docs/content/getting-started/linux.md @@ -5,6 +5,45 @@ weight: 9 url: '/installation/linux/' --- +## Desktop launcher AppImage (x86_64) + +Download `LocalAI-Launcher-x86_64.AppImage` and its `.sha256` file from +[GitHub Releases](https://github.com/mudler/LocalAI/releases). In that directory: + +```sh +sha256sum --check LocalAI-Launcher-x86_64.AppImage.sha256 +chmod +x LocalAI-Launcher-x86_64.AppImage +./LocalAI-Launcher-x86_64.AppImage +``` + +You can also double-click the executable image in a file manager. Normal +AppImage mounting requires FUSE support (on Ubuntu 22.04, `libfuse2`). If FUSE +is unavailable, extract it into a writable directory and run the launcher: + +```sh +./LocalAI-Launcher-x86_64.AppImage --appimage-extract +./squashfs-root/AppRun +``` + +This is the existing desktop/tray launcher, not the server bundled into an +image. First use needs internet access to download LocalAI. Models and backends +are downloaded separately. The launcher stores configuration in +`~/.localai/launcher.json`, runtime downloads in `~/.localai/bin`, and default +models/backends under `~/.localai/`. Fyne preferences use the user configuration +directory. These remain writable outside the read-only image. Keep your home +writable; replacing or deleting the AppImage does not delete your data. + +The supported release target is **x86_64 Ubuntu 22.04 or newer**, with glibc +2.35+, OpenGL and X11 desktop libraries (XWayland on Wayland). A tray-enabled +desktop is recommended. This is not a static executable: other distributions +must provide compatible graphics libraries. ARM64 AppImages are not currently +published; use the server binaries below instead. Server/backend requirements +are separate from launcher requirements. Replace the AppImage manually to +update the launcher; there is no AppImage self-updater. + +For build dependencies, pinned tools and smoke checks, see +[the AppImage build guide](https://github.com/mudler/LocalAI/blob/master/contrib/appimage/README.md). + ## Manual Installation ### Download Binary