1102 lines · 42.6 KB · Text
RawDownload
#!/bin/bash## Bundle the application for distribution.## See the parameter parsing section below for available options.## Note that there are several steps to complete before the app is ready to# be distributed, one of which is asynchronous and requires apple to process# our binary.## The exact steps depend on which artifact we are building, the desktop app or the CLI. Overall,# the steps are:## 1) Create the binary or mac bundle by running `cargo`.# 2) Create a keychain based on our distribution cert.# 3) Codesign the built artifact using the keychain.## Additionally, for an app, we create a dmg:# 4) Create the dmg using `hdiutil create`.# 5) Codesign our dmg using the keychain.# 6) Upload the app to Apple for it to be notarized - this is async, and we#    poll until it is done.# 7) "Staple" the notarization to the dmg.## Once the stapling is done, the app can be shared.## Three passwords are read from GCP Secret Manager (or from env if --read-passwords-from-env is set) if you are codesigning.# 1) WARP_NOTARIZATION_PASSWORD:  This is an "app-specific password" that#    is tied to the zach@warp.dev account.  See https://support.apple.com/en-us/HT204397# 2) WARP_DEVELOPER_ID_CERT_PASSWORD: This is a password tied to the private key#    of our cert - it is needed to use the cert to sign our binary.# 3) WARP_CODESIGN_KEYCHAIN_PASSWORD: This is an arbitrary password only used#    in the lifetime of this app for creating the keychain used to sign.  Can#    be anything.## See# https://github.com/burtonageo/cargo-bundle# https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS# https://wiki.lazarus.freepascal.org/Notarization_for_macOS_10.14.5%2B# https://developer.apple.com/developer-id/# https://github.com/atom/atom/blob/976cb9ef3a611163052f9d31c6c3685dc1e6c5b4/script/lib/code-sign-on-mac.js set -e # Determine the repository root directoryWORKSPACE_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # This is used to set the minimum deployment target for mac# https://cmake.org/cmake/help/latest/envvar/MACOSX_DEPLOYMENT_TARGET.html# If unset, it uses the system one, but we want to build for older versions# of mac os by default.## This should be kept in sync with the definition of this variable in# `.cargo/config.toml`.export MACOSX_DEPLOYMENT_TARGET="10.14" # Constants used later in the script.INTEL_ARCH="x86_64"INTEL_TARGET="$INTEL_ARCH-apple-darwin"ARM_ARCH="aarch64"ARM_TARGET="$ARM_ARCH-apple-darwin"DEFAULT_ARCH="$(rustc --print cfg | grep target_arch)" # Function to clean up temporary DMG filescleanup_dmg_files() {    local target_dir="$1"    if [ -d "$target_dir" ]; then        echo "Cleaning up temporary DMG files in $target_dir"        find "$target_dir" -name "*.dmg" -type f -delete        find "$target_dir" -name "rw.*.dmg" -type f -delete    fi    # Also check if any volumes are mounted and unmount them    hdiutil info | grep "/Volumes/Warp.*" | awk '{print $1}' | while read -r disk; do        echo "Unmounting disk image: $disk"        hdiutil detach "$disk" -force || true    done} # Clean up the temporary codesigning keychain.cleanup_codesign_keychain() {    if [[ "${CODESIGN:-false}" = true && -n "${CODESIGN_KEYCHAIN_NAME:-}" ]]; then        echo "Cleaning up by deleting $CODESIGN_KEYCHAIN_NAME keychain."        security delete-keychain "$CODESIGN_KEYCHAIN_NAME" || echo "No keychain to delete or already deleted."    fi} # Set up cleanup traptrap 'cleanup_dmg_files "$DMG_DIR"; cleanup_codesign_keychain' EXIT # Define a helper function that uses Python to compute a relative path.function relpath() {  python -c "import os,sys;print(os.path.relpath(*(sys.argv[1:])))" "$@";} # Retry timestamped signing because Apple's timestamp service can fail# transiently. All call sites use force signing so retries are idempotent.function codesign_with_retry() {  local max_attempts="${CODESIGN_MAX_ATTEMPTS:-4}"  local initial_retry_delay_seconds="${CODESIGN_INITIAL_RETRY_DELAY_SECONDS:-5}"  local retry_delay_seconds="$initial_retry_delay_seconds"  local attempt  local status   for ((attempt = 1; attempt <= max_attempts; attempt++)); do    if codesign "$@"; then      return 0    else      status=$?    fi     if (( attempt == max_attempts )); then      echo "Codesigning failed after $attempt attempts."      return "$status"    fi     echo "Codesigning failed; retrying in $retry_delay_seconds seconds ($attempt/$max_attempts)."    sleep "$retry_delay_seconds"    retry_delay_seconds=$((retry_delay_seconds * 2))  done} # Submit once so a transient polling failure can resume the same notarization# instead of uploading a duplicate artifact.function notarize_artifact() {  local artifact="$1"  local auth_args=(    --apple-id "$WARP_NOTARIZATION_APPLE_ID"    --password "$WARP_NOTARIZATION_PASSWORD"    --team-id "$APPLE_TEAM_ID"  )   local submission_output  if ! submission_output="$(    xcrun notarytool submit "$artifact" "${auth_args[@]}" --no-wait --output-format json  )"; then    echo "Failed to submit $artifact for notarization."    return 1  fi   local submission_id  if ! submission_id="$(printf '%s' "$submission_output" | plutil -extract id raw -o - -)"; then    echo "Failed to read the notarization submission ID."    return 1  fi  if [[ -z "$submission_id" ]]; then    echo "Apple did not return a notarization submission ID."    return 1  fi   echo "Notarization submission ID: $submission_id"   local poll_interval_seconds="${NOTARIZATION_POLL_INTERVAL_SECONDS:-15}"  local max_poll_failures="${NOTARIZATION_MAX_POLL_FAILURES:-5}"  local initial_retry_delay_seconds="${NOTARIZATION_INITIAL_RETRY_DELAY_SECONDS:-5}"  local timeout_seconds="${NOTARIZATION_TIMEOUT_SECONDS:-900}"  local retry_delay_seconds="$initial_retry_delay_seconds"  local consecutive_poll_failures=0  local polling_started_at="$SECONDS"   while true; do    if (( SECONDS - polling_started_at >= timeout_seconds )); then      echo "Timed out waiting for notarization submission $submission_id after $timeout_seconds seconds."      return 1    fi    local info_output    if info_output="$(      xcrun notarytool info "$submission_id" "${auth_args[@]}" --output-format json    )"; then      consecutive_poll_failures=0      retry_delay_seconds="$initial_retry_delay_seconds"       local status      if ! status="$(printf '%s' "$info_output" | plutil -extract status raw -o - -)"; then        echo "Failed to read the status of notarization submission $submission_id."        return 1      fi       echo "Current notarization status: $status"      case "$status" in        Accepted)          return 0          ;;        Invalid|Rejected)          echo "Notarization submission $submission_id was rejected by Apple."          xcrun notarytool log "$submission_id" "${auth_args[@]}" || true          return 1          ;;        "In Progress")          sleep "$poll_interval_seconds"          ;;        *)          echo "Unexpected notarization status for submission $submission_id: $status"          return 1          ;;      esac    else      consecutive_poll_failures=$((consecutive_poll_failures + 1))      if (( consecutive_poll_failures >= max_poll_failures )); then        echo "Failed to poll notarization submission $submission_id after $consecutive_poll_failures consecutive attempts."        return 1      fi       echo "Failed to poll notarization submission $submission_id; retrying in $retry_delay_seconds seconds ($consecutive_poll_failures/$max_poll_failures)."      sleep "$retry_delay_seconds"      retry_delay_seconds=$((retry_delay_seconds * 2))    fi  done} # Defaults for command-line flags.UNIVERSAL_BINARY=trueBUILD_BINARY=trueTARGET_ARCH=""DMG_NAME_SUFFIX=""# By default we build dev bundles.RELEASE_CHANNEL="dev"FEATURES="release_bundle,cocoa_sentry,extern_plist"REGISTER_SERVICES=trueDEBUG=falseARTIFACT="app"CODESIGN=trueSELFSIGN=falseOPEN_AFTER_BUNDLE=falseREAD_PASSWORDS_FROM_ENV=false PARAMS=""while (( "$#" )); do  case "$1" in    # Speed up compilation time by only compiling a debug version of the app,    # rather than a release version    --debug)      DEBUG=true      shift      ;;    # Only run `cargo check` without producing a bundle    --check-only)      echo 'Only running `cargo check` and not producing a bundle.'      CHECK_ONLY="true"      shift      ;;    # Skip building the binary (assume it's already built)    --skip-build)      echo "Skipping binary build step."      BUILD_BINARY=false      shift      ;;    # Skip code signing process    --nosign)      echo "Skipping code signing."      CODESIGN=false      SELFSIGN=false      shift      ;;    # Sign with a local Apple Development cert instead of the official Warp cert.    # Useful for local debug builds when you don't have access to the company signing key.    # Falls back to ad-hoc signing if no Apple Development cert is found.    --selfsign)      echo "Self-signing enabled."      CODESIGN=false      SELFSIGN=true      shift      ;;    # Build only for the default target architecture instead of universal binary    --nouniversal)      echo "Only building for default target $DEFAULT_TARGET, not a universal binary."      UNIVERSAL_BINARY=false      shift      ;;    # Build only for a specific architecture (implies --nouniversal)    --arch)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        if [ "$2" = "x86_64" -o "$2" = "aarch64" ]; then          echo "Building for specific architecture: $2"          TARGET_ARCH=$2          UNIVERSAL_BINARY=false          shift 2        else          echo "Error: --arch must be either x86_64 or aarch64, got '$2'" >&2          exit 1        fi      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    # Set a custom suffix for the DMG file    --dmg-name-suffix)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        echo "Setting custom DMG name suffix to $2"        DMG_NAME_SUFFIX=$2        shift 2      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    # Open the parent directory after bundling completes.    -o|--open)      OPEN_AFTER_BUNDLE=true      shift      ;;    # Specify the release channel (local, dev, preview, stable, oss)    -c|--channel)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        RELEASE_CHANNEL=$2        shift 2      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    # Set a specific Git release tag for the build    --release-tag)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        echo "Setting release tag to $2"        export GIT_RELEASE_TAG=$2        shift 2      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    # Read codesigning passwords from environment variables instead of GCP secret manager    --read-passwords-from-env)        echo "Reading codesigning passwords from env."        READ_PASSWORDS_FROM_ENV=true        shift        ;;    --features)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        echo "Adding Cargo features: $2"        FEATURES="$FEATURES,$2"        shift 2      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    --artifact)      if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then        if [[ "$2" != "app" && "$2" != "cli" && "$2" != "warpctrl" && "$2" != "tui" ]]; then          echo "Error: --artifact must be 'app', 'cli', 'warpctrl', or 'tui', got '$2'" >&2          exit 1        fi        ARTIFACT="$2"        shift 2      else        echo "Error: Argument for $1 is missing" >&2        exit 1      fi      ;;    *) # preserve positional arguments      PARAMS="$PARAMS $1"      shift      ;;  esacdone# set positional arguments in their proper placeeval set -- "$PARAMS" # Infer a cargo profile from the bundle configuration.if [[ $DEBUG = true ]]; then  CARGO_PROFILE="dev"elif [[ $RELEASE_CHANNEL = "local" || $RELEASE_CHANNEL = "dev" ]]; then  # For dev bundles, we want to enable debug assertions to  # catch violations that would otherwise silently pass in  # a normal release build (e.g. in stable).  if [[ "$ARTIFACT" == "tui" ]]; then    CARGO_PROFILE="release-tui-debug-assertions"  elif [[ "$ARTIFACT" == "cli" || "$ARTIFACT" == "warpctrl" ]]; then    CARGO_PROFILE="release-cli-debug_assertions"  else    CARGO_PROFILE="release-lto-debug_assertions"  fielse  if [[ "$ARTIFACT" == "tui" ]]; then    CARGO_PROFILE="release-tui"  elif [[ "$ARTIFACT" == "cli" || "$ARTIFACT" == "warpctrl" ]]; then    CARGO_PROFILE="release-cli"  else    CARGO_PROFILE="release-lto"  fifi TARGET_PROFILE_DIR="$CARGO_PROFILE"if [[ "$CARGO_PROFILE" == "dev" ]]; then  TARGET_PROFILE_DIR="debug"fi if [[ $RELEASE_CHANNEL = "local" ]]; then    WARP_BIN="warp"    BUNDLE_ID="dev.warp.Warp-Local"    WARP_APP_NAME="WarpLocal"    WARP_SCHEME_NAME="warplocal"    FEATURES="$FEATURES,agent_mode_debug"    # For local builds, use different versions of our bundled frameworks (e.g.:    # Sentry).  This needs to be exported so it can be referenced by    # app/build.rs later, while running `cargo bundle`.    export FRAMEWORK_OVERRIDE="dev"elif [[ $RELEASE_CHANNEL = "dev" ]]; then    WARP_BIN="dev"    BUNDLE_ID="dev.warp.Warp-Dev"    WARP_APP_NAME="WarpDev"    WARP_SCHEME_NAME="warpdev"    FEATURES="$FEATURES,agent_mode_debug"    # Enable heap usage tracking & profiling using jemalloc through pprof.    FEATURES="$FEATURES,jemalloc_pprof,heap_usage_tracking"    # Enable the Warp Control CLI wrapper for local scripting/automation.    FEATURES="$FEATURES,warp_control_cli"    # For dev builds, use different versions of our bundled frameworks (e.g.:    # Sentry).  This needs to be exported so it can be referenced by    # app/build.rs later, while running `cargo bundle`.    export FRAMEWORK_OVERRIDE="dev"    export HANDLE_MARKDOWN=1elif [[ $RELEASE_CHANNEL = "preview" ]]; then    WARP_BIN="preview"    BUNDLE_ID="dev.warp.Warp-Preview"    WARP_APP_NAME="WarpPreview"    WARP_SCHEME_NAME="warppreview"    FEATURES="$FEATURES,preview_channel"    # Enable heap usage tracking & profiling using jemalloc through pprof.    FEATURES="$FEATURES,jemalloc_pprof,heap_usage_tracking"elif [[ $RELEASE_CHANNEL = "stable" ]]; then    WARP_BIN="stable"    BUNDLE_ID="dev.warp.Warp-Stable"    WARP_APP_NAME="Warp"    WARP_SCHEME_NAME="warp"    # Enable heap usage tracking & profiling using jemalloc through pprof.    FEATURES="$FEATURES,jemalloc_pprof,heap_usage_tracking"elif [[ $RELEASE_CHANNEL = "oss" ]]; then    WARP_BIN="warp-oss"    BUNDLE_ID="dev.warp.WarpOss"    WARP_APP_NAME="WarpOss"    WARP_SCHEME_NAME="warposs"    # The OSS channel does not ship Sentry, so drop the cocoa_sentry feature    # (which would otherwise pull in the Sentry framework dependency).    FEATURES="release_bundle,extern_plist"fi OUT_DIR="target/$TARGET_PROFILE_DIR/bundle/osx"DOCK_TILE_PLUGIN_DIR="target/$TARGET_PROFILE_DIR/WarpDockTilePlugin.docktileplugin" # Handle specific architecture targetingif [[ -n "$TARGET_ARCH" ]]; then    # Building for a specific architecture    if [[ "$TARGET_ARCH" == "$INTEL_ARCH" ]]; then        echo "Building specifically for $INTEL_TARGET"        DEFAULT_TARGET="$INTEL_TARGET"        BUNDLE_DIR="target/$INTEL_TARGET/$TARGET_PROFILE_DIR/bundle/osx"    elif [[ "$TARGET_ARCH" == "$ARM_ARCH" ]]; then        echo "Building specifically for $ARM_TARGET"        DEFAULT_TARGET="$ARM_TARGET"        BUNDLE_DIR="target/$ARM_TARGET/$TARGET_PROFILE_DIR/bundle/osx"    fielse    # Auto-detect default target based on current architecture    if [[ "$DEFAULT_ARCH" == *"$INTEL_ARCH"* ]]; then        echo "Default target is $INTEL_TARGET"        DEFAULT_TARGET="$INTEL_TARGET"        ADDITIONAL_TARGET="$ARM_TARGET"        BUNDLE_DIR="target/$INTEL_TARGET/$TARGET_PROFILE_DIR/bundle/osx"    elif [[ "$DEFAULT_ARCH" == *"$ARM_ARCH"* ]]; then        echo "Default target is $ARM_TARGET"        DEFAULT_TARGET="$ARM_TARGET"        ADDITIONAL_TARGET="$INTEL_TARGET"        BUNDLE_DIR="target/$ARM_TARGET/$TARGET_PROFILE_DIR/bundle/osx"    fifi # Apple Silicon hosts transparently execute Intel binaries via Rosetta 2, but# Intel hosts cannot execute Apple Silicon binaries, so only that direction is# unsafe. Resolve this once, ahead of the artifact-specific configuration# below (which determines UNIVERSAL_BINARY's final value), so the# settings-schema defaults further down can fail clearly instead of hitting# an exec format error.## `uname -m` reports the architecture of the running process, not the# hardware: inside a Rosetta-translated x86_64 shell on Apple Silicon it# reports x86_64 even though the hardware can execute arm64 binaries# directly. `hw.optional.arm64` reflects the actual hardware instead, so# check it first.if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null)" == "1" ]]; then  HOST_ARCH="aarch64"else  case "$(uname -m)" in    arm64|aarch64) HOST_ARCH="aarch64" ;;    x86_64) HOST_ARCH="x86_64" ;;    *)      echo "Error: Unsupported host architecture: $(uname -m)" >&2      exit 1      ;;  esacfi # Set artifact-specific configuration.if [[ "$ARTIFACT" == cli || "$ARTIFACT" == warpctrl ]]; then  UNIVERSAL_BINARY=false  OPEN_AFTER_BUNDLE=false  FEATURES="$FEATURES,standalone"elif [[ "$ARTIFACT" == tui ]]; then

Lines 1–500 of 1102.