Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkgbuilds/vibecad-bin/.omarchy/package.json.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": "local",
"release_ring": "fast",
"min_release_age": "24h"
}
110 changes: 110 additions & 0 deletions pkgbuilds/vibecad-bin/.omarchy/upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/bin/bash
# VibeCAD publishes a checksum file beside each AppImage. Read the small
# checksum asset instead of downloading the AppImage merely to
# discover whether a package update is available.
set -euo pipefail

REPO='10-X-eng/vibecad'
RELEASES_URL="https://api.github.com/repos/${REPO}/releases?per_page=100"

current=$(awk -F= '/^pkgver=/ { print $2; exit }' PKGBUILD)
package=$(awk -F= '/^pkgname=/ { print $2; exit }' PKGBUILD)
case "$package" in
vibecad-bin) preview=false ;;
vibecad-preview-bin) preview=true ;;
*) echo "Unsupported VibeCAD package: $package" >&2; exit 1 ;;
esac
releases=$(curl -fsSL "$RELEASES_URL")
jq -e 'type == "array"' <<<"$releases" >/dev/null
now=$(date +%s)
min_age=${MIN_RELEASE_AGE_SECONDS:-0}
best_version=''
best_tag=''
best_asset_url=''
best_checksum_url=''
best_published_at=''

while IFS=$'\t' read -r tag published_at assets; do
[[ $tag =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-(alpha|beta|RC)([0-9]+))?-build([0-9]+)$ ]] || continue

upstream_version="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
build="${BASH_REMATCH[5]}"
suffix="${BASH_REMATCH[3]}"
# Require both GitHub's classification and the tag to match the track.
# RCs never enter stable, even if a release is mislabeled on GitHub.
if [[ $preview == true ]]; then
[[ $suffix == RC ]] || continue
else
[[ -z $suffix ]] || continue
fi
version="${upstream_version/-RC/rc}"
version="${version/-beta/beta}"
version="${version/-alpha/alpha}.build${build}"
if [[ -z $published_at ]] || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then
echo "Release ${tag} has an invalid publication time" >&2
exit 1
fi
if (( now - published_epoch < min_age )) && [[ ${BYPASS_MIN_RELEASE_AGE:-} != 1 ]]; then
continue
fi
asset="VibeCAD-${upstream_version}-build${build}-Linux-x86_64.AppImage"
checksum="${asset}-SHA256.txt"
asset_url=$(jq -r --arg name "$asset" '.[] | select(.name == $name) | .browser_download_url' <<<"$assets")
checksum_url=$(jq -r --arg name "$checksum" '.[] | select(.name == $name) | .browser_download_url' <<<"$assets")

[[ -n $asset_url && -n $checksum_url ]] || continue
expected_url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
if [[ $asset_url != "$expected_url" || $checksum_url != "${expected_url}-SHA256.txt" ]]; then
echo "Unexpected asset URL for ${tag}" >&2
exit 1
fi
if [[ -z $best_version ]] || (( $(vercmp "$version" "$best_version") > 0 )); then
best_version=$version
best_tag=$tag
best_asset_url=$asset_url
best_checksum_url=$checksum_url
best_published_at=$published_at
fi
done < <(jq -r --argjson preview "$preview" '.[] | select(.draft == false and .prerelease == $preview) | [.tag_name, .published_at, (.assets | tojson)] | @tsv' <<<"$releases")

if [[ -z $best_version ]]; then
echo "No eligible release for ${package}; package unchanged." >&2
echo '{}'
exit 0
fi

if (( $(vercmp "$best_version" "$current") <= 0 )); then
echo '{}'
exit 0
fi

checksum_text=$(curl -fsSL "$best_checksum_url")
read -r sha256 checksum_name _ <<<"$checksum_text"
expected_name="${best_asset_url##*/}"
if [[ ! $sha256 =~ ^[0-9a-f]{64}$ ]] || [[ $checksum_name != "$expected_name" ]]; then
echo "Invalid checksum file for ${expected_name}" >&2
exit 1
fi

