Last active
August 29, 2026 01:16
-
-
Save pansapiens/57d08c6ff616e4b2bced300dc5449d46 to your computer and use it in GitHub Desktop.
Build Debian/Ubuntu .deb packages for Antigravity IDE (or Antigravity app) from https://antigravity.google/download — auto-detects the latest version, with pi-agent LLM fallback
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| # Antigravity ships two distinct Linux products on https://antigravity.google/download: | |
| # - "Antigravity IDE (Standalone)" -> classic VS Code-like editor (default, most commonly wanted) | |
| # - "Antigravity 2.0" -> the newer standalone app/CLI product | |
| # The download page now renders its links inline in the HTML (static Astro site). | |
| # The two Linux tarballs are distinguishable by filename: | |
| # IDE -> .../linux-x64/Antigravity%20IDE.tar.gz | |
| # app -> .../linux-x64/Antigravity.tar.gz | |
| # so we must select the URL that belongs to the requested product rather than | |
| # just grabbing the first linux-x64 tar.gz link found (which previously grabbed | |
| # the wrong product). | |
| VARIANT="${VARIANT:-ide}" | |
| DRY_RUN=0 | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -v|--variant) | |
| VARIANT="$2" | |
| shift 2 | |
| ;; | |
| --variant=*) | |
| VARIANT="${1#*=}" | |
| shift | |
| ;; | |
| -n|--check) | |
| DRY_RUN=1 | |
| shift | |
| ;; | |
| *) | |
| echo "Unknown argument: $1" >&2 | |
| echo "Usage: $0 [--variant ide|app] [--check]" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| case "${VARIANT}" in | |
| ide) | |
| PACKAGE_NAME="antigravity-ide" | |
| DISPLAY_NAME="Antigravity IDE" | |
| FALLBACK_VERSION="2.5.5" | |
| FALLBACK_URL="https://edgedl.me.gvt1.com/edgedl/release2/j0qc3/antigravity/stable/2.5.5-4923483625488384/linux-x64/Antigravity%20IDE.tar.gz" | |
| ;; | |
| app) | |
| PACKAGE_NAME="antigravity" | |
| DISPLAY_NAME="Antigravity" | |
| FALLBACK_VERSION="2.11.0" | |
| FALLBACK_URL="https://storage.googleapis.com/antigravity-public/antigravity-hub/2.11.0-6376446768316416/linux-x64/Antigravity.tar.gz" | |
| ;; | |
| *) | |
| echo "Error: unknown variant '${VARIANT}' (expected 'ide' or 'app')" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "Building variant: ${VARIANT} (package: ${PACKAGE_NAME})" | |
| # LLM fallback settings (used when regex detection of the download page fails) | |
| # Instead of the openrouter/free alias (which currently HANGS inside pi), each | |
| # attempt picks a random free OpenRouter model from `pi --list-models ':free'` | |
| # and retries with a different model on failure. Set LLM_MODEL to pin one | |
| # specific model (disables random selection), e.g.: | |
| # LLM_MODEL='openrouter/google/gemma-4-31b-it:free' ./build_antigravity_deb.sh | |
| LLM_MODEL="${LLM_MODEL:-}" # optional explicit model pin | |
| LLM_TRIES="${LLM_TRIES:-3}" # attempts with different random models | |
| LLM_TIMEOUT="${LLM_TIMEOUT:-120}" # per-attempt timeout in seconds | |
| # Pick a random free OpenRouter model from pi's model list. | |
| # (The '[pi-cursor] Extension loaded' banner goes to stdout too, hence the awk filter.) | |
| pick_random_free_model() { | |
| pi --no-extensions --list-models ':free' 2>/dev/null \ | |
| | awk '$1 == "openrouter" {print $2}' \ | |
| | sort -u | shuf -n 1 | |
| } | |
| # Ask a pi agent to scrape the download page and return the tarball URL. | |
| # Args: <model> <variant>. Sets LLM_URL and returns 0 on success. | |
| scrape_with_pi() { | |
| local model="$1" variant="$2" product_desc llm_out candidate | |
| if [ "${variant}" = "ide" ]; then | |
| product_desc="the 'Antigravity IDE (Standalone)' product, whose linux-x64 tarball filename is 'Antigravity IDE.tar.gz' (NOT the 'Antigravity 2.0' app, whose tarball is 'Antigravity.tar.gz')" | |
| else | |
| product_desc="the 'Antigravity 2.0' standalone app, whose linux-x64 tarball filename is exactly 'Antigravity.tar.gz' (NOT the 'Antigravity IDE (Standalone)' editor, whose tarball is 'Antigravity IDE.tar.gz')" | |
| fi | |
| llm_out=$(timeout "${LLM_TIMEOUT}" pi --model "${model}" -p \ | |
| --no-session --no-extensions --no-skills --no-prompt-templates \ | |
| --no-context-files --no-themes \ | |
| "Run exactly: curl -s -L --compressed 'https://antigravity.google/download' -o /tmp/antigravity_page.html && grep -o 'https://[^\"]*linux-x64[^\"]*tar\.gz' /tmp/antigravity_page.html | |
| From the matching URLs, output the direct download URL for ${product_desc}. | |
| Percent-encode any spaces in the URL. | |
| Your final answer must be ONLY the URL on a single line, with no other text." 2>/dev/null || true) | |
| # Models may wrap the URL in prose/markdown; extract and validate it. | |
| candidate=$(echo "${llm_out}" | grep -o -E 'https://[^"[:space:]]+\.tar\.gz' | head -n 1 || true) | |
| [ -n "${candidate}" ] || return 1 | |
| case "${candidate}" in | |
| */linux-x64/*) | |
| if [ "${variant}" = "ide" ]; then | |
| [[ "${candidate}" =~ Antigravity(%20|\+)?IDE\.tar\.gz$ ]] || return 1 | |
| else | |
| [[ "${candidate}" =~ /Antigravity\.tar\.gz$ ]] || return 1 | |
| fi | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| LLM_URL="${candidate// /%20}" | |
| return 0 | |
| } | |
| # LLM fallback entry point: scrape via pi, picking random free models with | |
| # retries (or the pinned LLM_MODEL if set). Sets LLM_URL and returns 0 on success. | |
| detect_with_llm() { | |
| local variant="$1" try=1 model | |
| while [ "${try}" -le "${LLM_TRIES}" ]; do | |
| if [ -n "${LLM_MODEL}" ]; then | |
| model="${LLM_MODEL}" | |
| else | |
| model=$(pick_random_free_model || true) | |
| [ -n "${model}" ] || model="openrouter/google/gemma-4-31b-it:free" # known-good default if listing fails | |
| fi | |
| echo "LLM attempt ${try}/${LLM_TRIES} using ${model} (timeout ${LLM_TIMEOUT}s)..." | |
| if scrape_with_pi "${model}" "${variant}"; then | |
| return 0 | |
| fi | |
| echo "LLM attempt ${try} failed." | |
| try=$((try + 1)) | |
| [ -n "${LLM_MODEL}" ] && break # pinned model: retrying the same one is pointless | |
| done | |
| return 1 | |
| } | |
| # Allow overriding the package version and URL via environment variables | |
| VERSION="${VERSION:-}" | |
| URL="${URL:-}" | |
| if [ -z "${VERSION}" ] || [ -z "${URL}" ]; then | |
| echo "Attempting to dynamically fetch latest version and URL..." | |
| if HTML_CONTENT=$(curl -s -L -f --compressed "https://antigravity.google/download"); then | |
| # Primary: links are rendered inline in the static download page HTML. | |
| # Select by tarball filename so we don't grab the other product's link. | |
| if [ "${VARIANT}" = "ide" ]; then | |
| FETCHED_URL=$(echo "${HTML_CONTENT}" | grep -o -E 'https://[^"]+/linux-x64/Antigravity(%20| )?IDE\.tar\.gz' | head -n 1) | |
| else | |
| FETCHED_URL=$(echo "${HTML_CONTENT}" | grep -o -E 'https://[^"]+/linux-x64/Antigravity\.tar\.gz' | head -n 1) | |
| fi | |
| # Fallback: legacy JS-driven page that only exposed links in a main-*.js bundle | |
| if [ -z "${FETCHED_URL}" ] && JS_FILE=$(echo "${HTML_CONTENT}" | grep -o -E "main-[a-zA-Z0-9_.-]+\.js" | head -n 1); then | |
| echo "No inline links found; trying legacy main-*.js bundle..." | |
| JS_CONTENT=$(curl -s -L -f --compressed "https://antigravity.google/${JS_FILE}") | |
| if [ "${VARIANT}" = "ide" ]; then | |
| FETCHED_URL=$(echo "${JS_CONTENT}" | perl -0777 -ne 'print $1 if /title:"Antigravity IDE".{0,4000}?(https:\/\/[^"]+\/linux-x64\/Antigravity[^"]*\.tar\.gz)/s') | |
| else | |
| FETCHED_URL=$(echo "${JS_CONTENT}" | perl -0777 -ne 'print $1 if /title:"Antigravity [0-9][^"]*".{0,4000}?(https:\/\/[^"]+\/linux-x64\/Antigravity[^"]*\.tar\.gz)/s') | |
| fi | |
| fi | |
| if [ -n "${FETCHED_URL}" ]; then | |
| FETCHED_URL="${FETCHED_URL// /%20}" | |
| if [[ "${FETCHED_URL}" =~ /([0-9][^/]+)/linux-x64/ ]]; then | |
| DIR_VERSION="${BASH_REMATCH[1]}" | |
| FETCHED_VERSION="${DIR_VERSION%%-*}" | |
| if [ -z "${VERSION}" ]; then | |
| VERSION="${FETCHED_VERSION}" | |
| fi | |
| if [ -z "${URL}" ]; then | |
| URL="${FETCHED_URL}" | |
| fi | |
| echo "Successfully fetched version: ${VERSION}" | |
| echo "Fetched URL: ${URL}" | |
| fi | |
| fi | |
| fi | |
| fi | |
| # LLM fallback: have a pi agent scrape the page when regex detection failed | |
| if [ -z "${VERSION}" ] || [ -z "${URL}" ]; then | |
| echo "Regex detection failed; falling back to pi agent scraping (up to ${LLM_TRIES} tries, ${LLM_TIMEOUT}s each)..." | |
| if detect_with_llm "${VARIANT}" && [[ "${LLM_URL}" =~ /([0-9][^/]+)/linux-x64/ ]]; then | |
| VERSION="${VERSION:-${BASH_REMATCH[1]%%-*}}" | |
| URL="${LLM_URL}" | |
| echo "Successfully fetched version: ${VERSION}" | |
| echo "Fetched URL: ${URL}" | |
| else | |
| echo "LLM fallback failed to produce a valid URL." | |
| fi | |
| fi | |
| # Last-resort hardcoded fallback (only used if regex AND LLM detection failed) | |
| if [ -z "${VERSION}" ] || [ -z "${URL}" ]; then | |
| echo "All dynamic detection failed. Using hardcoded last-resort fallbacks." | |
| VERSION="${VERSION:-${FALLBACK_VERSION}}" | |
| URL="${URL:-${FALLBACK_URL}}" | |
| fi | |
| # --check: report the resolved version/URL without downloading or building | |
| if [ "${DRY_RUN}" = "1" ]; then | |
| echo "check: version=${VERSION}" | |
| echo "check: url=${URL}" | |
| exit 0 | |
| fi | |
| # Output directory: the directory this script lives in | |
| WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| BUILD_DIR="${WORKSPACE_DIR}/build_temp" | |
| # Clean up any existing directory from previous aborted builds | |
| if [ -d "${BUILD_DIR}" ]; then | |
| rm -rf "${BUILD_DIR}" | |
| fi | |
| # Create package staging directory structures | |
| mkdir -p "${BUILD_DIR}/extracted" | |
| mkdir -p "${BUILD_DIR}/pkg/DEBIAN" | |
| mkdir -p "${BUILD_DIR}/pkg/opt/${PACKAGE_NAME}" | |
| mkdir -p "${BUILD_DIR}/pkg/usr/share/applications" | |
| mkdir -p "${BUILD_DIR}/pkg/usr/share/pixmaps" | |
| mkdir -p "${BUILD_DIR}/pkg/usr/bin" | |
| echo "Downloading ${DISPLAY_NAME}..." | |
| curl -L -o "${BUILD_DIR}/${PACKAGE_NAME}.tar.gz" "${URL}" | |
| echo "Extracting archive..." | |
| tar -xzf "${BUILD_DIR}/${PACKAGE_NAME}.tar.gz" -C "${BUILD_DIR}/extracted" | |
| # Dynamically locate the main application directory to avoid hardcoding naming variations | |
| EXTRACTED_DIR=$(find "${BUILD_DIR}/extracted" -maxdepth 2 -type f -name "${PACKAGE_NAME}" -exec dirname {} \; | head -n 1) | |
| if [ -z "${EXTRACTED_DIR}" ]; then | |
| echo "Error: Could not locate ${PACKAGE_NAME} binary in extracted archive." >&2 | |
| exit 1 | |
| fi | |
| echo "Copying application files..." | |
| cp -r "${EXTRACTED_DIR}"/* "${BUILD_DIR}/pkg/opt/${PACKAGE_NAME}/" | |
| echo "Extracting icon..." | |
| CLASSIC_ICON="${BUILD_DIR}/pkg/opt/${PACKAGE_NAME}/resources/app/resources/linux/code.png" | |
| if [ -f "${CLASSIC_ICON}" ]; then | |
| cp "${CLASSIC_ICON}" "${BUILD_DIR}/pkg/usr/share/pixmaps/${PACKAGE_NAME}.png" | |
| else | |
| (cd "${BUILD_DIR}" && npx --yes asar extract-file "${BUILD_DIR}/pkg/opt/${PACKAGE_NAME}/resources/app.asar" icon.png) | |
| mv "${BUILD_DIR}/icon.png" "${BUILD_DIR}/pkg/usr/share/pixmaps/${PACKAGE_NAME}.png" | |
| fi | |
| echo "Creating CLI symlink..." | |
| CLASSIC_CLI="${BUILD_DIR}/pkg/opt/${PACKAGE_NAME}/bin/${PACKAGE_NAME}" | |
| if [ -f "${CLASSIC_CLI}" ]; then | |
| ln -s "/opt/${PACKAGE_NAME}/bin/${PACKAGE_NAME}" "${BUILD_DIR}/pkg/usr/bin/${PACKAGE_NAME}" | |
| else | |
| ln -s "/opt/${PACKAGE_NAME}/${PACKAGE_NAME}" "${BUILD_DIR}/pkg/usr/bin/${PACKAGE_NAME}" | |
| fi | |
| echo "Creating desktop entry..." | |
| cat << EOF >"${BUILD_DIR}/pkg/usr/share/applications/${PACKAGE_NAME}.desktop" | |
| [Desktop Entry] | |
| Name=${DISPLAY_NAME} | |
| Comment=${DISPLAY_NAME} Code Editor | |
| GenericName=Text Editor | |
| Exec=/opt/${PACKAGE_NAME}/${PACKAGE_NAME} %F | |
| Icon=${PACKAGE_NAME} | |
| Type=Application | |
| StartupNotify=true | |
| StartupWMClass=${PACKAGE_NAME} | |
| Categories=Utility;TextEditor;Development;IDE; | |
| MimeType=text/plain;inode/directory; | |
| Actions=new-empty-window; | |
| [Desktop Action new-empty-window] | |
| Name=New Empty Window | |
| Exec=/opt/${PACKAGE_NAME}/${PACKAGE_NAME} --new-window %F | |
| Icon=${PACKAGE_NAME} | |
| EOF | |
| echo "Creating DEBIAN/control..." | |
| cat << EOF >"${BUILD_DIR}/pkg/DEBIAN/control" | |
| Package: ${PACKAGE_NAME} | |
| Version: ${VERSION} | |
| Section: devel | |
| Priority: optional | |
| Architecture: amd64 | |
| Depends: libasound2, libatk1.0-0, libc6, libcairo2, libcups2, libdbus-1-3, libexpat1, libfontconfig1, libfreetype6, libgbm1, libglib2.0-0, libgtk-3-0, libnspr4, libnss3, libpango-1.0-0, libpangocairo-1.0-0, libsecret-1-0, libx11-6, libx11-xcb1, libxcb1, libxcomposite1, libxcursor1, libxdamage1, libxext6, libxfixes3, libxi6, libxrandr2, libxrender1, libxss1, libxtst6, libgl1 | |
| Maintainer: Antigravity IDE Team <support@antigravity.google> | |
| Description: ${DISPLAY_NAME} - Code Editor | |
| A powerful agentic AI coding assistant and IDE. | |
| EOF | |
| # The postinst script is run as root by the package manager during installation. | |
| # This allows us to set setuid on the chrome-sandbox binary. | |
| echo "Creating DEBIAN/postinst..." | |
| cat << EOF >"${BUILD_DIR}/pkg/DEBIAN/postinst" | |
| #!/bin/bash | |
| set -e | |
| # Make chrome-sandbox setuid root to enable Chromium's SUID sandbox | |
| if [ -f "/opt/${PACKAGE_NAME}/chrome-sandbox" ]; then | |
| chown root:root "/opt/${PACKAGE_NAME}/chrome-sandbox" | |
| chmod 4755 "/opt/${PACKAGE_NAME}/chrome-sandbox" | |
| fi | |
| # Ensure executable permissions on essential binaries | |
| chmod +x "/opt/${PACKAGE_NAME}/${PACKAGE_NAME}" | |
| chmod +x "/opt/${PACKAGE_NAME}/chrome_crashpad_handler" | |
| if [ -f "/opt/${PACKAGE_NAME}/bin/${PACKAGE_NAME}" ]; then | |
| chmod +x "/opt/${PACKAGE_NAME}/bin/${PACKAGE_NAME}" | |
| fi | |
| # Update Gnome desktop entries cache | |
| if [ -x "\$(command -v update-desktop-database)" ]; then | |
| update-desktop-database -q | |
| fi | |
| EOF | |
| # Make postinst executable so dpkg can run it | |
| chmod 755 "${BUILD_DIR}/pkg/DEBIAN/postinst" | |
| # Build the package. We use --root-owner-group so the built archive structure is | |
| # owned by root:root rather than the local building user. | |
| echo "Building deb package..." | |
| dpkg-deb --root-owner-group --build "${BUILD_DIR}/pkg" "${WORKSPACE_DIR}/${PACKAGE_NAME}_${VERSION}_amd64.deb" | |
| # Clean up build artifacts | |
| rm -rf "${BUILD_DIR}" | |
| echo "Build complete: ${WORKSPACE_DIR}/${PACKAGE_NAME}_${VERSION}_amd64.deb" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment