Compare commits
65 Commits
75806fd1d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
bf39c5b9ab
|
|||
|
1af2d440db
|
|||
|
e9abeccdea
|
|||
|
e7d7e6961f
|
|||
|
34c9cbb89d
|
|||
|
e0cb337538
|
|||
|
3e3090802a
|
|||
|
c1ffe7e62e
|
|||
|
0d6f48fcc0
|
|||
|
ace6037a2d
|
|||
|
661cf574f2
|
|||
|
a79eafd70f
|
|||
|
fff56a626c
|
|||
|
23283c375f
|
|||
|
9a316bad2f
|
|||
|
ef7e3a3183
|
|||
|
3e4191a7d9
|
|||
|
b6977eda02
|
|||
|
b8e568b8bf
|
|||
|
755f2175e5
|
|||
|
d9cddb4824
|
|||
|
291596cb71
|
|||
|
61cdc53e39
|
|||
|
6647ed299d
|
|||
|
10263e4a2b
|
|||
|
de96e7c687
|
|||
|
0ec89de36f
|
|||
|
9f57342810
|
|||
|
6dde36080e
|
|||
|
bb76402e5a
|
|||
|
38a875d06b
|
|||
|
3603c31e21
|
|||
|
7f9e857695
|
|||
|
a6cebc76ba
|
|||
|
85b78d0c0c
|
|||
|
6d3dca17fa
|
|||
|
6946682df1
|
|||
|
ff8e5437ef
|
|||
|
65a265c095
|
|||
|
54cffcfe81
|
|||
|
cacdbebbf7
|
|||
|
ba5eec78f1
|
|||
|
e3c403e98b
|
|||
|
087c5d5524
|
|||
|
7df736292c
|
|||
|
affc38213e
|
|||
|
9f0116bb2b
|
|||
|
3291f77fcd
|
|||
|
7575ec6a3c
|
|||
|
ecf38bb53f
|
|||
|
93d442d270
|
|||
|
33aa40ee85
|
|||
|
6ffbde4c30
|
|||
|
e902729ba4
|
|||
|
bab7d9850c
|
|||
|
70ae2108ee
|
|||
|
38e36e4547
|
|||
|
1919e14032
|
|||
|
94d890b82e
|
|||
|
ee693f638c
|
|||
|
f4e1008684
|
|||
|
0cb6a4f524
|
|||
|
4160334bf1
|
|||
|
0147e0fe32
|
|||
|
82a04c88dc
|
229
.gitea/workflows/build-prerelease.yml
Normal file
229
.gitea/workflows/build-prerelease.yml
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
name: build-prerelease
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
commit:
|
||||||
|
description: "mistral.rs upstream commit SHA"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
description: "Version from upstream Cargo.toml (e.g. 0.8.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
date:
|
||||||
|
description: "Commit date as YYYYMMDD"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
short_sha:
|
||||||
|
description: "Short commit SHA (7 chars)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: prerelease-build
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: ampere
|
||||||
|
fedora_version: "43"
|
||||||
|
runner: cuda-13.0
|
||||||
|
cuda_home: /usr/local/cuda-13.0
|
||||||
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
|
compute_caps: "86"
|
||||||
|
build_jobs: 12
|
||||||
|
nvcc_threads: 4
|
||||||
|
- name: ada
|
||||||
|
fedora_version: "43"
|
||||||
|
runner: cuda-13.0
|
||||||
|
cuda_home: /usr/local/cuda-13.0
|
||||||
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
|
compute_caps: "89"
|
||||||
|
build_jobs: 12
|
||||||
|
nvcc_threads: 4
|
||||||
|
- name: blackwell
|
||||||
|
fedora_version: "43"
|
||||||
|
runner: cuda-13.0
|
||||||
|
cuda_home: /usr/local/cuda-13.0
|
||||||
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
|
compute_caps: "120"
|
||||||
|
build_jobs: 12
|
||||||
|
nvcc_threads: 4
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install/update Rust toolchain
|
||||||
|
run: |
|
||||||
|
if command -v rustup &> /dev/null; then
|
||||||
|
rustup update stable
|
||||||
|
else
|
||||||
|
curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
||||||
|
fi
|
||||||
|
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Clone mistral.rs at commit
|
||||||
|
run: |
|
||||||
|
git clone https://github.com/EricLBuehler/mistral.rs.git src/
|
||||||
|
cd src
|
||||||
|
git checkout "${{ inputs.commit }}"
|
||||||
|
|
||||||
|
- name: Build mistralrs
|
||||||
|
run: |
|
||||||
|
export PATH="${{ matrix.cuda_home }}/bin:${PATH}"
|
||||||
|
export LD_LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LD_LIBRARY_PATH:-}"
|
||||||
|
export LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LIBRARY_PATH:-}"
|
||||||
|
cd src
|
||||||
|
cargo build --release --features "${{ matrix.cargo_features }}"
|
||||||
|
env:
|
||||||
|
CUDA_COMPUTE_CAP: ${{ matrix.compute_caps }}
|
||||||
|
CARGO_BUILD_JOBS: ${{ matrix.build_jobs }}
|
||||||
|
NVCC_THREADS: ${{ matrix.nvcc_threads }}
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
mkdir --parents artifacts
|
||||||
|
cp src/target/release/mistralrs "artifacts/mistralrs-${{ matrix.name }}"
|
||||||
|
echo "built: $(artifacts/mistralrs-${{ matrix.name }} --version 2>&1 | head -1)"
|
||||||
|
|
||||||
|
- name: Upload binary artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
|
path: artifacts/mistralrs-${{ matrix.name }}
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
package:
|
||||||
|
needs: build
|
||||||
|
runs-on: rpm
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: ampere
|
||||||
|
fedora_version: "43"
|
||||||
|
- name: ada
|
||||||
|
fedora_version: "43"
|
||||||
|
- name: blackwell
|
||||||
|
fedora_version: "43"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download binary
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
|
path: artifacts/
|
||||||
|
|
||||||
|
- name: Build RPM
|
||||||
|
run: |
|
||||||
|
rm -f ~/.rpmmacros
|
||||||
|
rpmdev-setuptree
|
||||||
|
cp artifacts/mistralrs-${{ matrix.name }} ~/rpmbuild/SOURCES/
|
||||||
|
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
||||||
|
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
||||||
|
rpmbuild -bb rpm/mistralrs.spec \
|
||||||
|
--define "mistralrs_version ${{ inputs.version }}" \
|
||||||
|
--define "mistralrs_flavour ${{ matrix.name }}" \
|
||||||
|
--define "mistralrs_prerelease 0.1.${{ inputs.date }}git${{ inputs.short_sha }}" \
|
||||||
|
--undefine dist \
|
||||||
|
--define "dist .fc${{ matrix.fedora_version }}"
|
||||||
|
|
||||||
|
- name: Upload RPM
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: rpm-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
|
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: package
|
||||||
|
runs-on: rpm
|
||||||
|
concurrency:
|
||||||
|
group: rpm-publish
|
||||||
|
cancel-in-progress: false
|
||||||
|
env:
|
||||||
|
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- fedora_version: "43"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download RPMs for fc${{ matrix.fedora_version }}
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: rpms/
|
||||||
|
pattern: rpm-*-fc${{ matrix.fedora_version }}
|
||||||
|
|
||||||
|
- name: Flatten RPM artifacts
|
||||||
|
run: |
|
||||||
|
find rpms/ -name '*.rpm' -exec mv --target-directory=rpms/ {} +
|
||||||
|
find rpms/ -mindepth 1 -type d -empty -delete
|
||||||
|
|
||||||
|
- name: Check for sequoia-sq
|
||||||
|
run: |
|
||||||
|
if ! command -v sq &> /dev/null; then
|
||||||
|
echo "ERROR: sequoia-sq is not installed. Install with: sudo dnf install sequoia-sq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Import signing key
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
||||||
|
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
|
||||||
|
echo "${fpr}:6:" | gpg --batch --import-ownertrust
|
||||||
|
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
|
||||||
|
|
||||||
|
- name: Sign RPMs
|
||||||
|
run: |
|
||||||
|
for rpm in rpms/*.rpm; do
|
||||||
|
echo "signing ${rpm}..."
|
||||||
|
rpm --addsign "${rpm}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
install --directory --mode 700 ~/.ssh
|
||||||
|
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||||
|
env:
|
||||||
|
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||||
|
|
||||||
|
- name: Test SSH connectivity
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||||
|
|
||||||
|
- name: Ensure unstable repo directory exists
|
||||||
|
run: |
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"mkdir --parents /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable"
|
||||||
|
|
||||||
|
- name: Sync RPMs to unstable repo
|
||||||
|
run: |
|
||||||
|
rsync \
|
||||||
|
--archive \
|
||||||
|
--verbose \
|
||||||
|
--chmod D755,F644 \
|
||||||
|
rpms/*.rpm \
|
||||||
|
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/"
|
||||||
|
|
||||||
|
- name: Update unstable repo metadata
|
||||||
|
run: |
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"cd /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable && createrepo_c --update ."
|
||||||
|
|
||||||
|
- name: Generate packages.json for unstable
|
||||||
|
run: |
|
||||||
|
scp script/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"python3 /tmp/generate-packages-json.py \
|
||||||
|
--repodata-dir /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/repodata \
|
||||||
|
--output /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/packages.json \
|
||||||
|
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64/unstable"
|
||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: poll-and-build
|
group: build-release
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -18,7 +18,24 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: cuda13
|
- name: ampere
|
||||||
|
fedora_version: "43"
|
||||||
|
runner: cuda-13.0
|
||||||
|
cuda_home: /usr/local/cuda-13.0
|
||||||
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
|
compute_caps: "86"
|
||||||
|
build_jobs: 12
|
||||||
|
nvcc_threads: 4
|
||||||
|
- name: ada
|
||||||
|
fedora_version: "43"
|
||||||
|
runner: cuda-13.0
|
||||||
|
cuda_home: /usr/local/cuda-13.0
|
||||||
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
|
compute_caps: "89"
|
||||||
|
build_jobs: 12
|
||||||
|
nvcc_threads: 4
|
||||||
|
- name: blackwell
|
||||||
|
fedora_version: "43"
|
||||||
runner: cuda-13.0
|
runner: cuda-13.0
|
||||||
cuda_home: /usr/local/cuda-13.0
|
cuda_home: /usr/local/cuda-13.0
|
||||||
cargo_features: "cuda cudnn flash-attn nccl"
|
cargo_features: "cuda cudnn flash-attn nccl"
|
||||||
@@ -38,96 +55,167 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
#- name: Check for NCCL
|
|
||||||
# run: |
|
|
||||||
# if ! ldconfig -p | grep -q libnccl.so.2; then
|
|
||||||
# echo "ERROR: libnccl not found. Install with:"
|
|
||||||
# echo " sudo dnf config-manager addrepo --from-repofile=https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo"
|
|
||||||
# echo " sudo dnf --repo=cuda-rhel9-x86_64 install libnccl libnccl-devel"
|
|
||||||
# exit 1
|
|
||||||
# fi
|
|
||||||
|
|
||||||
- name: Clone mistral.rs at tag
|
- name: Clone mistral.rs at tag
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch "${{ inputs.tag }}" \
|
git clone --depth 1 --branch "${{ inputs.tag }}" \
|
||||||
https://github.com/EricLBuehler/mistral.rs.git src/
|
https://github.com/EricLBuehler/mistral.rs.git src/
|
||||||
|
|
||||||
- name: Build
|
- name: Build mistralrs
|
||||||
run: ./script/build-binary.sh
|
run: |
|
||||||
|
export PATH="${{ matrix.cuda_home }}/bin:${PATH}"
|
||||||
|
export LD_LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LD_LIBRARY_PATH:-}"
|
||||||
|
export LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LIBRARY_PATH:-}"
|
||||||
|
cd src
|
||||||
|
cargo build --release --locked --features "${{ matrix.cargo_features }}"
|
||||||
env:
|
env:
|
||||||
FLAVOUR_NAME: ${{ matrix.name }}
|
|
||||||
CUDA_HOME: ${{ matrix.cuda_home }}
|
|
||||||
CARGO_FEATURES: ${{ matrix.cargo_features }}
|
|
||||||
CUDA_COMPUTE_CAP: ${{ matrix.compute_caps }}
|
CUDA_COMPUTE_CAP: ${{ matrix.compute_caps }}
|
||||||
CARGO_BUILD_JOBS: ${{ matrix.build_jobs }}
|
CARGO_BUILD_JOBS: ${{ matrix.build_jobs }}
|
||||||
NVCC_THREADS: ${{ matrix.nvcc_threads }}
|
NVCC_THREADS: ${{ matrix.nvcc_threads }}
|
||||||
SRC_DIR: src
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
mkdir --parents artifacts
|
||||||
|
cp src/target/release/mistralrs "artifacts/mistralrs-${{ matrix.name }}"
|
||||||
|
echo "built: $(artifacts/mistralrs-${{ matrix.name }} --version 2>&1 | head -1)"
|
||||||
|
|
||||||
- name: Upload binary artifact
|
- name: Upload binary artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: mistralrs-server-${{ matrix.name }}
|
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
path: artifacts/mistralrs-server-${{ matrix.name }}
|
path: artifacts/mistralrs-${{ matrix.name }}
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
package:
|
package:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: fedora
|
runs-on: rpm
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: cuda13
|
- name: ampere
|
||||||
|
fedora_version: "43"
|
||||||
|
- name: ada
|
||||||
|
fedora_version: "43"
|
||||||
|
- name: blackwell
|
||||||
|
fedora_version: "43"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download binary
|
- name: Download binary
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: mistralrs-server-${{ matrix.name }}
|
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
uses: https://git.lair.cafe/actions/rpm-changelog@v1
|
||||||
|
with:
|
||||||
|
spec: rpm/mistralrs.spec
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
repo-url: https://github.com/EricLBuehler/mistral.rs.git
|
||||||
|
|
||||||
- name: Build RPM
|
- name: Build RPM
|
||||||
run: |
|
run: |
|
||||||
rm -f ~/.rpmmacros
|
rm -f ~/.rpmmacros
|
||||||
version="${TAG#v}"
|
|
||||||
rpmdev-setuptree
|
rpmdev-setuptree
|
||||||
cp artifacts/mistralrs-server-${{ matrix.name }} ~/rpmbuild/SOURCES/
|
cp artifacts/mistralrs-${{ matrix.name }} ~/rpmbuild/SOURCES/
|
||||||
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
||||||
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
||||||
rpmbuild -bb rpm/mistralrs.spec \
|
rpmbuild -bb rpm/mistralrs.spec \
|
||||||
--define "mistralrs_version ${version}" \
|
--define "mistralrs_version ${{ steps.version.outputs.version }}" \
|
||||||
--define "mistralrs_flavour ${{ matrix.name }}"
|
--define "mistralrs_flavour ${{ matrix.name }}" \
|
||||||
env:
|
--undefine dist \
|
||||||
TAG: ${{ inputs.tag }}
|
--define "dist .fc${{ matrix.fedora_version }}"
|
||||||
|
|
||||||
- name: Upload RPM
|
- name: Upload RPM
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: rpm-${{ matrix.name }}
|
name: rpm-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||||
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: package
|
needs: package
|
||||||
runs-on: fedora
|
runs-on: rpm
|
||||||
|
env:
|
||||||
|
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- fedora_version: "43"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all RPMs
|
- name: Download RPMs for fc${{ matrix.fedora_version }}
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: rpms/
|
path: rpms/
|
||||||
pattern: rpm-*
|
pattern: rpm-*-fc${{ matrix.fedora_version }}
|
||||||
merge-multiple: true
|
|
||||||
|
- name: Flatten RPM artifacts
|
||||||
|
run: |
|
||||||
|
find rpms/ -name '*.rpm' -exec mv --target-directory=rpms/ {} +
|
||||||
|
find rpms/ -mindepth 1 -type d -empty -delete
|
||||||
|
|
||||||
|
- name: Check for sequoia-sq
|
||||||
|
run: |
|
||||||
|
if ! command -v sq &> /dev/null; then
|
||||||
|
echo "ERROR: sequoia-sq is not installed. Install with: sudo dnf install sequoia-sq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Import signing key
|
- name: Import signing key
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
||||||
echo "%_gpg_name ${{ secrets.RPM_SIGNING_KEY_ID }}" > ~/.rpmmacros
|
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
|
||||||
|
echo "${fpr}:6:" | gpg --batch --import-ownertrust
|
||||||
|
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
|
||||||
|
|
||||||
- name: Sign and publish
|
- name: Sign RPMs
|
||||||
run: ./script/publish-repo.sh rpms/
|
run: |
|
||||||
|
for rpm in rpms/*.rpm; do
|
||||||
|
echo "signing ${rpm}..."
|
||||||
|
rpm --addsign "${rpm}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
install --directory --mode 700 ~/.ssh
|
||||||
|
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||||
env:
|
env:
|
||||||
RSYNC_TARGET: ${{ secrets.RSYNC_TARGET }}
|
|
||||||
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||||
|
|
||||||
|
- name: Test SSH connectivity
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||||
|
|
||||||
|
- name: Sync RPMs to repo
|
||||||
|
run: |
|
||||||
|
rsync \
|
||||||
|
--archive \
|
||||||
|
--verbose \
|
||||||
|
--chmod D755,F644 \
|
||||||
|
rpms/*.rpm \
|
||||||
|
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/"
|
||||||
|
|
||||||
|
- name: Update repo metadata
|
||||||
|
run: |
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"cd /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64 && createrepo_c --update ."
|
||||||
|
|
||||||
|
- name: Generate packages.json
|
||||||
|
run: |
|
||||||
|
scp script/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"python3 /tmp/generate-packages-json.py \
|
||||||
|
--repodata-dir /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/repodata \
|
||||||
|
--output /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/packages.json \
|
||||||
|
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64"
|
||||||
|
|||||||
53
.gitea/workflows/deploy-ui.yml
Normal file
53
.gitea/workflows/deploy-ui.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: deploy-ui
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "ui/**"
|
||||||
|
- ".gitea/workflows/deploy-ui.yml"
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: fedora-43
|
||||||
|
env:
|
||||||
|
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd ui
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
run: |
|
||||||
|
cd ui
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
install --directory --mode 700 ~/.ssh
|
||||||
|
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||||
|
env:
|
||||||
|
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||||
|
|
||||||
|
- name: Test SSH connectivity
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||||
|
|
||||||
|
- name: Deploy UI to web root
|
||||||
|
run: |
|
||||||
|
rsync \
|
||||||
|
--recursive \
|
||||||
|
--links \
|
||||||
|
--verbose \
|
||||||
|
--delete \
|
||||||
|
--chmod D755,F644 \
|
||||||
|
--include="index.html" \
|
||||||
|
--include="*.repo" \
|
||||||
|
--include="assets/***" \
|
||||||
|
--exclude="*" \
|
||||||
|
ui/dist/ \
|
||||||
|
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/"
|
||||||
@@ -2,16 +2,16 @@ name: poll-upstream
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "*/15 * * * *"
|
- cron: "0 * * * *"
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: poll-and-build
|
group: poll-upstream
|
||||||
cancel-in-progress: false
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: fedora
|
runs-on: fedora-43
|
||||||
steps:
|
steps:
|
||||||
- name: Get upstream latest tag
|
- name: Get upstream latest tag
|
||||||
id: upstream
|
id: upstream
|
||||||
@@ -23,26 +23,54 @@ jobs:
|
|||||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Upstream latest: ${tag}"
|
echo "Upstream latest: ${tag}"
|
||||||
|
|
||||||
- name: Get published version from our repo
|
- name: Check if all packages are published
|
||||||
id: published
|
id: published
|
||||||
run: |
|
run: |
|
||||||
# Query our own dnf repo. If the version is there, we've already built it.
|
|
||||||
# Strip leading 'v' because RPM versions don't use it.
|
|
||||||
version="${UPSTREAM_TAG#v}"
|
version="${UPSTREAM_TAG#v}"
|
||||||
http_response_code=$(curl \
|
needs_build=false
|
||||||
--silent \
|
for target in "43:ampere" "43:ada" "43:blackwell"; do
|
||||||
--write-out "%{http_code}" \
|
fedora_version="${target%%:*}"
|
||||||
--output /dev/null \
|
flavour="${target##*:}"
|
||||||
--head \
|
base_url="https://rpm.lair.cafe/fedora/${fedora_version}/x86_64"
|
||||||
--url "https://rpm.lair.cafe/fedora/43/x86_64/mistralrs-server-cuda13-${version}-1.fc43.x86_64.rpm")
|
rpm_name="mistralrs-${flavour}-${version}-1.fc${fedora_version}.x86_64.rpm"
|
||||||
if [ "${http_response_code}" = "200" ]; then
|
|
||||||
echo "already_built=true" >> "$GITHUB_OUTPUT"
|
# check that the rpm file exists
|
||||||
elif [ "${http_response_code}" = "404" ]; then
|
http_code=$(curl \
|
||||||
echo "already_built=false" >> "$GITHUB_OUTPUT"
|
--silent \
|
||||||
else
|
--write-out "%{http_code}" \
|
||||||
echo "Unexpected HTTP response code: ${http_response_code}"
|
--output /dev/null \
|
||||||
exit 1
|
--head \
|
||||||
fi
|
--url "${base_url}/${rpm_name}")
|
||||||
|
if [ "${http_code}" = "404" ]; then
|
||||||
|
echo "missing: ${base_url}/${rpm_name}"
|
||||||
|
needs_build=true
|
||||||
|
continue
|
||||||
|
elif [ "${http_code}" != "200" ]; then
|
||||||
|
echo "unexpected HTTP ${http_code} for ${base_url}/${rpm_name}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "found: ${base_url}/${rpm_name}"
|
||||||
|
|
||||||
|
# check that the repo index references this package
|
||||||
|
if ! curl --silent --fail "${base_url}/repodata/repomd.xml" \
|
||||||
|
| grep --quiet 'primary'; then
|
||||||
|
echo "missing or invalid repomd.xml at ${base_url}/repodata/"
|
||||||
|
needs_build=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! dnf repoquery \
|
||||||
|
--repofrompath=check,"${base_url}" \
|
||||||
|
--repo=check \
|
||||||
|
--quiet \
|
||||||
|
"mistralrs-${flavour}-${version}" 2>&1 \
|
||||||
|
| grep --quiet "mistralrs-${flavour}"; then
|
||||||
|
echo "repo index missing: mistralrs-${flavour}-${version} not in ${base_url}/repodata/"
|
||||||
|
needs_build=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "indexed: mistralrs-${flavour}-${version} in ${base_url}/repodata/"
|
||||||
|
done
|
||||||
|
echo "already_built=$( [ "${needs_build}" = "true" ] && echo false || echo true )" >> "$GITHUB_OUTPUT"
|
||||||
env:
|
env:
|
||||||
UPSTREAM_TAG: ${{ steps.upstream.outputs.tag }}
|
UPSTREAM_TAG: ${{ steps.upstream.outputs.tag }}
|
||||||
|
|
||||||
@@ -55,3 +83,73 @@ jobs:
|
|||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--url "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/build-release.yml/dispatches" \
|
--url "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/build-release.yml/dispatches" \
|
||||||
--data "{\"ref\":\"refs/heads/main\",\"inputs\":{\"tag\":\"${{ steps.upstream.outputs.tag }}\"}}"
|
--data "{\"ref\":\"refs/heads/main\",\"inputs\":{\"tag\":\"${{ steps.upstream.outputs.tag }}\"}}"
|
||||||
|
|
||||||
|
check-prerelease:
|
||||||
|
runs-on: fedora-43
|
||||||
|
steps:
|
||||||
|
- name: Get upstream main branch HEAD
|
||||||
|
id: upstream
|
||||||
|
run: |
|
||||||
|
response=$(curl --silent --show-error --fail --location \
|
||||||
|
--header 'Accept: application/vnd.github+json' \
|
||||||
|
--url 'https://api.github.com/repos/EricLBuehler/mistral.rs/commits/master')
|
||||||
|
sha=$(echo "${response}" | jq -r .sha)
|
||||||
|
short_sha=$(echo "${sha}" | head --bytes=7)
|
||||||
|
date=$(echo "${response}" | jq -r '.commit.committer.date[:10]' | tr -d '-')
|
||||||
|
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "date=${date}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Upstream main HEAD: ${sha} (${date})"
|
||||||
|
|
||||||
|
- name: Get version from upstream Cargo.toml
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
version=$(curl --silent --show-error --fail --location \
|
||||||
|
--header 'Accept: application/vnd.github.raw+json' \
|
||||||
|
--url "https://api.github.com/repos/EricLBuehler/mistral.rs/contents/Cargo.toml?ref=${{ steps.upstream.outputs.sha }}" \
|
||||||
|
| grep '^version' | head --lines=1 | sed 's/.*"\(.*\)".*/\1/')
|
||||||
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Upstream Cargo.toml version: ${version}"
|
||||||
|
|
||||||
|
- name: Check if prerelease is already published
|
||||||
|
id: published
|
||||||
|
run: |
|
||||||
|
prerelease="0.1.${UPSTREAM_DATE}git${UPSTREAM_SHORT_SHA}"
|
||||||
|
needs_build=false
|
||||||
|
for target in "43:ampere" "43:ada" "43:blackwell"; do
|
||||||
|
fedora_version="${target%%:*}"
|
||||||
|
flavour="${target##*:}"
|
||||||
|
base_url="https://rpm.lair.cafe/fedora/${fedora_version}/x86_64/unstable"
|
||||||
|
rpm_name="mistralrs-${flavour}-${UPSTREAM_VERSION}-${prerelease}.fc${fedora_version}.x86_64.rpm"
|
||||||
|
|
||||||
|
http_code=$(curl \
|
||||||
|
--silent \
|
||||||
|
--write-out "%{http_code}" \
|
||||||
|
--output /dev/null \
|
||||||
|
--head \
|
||||||
|
--url "${base_url}/${rpm_name}")
|
||||||
|
if [ "${http_code}" = "404" ]; then
|
||||||
|
echo "missing: ${base_url}/${rpm_name}"
|
||||||
|
needs_build=true
|
||||||
|
continue
|
||||||
|
elif [ "${http_code}" != "200" ]; then
|
||||||
|
echo "unexpected HTTP ${http_code} for ${base_url}/${rpm_name}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "found: ${base_url}/${rpm_name}"
|
||||||
|
done
|
||||||
|
echo "already_built=$( [ "${needs_build}" = "true" ] && echo false || echo true )" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
UPSTREAM_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
UPSTREAM_DATE: ${{ steps.upstream.outputs.date }}
|
||||||
|
UPSTREAM_SHORT_SHA: ${{ steps.upstream.outputs.short_sha }}
|
||||||
|
|
||||||
|
- name: Trigger prerelease build workflow
|
||||||
|
if: steps.published.outputs.already_built == 'false'
|
||||||
|
run: |
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
--request POST \
|
||||||
|
--header "Authorization: token ${{ secrets.DISPATCH_TOKEN }}" \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--url "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/build-prerelease.yml/dispatches" \
|
||||||
|
--data "{\"ref\":\"refs/heads/main\",\"inputs\":{\"commit\":\"${{ steps.upstream.outputs.sha }}\",\"version\":\"${{ steps.version.outputs.version }}\",\"date\":\"${{ steps.upstream.outputs.date }}\",\"short_sha\":\"${{ steps.upstream.outputs.short_sha }}\"}}"
|
||||||
|
|||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.claude/
|
||||||
30
CLAUDE.md
30
CLAUDE.md
@@ -10,46 +10,46 @@ This repo packages [mistral.rs](https://github.com/EricLBuehler/mistral.rs) (a R
|
|||||||
|
|
||||||
### Pipeline flow
|
### Pipeline flow
|
||||||
|
|
||||||
1. **poll-upstream** (`.gitea/workflows/poll-upstream.yml`) — cron every 15 min, checks GitHub for latest mistral.rs release tag. If the corresponding RPM doesn't exist on `rpm.lair.cafe`, triggers `build-release`.
|
1. **poll-upstream** (`.gitea/workflows/poll-upstream.yml`) — cron every 15 min, checks GitHub for latest mistral.rs release tag. If the corresponding RPMs don't exist on `rpm.lair.cafe`, triggers `build-release`. Also checks upstream `main` branch HEAD and triggers `build-prerelease` for the unstable repo.
|
||||||
2. **build-release** (`.gitea/workflows/build-release.yml`) — three-stage pipeline:
|
2. **build-release** (`.gitea/workflows/build-release.yml`) — three-stage pipeline:
|
||||||
- **plan** — reads `flavours.yml`, emits a JSON matrix of flavours + stripped version.
|
- **build** — runs on a `cuda-13.0` runner. Clones upstream at tag, runs `cargo build --release --locked` with flavour-specific CUDA features.
|
||||||
- **build** — runs on a `cuda-13.0` runner. Clones upstream at tag, calls `script/build-binary.sh` to `cargo build --release --locked` with flavour-specific CUDA features.
|
|
||||||
- **package** — runs `rpmbuild -bb rpm/mistralrs.spec` with `--define` for version and flavour.
|
- **package** — runs `rpmbuild -bb rpm/mistralrs.spec` with `--define` for version and flavour.
|
||||||
- **publish** — GPG-signs RPMs, rsyncs to `rpm.lair.cafe`, runs `createrepo_c --update`. Uses concurrency group `rpm-publish` to prevent metadata races.
|
- **publish** — GPG-signs RPMs, rsyncs to `rpm.lair.cafe`, runs `createrepo_c --update`. Uses concurrency group `rpm-publish` to prevent metadata races.
|
||||||
|
3. **build-prerelease** (`.gitea/workflows/build-prerelease.yml`) — same structure as build-release but clones at a specific commit from `main`, omits `--locked`, uses prerelease release suffix, and publishes to the unstable repo at `rpm.lair.cafe/fedora/$releasever/$basearch/unstable/`.
|
||||||
|
|
||||||
### Flavours
|
### Flavours
|
||||||
|
|
||||||
Defined in `flavours.yml`. Each flavour specifies a name, `cuda_home`, `cargo_features`, and `compute_caps`. The RPM spec uses `update-alternatives` so multiple flavours can coexist, with priority: base=10, fa=20, nccl=30.
|
Defined in the workflow matrix. Each flavour targets a specific GPU generation using the same CUDA 13.0 toolkit and features (cuda, cudnn, flash-attn, nccl), varying only the compute capability.
|
||||||
|
|
||||||
|
| Flavour | Compute cap | GPU generation |
|
||||||
|
|------------|-------------|---------------------------|
|
||||||
|
| ampere | sm_86 | RTX 3060, A2000–A6000 |
|
||||||
|
| ada | sm_89 | RTX 4060–4090, L40 |
|
||||||
|
| blackwell | sm_120 | RTX 5090, B100, B200 |
|
||||||
|
|
||||||
### Key files
|
### Key files
|
||||||
|
|
||||||
- `flavours.yml` — flavour matrix definition (drives CI matrix)
|
|
||||||
- `rpm/mistralrs.spec` — RPM spec (binary-only package, no rebuild)
|
- `rpm/mistralrs.spec` — RPM spec (binary-only package, no rebuild)
|
||||||
- `rpm/systemd/mistralrs@.service` — templated systemd unit (`@BINARY@` and `@FLAVOUR@` are sed-replaced during rpmbuild)
|
- `rpm/systemd/mistralrs@.service` — templated systemd unit (`@BINARY@` and `@FLAVOUR@` are sed-replaced during rpmbuild)
|
||||||
- `rpm/systemd/mistralrs@.conf.example` — example env file for instances
|
- `rpm/systemd/mistralrs@.conf.example` — example env file for instances
|
||||||
- `script/build-binary.sh` — compiles mistralrs-server with cargo (requires `FLAVOUR_NAME`, `CUDA_HOME`, `CARGO_FEATURES`, `CUDA_COMPUTE_CAP`, `SRC_DIR` env vars)
|
- `script/setup/` — one-time infra setup scripts (DNS, TLS cert, nginx, GPG) for `rpm.lair.cafe` on host `oolon`
|
||||||
- `script/publish-repo.sh` — signs RPMs and rsyncs to the repo server
|
|
||||||
- `script/setup/` — one-time infra setup scripts (DNS, TLS cert, nginx) for `rpm.lair.cafe` on host `oolon`
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Build a binary locally (requires CUDA toolkit):
|
|
||||||
```bash
|
|
||||||
FLAVOUR_NAME=cuda13 CUDA_HOME=/usr/local/cuda-13.0 CARGO_FEATURES="cuda cudnn flash-attn nccl" CUDA_COMPUTE_CAP=120 SRC_DIR=./src ./script/build-binary.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Build an RPM from a pre-built binary:
|
Build an RPM from a pre-built binary:
|
||||||
```bash
|
```bash
|
||||||
rpmdev-setuptree
|
rpmdev-setuptree
|
||||||
cp artifacts/mistralrs-server-cuda13 ~/rpmbuild/SOURCES/
|
cp artifacts/mistralrs-ada ~/rpmbuild/SOURCES/
|
||||||
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
||||||
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
||||||
rpmbuild -bb rpm/mistralrs.spec --define "mistralrs_version 0.7.0" --define "mistralrs_flavour cuda13"
|
rpmbuild -bb rpm/mistralrs.spec --define "mistralrs_version 0.8.0" --define "mistralrs_flavour ada"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
- CI runs on Gitea Actions (self-hosted), not GitHub Actions
|
- CI runs on Gitea Actions (self-hosted), not GitHub Actions
|
||||||
- RPM repo hosted at `rpm.lair.cafe` on host `oolon.kosherinata.internal`
|
- RPM repo hosted at `rpm.lair.cafe` on host `oolon.kosherinata.internal`
|
||||||
|
- Stable repo: `rpm.lair.cafe/fedora/$releasever/$basearch/`
|
||||||
|
- Unstable repo: `rpm.lair.cafe/fedora/$releasever/$basearch/unstable/`
|
||||||
- TLS via Let's Encrypt with Cloudflare DNS challenge
|
- TLS via Let's Encrypt with Cloudflare DNS challenge
|
||||||
- Publish uses rsync over SSH as `gitea_ci` user
|
- Publish uses rsync over SSH as `gitea_ci` user
|
||||||
|
|||||||
@@ -10,16 +10,14 @@ server {
|
|||||||
|
|
||||||
root /var/www/rpm;
|
root /var/www/rpm;
|
||||||
|
|
||||||
autoindex on;
|
include /etc/nginx/mime.types;
|
||||||
autoindex_exact_size off;
|
|
||||||
autoindex_localtime on;
|
|
||||||
|
|
||||||
types {
|
|
||||||
application/x-rpm rpm;
|
|
||||||
application/xml xml;
|
|
||||||
}
|
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
location ~ \.rpm$ {
|
location ~ \.rpm$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
@@ -30,7 +28,16 @@ server {
|
|||||||
add_header Cache-Control "no-cache, must-revalidate";
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ packages\.json$ {
|
||||||
|
expires 5m;
|
||||||
|
add_header Cache-Control "public, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
location ~ \.gpg$ {
|
location ~ \.gpg$ {
|
||||||
default_type text/plain;
|
default_type text/plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
readme.md
84
readme.md
@@ -8,31 +8,34 @@ This repo does not contain the mistral.rs source. It clones upstream at a given
|
|||||||
|
|
||||||
Two Gitea Actions workflows drive the pipeline:
|
Two Gitea Actions workflows drive the pipeline:
|
||||||
|
|
||||||
1. **poll-upstream** runs every 15 minutes, checks GitHub for the latest mistral.rs release tag, and triggers a build if the corresponding RPM doesn't already exist on `rpm.lair.cafe`.
|
1. **poll-upstream** runs every 15 minutes, checks GitHub for the latest mistral.rs release tag, and triggers a build if the corresponding RPM doesn't already exist on `rpm.lair.cafe`. It also checks the upstream `main` branch HEAD and triggers prerelease builds for the unstable repo.
|
||||||
2. **build-release** runs in three stages:
|
2. **build-release** runs in three stages:
|
||||||
- **build** — clones upstream at the tag and compiles `mistralrs-server` with flavour-specific CUDA features on a `cuda-13.0` runner.
|
- **build** — clones upstream at the tag and compiles `mistralrs` with flavour-specific CUDA features on a `cuda-13.0` runner.
|
||||||
- **package** — builds an RPM from the compiled binary using `rpmbuild`.
|
- **package** — builds an RPM from the compiled binary using `rpmbuild`.
|
||||||
- **publish** — GPG-signs the RPMs, rsyncs them to `rpm.lair.cafe`, and updates the repo metadata with `createrepo_c`.
|
- **publish** — GPG-signs the RPMs, rsyncs them to `rpm.lair.cafe`, and updates the repo metadata with `createrepo_c`.
|
||||||
|
3. **build-prerelease** — same structure as build-release but clones at a specific commit from `main`, uses versioning from `Cargo.toml` with a prerelease release suffix (e.g. `0.8.1-0.1.20260511git1a2b3c4`), and publishes to the unstable repo.
|
||||||
|
|
||||||
### Flavours
|
### Flavours
|
||||||
|
|
||||||
Build flavours are defined in the workflow matrix. Each flavour specifies a name, CUDA home path, cargo features, and compute capabilities. The RPM spec uses `update-alternatives` so multiple flavours can coexist, with priority: base=10, fa=20, nccl=30.
|
Build flavours are defined in the workflow matrix. Each flavour targets a specific GPU generation with the same CUDA 13.0 toolkit and features (cuda, cudnn, flash-attn, nccl).
|
||||||
|
|
||||||
Currently defined:
|
Currently defined:
|
||||||
|
|
||||||
| Flavour | Features | Compute cap |
|
| Flavour | Compute cap | GPU generation |
|
||||||
|----------|-------------------------------|-------------|
|
|------------|-------------|---------------------------|
|
||||||
| cuda13 | cuda, cudnn, flash-attn, nccl | sm_120 |
|
| ampere | sm_86 | RTX 3060, A2000–A6000 |
|
||||||
|
| ada | sm_89 | RTX 4060–4090, L40 |
|
||||||
|
| blackwell | sm_120 | RTX 5090, B100, B200 |
|
||||||
|
|
||||||
### Systemd integration
|
### Systemd integration
|
||||||
|
|
||||||
Each RPM installs a templated systemd unit (`mistralrs-<flavour>@.service`). Instances are configured via environment files in `/etc/mistralrs/`:
|
Each RPM installs a templated systemd unit (`mistralrs@.service`). Instances are configured via environment files in `/etc/mistralrs/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# copy the example config
|
# copy the example config
|
||||||
sudo cp /etc/mistralrs/cuda13.conf.example /etc/mistralrs/mymodel.conf
|
sudo cp /etc/mistralrs/default.conf.example /etc/mistralrs/mymodel.conf
|
||||||
# edit MISTRALRS_ARGS, HF_TOKEN, etc.
|
# edit MISTRALRS_ARGS, HF_TOKEN, etc.
|
||||||
sudo systemctl start mistralrs-cuda13@mymodel
|
sudo systemctl start mistralrs@mymodel
|
||||||
```
|
```
|
||||||
|
|
||||||
## Infrastructure setup
|
## Infrastructure setup
|
||||||
@@ -93,29 +96,78 @@ gpg --homedir ~/.gnupg/lair --quick-add-key <master-fpr> ed25519 sign 1y
|
|||||||
|
|
||||||
Then update the `RPM_SIGNING_KEY` secret in Gitea with the new subkey. The public key served to users doesn't change since it's anchored to the master key.
|
Then update the `RPM_SIGNING_KEY` secret in Gitea with the new subkey. The public key served to users doesn't change since it's anchored to the master key.
|
||||||
|
|
||||||
|
### 5. Runner prerequisites
|
||||||
|
|
||||||
|
#### sequoia-sq (for RPM signing)
|
||||||
|
|
||||||
|
Runners that run the publish job need `sequoia-sq` installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install sequoia-sq
|
||||||
|
```
|
||||||
|
|
||||||
## Client setup
|
## Client setup
|
||||||
|
|
||||||
|
### Stable packages
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo rpm --import https://rpm.lair.cafe/<short-id>.gpg
|
sudo rpm --import https://rpm.lair.cafe/<short-id>.gpg
|
||||||
sudo dnf config-manager addrepo --from-repofile=/dev/stdin <<EOF
|
sudo tee /etc/yum.repos.d/lair-cafe.repo > /dev/null <<'EOF'
|
||||||
[lair-rpm]
|
[lair-cafe]
|
||||||
name=lair.cafe RPM repo
|
name=lair.cafe RPM Repository
|
||||||
baseurl=https://rpm.lair.cafe/fedora/43/x86_64
|
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
gpgkey=https://rpm.lair.cafe/<short-id>.gpg
|
gpgkey=https://rpm.lair.cafe/<short-id>.gpg
|
||||||
EOF
|
EOF
|
||||||
sudo dnf install mistralrs-server-cuda13
|
|
||||||
|
# install the package for your GPU generation
|
||||||
|
sudo dnf install mistralrs-ampere # RTX 3000 series
|
||||||
|
sudo dnf install mistralrs-ada # RTX 4000 series
|
||||||
|
sudo dnf install mistralrs-blackwell # RTX 5000 series
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Unstable (prerelease) packages
|
||||||
|
|
||||||
|
Unstable packages are built from the latest upstream `main` commit and published to a separate repo. The RPM release field uses the Fedora snapshot convention (e.g. `0.8.1-0.1.20260511git1a2b3c4.fc43`) so stable releases automatically supersede any installed prerelease.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/yum.repos.d/lair-cafe-unstable.repo > /dev/null <<'EOF'
|
||||||
|
[lair-cafe-unstable]
|
||||||
|
name=lair.cafe RPM Repository (unstable)
|
||||||
|
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/unstable/
|
||||||
|
enabled=0
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://rpm.lair.cafe/<short-id>.gpg
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# install from unstable on demand
|
||||||
|
sudo dnf --enablerepo=lair-cafe-unstable install mistralrs-ada
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forcing a rebuild
|
||||||
|
|
||||||
|
To force a rebuild of an already-published RPM (e.g. after a packaging change), remove the RPM from the repo server and update the index:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh oolon "
|
||||||
|
sudo rm /var/www/rpm/fedora/43/x86_64/mistralrs-ada-<version>-1.fc43.x86_64.rpm \
|
||||||
|
&& cd /var/www/rpm/fedora/43/x86_64 \
|
||||||
|
&& sudo createrepo_c --update .;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
The next poll-upstream cycle (every 15 minutes) will detect the missing package and trigger a full rebuild. You can also trigger poll-upstream manually from the Gitea Actions UI to avoid waiting.
|
||||||
|
|
||||||
|
Do not delete the RPM without running `createrepo_c --update` afterwards — this leaves the repo index referencing a missing file, which causes errors for dnf clients.
|
||||||
|
|
||||||
## CI secrets
|
## CI secrets
|
||||||
|
|
||||||
The build-release workflow requires the following secrets:
|
The build-release and build-prerelease workflows require the following secrets:
|
||||||
|
|
||||||
| Secret | Purpose |
|
| Secret | Purpose |
|
||||||
|------------------|----------------------------------------------|
|
|------------------|----------------------------------------------|
|
||||||
| `DISPATCH_TOKEN` | Gitea API token for triggering builds |
|
| `DISPATCH_TOKEN` | Gitea API token for triggering builds |
|
||||||
| `RPM_SIGNING_KEY`| ASCII-armored GPG signing subkey |
|
| `RPM_SIGNING_KEY`| ASCII-armored GPG signing subkey |
|
||||||
| `RPM_SIGNING_KEY_ID` | GPG key UID (`rpm@lair.cafe`) |
|
| `RPM_SIGNING_KEY_ID` | GPG key UID (`rpm@lair.cafe`) |
|
||||||
| `RSYNC_TARGET` | SSH target for rsync (e.g. `gitea_ci@oolon`) |
|
|
||||||
| `RSYNC_SSH_KEY` | SSH private key for the `gitea_ci` user |
|
| `RSYNC_SSH_KEY` | SSH private key for the `gitea_ci` user |
|
||||||
|
|||||||
@@ -4,46 +4,45 @@
|
|||||||
|
|
||||||
# Passed in via --define at rpmbuild time
|
# Passed in via --define at rpmbuild time
|
||||||
%{!?mistralrs_version: %global mistralrs_version 0.7.0}
|
%{!?mistralrs_version: %global mistralrs_version 0.7.0}
|
||||||
%{!?mistralrs_flavour: %global mistralrs_flavour cuda13}
|
%{!?mistralrs_flavour: %global mistralrs_flavour blackwell}
|
||||||
|
|
||||||
Name: mistralrs-server-%{mistralrs_flavour}
|
# For prerelease builds, pass --define "mistralrs_prerelease 0.1.YYYYMMDDgitSHORTSHA"
|
||||||
|
%if 0%{?mistralrs_prerelease:1}
|
||||||
|
%global mistralrs_release %{mistralrs_prerelease}
|
||||||
|
%else
|
||||||
|
%global mistralrs_release 1
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Name: mistralrs-%{mistralrs_flavour}
|
||||||
Version: %{mistralrs_version}
|
Version: %{mistralrs_version}
|
||||||
Release: 1%{?dist}
|
Release: %{mistralrs_release}%{?dist}
|
||||||
Summary: Fast, flexible LLM inference server (mistral.rs, %{mistralrs_flavour} flavour)
|
Summary: Fast, flexible LLM inference server (mistral.rs, %{mistralrs_flavour} flavour)
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
URL: https://github.com/EricLBuehler/mistral.rs
|
URL: https://github.com/EricLBuehler/mistral.rs
|
||||||
|
|
||||||
# Pre-built binary (produced in the build job, not rebuilt here)
|
# Pre-built binary (produced in the build job, not rebuilt here)
|
||||||
Source0: mistralrs-server-%{mistralrs_flavour}
|
Source0: mistralrs-%{mistralrs_flavour}
|
||||||
Source1: mistralrs@.service
|
Source1: mistralrs@.service
|
||||||
Source2: mistralrs@.conf.example
|
Source2: mistralrs@.conf.example
|
||||||
|
|
||||||
ExclusiveArch: x86_64
|
ExclusiveArch: x86_64
|
||||||
|
|
||||||
# Runtime requirements. We link against the CUDA runtime; consumers must have
|
# Suppress auto-detected CUDA library dependencies. The binary links against
|
||||||
# a matching CUDA installation or the rpmfusion nvidia driver's cuda-libs.
|
# the CUDA runtime, cuDNN, NCCL, etc. but we don't want rpm to pin exact
|
||||||
# We don't hard-require it at the RPM level because consumers may have CUDA
|
# soname versions — consumers may have CUDA from multiple sources (nvidia
|
||||||
# from multiple sources (nvidia direct, rpmfusion, etc.) — failing to load
|
# direct, rpmfusion, etc.) and different compatible versions. A runtime
|
||||||
# libcuda.so at runtime gives a clearer error than RPM dep resolution would.
|
# dlopen failure gives a clearer error than rpm dep resolution would.
|
||||||
|
%global __requires_exclude ^lib(cuda|cudart|cudnn|cublas|cublasLt|curand|nvrtc|nccl)
|
||||||
Requires: systemd
|
Requires: systemd
|
||||||
|
|
||||||
# Flavours are mutually exclusive with other flavours of themselves at the
|
|
||||||
# same install path, but you can install cuda13, cuda13-fa, cuda13-fa-nccl
|
|
||||||
# side by side — they all get separate /opt paths.
|
|
||||||
Provides: mistralrs-server = %{version}-%{release}
|
|
||||||
|
|
||||||
%description
|
%description
|
||||||
mistral.rs is a blazingly fast LLM inference engine written in Rust.
|
mistral.rs is a blazingly fast LLM inference engine written in Rust.
|
||||||
This package provides the %{mistralrs_flavour} flavour, built with features:
|
This package provides the %{mistralrs_flavour} flavour, built with features:
|
||||||
cuda, cudnn, and optionally flash-attn and nccl depending on flavour name.
|
cuda, cudnn, and optionally flash-attn and nccl depending on flavour name.
|
||||||
|
|
||||||
Binary installs to /opt/mistralrs/%{mistralrs_flavour}/bin/ and can coexist
|
|
||||||
with other flavours. Use `update-alternatives --config mistralrs-server` to
|
|
||||||
select the default /usr/bin/mistralrs-server symlink target.
|
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
# Nothing to unpack; Source0 is the binary itself
|
|
||||||
cp %{SOURCE0} .
|
cp %{SOURCE0} .
|
||||||
cp %{SOURCE1} .
|
cp %{SOURCE1} .
|
||||||
cp %{SOURCE2} .
|
cp %{SOURCE2} .
|
||||||
@@ -52,49 +51,35 @@ cp %{SOURCE2} .
|
|||||||
# Already built
|
# Already built
|
||||||
|
|
||||||
%install
|
%install
|
||||||
install -D -m 0755 mistralrs-server-%{mistralrs_flavour} \
|
install -D -m 0755 mistralrs-%{mistralrs_flavour} \
|
||||||
%{buildroot}/opt/mistralrs/%{mistralrs_flavour}/bin/mistralrs-server
|
%{buildroot}%{_bindir}/mistralrs
|
||||||
install -D -m 0644 mistralrs@.service \
|
install -D -m 0644 mistralrs@.service \
|
||||||
%{buildroot}%{_unitdir}/mistralrs-%{mistralrs_flavour}@.service
|
%{buildroot}%{_unitdir}/mistralrs@.service
|
||||||
install -D -m 0644 mistralrs@.conf.example \
|
install -D -m 0644 mistralrs@.conf.example \
|
||||||
%{buildroot}%{_sysconfdir}/mistralrs/%{mistralrs_flavour}.conf.example
|
%{buildroot}%{_sysconfdir}/mistralrs/default.conf.example
|
||||||
|
|
||||||
# Patch the unit to point at this flavour's binary
|
# Patch the unit to point at the binary
|
||||||
sed -i "s|@BINARY@|/opt/mistralrs/%{mistralrs_flavour}/bin/mistralrs-server|g" \
|
sed -i "s|@BINARY@|%{_bindir}/mistralrs|g" \
|
||||||
%{buildroot}%{_unitdir}/mistralrs-%{mistralrs_flavour}@.service
|
%{buildroot}%{_unitdir}/mistralrs@.service
|
||||||
sed -i "s|@FLAVOUR@|%{mistralrs_flavour}|g" \
|
sed -i "s|@FLAVOUR@|%{mistralrs_flavour}|g" \
|
||||||
%{buildroot}%{_unitdir}/mistralrs-%{mistralrs_flavour}@.service
|
%{buildroot}%{_unitdir}/mistralrs@.service
|
||||||
|
|
||||||
|
%pre
|
||||||
|
getent group mistralrs >/dev/null || groupadd -r mistralrs
|
||||||
|
getent passwd mistralrs >/dev/null || useradd -r -g mistralrs -d /var/lib/mistralrs -s /sbin/nologin mistralrs
|
||||||
|
|
||||||
%post
|
%post
|
||||||
# Register this flavour as an alternative for /usr/bin/mistralrs-server.
|
%systemd_post mistralrs@.service
|
||||||
# Priority = 10 for cuda13, 20 for cuda13-fa, 30 for cuda13-fa-nccl so that
|
|
||||||
# "more featureful" wins by default. Consumers can override with
|
|
||||||
# `update-alternatives --config mistralrs-server`.
|
|
||||||
priority=10
|
|
||||||
case "%{mistralrs_flavour}" in
|
|
||||||
*nccl*) priority=30 ;;
|
|
||||||
*fa*) priority=20 ;;
|
|
||||||
esac
|
|
||||||
update-alternatives --install /usr/bin/mistralrs-server mistralrs-server \
|
|
||||||
/opt/mistralrs/%{mistralrs_flavour}/bin/mistralrs-server "${priority}"
|
|
||||||
|
|
||||||
%systemd_post mistralrs-%{mistralrs_flavour}@.service
|
|
||||||
|
|
||||||
%preun
|
%preun
|
||||||
%systemd_preun mistralrs-%{mistralrs_flavour}@.service
|
%systemd_preun mistralrs@.service
|
||||||
|
|
||||||
%postun
|
%postun
|
||||||
if [ $1 -eq 0 ]; then
|
%systemd_postun_with_restart mistralrs@.service
|
||||||
update-alternatives --remove mistralrs-server \
|
|
||||||
/opt/mistralrs/%{mistralrs_flavour}/bin/mistralrs-server
|
|
||||||
fi
|
|
||||||
%systemd_postun_with_restart mistralrs-%{mistralrs_flavour}@.service
|
|
||||||
|
|
||||||
%files
|
%files
|
||||||
/opt/mistralrs/%{mistralrs_flavour}/
|
%{_bindir}/mistralrs
|
||||||
%{_unitdir}/mistralrs-%{mistralrs_flavour}@.service
|
%{_unitdir}/mistralrs@.service
|
||||||
%{_sysconfdir}/mistralrs/%{mistralrs_flavour}.conf.example
|
%{_sysconfdir}/mistralrs/default.conf.example
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Apr 23 2026 Robin Thijssen <grenade@lair.cafe> - %{mistralrs_version}-1
|
|
||||||
- Automated build for %{mistralrs_flavour} flavour
|
|
||||||
|
|||||||
1
rpm/rpmmacros
Normal file
1
rpm/rpmmacros
Normal file
@@ -0,0 +1 @@
|
|||||||
|
%_openpgp_sign_id @GPG_NAME@
|
||||||
@@ -9,7 +9,7 @@ User=mistralrs
|
|||||||
Group=mistralrs
|
Group=mistralrs
|
||||||
SupplementaryGroups=video render
|
SupplementaryGroups=video render
|
||||||
EnvironmentFile=/etc/mistralrs/%i.conf
|
EnvironmentFile=/etc/mistralrs/%i.conf
|
||||||
ExecStart=@BINARY@ $MISTRALRS_ARGS
|
ExecStart=@BINARY@ serve $MISTRALRS_ARGS
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10s
|
RestartSec=10s
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
: "${FLAVOUR_NAME:?}"
|
|
||||||
: "${CUDA_HOME:?}"
|
|
||||||
: "${CARGO_FEATURES:?}"
|
|
||||||
: "${CUDA_COMPUTE_CAP:?}"
|
|
||||||
: "${SRC_DIR:?}"
|
|
||||||
|
|
||||||
export PATH="${CUDA_HOME}/bin:${PATH}"
|
|
||||||
export LD_LIBRARY_PATH="${CUDA_HOME}/targets/x86_64-linux/lib:${CUDA_HOME}/lib64:${LD_LIBRARY_PATH:-}"
|
|
||||||
|
|
||||||
cd "${SRC_DIR}"
|
|
||||||
|
|
||||||
# --locked ensures Cargo.lock is respected; fails loud if it's out of sync
|
|
||||||
# rather than silently resolving to different versions.
|
|
||||||
cargo build --release --locked --features "${CARGO_FEATURES}"
|
|
||||||
|
|
||||||
mkdir -p ../artifacts
|
|
||||||
cp target/release/mistralrs-server "../artifacts/mistralrs-server-${FLAVOUR_NAME}"
|
|
||||||
|
|
||||||
# Also grab the other binaries if you want them
|
|
||||||
cp target/release/mistralrs "../artifacts/mistralrs-${FLAVOUR_NAME}" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "Built $(../artifacts/mistralrs-server-${FLAVOUR_NAME} --version 2>&1 | head -1)"
|
|
||||||
154
script/generate-packages-json.py
Executable file
154
script/generate-packages-json.py
Executable file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Parse RPM repodata and emit a packages.json manifest for the UI."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
RPM_NS = "http://linux.duke.edu/metadata/common"
|
||||||
|
OTHER_NS = "http://linux.duke.edu/metadata/other"
|
||||||
|
REPO_NS = "http://linux.duke.edu/metadata/repo"
|
||||||
|
|
||||||
|
|
||||||
|
def find_repodata_file(repodata_dir, data_type):
|
||||||
|
"""Read repomd.xml and return the path to a specific data type's file."""
|
||||||
|
repomd_path = os.path.join(repodata_dir, "repomd.xml")
|
||||||
|
tree = ET.parse(repomd_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
for data in root.findall(f"{{{REPO_NS}}}data"):
|
||||||
|
if data.get("type") == data_type:
|
||||||
|
location = data.find(f"{{{REPO_NS}}}location")
|
||||||
|
if location is not None:
|
||||||
|
href = location.get("href", "")
|
||||||
|
return os.path.join(os.path.dirname(repodata_dir), href)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def open_compressed(path):
|
||||||
|
"""Open a gzip or zstd compressed file for reading."""
|
||||||
|
if path.endswith(".zst"):
|
||||||
|
result = subprocess.run(
|
||||||
|
["zstdcat", path], capture_output=True, check=True
|
||||||
|
)
|
||||||
|
import io
|
||||||
|
return io.BytesIO(result.stdout)
|
||||||
|
else:
|
||||||
|
return gzip.open(path, "rb")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_primary(repodata_dir):
|
||||||
|
"""Parse primary.xml.{gz,zst} and return package metadata."""
|
||||||
|
path = find_repodata_file(repodata_dir, "primary")
|
||||||
|
if not path:
|
||||||
|
print("error: primary metadata not found in repomd.xml", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
packages = {}
|
||||||
|
with open_compressed(path) as f:
|
||||||
|
tree = ET.parse(f)
|
||||||
|
|
||||||
|
for pkg in tree.getroot().findall(f"{{{RPM_NS}}}package"):
|
||||||
|
if pkg.get("type") != "rpm":
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = pkg.findtext(f"{{{RPM_NS}}}name", "")
|
||||||
|
version_el = pkg.find(f"{{{RPM_NS}}}version")
|
||||||
|
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||||
|
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||||
|
arch = pkg.findtext(f"{{{RPM_NS}}}arch", "")
|
||||||
|
|
||||||
|
size_el = pkg.find(f"{{{RPM_NS}}}size")
|
||||||
|
size = int(size_el.get("package", "0")) if size_el is not None else 0
|
||||||
|
|
||||||
|
time_el = pkg.find(f"{{{RPM_NS}}}time")
|
||||||
|
build_time = int(time_el.get("build", "0")) if time_el is not None else 0
|
||||||
|
|
||||||
|
location_el = pkg.find(f"{{{RPM_NS}}}location")
|
||||||
|
filename = os.path.basename(location_el.get("href", "")) if location_el is not None else ""
|
||||||
|
|
||||||
|
key = f"{name}-{ver}-{rel}"
|
||||||
|
packages[key] = {
|
||||||
|
"name": name,
|
||||||
|
"version": ver,
|
||||||
|
"release": rel,
|
||||||
|
"arch": arch,
|
||||||
|
"summary": pkg.findtext(f"{{{RPM_NS}}}summary", ""),
|
||||||
|
"size": size,
|
||||||
|
"buildTime": build_time,
|
||||||
|
"rpmFilename": filename,
|
||||||
|
"changelog": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def parse_other(repodata_dir, packages):
|
||||||
|
"""Parse other.xml.gz and attach changelog entries to packages."""
|
||||||
|
path = find_repodata_file(repodata_dir, "other")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open_compressed(path) as f:
|
||||||
|
tree = ET.parse(f)
|
||||||
|
|
||||||
|
for pkg in tree.getroot().findall(f"{{{OTHER_NS}}}package"):
|
||||||
|
name = pkg.get("name", "")
|
||||||
|
version_el = pkg.find(f"{{{OTHER_NS}}}version")
|
||||||
|
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||||
|
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||||
|
key = f"{name}-{ver}-{rel}"
|
||||||
|
|
||||||
|
if key not in packages:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for entry in pkg.findall(f"{{{OTHER_NS}}}changelog"):
|
||||||
|
packages[key]["changelog"].append({
|
||||||
|
"author": entry.get("author", ""),
|
||||||
|
"date": int(entry.get("date", "0")),
|
||||||
|
"text": (entry.text or "").strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repodata-dir",
|
||||||
|
required=True,
|
||||||
|
help="path to the repodata/ directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
required=True,
|
||||||
|
help="path to write packages.json",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base-url",
|
||||||
|
required=True,
|
||||||
|
help="public base URL for the repo (e.g. https://rpm.lair.cafe/fedora/43/x86_64)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
packages = parse_primary(args.repodata_dir)
|
||||||
|
parse_other(args.repodata_dir, packages)
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"generated": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"baseUrl": args.base_url,
|
||||||
|
"packages": list(packages.values()),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(args.output, "w") as f:
|
||||||
|
json.dump(manifest, f, indent=2)
|
||||||
|
|
||||||
|
print(f"wrote {len(packages)} packages to {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
RPM_DIR="${1:?usage: $0 <rpm-directory>}"
|
|
||||||
REMOTE_DIR="/var/www/rpm/fedora/43/x86_64"
|
|
||||||
|
|
||||||
# sign each rpm with the imported gpg key
|
|
||||||
for rpm in "${RPM_DIR}"/*.rpm; do
|
|
||||||
rpm --addsign "${rpm}"
|
|
||||||
done
|
|
||||||
|
|
||||||
install --directory --mode 700 ~/.ssh
|
|
||||||
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan -H oolon.kosherinata.internal > ~/.ssh/known_hosts 2>/dev/null
|
|
||||||
|
|
||||||
rsync \
|
|
||||||
--archive \
|
|
||||||
--verbose \
|
|
||||||
--chmod D755,F644 \
|
|
||||||
"${RPM_DIR}/"*.rpm \
|
|
||||||
"${RSYNC_TARGET}:${REMOTE_DIR}/"
|
|
||||||
ssh "${RSYNC_TARGET}" "cd ${REMOTE_DIR} && createrepo_c --update ."
|
|
||||||
|
|
||||||
echo "Published $(ls ${RPM_DIR}/*.rpm | wc -l) RPMs"
|
|
||||||
54
script/setup/nvm.sh
Executable file
54
script/setup/nvm.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set up nvm and a stable default Node.js for a gitea_runner user on a CI runner.
|
||||||
|
#
|
||||||
|
# Usage: ./script/setup/nvm.sh <runner-host>
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Installs nvm for the gitea_runner user (if not already present)
|
||||||
|
# 2. Installs the current Node.js LTS version
|
||||||
|
# 3. Creates a stable symlink at ~/.nvm/default_bin pointing to the
|
||||||
|
# default node binary directory
|
||||||
|
# 4. Prints the systemd Environment line needed for the runner unit
|
||||||
|
#
|
||||||
|
# After running, add the following to the runner's systemd unit
|
||||||
|
# (/etc/systemd/system/gitea-action-runner.service):
|
||||||
|
#
|
||||||
|
# Environment=PATH=/var/lib/gitea_runner/.nvm/default_bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
||||||
|
#
|
||||||
|
# Then: sudo systemctl daemon-reload && sudo systemctl restart gitea-action-runner
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
runner_host="${1:?usage: $0 <runner-host>}"
|
||||||
|
runner_user="gitea_runner"
|
||||||
|
|
||||||
|
echo "installing nvm on ${runner_host} for ${runner_user}..."
|
||||||
|
ssh "${runner_host}" "sudo -u ${runner_user} bash -c '
|
||||||
|
export NVM_DIR=\"\${HOME}/.nvm\"
|
||||||
|
if [ -s \"\${NVM_DIR}/nvm.sh\" ]; then
|
||||||
|
echo \"nvm already installed at \${NVM_DIR}\"
|
||||||
|
else
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||||
|
fi
|
||||||
|
'"
|
||||||
|
|
||||||
|
echo "installing Node.js LTS and creating stable symlink..."
|
||||||
|
ssh "${runner_host}" "sudo -u ${runner_user} bash -c '
|
||||||
|
export NVM_DIR=\"\${HOME}/.nvm\"
|
||||||
|
. \"\${NVM_DIR}/nvm.sh\"
|
||||||
|
nvm install --lts
|
||||||
|
nvm alias default lts/*
|
||||||
|
ln -sfn \"\$(dirname \"\$(nvm which default)\")\" \"\${NVM_DIR}/default_bin\"
|
||||||
|
echo \"node: \$(node --version)\"
|
||||||
|
echo \"symlink: \$(readlink -f \"\${NVM_DIR}/default_bin\")\"
|
||||||
|
'"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "add the following to /etc/systemd/system/gitea-action-runner.service on ${runner_host}:"
|
||||||
|
echo ""
|
||||||
|
echo " Environment=PATH=/var/lib/${runner_user}/.nvm/default_bin:/usr/local/sbin:/usr/local/bin:/usr/bin"
|
||||||
|
echo ""
|
||||||
|
echo "then run:"
|
||||||
|
echo " ssh ${runner_host} 'sudo systemctl daemon-reload && sudo systemctl restart gitea-action-runner'"
|
||||||
|
echo ""
|
||||||
|
echo "also add 'nvm' to the runner's labels in /etc/act_runner/config.yml"
|
||||||
24
ui/.gitignore
vendored
Normal file
24
ui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
12
ui/index.html
Normal file
12
ui/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>rpm.lair.cafe</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1733
ui/package-lock.json
generated
Normal file
1733
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
ui/package.json
Normal file
25
ui/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^8.0.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
|
"react-dom": "^19.2.5",
|
||||||
|
"react-router": "^7.14.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
ui/public/lair-cafe-unstable.repo
Normal file
6
ui/public/lair-cafe-unstable.repo
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[lair-cafe-unstable]
|
||||||
|
name=lair.cafe RPM Repository (unstable)
|
||||||
|
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/unstable/
|
||||||
|
enabled=0
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://rpm.lair.cafe/8b2023ce.gpg
|
||||||
6
ui/public/lair-cafe.repo
Normal file
6
ui/public/lair-cafe.repo
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[lair-cafe]
|
||||||
|
name=lair.cafe RPM Repository
|
||||||
|
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://rpm.lair.cafe/8b2023ce.gpg
|
||||||
25
ui/src/App.tsx
Normal file
25
ui/src/App.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||||
|
import { ThemeProvider } from "./theme/ThemeContext.tsx";
|
||||||
|
import { Layout } from "./components/Layout.tsx";
|
||||||
|
import { Home } from "./pages/Home.tsx";
|
||||||
|
import { PackageList } from "./pages/PackageList.tsx";
|
||||||
|
import { PackageDetail } from "./pages/PackageDetail.tsx";
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
element: <Layout />,
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <Home /> },
|
||||||
|
{ path: "packages", element: <PackageList /> },
|
||||||
|
{ path: "packages/:name", element: <PackageDetail /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
ui/src/components/CodeBlock.tsx
Normal file
35
ui/src/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
children: string;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({ children, language }: CodeBlockProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
async function copy() {
|
||||||
|
await navigator.clipboard.writeText(children.trim());
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="position-relative">
|
||||||
|
<pre className="bg-body-tertiary rounded p-3 overflow-auto">
|
||||||
|
<code className={language ? `language-${language}` : undefined}>
|
||||||
|
{children.trim()}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<Button
|
||||||
|
variant="outline-secondary"
|
||||||
|
size="sm"
|
||||||
|
className="position-absolute top-0 end-0 m-2"
|
||||||
|
onClick={copy}
|
||||||
|
>
|
||||||
|
{copied ? "Copied" : "Copy"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
ui/src/components/Layout.tsx
Normal file
14
ui/src/components/Layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Container } from "react-bootstrap";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
import { NavHeader } from "./NavHeader.tsx";
|
||||||
|
|
||||||
|
export function Layout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavHeader />
|
||||||
|
<Container as="main" className="pb-5">
|
||||||
|
<Outlet />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
ui/src/components/NavHeader.tsx
Normal file
37
ui/src/components/NavHeader.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Container, Nav, Navbar } from "react-bootstrap";
|
||||||
|
import { Link, useLocation } from "react-router";
|
||||||
|
import { ThemeToggle } from "../theme/ThemeToggle.tsx";
|
||||||
|
|
||||||
|
export function NavHeader() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar expand="sm" className="border-bottom mb-4">
|
||||||
|
<Container>
|
||||||
|
<Navbar.Brand as={Link} to="/">
|
||||||
|
rpm.lair.cafe
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Navbar.Toggle />
|
||||||
|
<Navbar.Collapse>
|
||||||
|
<Nav className="me-auto">
|
||||||
|
<Nav.Link
|
||||||
|
as={Link}
|
||||||
|
to="/"
|
||||||
|
active={location.pathname === "/"}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link
|
||||||
|
as={Link}
|
||||||
|
to="/packages"
|
||||||
|
active={location.pathname.startsWith("/packages")}
|
||||||
|
>
|
||||||
|
Packages
|
||||||
|
</Nav.Link>
|
||||||
|
</Nav>
|
||||||
|
<ThemeToggle />
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
ui/src/hooks/usePackages.ts
Normal file
60
ui/src/hooks/usePackages.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { Channel, PackagesManifest, PackageVersion } from "../types/packages.ts";
|
||||||
|
|
||||||
|
const STABLE_URL = "/fedora/43/x86_64/packages.json";
|
||||||
|
const UNSTABLE_URL = "/fedora/43/x86_64/unstable/packages.json";
|
||||||
|
|
||||||
|
function tagPackages(
|
||||||
|
manifest: PackagesManifest,
|
||||||
|
channel: Channel,
|
||||||
|
): PackageVersion[] {
|
||||||
|
return manifest.packages.map((p) => ({
|
||||||
|
...p,
|
||||||
|
channel,
|
||||||
|
baseUrl: manifest.baseUrl,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePackages() {
|
||||||
|
const [packages, setPackages] = useState<PackageVersion[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const fetchManifest = async (
|
||||||
|
url: string,
|
||||||
|
channel: Channel,
|
||||||
|
): Promise<PackageVersion[]> => {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) return [];
|
||||||
|
throw new Error(`HTTP ${res.status} fetching ${channel} manifest`);
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as PackagesManifest;
|
||||||
|
return tagPackages(data, channel);
|
||||||
|
};
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
fetchManifest(STABLE_URL, "stable"),
|
||||||
|
fetchManifest(UNSTABLE_URL, "unstable"),
|
||||||
|
])
|
||||||
|
.then(([stable, unstable]) => {
|
||||||
|
if (!cancelled) setPackages([...stable, ...unstable]);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!cancelled)
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { packages, loading, error };
|
||||||
|
}
|
||||||
10
ui/src/main.tsx
Normal file
10
ui/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
91
ui/src/pages/Home.tsx
Normal file
91
ui/src/pages/Home.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Card, Col, Row } from "react-bootstrap";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock.tsx";
|
||||||
|
|
||||||
|
const GPG_KEY_URL = "https://rpm.lair.cafe/8b2023ce.gpg";
|
||||||
|
const REPO_URL = "https://rpm.lair.cafe/lair-cafe.repo";
|
||||||
|
const UNSTABLE_REPO_URL = "https://rpm.lair.cafe/lair-cafe-unstable.repo";
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="mb-3">rpm.lair.cafe</h1>
|
||||||
|
<p className="lead mb-4">
|
||||||
|
Self-hosted RPM repository for Fedora, currently hosting
|
||||||
|
CUDA-accelerated builds of{" "}
|
||||||
|
<a href="https://github.com/EricLBuehler/mistral.rs">mistral.rs</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Row className="g-4">
|
||||||
|
<Col lg={12}>
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>Quick start</Card.Title>
|
||||||
|
|
||||||
|
<h6 className="mt-4">1. Import the signing key</h6>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`sudo rpm --import ${GPG_KEY_URL}`}
|
||||||
|
</CodeBlock>
|
||||||
|
|
||||||
|
<h6 className="mt-4">2. Add the repository</h6>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`sudo dnf config-manager addrepo --from-repofile=${REPO_URL}`}
|
||||||
|
</CodeBlock>
|
||||||
|
|
||||||
|
<h6 className="mt-4">3. Install a package</h6>
|
||||||
|
<p className="text-body-secondary">
|
||||||
|
Choose the package matching your GPU generation:
|
||||||
|
</p>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`# RTX 3000 series (Ampere)\nsudo dnf install mistralrs-ampere\n\n# RTX 4000 series (Ada Lovelace)\nsudo dnf install mistralrs-ada\n\n# RTX 5000 series (Blackwell)\nsudo dnf install mistralrs-blackwell`}
|
||||||
|
</CodeBlock>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col lg={12}>
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>Unstable (prerelease) packages</Card.Title>
|
||||||
|
<p>
|
||||||
|
Unstable packages are built automatically from the latest
|
||||||
|
upstream <code>main</code> branch commit. They use the
|
||||||
|
next release version from <code>Cargo.toml</code> with a
|
||||||
|
prerelease suffix (e.g.{" "}
|
||||||
|
<code>0.8.1-0.1.20260511git1a2b3c4</code>). When the
|
||||||
|
upstream version is officially released, the stable package
|
||||||
|
will automatically supersede any installed prerelease.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6 className="mt-4">Add the unstable repository</h6>
|
||||||
|
<p className="text-body-secondary">
|
||||||
|
The unstable repo is disabled by default. Add it alongside the
|
||||||
|
stable repo:
|
||||||
|
</p>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`sudo dnf config-manager addrepo --from-repofile=${UNSTABLE_REPO_URL}`}
|
||||||
|
</CodeBlock>
|
||||||
|
|
||||||
|
<h6 className="mt-4">
|
||||||
|
Install or update from unstable
|
||||||
|
</h6>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`sudo dnf --enablerepo=lair-cafe-unstable install mistralrs-ada`}
|
||||||
|
</CodeBlock>
|
||||||
|
|
||||||
|
<h6 className="mt-4">
|
||||||
|
Pin to stable
|
||||||
|
</h6>
|
||||||
|
<p className="text-body-secondary">
|
||||||
|
If you have the unstable repo enabled and want to stay on
|
||||||
|
stable releases, exclude prerelease versions:
|
||||||
|
</p>
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`sudo dnf --disablerepo=lair-cafe-unstable update mistralrs-ada`}
|
||||||
|
</CodeBlock>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
ui/src/pages/PackageDetail.tsx
Normal file
115
ui/src/pages/PackageDetail.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Accordion, Alert, Badge, Spinner, Table } from "react-bootstrap";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { usePackages } from "../hooks/usePackages.ts";
|
||||||
|
import { CodeBlock } from "../components/CodeBlock.tsx";
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageDetail() {
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
const { packages, loading, error } = usePackages();
|
||||||
|
|
||||||
|
if (loading) return <Spinner animation="border" />;
|
||||||
|
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||||
|
if (packages.length === 0) return <Alert variant="info">No package data available.</Alert>;
|
||||||
|
|
||||||
|
const versions = packages
|
||||||
|
.filter((p) => p.name === name)
|
||||||
|
.sort((a, b) => b.buildTime - a.buildTime);
|
||||||
|
|
||||||
|
if (versions.length === 0)
|
||||||
|
return <Alert variant="warning">Package not found: {name}</Alert>;
|
||||||
|
|
||||||
|
const latest = versions[0];
|
||||||
|
const hasUnstable = versions.some((v) => v.channel === "unstable");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="mb-1">{name}</h1>
|
||||||
|
<p className="text-body-secondary mb-4">{latest.summary}</p>
|
||||||
|
|
||||||
|
<CodeBlock language="bash">{`sudo dnf install ${name}`}</CodeBlock>
|
||||||
|
|
||||||
|
{hasUnstable && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{`# install latest unstable version\nsudo dnf --enablerepo=lair-cafe-unstable install ${name}`}
|
||||||
|
</CodeBlock>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className="mt-4 mb-3">
|
||||||
|
Versions <Badge bg="secondary">{versions.length}</Badge>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Table striped hover responsive>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Channel</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Built</th>
|
||||||
|
<th>Download</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{versions.map((pkg) => (
|
||||||
|
<tr key={`${pkg.version}-${pkg.release}-${pkg.channel}`}>
|
||||||
|
<td>
|
||||||
|
{pkg.version}-{pkg.release}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Badge bg={pkg.channel === "stable" ? "success" : "warning"}>
|
||||||
|
{pkg.channel}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td>{formatBytes(pkg.size)}</td>
|
||||||
|
<td>{new Date(pkg.buildTime * 1000).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
<a href={`${pkg.baseUrl}/${pkg.rpmFilename}`}>
|
||||||
|
{pkg.rpmFilename}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{versions.some((v) => v.changelog.length > 0) && (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-4 mb-3">Changelog</h2>
|
||||||
|
<Accordion>
|
||||||
|
{versions
|
||||||
|
.filter((v) => v.changelog.length > 0)
|
||||||
|
.map((pkg) => (
|
||||||
|
<Accordion.Item
|
||||||
|
key={`${pkg.version}-${pkg.release}`}
|
||||||
|
eventKey={`${pkg.version}-${pkg.release}`}
|
||||||
|
>
|
||||||
|
<Accordion.Header>
|
||||||
|
{pkg.version}-{pkg.release} —{" "}
|
||||||
|
{new Date(pkg.buildTime * 1000).toLocaleDateString()}
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Body>
|
||||||
|
{pkg.changelog.map((entry, i) => (
|
||||||
|
<div key={i} className="mb-3">
|
||||||
|
<small className="text-body-secondary">
|
||||||
|
{new Date(entry.date * 1000).toLocaleDateString()}{" "}
|
||||||
|
— {entry.author}
|
||||||
|
</small>
|
||||||
|
<pre className="mb-0 mt-1">{entry.text}</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Accordion.Body>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
ui/src/pages/PackageList.tsx
Normal file
71
ui/src/pages/PackageList.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Alert, Badge, Spinner, Table } from "react-bootstrap";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { usePackages } from "../hooks/usePackages.ts";
|
||||||
|
|
||||||
|
export function PackageList() {
|
||||||
|
const { packages, loading, error } = usePackages();
|
||||||
|
|
||||||
|
if (loading) return <Spinner animation="border" />;
|
||||||
|
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||||
|
if (packages.length === 0)
|
||||||
|
return <Alert variant="info">No packages published yet.</Alert>;
|
||||||
|
|
||||||
|
const byName = Map.groupBy(packages, (p) => p.name);
|
||||||
|
const summaries = [...byName.entries()].map(([name, versions]) => {
|
||||||
|
const stable = versions.filter((v) => v.channel === "stable");
|
||||||
|
const unstable = versions.filter((v) => v.channel === "unstable");
|
||||||
|
const latest = versions.reduce((a, b) =>
|
||||||
|
a.buildTime >= b.buildTime ? a : b,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
latest,
|
||||||
|
stableCount: stable.length,
|
||||||
|
unstableCount: unstable.length,
|
||||||
|
versionCount: versions.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
summaries.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="mb-3">Packages</h1>
|
||||||
|
<Table striped hover responsive>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Package</th>
|
||||||
|
<th>Latest version</th>
|
||||||
|
<th>Versions</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
<th>Built</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{summaries.map(({ name, latest, stableCount, unstableCount }) => (
|
||||||
|
<tr key={name}>
|
||||||
|
<td>
|
||||||
|
<Link to={`/packages/${name}`}>{name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{latest.version}-{latest.release}{" "}
|
||||||
|
<Badge bg={latest.channel === "stable" ? "success" : "warning"} className="ms-1">
|
||||||
|
{latest.channel}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{stableCount > 0 && (
|
||||||
|
<Badge bg="success" className="me-1">{stableCount} stable</Badge>
|
||||||
|
)}
|
||||||
|
{unstableCount > 0 && (
|
||||||
|
<Badge bg="warning">{unstableCount} unstable</Badge>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{latest.summary}</td>
|
||||||
|
<td>{new Date(latest.buildTime * 1000).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
ui/src/theme/ThemeContext.tsx
Normal file
74
ui/src/theme/ThemeContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
type ThemeChoice = "system" | "light" | "dark";
|
||||||
|
type ResolvedTheme = "light" | "dark";
|
||||||
|
|
||||||
|
interface ThemeContextValue {
|
||||||
|
choice: ThemeChoice;
|
||||||
|
resolved: ResolvedTheme;
|
||||||
|
setChoice: (choice: ThemeChoice) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
|
|
||||||
|
function resolveSystemTheme(): ResolvedTheme {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(choice: ThemeChoice): ResolvedTheme {
|
||||||
|
return choice === "system" ? resolveSystemTheme() : choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [choice, setChoiceState] = useState<ThemeChoice>(() => {
|
||||||
|
const stored = localStorage.getItem("theme");
|
||||||
|
return stored === "light" || stored === "dark" ? stored : "system";
|
||||||
|
});
|
||||||
|
const [resolved, setResolved] = useState<ResolvedTheme>(() =>
|
||||||
|
resolve(choice),
|
||||||
|
);
|
||||||
|
|
||||||
|
function setChoice(next: ThemeChoice) {
|
||||||
|
setChoiceState(next);
|
||||||
|
if (next === "system") {
|
||||||
|
localStorage.removeItem("theme");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setResolved(resolve(choice));
|
||||||
|
|
||||||
|
if (choice !== "system") return;
|
||||||
|
|
||||||
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handler = () => setResolved(resolveSystemTheme());
|
||||||
|
mq.addEventListener("change", handler);
|
||||||
|
return () => mq.removeEventListener("change", handler);
|
||||||
|
}, [choice]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", resolved);
|
||||||
|
}, [resolved]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ choice, resolved, setChoice }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme(): ThemeContextValue {
|
||||||
|
const ctx = useContext(ThemeContext);
|
||||||
|
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
32
ui/src/theme/ThemeToggle.tsx
Normal file
32
ui/src/theme/ThemeToggle.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Dropdown } from "react-bootstrap";
|
||||||
|
import { useTheme } from "./ThemeContext.tsx";
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ key: "light" as const, label: "Light", icon: "☀️" },
|
||||||
|
{ key: "dark" as const, label: "Dark", icon: "🌙" },
|
||||||
|
{ key: "system" as const, label: "System", icon: "💻" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { choice, setChoice } = useTheme();
|
||||||
|
const current = options.find((o) => o.key === choice) ?? options[2];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown align="end">
|
||||||
|
<Dropdown.Toggle variant="outline-secondary" size="sm">
|
||||||
|
{current.icon}
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{options.map((o) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={o.key}
|
||||||
|
active={o.key === choice}
|
||||||
|
onClick={() => setChoice(o.key)}
|
||||||
|
>
|
||||||
|
{o.icon} {o.label}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
ui/src/types/packages.ts
Normal file
27
ui/src/types/packages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface ChangelogEntry {
|
||||||
|
author: string;
|
||||||
|
date: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Channel = "stable" | "unstable";
|
||||||
|
|
||||||
|
export interface PackageVersion {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
release: string;
|
||||||
|
arch: string;
|
||||||
|
summary: string;
|
||||||
|
size: number;
|
||||||
|
buildTime: number;
|
||||||
|
rpmFilename: string;
|
||||||
|
changelog: ChangelogEntry[];
|
||||||
|
channel: Channel;
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackagesManifest {
|
||||||
|
generated: string;
|
||||||
|
baseUrl: string;
|
||||||
|
packages: PackageVersion[];
|
||||||
|
}
|
||||||
25
ui/tsconfig.json
Normal file
25
ui/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2023",
|
||||||
|
"module": "esnext",
|
||||||
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
ui/vite.config.ts
Normal file
7
ui/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: "/",
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user