license_sha256=$(curl -fsSL \
"https://raw.githubusercontent.com/${REPO}/${best_tag}/LICENSE" | sha256sum | cut -d' ' -f1)
icon_sha256=$(curl -fsSL \
"https://raw.githubusercontent.com/${REPO}/${best_tag}/docs/images/vibecad-mark.svg" | sha256sum | cut -d' ' -f1)

jq -n \
--arg pkgver "$best_version" \
--arg published_at "$best_published_at" \
--arg launcher "$(sha256sum vibecad | cut -d' ' -f1)" \
--arg desktop "$(sha256sum vibecad.desktop | cut -d' ' -f1)" \
--arg policy "$(sha256sum update-policy.json | cut -d' ' -f1)" \
--arg license "$license_sha256" \
--arg icon "$icon_sha256" \
--arg sha256 "$sha256" \
'{
pkgver: $pkgver,
published_at: $published_at,
sha256sums: {
any: [$launcher, $desktop, $policy, $license, $icon],
x86_64: [$sha256]
}
}'
63 changes: 63 additions & 0 deletions pkgbuilds/vibecad-bin/PKGBUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Maintainer: monomyth

pkgname=vibecad-bin
pkgver=@STABLE_PKGVER@
pkgrel=1
pkgdesc="AI-native parametric CAD platform"
arch=('x86_64')
url="https://github.com/10-X-eng/vibecad"
license=('LGPL-2.1-or-later')

depends=(
'bash'
'coreutils'
'fuse2'
'hicolor-icon-theme'
'jq'
'libdrm'
)

provides=("vibecad=${pkgver}")
conflicts=('vibecad')
options=('!debug' '!strip')
backup=('etc/vibecad/update-policy.json')

_upstream_version="${pkgver%.build*}"
_upstream_version="${_upstream_version/rc/-RC}"
_upstream_version="${_upstream_version/beta/-beta}"
_upstream_version="${_upstream_version/alpha/-alpha}"
_upstream_build="${pkgver##*.build}"
_tag="v${_upstream_version}-build${_upstream_build}"
_appimage="VibeCAD-${_upstream_version}-build${_upstream_build}-Linux-x86_64.AppImage"

source=(
'vibecad'
'vibecad.desktop'
'update-policy.json'
"LICENSE::https://raw.githubusercontent.com/10-X-eng/vibecad/${_tag}/LICENSE"
"vibecad.svg::https://raw.githubusercontent.com/10-X-eng/vibecad/${_tag}/docs/images/vibecad-mark.svg"
)
source_x86_64=("${_appimage}::https://github.com/10-X-eng/vibecad/releases/download/${_tag}/${_appimage}")
noextract=("${_appimage}")
sha256sums=(
'5dc3e8b5dcade0d54c1334533f318485179c62ab3866c4c22cc739af63aa58e5'
'd31836d2aeaa48d113d7edf75572c57ce945de1867a78c07296d694fd87edebd'
'bed3d14591ade955eceecaa0a27f8a196fcf0f32b7986ac758f9cec0e58809fa'
'@STABLE_LICENSE_SHA256@'
'@STABLE_ICON_SHA256@'
)
sha256sums_x86_64=('@STABLE_APPIMAGE_SHA256@')

package() {
install -Dm755 "${srcdir}/${_appimage}" \
"${pkgdir}/opt/vibecad/VibeCAD.AppImage"
install -Dm755 "${srcdir}/vibecad" "${pkgdir}/usr/bin/vibecad"
install -Dm644 "${srcdir}/vibecad.desktop" \
"${pkgdir}/usr/share/applications/vibecad.desktop"
install -Dm644 "${srcdir}/vibecad.svg" \
"${pkgdir}/usr/share/icons/hicolor/scalable/apps/vibecad.svg"
install -Dm644 "${srcdir}/update-policy.json" \
"${pkgdir}/etc/vibecad/update-policy.json"
install -Dm644 "${srcdir}/LICENSE" \
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
14 changes: 14 additions & 0 deletions pkgbuilds/vibecad-bin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# VibeCAD stable package stub

Inactive until upstream publishes a stable Linux release. `PKGBUILD.in` and `.omarchy/package.json.in` are templates, not registered package inputs; Omarchy's build and sync discovery skip this directory without `PKGBUILD`.

The launcher contains the same automatic Hyprland/Qt scaling and AMD DRM fixes as `vibecad-preview-bin`. Keep `vibecad`, `update-policy.json`, and `.omarchy/upstream.sh` byte-identical between variants. The update hook selects stable releases for `pkgname=vibecad-bin` and rejects RC tags even if GitHub incorrectly labels them stable.

To activate after a stable release exists:

1. Verify the official GitHub release is not a draft or prerelease and has a version tag without an alpha, beta, or RC suffix.
2. Fill `@STABLE_PKGVER@` (for example, `26.3.1.build1`) and the three stable source checksum placeholders with the actual release metadata. Verify all local-source checksums too.
3. Rename `PKGBUILD.in` to `PKGBUILD` and `.omarchy/package.json.in` to `.omarchy/package.json`.
4. Confirm the launcher, policy, and update hook still match Preview. Build, test, and review the package before enabling its Omarchy menu entry.

Do not substitute an RC for a missing stable release. Once published, this package follows normal Omarchy/pacman updates. It conflicts with the Preview variant through their shared `vibecad` provide; neither track silently switches to the other.
4 changes: 4 additions & 0 deletions pkgbuilds/vibecad-bin/update-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"enabled": false,
"automatic_checks": false
}
33 changes: 33 additions & 0 deletions pkgbuilds/vibecad-bin/vibecad
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

