diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml new file mode 100644 index 000000000000..6830b79fe14c --- /dev/null +++ b/.github/workflows/fork-release.yml @@ -0,0 +1,268 @@ +# Fork release: prebuilt Codex packages for this fork (JSBtechnologies/codex). +# +# This is the fork's counterpart to upstream's rust-release.yml, which needs +# OpenAI's self-hosted runners, signing, Bazel and R2. This one runs on +# GitHub-hosted runners and publishes the same artifact upstream ships: a +# "Codex package" (codex-package-.tar.gz) holding bin/codex, +# bin/codex-code-mode-host, codex-resources/bwrap and codex-path/rg, built by +# the upstream package builder (scripts/build_codex_package.py). +# +# Everything is explicitly fork-labelled so it can never be mistaken for an +# upstream release: +# - tags are fork-v (upstream uses rust-v) +# - the binary reports +fork (`codex --version`) +# - the GitHub release title and notes say it is the fork +# +# Release: git tag fork-v0.1.0 && git push origin fork-v0.1.0 +# Install: curl -fsSL https://github.com/JSBtechnologies/codex/releases/latest/download/install-fork.sh | sh +# +# Linux first. Windows needs codex-command-runner.exe and +# codex-windows-sandbox-setup.exe in the package plus Authenticode signing; see +# upstream's rust-release-windows.yml when adding it. + +name: fork-release + +on: + push: + tags: + - "fork-v*" + workflow_dispatch: + inputs: + version: + description: "Version to build (semver, e.g. 0.1.0). Builds the package only; no release is created." + required: true + +permissions: + contents: write + +concurrency: + group: fork-release-${{ github.ref }} + cancel-in-progress: false + +env: + FORK_REPO: JSBtechnologies/codex + +jobs: + resolve-version: + name: Resolve fork version + runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.v.outputs.version }} + tag: ${{ steps.v.outputs.tag }} + is_release: ${{ steps.v.outputs.is_release }} + steps: + - id: v + shell: bash + run: | + set -euo pipefail + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + tag="${GITHUB_REF_NAME}" + [[ "$tag" == fork-v* ]] || { echo "tag '$tag' must start with fork-v"; exit 1; } + version="${tag#fork-v}" + is_release=true + else + version="${{ github.event.inputs.version }}" + tag="fork-v${version}" + is_release=false + fi + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] \ + || { echo "version '$version' is not semver"; exit 1; } + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "is_release=$is_release" >> "$GITHUB_OUTPUT" + echo "fork version $version (tag $tag, release=$is_release)" + + build: + name: Build package - ${{ matrix.target }} + needs: resolve-version + runs-on: ${{ matrix.runner }} + timeout-minutes: 300 + strategy: + fail-fast: false + matrix: + include: + # glibc build. The package needs the builder's glibc or newer, so + # build on the oldest supported runner. Add + # x86_64-unknown-linux-musl (static, needs Zig + musl tools; see + # .github/scripts/install-musl-build-tools.sh) once this is green. + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: "0" + # The release profile keeps line tables; the package copies are stripped. + CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO: off + STABLE_GIT_COMMIT: ${{ github.sha }} + CODEX_REPO_ROOT: ${{ github.workspace }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Free disk space + shell: bash + run: | + set -euo pipefail + df -h / | tail -n 1 + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/share/boost + sudo docker image prune --all --force >/dev/null 2>&1 || true + df -h / | tail -n 1 + + - name: Install build dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update -qq + # pkg-config + libssl-dev: openssl-sys; libcap-dev: the bwrap helper; + # zstd: the .tar.zst package archive. + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + build-essential pkg-config libssl-dev libcap-dev zstd python3 + + # The package builder uses tomllib (Python 3.11+); ubuntu-22.04 ships 3.10. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.99.1 + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: codex-rs + key: fork-release-${{ matrix.target }} + + - name: Stamp fork version into the workspace + shell: bash + env: + VERSION: ${{ needs.resolve-version.outputs.version }} + run: | + set -euo pipefail + # Workspace version is 0.0.0 on main; upstream stamps it at release + # time too. "+fork" is semver build metadata, so cargo and the + # package manifest accept it and `codex --version` says it is the fork. + fork_version="${VERSION}+fork" + sed -i "0,/^version = \"0.0.0\"/s//version = \"${fork_version}\"/" codex-rs/Cargo.toml + grep -m1 '^version' codex-rs/Cargo.toml + # Refresh Cargo.lock entries for workspace members only. + (cd codex-rs && cargo update --workspace --offline 2>/dev/null || cargo update --workspace) + echo "FORK_VERSION=${fork_version}" >> "$GITHUB_ENV" + + - name: Build Codex package + shell: bash + run: | + set -euo pipefail + mkdir -p dist + # Builds codex, codex-code-mode-host and bwrap with cargo, fetches + # the pinned ripgrep, strips the package copies and writes the + # archives. RUSTY_V8_MIRROR/_TAG come from .cargo/config.toml. + python3 scripts/build_codex_package.py \ + --target "${{ matrix.target }}" \ + --variant codex \ + --cargo-profile release \ + --strip all \ + --package-version "${FORK_VERSION}" \ + --package-dir "${RUNNER_TEMP}/codex-package-${{ matrix.target }}" \ + --archive-output "dist/codex-package-${{ matrix.target }}.tar.gz" \ + --archive-output "dist/codex-package-${{ matrix.target }}.tar.zst" \ + --force + ls -l dist + cat "${RUNNER_TEMP}/codex-package-${{ matrix.target }}/codex-package.json" + + - name: Smoke test the package + shell: bash + run: | + set -euo pipefail + pkg="${RUNNER_TEMP}/codex-package-${{ matrix.target }}" + "$pkg/bin/codex" --version | tee /dev/stderr | grep -F "${FORK_VERSION}" + "$pkg/bin/codex" --help | grep -q -- "--trust-folder" + test -x "$pkg/bin/codex-code-mode-host" + test -x "$pkg/codex-resources/bwrap" + test -x "$pkg/codex-path/rg" + # A fresh tarball must round-trip to the same layout. + mkdir -p "${RUNNER_TEMP}/roundtrip" + tar -xzf "dist/codex-package-${{ matrix.target }}.tar.gz" -C "${RUNNER_TEMP}/roundtrip" + "${RUNNER_TEMP}/roundtrip/bin/codex" --version + echo "glibc floor: $(objdump -T "$pkg/bin/codex" | grep -o 'GLIBC_[0-9.]*' | sort -V | tail -n 1)" + + - uses: actions/upload-artifact@v4 + with: + name: codex-package-${{ matrix.target }} + path: dist/* + if-no-files-found: error + + release: + name: Publish fork release + needs: [resolve-version, build] + if: needs.resolve-version.outputs.is_release == 'true' + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Assemble release assets + shell: bash + run: | + set -euo pipefail + cp scripts/install/install-fork.sh dist/install-fork.sh + (cd dist && sha256sum codex-package-*.tar.gz codex-package-*.tar.zst > codex-package_SHA256SUMS) + ls -l dist + cat dist/codex-package_SHA256SUMS + + - name: Write release notes + shell: bash + env: + VERSION: ${{ needs.resolve-version.outputs.version }} + TAG: ${{ needs.resolve-version.outputs.tag }} + run: | + set -euo pipefail + cat > notes.md <.tar.gz\` / \`.tar.zst\`: the Codex package (bin/codex, bin/codex-code-mode-host, codex-resources/bwrap, codex-path/rg, codex-package.json) + - \`codex-package_SHA256SUMS\`: checksums the installer verifies + - \`install-fork.sh\`: the installer + EOF + cat notes.md + + - name: Create or update the GitHub release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.resolve-version.outputs.version }} + TAG: ${{ needs.resolve-version.outputs.tag }} + run: | + set -euo pipefail + title="Codex fork v${VERSION} (JSBtechnologies)" + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file notes.md + else + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \ + --title "$title" --notes-file notes.md --latest + fi + gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber + gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' diff --git a/README.md b/README.md index aa279c347254..ec40b34c5512 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), codex-rs/linux-sandbox/README.md for the matching rules and per-kernel behavior. +

