#!/bin/bash # Drift Desktop Installer - Hosted on godrift.ai # Download URLs are resolved server-side to avoid GitHub API rate limits. # Installs the JOINT drift_*.deb → /usr/bin/drift-desktop (IDE) + drift (CLI) + drift-sidecar. set -e # Pre-resolved release info (injected by Cloud Function) RELEASE_VERSION="2.0.1" DEB_URL_AMD64="https://github.com/godrift-ai/drift-releases/releases/download/2.0.1/drift_2.0.1_amd64.deb" DEB_URL_ARM64="" ANALYTICS_ENDPOINT="https://drift-analytics-service-947145620113.us-central1.run.app/install" # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # Generate or read persistent device ID (same file the CLI uses: ~/.drift/user_id). # Sharing the file means an install event from this installer stitches to the same # Mixpanel user as later `drift`/`drift-desktop` sessions — no duplicate identities. DRIFT_DIR="$HOME/.drift" DEVICE_ID_FILE="$DRIFT_DIR/user_id" if [ -f "$DEVICE_ID_FILE" ] && [ -s "$DEVICE_ID_FILE" ]; then DEVICE_ID=$(cat "$DEVICE_ID_FILE") else mkdir -p "$DRIFT_DIR" DEVICE_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || echo "install_$(date +%s%N)_$RANDOM") echo -n "$DEVICE_ID" > "$DEVICE_ID_FILE" fi # Fire-and-forget analytics. One install puts TWO products on the machine — the drift # CLI and Drift Desktop — so every step fires one event from each family: the CLI-lane # name it is called with (`install_started`, …) and the paired desktop-lane name # (`install_desktop_started`, …). Both funnels count the install; neither has to filter # on a property. `surface: desktop` is still stamped on both so the joint installer can # be told apart from the retired standalone CLI installer inside the CLI funnel. track_event() { local event_name="$1" local success="$2" local error_msg="$3" local extra_props="" if [ -n "$success" ] && [ -n "$error_msg" ]; then extra_props=", \"success\": $success, \"error\": \"$error_msg\"" elif [ -n "$success" ]; then extra_props=", \"success\": $success" fi local ts=$(date +%s) # install_ -> install_desktop_ local desktop_event="install_desktop_${event_name#install_}" local ev for ev in "$event_name" "$desktop_event"; do local payload="{\"action\": \"track\", \"distinct_id\": \"\", \"event\": \"$ev\", \"properties\": {\"\$device_id\": \"$DEVICE_ID\", \"time\": $ts, \"\$os\": \"Linux\", \"platform\": \"linux\", \"os\": \"Linux\", \"arch\": \"${ARCH_DEB:-unknown}\", \"version\": \"${RELEASE_VERSION:-unknown}\", \"surface\": \"desktop\"$extra_props}}" (curl -s -X POST "$ANALYTICS_ENDPOINT" \ -H "Content-Type: application/json" \ -d "$payload" \ >/dev/null 2>&1) & done } echo "" echo " ╔══════════════════════════════════════╗" echo " ║ DRIFT INSTALLER ║" echo " ║ Intelligence for Robotics Sim ║" echo " ║ robots real now ║" echo " ║ godrift.ai ║" echo " ╚══════════════════════════════════════╝" echo "" echo "Installing Drift Desktop (includes the drift CLI)..." track_event "install_started" "" "" if [ "$(uname)" != "Linux" ]; then echo -e "${RED}Error: This installer is only for Linux systems${NC}" echo "For other platforms, visit: https://godrift.ai" track_event "install_incomplete" "false" "not_linux" exit 1 fi # Detect architecture (currently only amd64 is produced by combine-release.sh — arm64 is # accepted here for forward compatibility with a future cross-target build). ARCH=$(uname -m) case $ARCH in x86_64) ARCH_DEB="amd64" DEB_URL="$DEB_URL_AMD64" ;; aarch64|arm64) ARCH_DEB="arm64" DEB_URL="$DEB_URL_ARM64" ;; *) echo -e "${RED}Error: Unsupported architecture: $ARCH${NC}" echo "Supported: x64 (amd64), arm64" track_event "install_incomplete" "false" "unsupported_arch" exit 1 ;; esac echo "Detected: Linux $ARCH_DEB" if command -v curl >/dev/null 2>&1; then DOWNLOAD_CMD="curl -fsSL" DOWNLOAD_OUTPUT="-o" elif command -v wget >/dev/null 2>&1; then DOWNLOAD_CMD="wget -q" DOWNLOAD_OUTPUT="-O" else echo -e "${RED}Error: curl or wget is required${NC}" echo "Install with: sudo apt-get install curl" track_event "install_incomplete" "false" "no_curl_wget" exit 1 fi if [ -z "$DEB_URL" ]; then echo -e "${RED}Error: No .deb package available for $ARCH_DEB${NC}" echo "Available releases: https://github.com/godrift-ai/drift-releases/releases" track_event "install_incomplete" "false" "no_deb_package" exit 1 fi echo "Latest version: $RELEASE_VERSION" echo "Downloading Drift Desktop $RELEASE_VERSION..." # mktemp, not a predictable /tmp name: the file is handed to `sudo dpkg -i`, and a fixed # path in a world-writable directory is something another local user can pre-create. # mktemp creates it 0600 under our uid, so there is nothing to race. TMP_DEB="$(mktemp --suffix=.deb /tmp/drift_XXXXXX)" # `|| true` so a failed download reaches the check below instead of `set -e` aborting # silently before the error message and the install_incomplete event. $DOWNLOAD_CMD "$DEB_URL" $DOWNLOAD_OUTPUT "$TMP_DEB" || true if [ ! -s "$TMP_DEB" ]; then echo -e "${RED}Error: Download failed${NC}" rm -f "$TMP_DEB" track_event "install_incomplete" "false" "download_failed" exit 1 fi echo "Installing package..." # `dpkg -i` first — if a dependency is missing, `apt-get install -f -y` pulls it in and # a second `dpkg -i` finishes. The joint .deb declares Depends: libwebkit2gtk-4.1-0 + # libgtk-3-0; on a headless minimal Linux those often need pulling. Every failing step # carries `|| …` because `set -e` is on: without it the retry path would abort before # the retry, and a final failure would exit without the error line or the event. if command -v dpkg >/dev/null 2>&1; then sudo dpkg -i "$TMP_DEB" 2>/dev/null || { echo "Installing dependencies..." sudo apt-get update >/dev/null 2>&1 || true sudo apt-get install -f -y >/dev/null 2>&1 || true sudo dpkg -i "$TMP_DEB" || { echo -e "${RED}Error: package install failed${NC}" track_event "install_incomplete" "false" "dpkg_failed" exit 1 } } elif command -v apt >/dev/null 2>&1; then sudo apt install "$TMP_DEB" -y || { echo -e "${RED}Error: package install failed${NC}" track_event "install_incomplete" "false" "apt_failed" exit 1 } else echo -e "${RED}Error: No supported package manager found${NC}" echo "Manual installation required: sudo dpkg -i $TMP_DEB" track_event "install_incomplete" "false" "no_package_manager" exit 1 fi rm -f "$TMP_DEB" hash -r 2>/dev/null || true # Verify BOTH binaries are on PATH — the joint .deb is the point. if ! command -v drift-desktop >/dev/null 2>&1; then echo -e "${RED}Error: drift-desktop not on PATH after install${NC}" echo "The .deb may not have installed cleanly. Try:" echo " sudo apt-get install -f -y # to fix any missing dependencies" track_event "install_incomplete" "false" "no_cockpit_after_install" exit 1 fi if ! command -v drift >/dev/null 2>&1; then # Not fatal — the IDE works without the CLI on PATH — but worth flagging. echo -e "${YELLOW}Warning: drift CLI not on PATH after install (open a new shell to pick it up)${NC}" fi # Read the version from dpkg's database — NEVER run `drift-desktop --version`. The # Tauri host does no argv parsing, so any invocation, flags or not, opens the window; # inside a `$( … )` capture that blocks the installer until the user closes it, and # yields an empty string. That was the "IDE launches during install, terminal hangs, # then launches again after" symptom. INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' drift-desktop 2>/dev/null || echo unknown) echo "" echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Drift installed successfully! ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" echo "" echo "Version: $INSTALLED_VERSION" echo "IDE binary: $(command -v drift-desktop)" echo "CLI binary: $(command -v drift 2>/dev/null || echo 'not on PATH — open a new shell')" echo "" # Launch the app only when there is a display server to launch into. A headless box or a # plain SSH session has neither $DISPLAY nor $WAYLAND_DISPLAY set; SSH with X-forwarding # sets $DISPLAY and launching is then correct. (A stale exported $DISPLAY that points at # nothing is not detectable cheaply — xdpyinfo/xset are not always installed — so that # case reports a launch and the app exits on its own; the log line names the file.) _has_display="" if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then _has_display=1 fi if [ -n "$_has_display" ]; then # DEFERRED launch — the app opens AFTER this installer script and the curl|bash # pipeline that hosts it have fully exited. The naive `drift-desktop &` (even # with `nohup`, `disown`, `setsid`, or a subshell) leaves fds and process-group # relationships that a `curl | bash` outer shell waits on, so the terminal prompt # only returns when the IDE window is closed. Verified failure mode. # # So we write a tiny launcher script to /tmp that polls for THIS installer's PID # to disappear, then execs drift-desktop. The launcher itself is fired fully # detached (subshell + setsid --fork) so nothing about it holds the pipeline # open, but it also does nothing (just sleeps) until the pipeline is gone — # meaning the user sees the shell prompt come back, THEN the IDE opens. _log="$DRIFT_DIR/cockpit-launch.log" _launcher="$(mktemp --suffix=.sh /tmp/drift-launch.XXXXXX.sh 2>/dev/null || mktemp)" _installer_pid=$$ cat > "$_launcher" </dev/null; do sleep 0.3 [ "\$(date +%s)" -gt "\$_deadline" ] && break done # A small extra beat so the user's next shell prompt lands BEFORE the IDE window # steals focus — otherwise the two arrive in a race. sleep 0.5 # Self-delete before exec: exec never returns, so a rm afterwards would never run. # Removed here so a failed exec still cleans up. rm -f "\$0" 2>/dev/null exec drift-desktop >"$_log" 2>&1 /dev/null 2>&1 & ) /dev/null 2>&1 echo -e "${BLUE}Drift Desktop will open in a moment (logs: $_log).${NC}" track_event "install_completed" "true" "" else echo -e "${YELLOW}No display server detected — the IDE was not launched.${NC}" echo "To open it in a graphical session, run:" echo -e " ${GREEN}drift-desktop${NC}" echo "Or use the terminal CLI:" echo -e " ${GREEN}drift${NC}" track_event "install_completed" "true" "no_display" fi echo ""