Skip to content

Instantly share code, notes, and snippets.

@pansapiens
Last active August 29, 2026 01:16
Show Gist options
  • Select an option

  • Save pansapiens/57d08c6ff616e4b2bced300dc5449d46 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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