diff --git a/.github/workflows/cachyos-release.yml b/.github/workflows/cachyos-release.yml new file mode 100644 index 0000000000..b484cb564b --- /dev/null +++ b/.github/workflows/cachyos-release.yml @@ -0,0 +1,246 @@ +# Sync fork main with upstream, rebase the feature branch, then build a +# CachyOS-optimized .pkg.tar.zst and publish it as a GitHub pre-release. +# +# Lives on feat/xai-supergrok-oauth only — fork main stays a clean upstream mirror. +# Primary trigger: push to this branch (workflow file must be on the pushed branch). +# workflow_dispatch only works once GitHub indexes the workflow from default branch; +# push is the reliable path for feature-branch-only workflows. +# +# Co-Authored-By: ForgeCode + +name: CachyOS Release + +on: + push: + branches: + - feat/xai-supergrok-oauth + workflow_dispatch: + inputs: + feature_branch: + description: Feature branch to rebase and build + type: string + default: feat/xai-supergrok-oauth + required: true + skip_sync: + description: Skip syncing fork main with upstream + type: boolean + default: false + target_cpu: + description: Rust/CPU target (x86-64-v3 recommended for most CachyOS installs) + type: choice + options: + - x86-64-v3 + - x86-64-v4 + default: x86-64-v3 + required: true + +permissions: + contents: write + +env: + UPSTREAM_REPO: tailcallhq/forgecode + UPSTREAM_BRANCH: main + FEATURE_BRANCH: ${{ github.event.inputs.feature_branch || 'feat/xai-supergrok-oauth' }} + SKIP_SYNC: ${{ github.event.inputs.skip_sync || 'false' }} + TARGET_CPU: ${{ github.event.inputs.target_cpu || 'x86-64-v3' }} + +jobs: + should-run: + name: Gate (skip bot re-push loops) + if: github.event_name == 'workflow_dispatch' || github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - run: echo "Running CachyOS release pipeline" + + sync-main: + name: Sync fork main with upstream + needs: should-run + if: github.event_name == 'push' || !inputs.skip_sync + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fast-forward main to upstream + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" \ + || git remote set-url upstream "https://github.com/${UPSTREAM_REPO}.git" + git fetch upstream "${UPSTREAM_BRANCH}" + + git checkout -B "${UPSTREAM_BRANCH}" "upstream/${UPSTREAM_BRANCH}" + git push origin "${UPSTREAM_BRANCH}" + + rebase-feature: + name: Rebase feature branch onto main + needs: [should-run, sync-main] + if: always() && needs.should-run.result == 'success' && (needs.sync-main.result == 'success' || needs.sync-main.result == 'skipped') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Rebase and push + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git fetch origin main + git checkout "${FEATURE_BRANCH}" + git rebase origin/main + git push --force-with-lease origin "${FEATURE_BRANCH}" + + build-cachyos: + name: Build CachyOS package + needs: [should-run, rebase-feature] + if: needs.should-run.result == 'success' && needs.rebase-feature.result == 'success' + runs-on: ubuntu-latest + container: + image: archlinux:latest + outputs: + package_name: ${{ steps.build.outputs.package_name }} + app_version: ${{ steps.build.outputs.app_version }} + short_sha: ${{ steps.meta.outputs.short_sha }} + date_tag: ${{ steps.meta.outputs.date_tag }} + release_tag: ${{ steps.meta.outputs.release_tag }} + steps: + - name: Install base tooling + run: | + set -euo pipefail + pacman -Syu --noconfirm + pacman -S --needed --noconfirm \ + base-devel git curl protobuf cmake nasm perl pkgconf sqlite sudo + + - name: Create builder user + run: | + set -euo pipefail + useradd -m -G wheel builder + echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + + - name: Checkout feature branch + uses: actions/checkout@v4 + with: + ref: ${{ env.FEATURE_BRANCH }} + fetch-depth: 0 + + - name: Configure git for container checkout + run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" + + - name: Build metadata + id: meta + run: | + short_sha="$(git rev-parse --short HEAD)" + date_tag="$(date -u +%Y%m%d)" + echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT" + echo "date_tag=${date_tag}" >> "$GITHUB_OUTPUT" + echo "release_tag=cachyos-${date_tag}-${short_sha}" >> "$GITHUB_OUTPUT" + + - name: Build package + id: build + env: + TARGET_CPU: ${{ env.TARGET_CPU }} + run: | + set -euo pipefail + chown -R builder:builder . + build_log="${RUNNER_TEMP}/cachyos-build.log" + su - builder -c " + set -euo pipefail + export TARGET_CPU='${TARGET_CPU}' + cd '${GITHUB_WORKSPACE}' + chmod +x scripts/build-cachyos-pkg.sh + ./scripts/build-cachyos-pkg.sh + " | tee "${build_log}" + package_file="$(grep '^::package_file::' "${build_log}" | tail -1 | sed 's/^::package_file:://')" + package_name="$(grep '^::package_name::' "${build_log}" | tail -1 | sed 's/^::package_name:://')" + app_version="$(grep '^::app_version::' "${build_log}" | tail -1 | sed 's/^::app_version:://')" + [[ -n "${package_file}" && -f "${package_file}" ]] || { + echo "Package artifact missing after build" >&2 + exit 1 + } + { + echo "package_file=${package_file}" + echo "package_name=${package_name}" + echo "app_version=${app_version}" + } >> "${GITHUB_OUTPUT}" + + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: cachyos-forge-package + path: | + forge-*.pkg.tar.zst + forge-*.pkg.tar.zst.sha256 + if-no-files-found: error + + publish-release: + name: Publish GitHub pre-release + needs: build-cachyos + runs-on: ubuntu-latest + steps: + - name: Download package + uses: actions/download-artifact@v4 + with: + name: cachyos-forge-package + path: dist + + - name: Resolve release notes + id: notes + run: | + if [[ "${SKIP_SYNC}" == "true" ]]; then + echo "sync_status=skipped" >> "$GITHUB_OUTPUT" + else + echo "sync_status=yes" >> "$GITHUB_OUTPUT" + fi + + - name: Create pre-release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.build-cachyos.outputs.release_tag }} + name: CachyOS forge ${{ needs.build-cachyos.outputs.app_version }} + prerelease: true + generate_release_notes: false + files: | + dist/*.pkg.tar.zst + dist/*.sha256 + body: | + CachyOS-optimized **forge** build from branch `${{ env.FEATURE_BRANCH }}`. + + ## Install on CachyOS / Arch + + ```bash + # Option A: GitHub CLI + gh release download "${{ needs.build-cachyos.outputs.release_tag }}" \ + --repo "${{ github.repository }}" \ + --pattern '*.pkg.tar.zst' + + # Option B: browser download, then: + sudo pacman -U forge-*.pkg.tar.zst + forge zsh setup + exec zsh + ``` + + ## Verify + + ```bash + forge --version + :doctor + ``` + + **CPU target:** `${{ env.TARGET_CPU }}` + **Package version:** `${{ needs.build-cachyos.outputs.app_version }}` + **Upstream synced:** `${{ steps.notes.outputs.sync_status }}` + + The package ships only `/usr/bin/forge` (ZSH plugin is embedded at compile time). + Re-run `forge zsh setup` after upgrades if your shell config changed upstream. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ae359692b2..3f8bfbf822 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,14 @@ jobs/** node_modules/ bench/__pycache__ .ai/ + +# Beads / Dolt files (added by bd init) +.dolt/ +.beads-credential-key + +# makepkg / CachyOS packaging artifacts +PKGBUILD +*.pkg.tar.zst +*.pkg.tar.zst.sha256 +pkg/ +src/ diff --git a/cachyos/PKGBUILD b/cachyos/PKGBUILD new file mode 100644 index 0000000000..3000ae9d8b --- /dev/null +++ b/cachyos/PKGBUILD @@ -0,0 +1,152 @@ +# Maintainer: CachyOS Custom Overlay +# Contributor: ForgeCode packaging +# +# PKGBUILD for Forge (https://forgecode.dev) built with CachyOS optimizations. +# Intended for use in a private/custom CachyOS overlay repository served from +# a Proxmox VM (or similar) using devtools + clean chroot (extra-x86_64-build). +# +# CachyOS makepkg.conf (or equivalent) supplies the RUSTFLAGS containing +# -C target-cpu=x86-64-v3 (or v4 for supported CPUs) +# plus any other CachyOS LTO / march / mtune settings. +# This PKGBUILD does *not* override RUSTFLAGS so that the chroot environment +# (and local makepkg.conf on CachyOS workstations) fully controls the opts. +# +# The binary is completely self-contained for the ZSH plugin: +# * shell-plugin/forge.setup.zsh , lib/*.zsh and completions are include_dir!'d +# and include_str!'d at compile time into the executable. +# * Therefore we only ship /usr/bin/forge ; no extra runtime data files required. +# * After package install users run `forge zsh setup` (or the interactive first-run +# banner) to populate the # >>> forge initialize >>> block in their ~/.zshrc. +# +# Compatible with the from-source scripts/install.sh (in the forge repo) for +# local dev/CachyOS workstation testing outside of packaging: +# RUSTFLAGS="-C target-cpu=x86-64-v3" ./scripts/install.sh --reinstall +# +# Versioning strategy for custom/rebase/feature builds: +# - pkgver is the upstream base (0.1.0 here; update when rebasing on tags) +# - pkgrel carries a local suffix (e.g. 1.cachy , 1.xai20260606) so that +# our overlay packages never accidentally satisfy an official repo version +# and we can force upgrades on re-packs. +# +# Build in clean chroot example (from the overlay checkout containing this PKGBUILD): +# extra-x86_64-build # or cachy-x86_64-build if the overlay provides a wrapper +# +# Local (dirty) test build from inside a forge source tree (for quick iteration): +# cd /path/to/forgecode +# cp cachyos/PKGBUILD . +# # (edit pkgrel if desired) +# makepkg -s --nocheck # will use current tree as "source" via the hack below +# +# Co-Authored-By: ForgeCode + +pkgname=forge +pkgver=0.1.0 +pkgrel=1 +pkgdesc="AI-enabled pair programmer for 300+ models (Claude, GPT, Grok, Deepseek, Gemini...)" +arch=('x86_64') +url="https://forgecode.dev" +license=('MIT') +depends=('git' 'fd') +optdepends=( + 'bat: enhanced file previews in :doctor and the ZSH plugin' + 'zsh: to use the : prefix ZSH plugin and theme' + 'fzf: improved interactive pickers (some plugin features)' +) +makedepends=( + 'cargo' + 'cmake' + 'nasm' + 'perl' + 'pkgconf' + 'protobuf' + 'sqlite' +) +provides=('forge') +conflicts=('forge-bin') # if an official bin package ever appears +options=('!lto' '!debug') # LTO is controlled by the Cargo release profile + RUSTFLAGS +source=() +sha256sums=() +# No separate .install file: post_install / post_upgrade are defined at the bottom of +# this PKGBUILD and are executed by makepkg/pacman directly. This keeps the packaging +# artifact count to the single PKGBUILD (plus the built package). + +# When this PKGBUILD lives next to a full forge checkout (for local makepkg testing +# of CachyOS-optimized builds) we treat $startdir as the source tree. +# In a real clean-chroot / overlay build you would normally use a git source entry +# pointing at the desired branch/commit and a proper pkgver() function. +# The logic below makes both scenarios work without modification. +prepare() { + # If we were given a real source tarball/git clone by makepkg, use $srcdir. + # Otherwise fall back to the directory containing the PKGBUILD (local tree test). + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + # Last resort: assume user did `makepkg` while cwd is the forge tree + cd "$startdir" + fi + + cargo fetch --locked +} + +build() { + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + cd "$startdir" + fi + + echo "==> Building with CachyOS-provided RUSTFLAGS (from makepkg.conf / chroot)" + echo " RUSTFLAGS=${RUSTFLAGS:-}" + echo " APP_VERSION will be set to ${APP_VERSION:-${pkgver}-${pkgrel}} for build.rs embedding" + + export APP_VERSION="${APP_VERSION:-${pkgver}-${pkgrel}}" + + # --locked honors Cargo.lock; deps are prefetched in prepare() + cargo build --locked --release -p forge_main --bin forge +} + +package() { + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + cd "$startdir" + fi + + install -Dm755 target/release/forge "$pkgdir/usr/bin/forge" + + # License (required by Arch packaging guidelines) + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + + # Optional: ship the original shell-plugin sources under /usr/share for reference + # or for people who want to inspect / diff the embedded code. Not required at + # runtime because everything the binary needs is compiled in via include_dir!. + # Uncomment if your overlay wants to expose them: + # + # install -d "$pkgdir/usr/share/forge/shell-plugin" + # cp -a --no-preserve=ownership shell-plugin/* "$pkgdir/usr/share/forge/shell-plugin/" +} + +# The .install file is written inline by makepkg when this PKGBUILD is processed +# (because we declared `install=forge.install` and the content follows). +# +# It only prints a friendly message after pacman -S / -U. +post_install() { + echo "==> Forge installed to /usr/bin/forge" + echo "==> To enable the fast :command ZSH plugin (recommended):" + echo " forge zsh setup" + echo " exec zsh" + echo "==> Then try :doctor or just type : followed by a prompt at your shell." + echo "==> CachyOS-optimized build (RUSTFLAGS from makepkg.conf at build time)." +} + +post_upgrade() { + post_install +} + +# vim: set ts=2 sw=2 et: diff --git a/cachyos/makepkg.conf.d/99-cachyos-optimizations.conf b/cachyos/makepkg.conf.d/99-cachyos-optimizations.conf new file mode 100644 index 0000000000..d783b1ab54 --- /dev/null +++ b/cachyos/makepkg.conf.d/99-cachyos-optimizations.conf @@ -0,0 +1,15 @@ +# CachyOS-style compiler flags for forge package builds. +# Installed into /etc/makepkg.conf.d/ by scripts/build-cachyos-pkg.sh and CI. +# TARGET_CPU is substituted at install time (default: x86-64-v3). + +CARCH="x86_64" +CHOST="x86_64-pc-linux-gnu" +CFLAGS="-march=@TARGET_CPU@ -mtune=generic -O3 -pipe -fno-plt -fexceptions \ + -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security \ + -fstack-clash-protection -fcf-protection \ + -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -g1" +CXXFLAGS="${CFLAGS}" +LDFLAGS="-Wl,-O1 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now \ + -Wl,-z,pack-relative-relocs" +RUSTFLAGS="-C target-cpu=@TARGET_CPU@ -C opt-level=3 -C codegen-units=1 -C lto=fat" +MAKEFLAGS="-j$(nproc)" \ No newline at end of file diff --git a/crates/forge_infra/src/auth/strategy.rs b/crates/forge_infra/src/auth/strategy.rs index 559a365f19..3ee09a8f82 100644 --- a/crates/forge_infra/src/auth/strategy.rs +++ b/crates/forge_infra/src/auth/strategy.rs @@ -1319,6 +1319,62 @@ mod tests { assert!(matches!(actual.unwrap(), AnyAuthStrategy::CodexDevice(_))); } + #[test] + fn test_create_auth_strategy_xai_oauth_code_uses_standard() { + // xAI is neither CLAUDE_CODE nor GITHUB_COPILOT, so the OAuthCode + // (SuperGrok loopback) flow must fall through to the generic + // StandardHttpProvider with zero per-provider code. + let config = OAuthConfig { + client_id: "b1a00492-073a-47ea-816f-4c329264a828".to_string().into(), + auth_url: Url::parse("https://auth.x.ai/oauth2/authorize").unwrap(), + token_url: Url::parse("https://auth.x.ai/oauth2/token").unwrap(), + scopes: vec!["api:access".to_string()], + redirect_uri: Some("http://127.0.0.1:56121/callback".to_string()), + use_pkce: true, + token_refresh_url: None, + extra_auth_params: None, + custom_headers: None, + }; + + let factory = ForgeAuthStrategyFactory; + let actual = factory + .create_auth_strategy( + ProviderId::XAI, + forge_domain::AuthMethod::OAuthCode(config), + vec![], + ) + .unwrap(); + assert!(matches!(actual, AnyAuthStrategy::OAuthCodeStandard(_))); + } + + #[test] + fn test_create_auth_strategy_xai_oauth_device_uses_device() { + // The xAI headless device flow omits token_refresh_url, so it must + // route to the plain OAuthDevice strategy (RFC 8628), not the + // GitHub-Copilot OAuthWithApiKey hybrid. + let config = OAuthConfig { + client_id: "b1a00492-073a-47ea-816f-4c329264a828".to_string().into(), + auth_url: Url::parse("https://auth.x.ai/oauth2/device/code").unwrap(), + token_url: Url::parse("https://auth.x.ai/oauth2/token").unwrap(), + scopes: vec!["api:access".to_string()], + redirect_uri: None, + use_pkce: false, + token_refresh_url: None, + extra_auth_params: None, + custom_headers: None, + }; + + let factory = ForgeAuthStrategyFactory; + let actual = factory + .create_auth_strategy( + ProviderId::XAI, + forge_domain::AuthMethod::OAuthDevice(config), + vec![], + ) + .unwrap(); + assert!(matches!(actual, AnyAuthStrategy::OAuthDevice(_))); + } + /// Helper to build a JWT token with the given claims payload. fn build_jwt(claims: &serde_json::Value) -> String { use base64::Engine; diff --git a/crates/forge_repo/src/provider/provider.json b/crates/forge_repo/src/provider/provider.json index e01ef63a47..5d3f37fa8c 100644 --- a/crates/forge_repo/src/provider/provider.json +++ b/crates/forge_repo/src/provider/provider.json @@ -108,7 +108,46 @@ "response_type": "OpenAI", "url": "https://api.x.ai/v1/chat/completions", "models": "https://api.x.ai/v1/models", - "auth_methods": ["api_key"] + "auth_methods": [ + { + "oauth_code": { + "auth_url": "https://auth.x.ai/oauth2/authorize", + "token_url": "https://auth.x.ai/oauth2/token", + "client_id": "b1a00492-073a-47ea-816f-4c329264a828", + "scopes": [ + "openid", + "profile", + "email", + "offline_access", + "grok-cli:access", + "api:access" + ], + "redirect_uri": "http://127.0.0.1:56121/callback", + "use_pkce": true, + "extra_auth_params": { + "plan": "generic", + "referrer": "forgecode" + } + } + }, + { + "oauth_device": { + "auth_url": "https://auth.x.ai/oauth2/device/code", + "token_url": "https://auth.x.ai/oauth2/token", + "client_id": "b1a00492-073a-47ea-816f-4c329264a828", + "scopes": [ + "openid", + "profile", + "email", + "offline_access", + "grok-cli:access", + "api:access" + ], + "use_pkce": false + } + }, + "api_key" + ] }, { "id": "openai", diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 9f9d2a5877..92ff77843c 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -713,6 +713,84 @@ mod tests { ); } + #[test] + fn test_xai_oauth_config() { + let configs = get_provider_configs(); + let config = configs.iter().find(|c| c.id == ProviderId::XAI).unwrap(); + + assert_eq!(config.id, ProviderId::XAI); + assert_eq!(config.api_key_vars, Some("XAI_API_KEY".to_string())); + assert_eq!(config.response_type, Some(ProviderResponse::OpenAI)); + assert_eq!(config.url.as_str(), "https://api.x.ai/v1/chat/completions"); + + // Three auth methods: loopback OAuth, headless device OAuth, manual key. + assert_eq!(config.auth_methods.len(), 3); + assert!(config.auth_methods.contains(&AuthMethod::ApiKey)); + + let expected_scopes = vec![ + "openid".to_string(), + "profile".to_string(), + "email".to_string(), + "offline_access".to_string(), + "grok-cli:access".to_string(), + "api:access".to_string(), + ]; + + // Loopback authorization-code + PKCE (SuperGrok subscription). + let code = config + .auth_methods + .iter() + .find_map(|m| match m { + AuthMethod::OAuthCode(cfg) => Some(cfg), + _ => None, + }) + .expect("xai should expose an oauth_code auth method"); + assert_eq!( + code.client_id.as_str(), + "b1a00492-073a-47ea-816f-4c329264a828" + ); + assert_eq!(code.auth_url.as_str(), "https://auth.x.ai/oauth2/authorize"); + assert_eq!(code.token_url.as_str(), "https://auth.x.ai/oauth2/token"); + assert_eq!(code.scopes, expected_scopes); + assert_eq!( + code.redirect_uri.as_deref(), + Some("http://127.0.0.1:56121/callback") + ); + assert!(code.use_pkce); + let extra = code + .extra_auth_params + .as_ref() + .expect("oauth_code should set extra_auth_params"); + // plan=generic is mandatory: xAI rejects loopback OAuth from + // non-allowlisted clients without it. + assert_eq!(extra.get("plan").map(String::as_str), Some("generic")); + assert_eq!(extra.get("referrer").map(String::as_str), Some("forgecode")); + + // Headless device-code (remote / VPS). auth_url MUST be the + // device-authorization endpoint, and token_refresh_url must be absent + // so the factory routes to the plain device flow. + let device = config + .auth_methods + .iter() + .find_map(|m| match m { + AuthMethod::OAuthDevice(cfg) => Some(cfg), + _ => None, + }) + .expect("xai should expose an oauth_device auth method"); + assert_eq!( + device.client_id.as_str(), + "b1a00492-073a-47ea-816f-4c329264a828" + ); + assert_eq!( + device.auth_url.as_str(), + "https://auth.x.ai/oauth2/device/code" + ); + assert_eq!(device.token_url.as_str(), "https://auth.x.ai/oauth2/token"); + assert_eq!(device.scopes, expected_scopes); + assert!(device.redirect_uri.is_none()); + assert!(device.token_refresh_url.is_none()); + } + #[test] fn test_vertex_ai_config() { let configs = get_provider_configs(); diff --git a/scripts/build-cachyos-pkg.sh b/scripts/build-cachyos-pkg.sh new file mode 100755 index 0000000000..719f28fa39 --- /dev/null +++ b/scripts/build-cachyos-pkg.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Build a CachyOS-optimized forge .pkg.tar.zst from the current source tree. +# Used locally on CachyOS/Arch hosts and by .github/workflows/cachyos-release.yml. +# +# The PKGBUILD in cachyos/ is copied to the repo root with a dynamic pkgrel +# (date + git sha) so overlay installs always upgrade cleanly. +# +# Usage: +# ./scripts/build-cachyos-pkg.sh +# TARGET_CPU=x86-64-v4 ./scripts/build-cachyos-pkg.sh +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" +TARGET_CPU="${TARGET_CPU:-x86-64-v3}" +MIN_RUST_VERSION="${MIN_RUST_VERSION:-1.94}" + +log() { printf '==> %s\n' "$*"; } +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +require_linux_x86_64() { + [[ "$(uname -s)" == Linux ]] || die "CachyOS packages must be built on Linux" + [[ "$(uname -m)" == x86_64 ]] || die "Only x86_64 is supported" +} + +read_pkgver() { + awk -F'"' '/^version = / { print $2; exit }' "$REPO_ROOT/Cargo.toml" +} + +install_cachyos_makepkg_conf() { + local dest="/etc/makepkg.conf.d/99-forge-cachyos-optimizations.conf" + if [[ "$(id -u)" -eq 0 ]]; then + mkdir -p /etc/makepkg.conf.d + sed "s/@TARGET_CPU@/${TARGET_CPU}/g" \ + "$REPO_ROOT/cachyos/makepkg.conf.d/99-cachyos-optimizations.conf" \ + >"$dest" + log "Installed ${dest} (target-cpu=${TARGET_CPU})" + elif command -v sudo >/dev/null 2>&1; then + sudo mkdir -p /etc/makepkg.conf.d + sed "s/@TARGET_CPU@/${TARGET_CPU}/g" \ + "$REPO_ROOT/cachyos/makepkg.conf.d/99-cachyos-optimizations.conf" \ + | sudo tee "$dest" >/dev/null + log "Installed ${dest} via sudo (target-cpu=${TARGET_CPU})" + else + log "Skipping system makepkg.conf.d install (not root, no sudo); using RUSTFLAGS env" + fi +} + +rustc_version_meets_minimum() { + local current + current="$(rustc --version 2>/dev/null | awk '{print $2}' | cut -d- -f1 || true)" + [[ -n "$current" ]] || return 1 + rustc --version >/dev/null 2>&1 || return 1 + printf '%s\n%s\n' "$MIN_RUST_VERSION" "$current" | sort -C -V +} + +ensure_rust_toolchain() { + if rustc_version_meets_minimum; then + log "Rust $(rustc --version | awk '{print $2}') satisfies >= ${MIN_RUST_VERSION}" + return + fi + + log "Installing rustup stable (need rustc >= ${MIN_RUST_VERSION})" + if ! command -v curl >/dev/null 2>&1; then + die "curl is required to install rustup" + fi + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + # shellcheck disable=SC1091 + source "${HOME}/.cargo/env" + rustup default stable + rustc_version_meets_minimum || die "rustup stable is still below ${MIN_RUST_VERSION}" + export PATH="${HOME}/.cargo/bin:${PATH}" +} + +prepare_pkgbuild() { + local pkgver="$1" + local pkgrel="$2" + + cp "$REPO_ROOT/cachyos/PKGBUILD" "$REPO_ROOT/PKGBUILD" + sed -i "s/^pkgver=.*/pkgver=${pkgver}/" "$REPO_ROOT/PKGBUILD" + sed -i "s/^pkgrel=.*/pkgrel=${pkgrel}/" "$REPO_ROOT/PKGBUILD" + + # When rustup supplies cargo, drop pacman's cargo makedep to avoid version skew. + if [[ -x "${HOME}/.cargo/bin/cargo" ]]; then + sed -i "/'cargo'/d" "$REPO_ROOT/PKGBUILD" + fi +} + +run_makepkg() { + cd "$REPO_ROOT" + export APP_VERSION="${APP_VERSION:-${pkgver}-${pkgrel}}" + export RUSTFLAGS="${RUSTFLAGS:--C target-cpu=${TARGET_CPU} -C opt-level=3 -C codegen-units=1 -C lto=fat}" + export PATH="${HOME}/.cargo/bin:${PATH}" + + log "Building forge ${pkgver}-${pkgrel}" + log " TARGET_CPU=${TARGET_CPU}" + log " APP_VERSION=${APP_VERSION}" + log " RUSTFLAGS=${RUSTFLAGS}" + + makepkg -f -s --nocheck --noconfirm +} + +write_outputs() { + local pkg_file + pkg_file="$(ls -1 "$REPO_ROOT"/forge-"${pkgver}"-*.pkg.tar.zst | tail -1)" + [[ -f "$pkg_file" ]] || die "Expected package artifact not found" + + sha256sum "$pkg_file" >"${pkg_file}.sha256" + + log "Built package: ${pkg_file}" + cat "${pkg_file}.sha256" + + if [[ -n "${GITHUB_OUTPUT:-}" && -w "${GITHUB_OUTPUT}" ]]; then + { + echo "package_file=${pkg_file}" + echo "package_name=$(basename "$pkg_file")" + echo "app_version=${APP_VERSION}" + echo "pkgver=${pkgver}" + echo "pkgrel=${pkgrel}" + } >>"$GITHUB_OUTPUT" + fi + + # Machine-readable marker for CI when GITHUB_OUTPUT is not writable (e.g. builder user). + echo "::package_file::${pkg_file}" + echo "::package_name::$(basename "$pkg_file")" + echo "::app_version::${APP_VERSION}" +} + +cleanup() { + rm -f "$REPO_ROOT/PKGBUILD" +} + +require_linux_x86_64 +install_cachyos_makepkg_conf +ensure_rust_toolchain + +pkgver="$(read_pkgver)" +git_sha="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" +date_tag="$(date -u +%Y%m%d)" +# pkgrel must be integer[.integer] for makepkg; encode cachy + git sha in APP_VERSION. +pkgrel="${date_tag}" +export APP_VERSION="${pkgver}-${pkgrel}+cachy+${git_sha}" + +trap cleanup EXIT +prepare_pkgbuild "$pkgver" "$pkgrel" +run_makepkg +write_outputs \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000000..bfa0ef2b4c --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,588 @@ +#!/usr/bin/env bash +# +# Local from-source build + install/uninstall/reinstall script for Forge. +# +# This is the equivalent of the official installer (curl -fsSL https://forgecode.dev/cli | sh) +# but for building and installing directly from a source checkout. It is intended for: +# - Local development on the feat/xai-supergrok-oauth (and similar) branches +# - CachyOS custom packaging / optimized builds before feeding into PKGBUILD + devtools chroot +# +# Features: +# - Builds with: cargo build --release -p forge_main --bin forge +# - Honors RUSTFLAGS (for CachyOS: -C target-cpu=x86-64-v3 , x86-64-v4, LTO etc.) +# - Honors APP_VERSION (baked via build.rs into CARGO_PKG_VERSION for the binary) +# - Installs the resulting binary (default: $HOME/.local/bin/forge or /usr/local/bin with sudo) +# - Replicates 'forge zsh setup' NON-INTERACTIVELY: inserts the exact marker block +# (# >>> forge initialize >>> ... # <<< forge initialize <<<) using content from +# shell-plugin/forge.setup.zsh (same as the one baked into the binary). +# - Supports uninstall: removes binary + cleans the forge initialize markers from .zshrc +# (and creates timestamped .bak like the Rust implementation). Also cleans any PATH +# markers we may have added. +# - --reinstall = uninstall + install +# - --build-only to just produce target/release/forge +# - --prefix=..., --force, --no-zsh, --help +# +# Usage examples (CachyOS optimized dev build): +# RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="0.1.0-cachy1" ./scripts/install.sh +# ./scripts/install.sh --prefix=/usr/local --force +# ./scripts/install.sh --reinstall +# ./scripts/install.sh --uninstall +# ./scripts/install.sh --build-only +# +# The script is self-contained, uses the local tree for the embedded shell-plugin content, +# and can be used inside a PKGBUILD %build / %package or for manual custom package testing. +# +# After install (for ZSH users): +# exec zsh # or open a new terminal +# # then test: +# forge --version +# :doctor +# +# IMPORTANT: This script does NOT run the interactive `forge zsh setup` (which asks about +# Nerd Fonts + editor). It performs a non-interactive default setup (no NERD_FONT=0, no +# FORGE_EDITOR override). Run `forge zsh setup` manually afterwards if you want the prompts. +# +# Follows project conventions where applicable (e.g. no new *.md docs created here). +# Errors use clear messages; the script is executable after `chmod +x`. +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +# Colors (mimic official installer style, using printf) +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Resolve repo root relative to this script (works when invoked from anywhere) +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)" + +# Defaults (can be overridden by env or flags) +PREFIX="${PREFIX:-${HOME}/.local}" +APP_VERSION="${APP_VERSION:-}" +RUSTFLAGS="${RUSTFLAGS:-}" +UNINSTALL=false +REINSTALL=false +BUILD_ONLY=false +FORCE=false +NO_ZSH=false +VERBOSE=false + +usage() { + cat <<'EOF' +Forge local from-source installer (like official curl | sh, but builds here) + +Usage: + ./scripts/install.sh [options] + +Options: + --help, -h Show this help + --prefix DIR Installation prefix (binary goes to DIR/bin/forge) + Default: $HOME/.local (so ~/.local/bin/forge) + Use --prefix=/usr/local for system-wide (may need sudo) + --uninstall Remove installed binary and clean ZSH markers from .zshrc + --reinstall Uninstall then install (useful for upgrades from source) + --build-only Only run the cargo release build; do not install or touch shell + --force Overwrite existing binary without warning + --no-zsh Skip non-interactive ZSH marker block setup/update + --verbose, -v More output during build + +Environment variables respected (for CachyOS etc.): + RUSTFLAGS Passed through (e.g. "-C target-cpu=x86-64-v3") + APP_VERSION Baked into binary via build.rs (e.g. "0.1.0-mybuild") + PREFIX Same as --prefix + +Typical CachyOS optimized flow (before or instead of full PKGBUILD): + RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="$(date +%Y%m%d)-cachy" \ + ./scripts/install.sh --reinstall + +The script will: + 1. Build target/release/forge (honoring RUSTFLAGS + APP_VERSION) + 2. Install the binary + 3. Ensure the install bin dir is mentioned in PATH (via marker in rc files) + 4. Insert/update the exact "forge initialize" marker block in .zshrc / $ZDOTDIR/.zshrc + (content taken from shell-plugin/forge.setup.zsh at build time of *this* script) + +Uninstall will: + - Delete the forge binary from common locations under the chosen/current prefix + - Remove the >>> / <<< forge initialize block (with .bak timestamp like Rust code) + - Remove any PATH marker lines we added + +See also: + - Official installer: curl -fsSL https://forgecode.dev/cli | sh + - After install: forge zsh setup (for the interactive Nerd Font / editor flow) + - ZSH doctor: :doctor or forge zsh doctor +EOF +} + +log_info() { printf "${BLUE}%s${NC}\n" "$*"; } +log_ok() { printf "${GREEN}✓ %s${NC}\n" "$*"; } +log_warn() { printf "${YELLOW}%s${NC}\n" "$*"; } +log_error() { printf "${RED}Error: %s${NC}\n" "$*" >&2; } + +# Parse command line (support --foo=bar and --foo bar) +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --help|-h) + usage + exit 0 + ;; + --prefix=*) + PREFIX="${1#*=}" + ;; + --prefix) + shift + PREFIX="${1:-}" + ;; + --uninstall) + UNINSTALL=true + ;; + --reinstall) + REINSTALL=true + ;; + --build-only) + BUILD_ONLY=true + ;; + --force) + FORCE=true + ;; + --no-zsh) + NO_ZSH=true + ;; + --verbose|-v) + VERBOSE=true + ;; + --) + shift + break + ;; + -*) + log_error "Unknown option: $1" + usage + exit 1 + ;; + *) + log_error "Unexpected argument: $1" + usage + exit 1 + ;; + esac + shift + done +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + log_error "Required command not found: $1" + exit 1 + fi +} + +# Build step (always uses release profile from Cargo.toml which has lto + opt-level=3 + strip) +do_build() { + log_info "Building Forge from source in $REPO_ROOT" + + if [ -n "$APP_VERSION" ]; then + log_info " APP_VERSION=$APP_VERSION (will be baked via build.rs)" + export APP_VERSION + fi + + if [ -n "$RUSTFLAGS" ]; then + log_info " RUSTFLAGS=$RUSTFLAGS (CachyOS / custom target-cpu etc.)" + export RUSTFLAGS + else + log_warn " RUSTFLAGS is empty. For CachyOS x86-64-v3+ optimizations run with:" + log_warn " RUSTFLAGS=\"-C target-cpu=x86-64-v3\" $0 ..." + log_warn " (or -C target-cpu=x86-64-v4 if your CPU and CachyOS makepkg.conf support it)" + fi + + require_command cargo + + # We intentionally build --release here; this script exists precisely to produce + # optimized binaries (the AGENTS.md guidance against `cargo build --release` in + # day-to-day dev does not apply to this packaging/install helper). + BUILD_CMD=(cargo build --release -p forge_main --bin forge) + if $VERBOSE; then + BUILD_CMD+=(--verbose) + fi + + log_info " ${BUILD_CMD[*]}" + (cd "$REPO_ROOT" && "${BUILD_CMD[@]}") + + local src_bin="$REPO_ROOT/target/release/forge" + if [ ! -x "$src_bin" ]; then + log_error "Build succeeded but binary not found or not executable: $src_bin" + exit 1 + fi + + log_ok "Build complete: $src_bin" + "$src_bin" --version || true +} + +# Locate the just-built binary (or fail) +get_src_bin() { + local src_bin="$REPO_ROOT/target/release/forge" + if [ ! -x "$src_bin" ]; then + log_error "No built binary at $src_bin. Run without --build-only first, or use --reinstall." + exit 1 + fi + printf '%s' "$src_bin" +} + +# Install the binary to $PREFIX/bin/forge (with sudo if the dir is not writable) +do_install_binary() { + local src_bin + src_bin="$(get_src_bin)" + + local bin_dir="$PREFIX/bin" + local dest="$bin_dir/forge" + + log_info "Installing binary to $dest" + + mkdir -p "$bin_dir" 2>/dev/null || true + + local use_sudo="" + if [ ! -w "$bin_dir" ] && [ "$(id -u)" -ne 0 ]; then + if command -v sudo >/dev/null 2>&1; then + use_sudo="sudo" + log_warn "Directory $bin_dir not writable by $(id -un); using sudo" + else + log_error "Cannot write to $bin_dir and no sudo available" + exit 1 + fi + fi + + if [ -e "$dest" ] && ! $FORCE && ! $REINSTALL; then + log_warn "Forge already exists at $dest" + log_warn "Use --force or --reinstall to overwrite" + # Still continue so that zsh setup etc. can be (re)done + fi + + $use_sudo install -Dm755 "$src_bin" "$dest" 2>/dev/null || { + # Fallback for systems without GNU install -D + $use_sudo mkdir -p "$bin_dir" + $use_sudo cp "$src_bin" "$dest" + $use_sudo chmod 755 "$dest" + } + + log_ok "forge installed: $dest" + "$dest" --version || true + + # Also ensure PATH contains the bin dir (mimics official installer behavior) + ensure_path_entry "$bin_dir" +} + +# Ensure a bin dir is on PATH in the usual shell rc files (using a marker so uninstall can clean it) +# We only touch .zshrc and .bashrc to stay close to official. +ensure_path_entry() { + local bin_dir="$1" + local path_marker="# Added by Forge (local from-source installer)" + local export_line="export PATH=\"$bin_dir:\$PATH\"" + + for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do + # Ensure file exists + if [ ! -f "$rc" ]; then + # Create with the marker + export (harmless if user never uses that shell) + { + printf '%s\n' "$path_marker" + printf '%s\n' "$export_line" + } >> "$rc" + log_info "Created $rc with PATH entry for $bin_dir" + continue + fi + + # If the exact export is already there (under our marker or otherwise), do nothing + if grep -Fq "$export_line" "$rc" 2>/dev/null; then + continue + fi + + # Remove any previous lines we added (by marker or the exact export) then prepend fresh + local tmp + tmp="$(mktemp)" + grep -vF "$path_marker" "$rc" | grep -vF "$export_line" > "$tmp" || true + + # Prepend our block at the very top (after possible shebang or first lines is ok for PATH) + { + printf '%s\n' "$path_marker" + printf '%s\n' "$export_line" + printf '\n' + cat "$tmp" + } > "$rc" + rm -f "$tmp" + + log_info "Updated $rc to ensure $bin_dir is on PATH (marker: $path_marker)" + done +} + +# Remove PATH marker lines we may have added (best effort) +clean_path_markers() { + local path_marker="# Added by Forge (local from-source installer)" + for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do + if [ -f "$rc" ] && grep -Fq "$path_marker" "$rc" 2>/dev/null; then + local tmp + tmp="$(mktemp)" + grep -vF "$path_marker" "$rc" > "$tmp" || true + # Also drop any orphan export lines that look like ours for this installer + # (we keep other PATH manipulation) + mv "$tmp" "$rc" + log_ok "Removed Forge PATH marker from $rc" + fi + done +} + +# Replicate the non-interactive equivalent of setup_zsh_integration() from +# crates/forge_main/src/zsh/plugin.rs using the same markers and the exact +# content of shell-plugin/forge.setup.zsh (no nerd-font or editor overrides). +# +# This inserts: +# # >>> forge initialize >>> +# +# # <<< forge initialize <<< +# +# And creates a timestamped backup on change, exactly like the Rust code. +do_zsh_setup() { + if $NO_ZSH; then + log_info "Skipping ZSH integration (--no-zsh)" + return 0 + fi + + log_info "Setting up ZSH integration (non-interactive 'forge zsh setup' equivalent)" + + local zdotdir="${ZDOTDIR:-$HOME}" + local zshrc="$zdotdir/.zshrc" + + local start_marker="# >>> forge initialize >>>" + local end_marker="# <<< forge initialize <<<" + + local setup_src="$REPO_ROOT/shell-plugin/forge.setup.zsh" + if [ ! -f "$setup_src" ]; then + log_error "Cannot find embedded setup block source: $setup_src" + log_error "This script must be run from inside a full forge source tree." + exit 1 + fi + + # Normalize (strip stray CRs like the Rust normalize_script does) + local forge_init_config + forge_init_config="$(sed 's/\r$//' "$setup_src")" + + # Build the full block exactly as Rust does (no extra nerd/editor lines) + local forge_config + forge_config="${start_marker} +${forge_init_config} +${end_marker}" + + # Create backup if file exists (timestamp format matches Rust: %Y-%m-%d_%H-%M-%S) + local backup_path="" + if [ -f "$zshrc" ]; then + local ts + ts="$(date +%Y-%m-%d_%H-%M-%S)" + backup_path="$zshrc.bak.$ts" + cp "$zshrc" "$backup_path" + log_info "Backup created: $backup_path" + fi + + # Use bash arrays + simple scan to replicate parse + splice logic. + # This keeps behavior very close to the Rust implementation (replace in place + # or append at end, preserving other content). + local -a lines=() + if [ -f "$zshrc" ]; then + mapfile -t lines < "$zshrc" + fi + + local start_idx=-1 + local end_idx=-1 + local i + for i in "${!lines[@]}"; do + if [ "${lines[$i]}" = "$start_marker" ]; then + start_idx=$i + fi + if [ "${lines[$i]}" = "$end_marker" ]; then + end_idx=$i + fi + done + + # Split the block into lines (preserve exact content including internal blanks) + local -a block_lines=() + while IFS= read -r line || [ -n "$line" ]; do + block_lines+=("$line") + done <<< "$forge_config" + + local -a new_lines=() + if [ $start_idx -ge 0 ] && [ $end_idx -gt $start_idx ]; then + # Valid existing block: replace it (splice) + new_lines=( "${lines[@]:0:$start_idx}" ) + new_lines+=( "${block_lines[@]}" ) + local after_start=$(( end_idx + 1 )) + if [ $after_start -le ${#lines[@]} ]; then + new_lines+=( "${lines[@]:$after_start}" ) + fi + else + # No (valid) block: append at end, with a separating blank line if needed + new_lines=( "${lines[@]}" ) + if [ ${#new_lines[@]} -gt 0 ]; then + local last="${new_lines[-1]}" + if [ -n "${last//[[:space:]]/}" ]; then + new_lines+=( "" ) + fi + fi + new_lines+=( "${block_lines[@]}" ) + fi + + # Write atomically via temp + mv + local tmp + tmp="$(mktemp)" + if [ ${#new_lines[@]} -gt 0 ]; then + printf '%s\n' "${new_lines[@]}" > "$tmp" + else + : > "$tmp" + fi + mv "$tmp" "$zshrc" + + log_ok "forge plugins added/updated in $zshrc" + if [ -n "$backup_path" ]; then + log_info "(previous version backed up)" + fi + + # Friendly next steps (mimic what the interactive on_zsh_setup prints) + log_info "Run: exec zsh (or open a new terminal) to load the updated config" + log_info "Then try: :doctor or forge zsh doctor" +} + +# Remove the forge initialize marker block (and the markers themselves) from .zshrc +# Best-effort; also handles the case where only one marker exists. +clean_zsh_markers() { + local zdotdir="${ZDOTDIR:-$HOME}" + local zshrc="$zdotdir/.zshrc" + + if [ ! -f "$zshrc" ]; then + return 0 + fi + + local start_marker="# >>> forge initialize >>>" + local end_marker="# <<< forge initialize <<<" + + if ! grep -Fq "$start_marker" "$zshrc" && ! grep -Fq "$end_marker" "$zshrc"; then + return 0 + fi + + # Backup before mutating (always, like the Rust update path) + local ts + ts="$(date +%Y-%m-%d_%H-%M-%S)" + local backup_path="$zshrc.bak.$ts" + cp "$zshrc" "$backup_path" + + # Remove the entire block (from start to end inclusive). If markers are + # mismatched we still do a best-effort removal of any lines containing them. + local tmp + tmp="$(mktemp)" + awk -v s="$start_marker" -v e="$end_marker" ' + BEGIN { skipping=0 } + $0 == s { skipping=1; next } + $0 == e { skipping=0; next } + !skipping { print } + ' "$zshrc" > "$tmp" || { + # Fallback: at least strip the literal marker lines + grep -vF "$start_marker" "$zshrc" | grep -vF "$end_marker" > "$tmp" || true + } + + mv "$tmp" "$zshrc" + log_ok "Cleaned forge initialize markers from $zshrc (backup: $backup_path)" +} + +# Full uninstall (binary + markers + path hints) +do_uninstall() { + log_info "Uninstalling Forge (local from-source)" + + local bin_dir="$PREFIX/bin" + local candidates=( + "$bin_dir/forge" + "$HOME/.local/bin/forge" + "/usr/local/bin/forge" + "$HOME/.forge/bin/forge" # in case someone used an older layout + ) + + local removed=0 + local cand + for cand in "${candidates[@]}"; do + if [ -f "$cand" ] || [ -L "$cand" ]; then + local dir + dir="$(dirname "$cand")" + if [ -w "$dir" ]; then + rm -f "$cand" + else + if command -v sudo >/dev/null 2>&1; then + sudo rm -f "$cand" + else + log_warn "Cannot remove $cand (no write permission and no sudo)" + continue + fi + fi + log_ok "Removed binary: $cand" + removed=1 + fi + done + + if [ $removed -eq 0 ]; then + log_warn "No forge binary found in common locations for prefix $PREFIX" + fi + + clean_zsh_markers + clean_path_markers + + log_ok "Uninstall complete" + log_info "Note: user config in ~/.config/forge (if any) and cloned workspaces are left untouched." + log_info "Use rm -rf ~/.config/forge if you also want a full purge (not done by default)." +} + +main() { + parse_args "$@" + + if [ "$PREFIX" = "" ]; then + log_error "--prefix cannot be empty" + exit 1 + fi + + # Always start from the repo root for cargo and for locating shell-plugin/ + cd "$REPO_ROOT" + + if $UNINSTALL && $REINSTALL; then + log_error "Cannot combine --uninstall and --reinstall" + exit 1 + fi + + if $UNINSTALL || $REINSTALL; then + do_uninstall + fi + + if ! $UNINSTALL; then + if ! $REINSTALL && [ -x "$REPO_ROOT/target/release/forge" ] && ! $FORCE && ! $BUILD_ONLY; then + log_warn "A release binary already exists at target/release/forge" + log_warn "Use --force to rebuild, or --reinstall" + fi + + do_build + + if ! $BUILD_ONLY; then + do_install_binary + do_zsh_setup + fi + fi + + if ! $UNINSTALL && ! $BUILD_ONLY; then + log_ok "Done." + local installed_bin="$PREFIX/bin/forge" + if [ -x "$installed_bin" ]; then + log_info "Binary: $installed_bin" + log_info "Run 'forge --help' (after ensuring $PREFIX/bin is on PATH)" + fi + if ! $NO_ZSH; then + log_info "ZSH users: exec zsh (or new terminal), then try ':doctor'" + fi + fi +} + +main "$@" diff --git a/scripts/setup-cachyos-build-env.sh b/scripts/setup-cachyos-build-env.sh new file mode 100755 index 0000000000..d4700276ac --- /dev/null +++ b/scripts/setup-cachyos-build-env.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +# +# Setup helper / documented procedure for a CachyOS-optimized build environment +# on a Proxmox VM (LXC or full VM) that will host clean chroots for custom +# overlay packages (Forge, and similar projects). +# +# This script does NOT create .md documentation files (per project AGENTS.md). +# Instead it *is* the script + living documentation: run it with --help (or +# without args) to see the exact steps, commands, and rationale. It can also +# perform local sanity checks (--check) when executed on a CachyOS-like host. +# +# Why a dedicated build host / chroot: +# - Reproducible packages with CachyOS performance flags (RUSTFLAGS target-cpu +# x86-64-v3 / v4 + makepkg.conf CFLAGS etc.) +# - Clean chroot prevents host contamination (devtools + extra-x86_64-build) +# - Ability to serve a small local pacman repo (nginx/caddy + repo-add) to +# Proxmox nodes / workstations / the overlay users. +# - Snapshottable storage (ZFS / Btrfs subvols) for /var/cache/pacman and chroot roots. +# +# Relationship to the other deliverables on this branch: +# - scripts/install.sh : use *outside* the chroot on the build host or on +# developer workstations for fast "from source + same RUSTFLAGS" iteration: +# RUSTFLAGS="-C target-cpu=x86-64-v3" ./scripts/install.sh --reinstall +# - cachyos/PKGBUILD : the packaging recipe consumed by makepkg inside the +# clean chroot. It respects the RUSTFLAGS coming from the chroot's +# /etc/makepkg.conf (or the one injected by devtools) and bakes APP_VERSION. +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)" + +usage() { + cat <<'EOM' +CachyOS optimized build environment setup for custom packages (Proxmox VM) + +This script prints (and can partially validate) the procedure to turn a +CachyOS installation (LXC or VM under Proxmox) into a build host that +produces x86-64-v3 / v4 optimized .pkg.tar.zst packages using clean chroots. + +Run on the target build machine (or just read the output anywhere): + ./scripts/setup-cachyos-build-env.sh # show full guide + ./scripts/setup-cachyos-build-env.sh --help + ./scripts/setup-cachyos-build-env.sh --check # run local sanity (if on CachyOS) + +Key produced artifacts that live in the forge repo: + - scripts/install.sh (local from-source + Cachy RUSTFLAGS + zsh) + - cachyos/PKGBUILD (the recipe for the overlay) + +The two are deliberately compatible: use the install script for quick dev +builds on the host; use the PKGBUILD + this chroot for "official" overlay pkgs. + +EOM + print_steps +} + +print_steps() { + cat <<'STEPS' + +================================================================================ +1. Base OS on the Proxmox VM / LXC +================================================================================ +- Create a CachyOS LXC (or VM) from the official CachyOS template / ISO. + Recommended: privileged LXC for easier bind-mounts of ZFS datasets, or + unprivileged with proper idmap if you prefer. + +- Update: + sudo pacman -Syu + +- Install core build tooling: + sudo pacman -S --needed base-devel devtools git pacman-contrib + + (devtools brings extra-x86_64-build, makechrootpkg, etc.) + +================================================================================ +2. Storage layout (highly recommended on Proxmox) +================================================================================ +Use ZFS or Btrfs on the build host so you can snapshot before/after big builds +and roll back the chroot or the pacman cache. + +Example ZFS (from the Proxmox host or inside the guest if it owns a dataset): + zfs create -o mountpoint=/var/lib/archbuild rpool/ARCHBUILD + zfs create -o mountpoint=/var/cache/pacman/pkg rpool/PACCACHE + zfs create -o mountpoint=/var/cache/pacman/cachyos rpool/OUR_REPO + + # Optional: separate dataset for sources + zfs create -o mountpoint=/build/sources rpool/BUILDSRC + +Inside the guest, make sure the mounts are there and have sane permissions: + sudo mkdir -p /var/lib/archbuild /var/cache/pacman/pkg + sudo chown -R builduser:builduser /var/lib/archbuild /var/cache/pacman 2>/dev/null || true + +Btrfs equivalent: subvolumes + snapshots. + +================================================================================ +3. CachyOS makepkg.conf & RUSTFLAGS (the optimization source of truth) +================================================================================ +CachyOS ships a tuned /etc/makepkg.conf (or in /usr/share/makepkg.conf.d/cachyos.conf +included by the cachyos-keyring / cachyos-mirrorlist packages). + +Typical interesting bits that the PKGBUILD relies on: + CFLAGS+=" -march=x86-64-v3 -mtune=znver3 ..." # or v4 + RUSTFLAGS+=" -C target-cpu=x86-64-v3 -C opt-level=3 ..." + LDFLAGS+=" -Wl,-O2,..." + +If you are on a stock Arch in the VM and want CachyOS-like flags, you can +drop in the relevant snippets from a real CachyOS /etc/makepkg.conf or set +them in /etc/makepkg.conf.d/99-cachyos-optimizations.conf : + + # /etc/makepkg.conf.d/99-cachyos-optimizations.conf + CARCH="x86_64" + CHOST="x86_64-pc-linux-gnu" + CFLAGS="-march=x86-64-v3 -mtune=generic -O3 -pipe ..." + CXXFLAGS="${CFLAGS}" + RUSTFLAGS="-C target-cpu=x86-64-v3 -C opt-level=3 -C codegen-units=1 -C lto ..." + MAKEFLAGS="-j$(nproc)" + +The cachyos/PKGBUILD in this repo deliberately does *not* set RUSTFLAGS +itself; it lets whatever the chroot's makepkg.conf exports win. This way +the same PKGBUILD produces v3 on a v3 host and v4 on a v4 host when the +overlay's makepkg.conf is the source of the flags. + +================================================================================ +4. Create the clean chroot (once) +================================================================================ +As your normal user (or a dedicated "build" user): + + # The first time this will download a full base chroot (~1-2 GiB) + extra-x86_64-build -- --syncdeps -- --noconfirm + +This creates /var/lib/archbuild/extra-x86_64/root + +You can also create a cachyos-specific root if your overlay has its own +mirrorlist / pacman.conf : + + # Advanced: custom pacman.conf for the chroot that includes your overlay + # and the upstream CachyOS mirrors + pacoloco cache. + +To enter a shell in the chroot for debugging: + arch-nspawn /var/lib/archbuild/extra-x86_64/root + +================================================================================ +5. Building packages (using the PKGBUILD from this repo) +================================================================================ +Typical flow inside your overlay checkout (the dir that contains the copied +or symlinked cachyos/PKGBUILD for the "forge" package): + + # From the dir containing the PKGBUILD + extra-x86_64-build + +This will: + - rsync the PKGBUILD + any needed sources into the clean chroot + - run prepare/build/package under the CachyOS-tuned makepkg.conf + - produce forge-0.1.0-1.cachy-x86_64.pkg.tar.zst in the current dir + +For even faster local iteration (outside any chroot) on the build host itself +or on a dev workstation that is already CachyOS: + cd /path/to/forge-source-checkout + RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="0.1.0-cachy-test" \ + ./scripts/install.sh --reinstall --prefix=/tmp/forge-test-install + +(The install.sh and the PKGBUILD share the same build command + env contract.) + +After a successful chroot build, sign if your overlay uses signed packages: + gpg --detach-sign --default-key $KEYID *.pkg.tar.zst + +================================================================================ +6. Local repository serving (so other machines can pacman -Syu your builds) +================================================================================ +After builds: + + mkdir -p /var/cache/pacman/cachyos/forge/x86_64 + cp *.pkg.tar.zst *.pkg.tar.zst.sig /var/cache/pacman/cachyos/forge/x86_64/ + repo-add /var/cache/pacman/cachyos/forge/x86_64/forge.db.tar.gz \ + /var/cache/pacman/cachyos/forge/x86_64/*.pkg.tar.zst + +Serve it (simple static + directory index is enough): + + # Option A: caddy (one-liner) + caddy file-server --root /var/cache/pacman/cachyos --listen :8080 + + # Option B: nginx snippet + # location /cachyos/ { + # alias /var/cache/pacman/cachyos/; + # autoindex on; + # } + +On client machines (or other Proxmox containers) add to /etc/pacman.conf : + + [cachyos-forge] + SigLevel = Optional TrustAll # or proper key setup + Server = http://buildhost:8080/forge/x86_64 + + # (and also keep the real CachyOS mirrors + pacoloco if you run it) + +Then: pacman -Sy forge + +================================================================================ +7. Caching & mirrors (Pacoloco recommended) +================================================================================ +Run pacoloco on the build host (or a dedicated proxy LXC). It acts as a +transparent cache for all upstream Arch / CachyOS mirrors and dramatically +speeds up repeated chroot bootstraps and dep downloads. + +Example pacoloco config + systemd socket activation is in the CachyOS wiki / +pacoloco README. Point makepkg / chroot pacman.conf at +http://localhost:9129/repo/... + +Also consider: + paccache -rk 2 # keep only last 2 versions of cached pkgs + +================================================================================ +8. Automation / CI on the build host (optional but nice) +================================================================================ +- A small cron / systemd timer that pulls the latest feat/xai... (or main), + runs extra-x86_64-build for each package that has a PKGBUILD in the overlay, + signs, repo-add, and rsyncs to a "latest" dir. +- Or a tiny webhook receiver that reacts to GitHub "release" or push events + on the feature branch. +- Store the overlay git repo on the same ZFS dataset so you can `git pull` + inside a snapshot. + +================================================================================ +9. Pairing with the deliverables in this branch (feat/xai-supergrok-oauth) +================================================================================ +- Use scripts/install.sh for "YOLO but optimized" builds on any CachyOS box + (including the build VM itself) during development of the XAI / supergrok + OAuth changes. +- Use cachyos/PKGBUILD inside the clean chroot when you are ready to cut a + package for the overlay mirror that other machines will consume. +- The Proxmox VM becomes the single source of truth for "our" builds of Forge + with the exact CachyOS CPU targeting + the latest from the feature branch. + +================================================================================ +10. One-time first-run checklist on the new build VM +================================================================================ +[ ] CachyOS guest installed + fully updated +[ ] base-devel + devtools installed +[ ] ZFS/Btrfs datasets mounted for archbuild + paccache + our_repo +[ ] /etc/makepkg.conf.d/* shows the v3/v4 RUSTFLAGS (or you added the snippet) +[ ] extra-x86_64-build ran successfully at least once (created the root) +[ ] A test build of the forge PKGBUILD succeeded and produced a .pkg.tar.zst +[ ] repo-add + a trivial http server works from another container +[ ] (optional) pacoloco running and pacman.conf points at it +[ ] (optional) the scripts/install.sh from a fresh clone of the branch also + works with the same RUSTFLAGS and produces a runnable forge + +Happy building! + +STEPS +} + +do_check() { + echo "Running local sanity checks (best effort - works best on a real CachyOS host)..." + local ok=0 + + for cmd in pacman makepkg extra-x86_64-build arch-nspawn repo-add caddy nginx pacoloco; do + if command -v "$cmd" >/dev/null 2>&1; then + printf " ${GREEN}✓${NC} %s present\n" "$cmd" + else + printf " ${YELLOW}?${NC} %s not found in PATH (may be ok depending on role)\n" "$cmd" + fi + done + + if [ -f /etc/makepkg.conf ]; then + if grep -q 'x86-64-v3\|target-cpu' /etc/makepkg.conf /etc/makepkg.conf.d/* 2>/dev/null; then + printf " ${GREEN}✓${NC} RUSTFLAGS / target-cpu hints found in makepkg.conf\n" + else + printf " ${YELLOW}!${NC} No obvious x86-64-v3 RUSTFLAGS in makepkg.conf (you may need to add the CachyOS tuning snippet)\n" + fi + fi + + if [ -d /var/lib/archbuild ]; then + printf " ${GREEN}✓${NC} /var/lib/archbuild exists (chroots live here)\n" + else + printf " ${YELLOW}!${NC} /var/lib/archbuild missing - run extra-x86_64-build first\n" + fi + + if [ -d "$REPO_ROOT/cachyos" ] && [ -f "$REPO_ROOT/cachyos/PKGBUILD" ]; then + printf " ${GREEN}✓${NC} cachyos/PKGBUILD found relative to this script\n" + fi + if [ -f "$REPO_ROOT/scripts/install.sh" ]; then + printf " ${GREEN}✓${NC} scripts/install.sh present (the companion from-source tool)\n" + fi + + echo "Check finished." +} + +main() { + case "${1:-}" in + --help|-h) + usage + exit 0 + ;; + --check) + do_check + exit 0 + ;; + *) + usage + exit 0 + ;; + esac +} + +main "$@" diff --git a/scripts/test-cachyos-build-podman.sh b/scripts/test-cachyos-build-podman.sh new file mode 100755 index 0000000000..da7d66fe5d --- /dev/null +++ b/scripts/test-cachyos-build-podman.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# +# Temporary local CI mirror: build the CachyOS package inside archlinux via podman. +# Use this to iterate on packaging before pushing to GitHub Actions. +# +# Usage: +# ./scripts/test-cachyos-build-podman.sh +# TARGET_CPU=x86-64-v4 ./scripts/test-cachyos-build-podman.sh +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" +TARGET_CPU="${TARGET_CPU:-x86-64-v3}" +RUNTIME="${CONTAINER_RUNTIME:-podman}" +HOST_UID="$(id -u)" +HOST_GID="$(id -g)" + +if ! command -v "$RUNTIME" >/dev/null 2>&1; then + RUNTIME=docker +fi +command -v "$RUNTIME" >/dev/null 2>&1 || { + echo "ERROR: need podman or docker" >&2 + exit 1 +} + +echo "==> Using ${RUNTIME} with archlinux:latest" +echo "==> Repo: ${REPO_ROOT}" +echo "==> TARGET_CPU: ${TARGET_CPU}" +echo "==> Expect ~10-15 min for a full release+LTO build" + +"$RUNTIME" run --rm \ + --security-opt label=disable \ + -e TARGET_CPU="${TARGET_CPU}" \ + -e HOST_UID="${HOST_UID}" \ + -e HOST_GID="${HOST_GID}" \ + -v "${REPO_ROOT}:/src:Z" \ + -w /src \ + archlinux:latest \ + bash -euxo pipefail -c ' + pacman -Syu --noconfirm + pacman -S --needed --noconfirm \ + base-devel git curl protobuf cmake nasm perl pkgconf sqlite sudo + + useradd -m -G wheel builder + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + chown -R builder:builder /src + git config --global --add safe.directory /src + + su - builder -c " + set -euo pipefail + cd /src + chmod +x scripts/build-cachyos-pkg.sh + ./scripts/build-cachyos-pkg.sh + " + + ls -lh /src/forge-*.pkg.tar.zst + cat /src/forge-*.pkg.tar.zst.sha256 + + # Container uid 0 maps to the host user in rootless podman. + chown -R 0:0 /src + ' + +echo "==> Done. Package artifacts are in ${REPO_ROOT}" \ No newline at end of file