1012 lines · 38.8 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:])))" "$@";} # 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 # Set artifact-specific configuration.if [[ "$ARTIFACT" == cli || "$ARTIFACT" == warpctrl ]]; then  UNIVERSAL_BINARY=false  OPEN_AFTER_BUNDLE=false  FEATURES="$FEATURES,standalone"elif [[ "$ARTIFACT" == tui ]]; then  # The TUI is a separate console crate (`warp_tui`) with its own per-channel  # bins and a minimal feature set, so it does NOT reuse the app's accumulated  # FEATURES. It is inherently headless (via `warp`'s `tui` feature). It needs  # `release_bundle` to embed the channel config, plus `standalone` so  # `bundled_resources_dir()` resolves the sibling `resources/` dir next to the  # binary (the TUI is not shipped inside a `.app` bundle).  UNIVERSAL_BINARY=false  OPEN_AFTER_BUNDLE=false  case "$RELEASE_CHANNEL" in    local) WARP_BIN="warp-tui" ;;    oss) WARP_BIN="warp-tui-oss" ;;    *) WARP_BIN="warp-tui-$RELEASE_CHANNEL" ;;  esac  WARP_TUI_FEATURES="release_bundle,standalone"  if [[ "$RELEASE_CHANNEL" != "oss" ]]; then    WARP_TUI_FEATURES="$WARP_TUI_FEATURES,crash_reporting"  fielif [[ "$ARTIFACT" == app ]]; then  # All channels ship the v3 classifier and v2 heuristic.  FEATURES="$FEATURES,gui,nld_classifier_v3,nld_heuristic_v2"fi # If we're building a universal bundle for the app artifact, make sure the additional target is available.if [[ $UNIVERSAL_BINARY = true ]]; then    rustup target add "$ADDITIONAL_TARGET"fi # If we only want to check that compilation will succeed, perform the checks# then exit.  We use this script to invoke `cargo check` to ensure that we are# using the same feature flags and profile that we would be using in production.if [[ "$CHECK_ONLY" == "true" ]]; then  if [[ "$ARTIFACT" == "tui" ]]; then    cargo check -p warp_tui --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$DEFAULT_TARGET" --features "$WARP_TUI_FEATURES"  else    cargo check --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$DEFAULT_TARGET" --features "$FEATURES"    if [[ $UNIVERSAL_BINARY = true && "$ARTIFACT" != "cli" && "$ARTIFACT" != "warpctrl" ]]; then      cargo check --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$ADDITIONAL_TARGET" --features "$FEATURES"    fi  fi  exit 0fi DMG_DIR="$BUNDLE_DIR/dmg/$WARP_BIN"DMG_NAME="$WARP_APP_NAME.dmg"if [[ -z "$FINAL_DMG_NAME" && -n "$DMG_NAME_SUFFIX" ]]; then  FINAL_DMG_NAME="$WARP_APP_NAME-$DMG_NAME_SUFFIX.dmg"else  FINAL_DMG_NAME="$WARP_APP_NAME.dmg"fi # First clean up and prep the outdirmkdir -p "$OUT_DIR"

Lines 1–500 of 1012.