# Upstream forces XCB, so Qt misses Hyprland's per-monitor scale. Use the
# focused monitor at launch, while respecting explicit Qt scaling overrides.
if [[ ! ${QT_SCALE_FACTOR+x} && ! ${QT_SCREEN_SCALE_FACTORS+x} &&
! ${QT_FONT_DPI+x} && ! ${QT_USE_PHYSICAL_DPI+x} &&
! ${QT_ENABLE_HIGHDPI_SCALING+x} && -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] &&
command -v hyprctl >/dev/null; then
scale=$(timeout 2s hyprctl -j monitors 2>/dev/null | jq -er '
[.[] | select(.disabled != true)]
| (map(select(.focused == true))[0] // .[0])
| .scale
| select(type == "number" and . >= 0.5 and . <= 8)
' 2>/dev/null)
if [[ -n $scale ]]; then
export QT_SCALE_FACTOR="$scale"
fi
fi

# VibeCAD's AppImage bundles libdrm_amdgpu from its build environment. On
# current Arch/Mesa systems that copy can make Qt's GLX probe see no usable
# framebuffer configurations. Preloading the host-matched library keeps the
# AppImage portable while preserving hardware acceleration.
for vendor_path in /sys/class/drm/card*/device/vendor; do
[ -r "$vendor_path" ] || continue
if [ "$(cat "$vendor_path")" = "0x1002" ] && [ -r /usr/lib/libdrm_amdgpu.so.1 ]; then
LD_PRELOAD="/usr/lib/libdrm_amdgpu.so.1${LD_PRELOAD:+:$LD_PRELOAD}"
export LD_PRELOAD
break
fi
done

exec /opt/vibecad/VibeCAD.AppImage "$@"
12 changes: 12 additions & 0 deletions pkgbuilds/vibecad-bin/vibecad.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Desktop Entry]
Name=VibeCAD
Comment=Design editable 3D parts with AI-native parametric CAD
GenericName=CAD Application
Exec=vibecad --single-instance %F
Terminal=false
Type=Application
Icon=vibecad
Categories=Graphics;Engineering;
StartupNotify=true
StartupWMClass=VibeCAD
MimeType=application/x-extension-fcstd;model/obj;image/vnd.dwg;image/vnd.dxf;model/vnd.collada+xml;application/iges;model/iges;model/step;model/step+zip;model/stl;application/vnd.shp;model/vrml;
5 changes: 5 additions & 0 deletions pkgbuilds/vibecad-preview-bin/.omarchy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": "local",
"release_ring": "fast",
"min_release_age": "24h"
}
91 changes: 91 additions & 0 deletions pkgbuilds/vibecad-preview-bin/.omarchy/test-upstream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Offline release-track tests. Run with python3 on an Arch host (vercmp)."""

import datetime
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest

PACKAGE = Path(__file__).resolve().parents[1]


def release(tag, preview, *, draft=False, recent=False):
asset = f"VibeCAD-{tag[1:]}-Linux-x86_64.AppImage"
url = f"https://github.com/10-X-eng/vibecad/releases/download/{tag}/{asset}"
return {
"tag_name": tag, "prerelease": preview, "draft": draft,
"published_at": datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
if recent else '2025-01-01T00:00:00Z',
"assets": [{"name": asset, "browser_download_url": url},
{"name": asset + '-SHA256.txt', "browser_download_url": url + '-SHA256.txt'}],
}


class Tracks(unittest.TestCase):
def run_hook(self, package, releases, *, age=0, invalid_hash=False):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for filename in ('vibecad', 'vibecad.desktop', 'update-policy.json'):
shutil.copy2(PACKAGE / filename, root / filename)
(root / 'PKGBUILD').write_text(f'pkgname={package}\npkgver=0\n')
stub = root / 'bin'
stub.mkdir()
curl = stub / 'curl'
curl.write_text('''#!/bin/bash
url="${@: -1}"
case "$url" in
*'/releases?per_page=100') printf '%s' "$RELEASE_FIXTURE" ;;
*'-SHA256.txt') name=${url##*/}; printf '%s %s\\n' "$TEST_HASH" "${name%-SHA256.txt}" ;;
*'/LICENSE') printf 'license' ;;
*'/vibecad-mark.svg') printf '<svg/>' ;;
*) exit 99 ;;
esac
''')
curl.chmod(0o755)
env = dict(os.environ, PATH=f"{stub}:{os.environ['PATH']}",
RELEASE_FIXTURE=json.dumps(releases), TEST_HASH='bad' if invalid_hash else 'a' * 64,
MIN_RELEASE_AGE_SECONDS=str(age), BYPASS_MIN_RELEASE_AGE='')
return subprocess.run(['bash', str(PACKAGE / '.omarchy/upstream.sh')],
cwd=root, env=env, capture_output=True, text=True)

def version(self, package, releases, **kwargs):
proc = self.run_hook(package, releases, **kwargs)
self.assertEqual(proc.returncode, 0, proc.stderr)
return json.loads(proc.stdout).get('pkgver')

def test_stable_excludes_rc_even_when_mislabeled(self):
rows = [release('v26.3.1-build2', False), release('v27.0.0-RC1-build1', False),
release('v27.0.0-RC2-build1', True)]
self.assertEqual(self.version('vibecad-bin', rows), '26.3.1.build2')

def test_preview_excludes_stable_drafts_and_other_prereleases(self):
rows = [release('v26.3.1-RC6-build1', True), release('v27.0.0-build1', False),
release('v27.0.0-RC1-build1', True, draft=True), release('v27.0.0-beta1-build1', True)]
self.assertEqual(self.version('vibecad-preview-bin', rows), '26.3.1rc6.build1')

def test_rc_numbers_use_pacman_order(self):
rows = [release('v26.3.1-RC9-build1', True), release('v26.3.1-RC10-build1', True)]
self.assertEqual(self.version('vibecad-preview-bin', rows), '26.3.1rc10.build1')

def test_no_stable_does_not_promote_rc(self):
self.assertIsNone(self.version('vibecad-bin', [release('v26.3.1-RC6-build1', True)]))

def test_quarantine_selects_older_eligible_rc(self):
rows = [release('v26.3.1-RC7-build1', True, recent=True), release('v26.3.1-RC6-build1', True)]
self.assertEqual(self.version('vibecad-preview-bin', rows, age=86400), '26.3.1rc6.build1')

def test_invalid_checksum_fails(self):
proc = self.run_hook('vibecad-preview-bin', [release('v26.3.1-RC6-build1', True)], invalid_hash=True)
self.assertNotEqual(proc.returncode, 0)

def test_incorrect_asset_location_fails(self):
row = release('v26.3.1-RC6-build1', True)
row['assets'][0]['browser_download_url'] = 'https://example.org/wrong.AppImage'
self.assertNotEqual(self.run_hook('vibecad-preview-bin', [row]).returncode, 0)


if __name__ == '__main__':
unittest.main()
Loading