+Installing the fork (Linux x86_64, prebuilt): +
curl -fsSL https://github.com/JSBtechnologies/codex/releases/latest/download/install-fork.sh | sh
+Fork releases are tagged fork-v<version>, the binary reports <version>+fork, and they are built by .github/workflows/fork-release.yml from the same package layout upstream ships (codex-package-<target>.tar.gz). The installer puts the package under ~/.codex/packages/standalone and links ~/.local/bin/codex. Needs glibc 2.35+ (Ubuntu 22.04+, Debian 12+, WSL2). Windows packages are the next step. To cut a release: git tag fork-v0.1.0 && git push origin fork-v0.1.0. To build from source instead, see atrium-smoke/README.md. ## Quickstart diff --git a/codex-rs/tui/src/update_versions.rs b/codex-rs/tui/src/update_versions.rs index b0aa3deb4766..f6b85872f573 100644 --- a/codex-rs/tui/src/update_versions.rs +++ b/codex-rs/tui/src/update_versions.rs @@ -19,6 +19,18 @@ pub(crate) fn is_source_build_version(version: &str) -> bool { parse_version(version) == Some((0, 0, 0)) } +/// Fork releases stamp `+fork` build metadata into the version +/// (`.github/workflows/fork-release.yml`). Upstream's update check compares +/// against openai/codex releases, which a fork can never "upgrade" to, so such +/// builds skip it like a source build does. +#[cfg(any(not(debug_assertions), test))] +pub(crate) fn is_fork_build_version(version: &str) -> bool { + version + .split('+') + .nth(1) + .is_some_and(|meta| meta == "fork" || meta.starts_with("fork.")) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ServerVersionNoticeKind { Older, @@ -90,6 +102,15 @@ mod tests { assert!(!is_source_build_version("0.1.0")); } + #[test] + fn detects_fork_build_versions() { + assert!(is_fork_build_version("0.1.0+fork")); + assert!(is_fork_build_version("0.1.0+fork.jsb")); + assert!(!is_fork_build_version("0.1.0")); + assert!(!is_fork_build_version("0.1.0+build.5")); + assert!(!is_fork_build_version("0.1.0-forky")); + } + #[test] fn whitespace_is_ignored() { assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3))); diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index 9bbdf310a861..b8cdffbe0e9b 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -6,6 +6,7 @@ use crate::npm_registry::NpmPackageInfo; use crate::update_action; use crate::update_action::UpdateAction; use crate::update_versions::extract_version_from_latest_tag; +use crate::update_versions::is_fork_build_version; use crate::update_versions::is_newer; use crate::update_versions::is_source_build_version; use crate::updates_cache::VersionInfo; @@ -25,7 +26,10 @@ use crate::version::CODEX_CLI_VERSION; pub(crate) use crate::updates_cache::dismiss_version; pub fn get_upgrade_version(config: &Config) -> Option { - if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) { + if !config.check_for_update_on_startup + || is_source_build_version(CODEX_CLI_VERSION) + || is_fork_build_version(CODEX_CLI_VERSION) + { return None; } @@ -150,7 +154,10 @@ async fn fetch_latest_github_release_version( /// Returns the latest version to show in a popup, if it should be shown. /// This respects the user's dismissal choice for the current latest version. pub fn get_upgrade_version_for_popup(config: &Config) -> Option { - if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) { + if !config.check_for_update_on_startup + || is_source_build_version(CODEX_CLI_VERSION) + || is_fork_build_version(CODEX_CLI_VERSION) + { return None; } diff --git a/scripts/install/install-fork.sh b/scripts/install/install-fork.sh new file mode 100644 index 000000000000..839770150cfb --- /dev/null +++ b/scripts/install/install-fork.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# Installer for the JSBtechnologies fork of Codex (Linux). +# +# This is NOT the upstream OpenAI installer (install.sh beside it). It +# downloads a fork release (tag fork-v) from +# https://github.com/JSBtechnologies/codex/releases and installs it in the same +# layout upstream's standalone installer uses, so the daemon tooling recognises +# it as a complete package: +# +# $CODEX_HOME/packages/standalone/releases// the extracted package +# $CODEX_HOME/packages/standalone/current symlink to the release +# $CODEX_INSTALL_DIR/codex symlink to current/bin/codex +# +# Usage: +# curl -fsSL https://github.com/JSBtechnologies/codex/releases/latest/download/install-fork.sh | sh +# +# Environment: +# CODEX_FORK_REPO GitHub repo to install from (default: JSBtechnologies/codex) +# CODEX_RELEASE fork version, e.g. 0.1.0, or latest (default: latest) +# CODEX_FORK_BASE_URL download assets from this URL instead of the GitHub +# release (a mirror, or a local http.server when testing); +# CODEX_RELEASE must then name a version +# CODEX_INSTALL_DIR where the codex symlink goes (default: ~/.local/bin) +# CODEX_HOME Codex home (default: ~/.codex) +# +# Needs: curl, tar, sha256sum. Linux x86_64 only for now. +set -eu + +REPO="${CODEX_FORK_REPO:-JSBtechnologies/codex}" +RELEASE="${CODEX_RELEASE:-latest}" +BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" +CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}" +STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone" +RELEASES_DIR="$STANDALONE_ROOT/releases" +CURRENT_LINK="$STANDALONE_ROOT/current" + +say() { printf '%s\n' "$*"; } +fail() { printf 'install-fork: %s\n' "$*" >&2; exit 1; } + +for tool in curl tar sha256sum; do + command -v "$tool" >/dev/null 2>&1 || fail "missing required tool: $tool" +done + +os="$(uname -s)" +arch="$(uname -m)" +[ "$os" = "Linux" ] || fail "this fork currently ships Linux packages only (got $os)" +case "$arch" in + x86_64|amd64) target="x86_64-unknown-linux-gnu" ;; + *) fail "no fork package for architecture $arch yet (x86_64 only)" ;; +esac + +# Resolve the fork tag. "latest" asks GitHub for the repo's latest release, +# which the fork-release workflow marks; anything else is a fork version. +if [ "$RELEASE" = "latest" ]; then + tag="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \ + | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)" + [ -n "$tag" ] || fail "could not resolve the latest release of $REPO" +else + case "$RELEASE" in + fork-v*) tag="$RELEASE" ;; + v*) tag="fork-$RELEASE" ;; + *) tag="fork-v$RELEASE" ;; + esac +fi +case "$tag" in + fork-v*) ;; + *) fail "release tag '$tag' is not a fork release (expected fork-v)" ;; +esac +version="${tag#fork-v}" +base_url="${CODEX_FORK_BASE_URL:-https://github.com/$REPO/releases/download/$tag}" +asset="codex-package-$target.tar.gz" + +say "Installing Codex fork $version ($REPO, $target)" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT INT TERM + +say "Downloading $asset" +curl -fsSL --progress-bar -o "$tmp/$asset" "$base_url/$asset" +curl -fsSL -o "$tmp/SHA256SUMS" "$base_url/codex-package_SHA256SUMS" +expected="$(grep " $asset\$" "$tmp/SHA256SUMS" | awk '{print $1}')" +[ -n "$expected" ] || fail "no checksum for $asset in the release" +actual="$(sha256sum "$tmp/$asset" | awk '{print $1}')" +[ "$actual" = "$expected" ] || fail "checksum mismatch for $asset: expected $expected, got $actual" +say "Checksum OK" + +# Stage, then move into place so a failed install never leaves a half release. +release_dir="$RELEASES_DIR/$version" +stage="$RELEASES_DIR/.staging.$version.$$" +mkdir -p "$RELEASES_DIR" +rm -rf "$stage" +mkdir -p "$stage" +tar -xzf "$tmp/$asset" -C "$stage" +[ -f "$stage/codex-package.json" ] || fail "archive has no codex-package.json; not a Codex package" +chmod 0755 "$stage/bin/codex" "$stage/bin/codex-code-mode-host" "$stage/codex-path/rg" +[ -f "$stage/codex-resources/bwrap" ] && chmod 0755 "$stage/codex-resources/bwrap" +ln -sf "bin/codex" "$stage/codex" +rm -rf "$release_dir" +mv "$stage" "$release_dir" + +# Point `current` at the new release atomically. +tmp_link="$STANDALONE_ROOT/.current.$$" +ln -s "$release_dir" "$tmp_link" +mv -T "$tmp_link" "$CURRENT_LINK" 2>/dev/null || { rm -f "$CURRENT_LINK"; mv "$tmp_link" "$CURRENT_LINK"; } + +# Visible command. Only the entrypoint is linked; codex finds +# codex-code-mode-host, bwrap and rg beside the real binary. +mkdir -p "$BIN_DIR" +if [ -e "$BIN_DIR/codex" ] && [ ! -L "$BIN_DIR/codex" ]; then + say "Note: $BIN_DIR/codex is a regular file (another install?); replacing it with a symlink" +fi +tmp_bin="$BIN_DIR/.codex.$$" +ln -s "$CURRENT_LINK/bin/codex" "$tmp_bin" +mv -T "$tmp_bin" "$BIN_DIR/codex" 2>/dev/null || { rm -f "$BIN_DIR/codex"; mv "$tmp_bin" "$BIN_DIR/codex"; } + +reported="$("$BIN_DIR/codex" --version 2>/dev/null || true)" +[ -n "$reported" ] || fail "installed codex does not run; check glibc (needs 2.35+): $(ldd --version 2>&1 | head -n 1)" +say "Installed: $reported -> $release_dir" + +if command -v codex >/dev/null 2>&1 && [ "$(command -v codex)" != "$BIN_DIR/codex" ]; then + say "Warning: another codex is earlier on PATH: $(command -v codex)" +fi +case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) say "Add to PATH: export PATH=\"$BIN_DIR:\$PATH\"" ;; +esac +say "Run: codex"