#!/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 directory
WORKSPACE_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 files
cleanup_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 trap
trap '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:])))" "$@";
}

# Defaults for command-line flags.
UNIVERSAL_BINARY=true
BUILD_BINARY=true
TARGET_ARCH=""
DMG_NAME_SUFFIX=""
# By default we build dev bundles.
RELEASE_CHANNEL="dev"
FEATURES="release_bundle,cocoa_sentry,extern_plist"
REGISTER_SERVICES=true
DEBUG=false
ARTIFACT="app"
CODESIGN=true
SELFSIGN=false
OPEN_AFTER_BUNDLE=false
READ_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" ]]; then
          echo "Error: --artifact must be either 'app' or 'cli', 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
      ;;
  esac
done
# set positional arguments in their proper place
eval 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" == "cli" ]]; then
    CARGO_PROFILE="release-cli-debug_assertions"
  else
    CARGO_PROFILE="release-lto-debug_assertions"
  fi
else
  if [[ "$ARTIFACT" == "cli" ]]; then
    CARGO_PROFILE="release-cli"
  else
    CARGO_PROFILE="release-lto"
  fi
fi

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"
    # 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=1
elif [[ $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"
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 targeting
if [[ -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"
    fi
else
    # 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"
    fi
fi

# Set artifact-specific configuration.
if [[ "$ARTIFACT" == cli ]]; then
  UNIVERSAL_BINARY=false
  OPEN_AFTER_BUNDLE=false
  FEATURES="$FEATURES,standalone"
elif [[ "$ARTIFACT" == app ]]; then
  FEATURES="$FEATURES,gui,nld_improvements"
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
  cargo check --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$DEFAULT_TARGET" --features "$FEATURES"
  if [[ $UNIVERSAL_BINARY = true && "$ARTIFACT" != "cli" ]]; then
    cargo check --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$ADDITIONAL_TARGET" --features "$FEATURES"
  fi
  exit 0
fi

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 outdir
mkdir -p "$OUT_DIR"
rm -R "$OUT_DIR/$WARP_APP_NAME.app" || echo "No old app to remove"
rm -R "$OUT_DIR/$FINAL_DMG_NAME" || echo "No old dmg to remove"

###########################
## Step 1: Build the app ##
###########################

if [[ "$ARTIFACT" == "app" ]]; then
  if [[ $BUILD_BINARY != true ]]; then
    echo "Skipping binary build due to --skip-build flag"
    export CARGO_BUNDLE_SKIP_BUILD=1
  fi

  pushd app > /dev/null
  echo "Building and bundling $DEFAULT_TARGET for channel $RELEASE_CHANNEL and bundle id $BUNDLE_ID with profile $CARGO_PROFILE"
  cargo bundle --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$DEFAULT_TARGET" --features "$FEATURES"
  popd > /dev/null

  echo "Adding rpath to support mac frameworks (e.g. Sentry)"
  install_name_tool -add_rpath "@executable_path/../Frameworks" "$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/MacOS/$WARP_BIN"

  export WARP_SCHEME_NAME
  export WARP_PLIST_PATH="$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/Info.plist"
  ./script/update_plist

  if [[ $REGISTER_SERVICES = true ]]; then
    plutil -insert NSServices -xml "
    <array>
      <dict>
        <key>NSMenuItem</key>
        <dict>
          <key>default</key>
          <string>New $WARP_APP_NAME Tab Here</string>
        </dict>
        <key>NSMessage</key>
        <string>openTab</string>
        <key>NSRequiredContext</key>
        <dict>
          <key>NSTextContent</key>
          <string>FilePath</string>
        </dict>
        <key>NSSendTypes</key>
        <array>
          <string>NSFilenamesPboardType</string>
          <string>public.plain-text</string>
        </array>
      </dict>
      <dict>
        <key>NSMenuItem</key>
        <dict>
          <key>default</key>
          <string>New $WARP_APP_NAME Window Here</string>
        </dict>
        <key>NSMessage</key>
        <string>openWindow</string>
        <key>NSRequiredContext</key>
        <dict>
          <key>NSTextContent</key>
          <string>FilePath</string>
        </dict>
        <key>NSSendTypes</key>
        <array>
          <string>NSFilenamesPboardType</string>
          <string>public.plain-text</string>
        </array>
      </dict>
    </array>" "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/Info.plist
  fi

  # Temporary key that ChatGPT Desktop can use to determine if the latest version of Warp supports the ChatGPT integration.
  # Once support has been rolled out for a sufficient amount of time we (and ChatGPT) can remove this.
  plutil -insert SUPPORTS_CHAT_GPT_WORK_WITH_APPS -bool true "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/Info.plist

  # Add LSBackgroundOnly key and set it to false since we need a UI app
  plutil -insert LSBackgroundOnly -bool false "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/Info.plist

  # Add SMAuthorizedClients for macOS 13+ (Ventura) to support login item functionality
  plutil -insert SMAuthorizedClients -xml "<array><string>$BUNDLE_ID</string></array>" "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/Info.plist

  if [[ $UNIVERSAL_BINARY = true ]]; then
      if [[ $BUILD_BINARY = true ]]; then
          echo "Building $ADDITIONAL_TARGET to include in universal binary"

          pushd app > /dev/null
          cargo build --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$ADDITIONAL_TARGET" --features "$FEATURES"
          popd > /dev/null
      else
          echo "Skipping build of $ADDITIONAL_TARGET due to --skip-build flag"
      fi

      tmp_bundle_dir=$(mktemp -d -t ci-XXXXXXXXXX)

      echo "Adding rpath to both binaries for universal executable"
      install_name_tool -add_rpath "@executable_path/../Frameworks" "target/$INTEL_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN"
      install_name_tool -add_rpath "@executable_path/../Frameworks"  "target/$ARM_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN"

      echo "Building universal binary using lipo."
      lipo -create -output "$tmp_bundle_dir/$WARP_BIN" \
          "target/$INTEL_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN" \
          "target/$ARM_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN"

      # Compute the real absolute path to the dSYM file, factoring in symlinks.
      # Starting in Rust 1.56, the generated dSYMs are symlinks to files within the target/dep directory. For example,
      # a dSYM for dev may be symlinked to target/deps/dev-080cf7e291fb066d.dSYM. This means the actual debug symbols are
      # would be located at dev.dSYM/Contents/Resources/DWARF/dev-080cf7e291fb066d.dSYM where dev.dSYM is symlink to
      # deps/dev-080cf7e291fb066d.dSYM. To fix this, parse out the actual name of the directory that the dSYM is
      # symlinked to, since this is also the name of file that contains the debug symbols within the dSYM.
      INTEL_DSYM_NAME=$(basename "$(realpath target/$INTEL_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM)" .dSYM)
      INTEL_DSYM_PATH="target/$INTEL_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM/Contents/Resources/DWARF/$INTEL_DSYM_NAME"
      ARM_DSYM_NAME=$(basename "$(realpath target/$ARM_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM)" .dSYM)
      ARM_DSYM_PATH="target/$ARM_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM/Contents/Resources/DWARF/$ARM_DSYM_NAME"
      if [[ -e "$INTEL_DSYM_PATH" && -e "$ARM_DSYM_PATH" ]]; then
        echo "Building universal binary .dSYM using lipo and storing in $OUT_DIR/$WARP_BIN.dSYM"

        # Use lipo to merge the .dSYM files into a single universal .dSYM file.
        # It expects the base name for the file (with the .dSYM suffix removed).
        lipo -create -output "$OUT_DIR/$WARP_BIN.dSYM" "${INTEL_DSYM_PATH}" "${ARM_DSYM_PATH}"
      fi

      echo "Storing result in $BUNDLE_DIR, replacing $DEFAULT_TARGET binary with fat binary."
      mv "$tmp_bundle_dir/$WARP_BIN" "$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/MacOS"
  elif [[ -n "$TARGET_ARCH" && -e "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM" ]]; then
    echo "Copying .dSYM into $OUT_DIR/$WARP_BIN.dSYM"
    cp -HR "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM" "$OUT_DIR/"
  fi

  # Note that the dock tile plugin is pre-built for both arm64 and x86_64 so we don't need to run lipo on it.
  echo "Creating PlugIns directory and copying pre-built DockTilePlugin..."
  mkdir -p "$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/PlugIns"
  cp -R "$DOCK_TILE_PLUGIN_DIR" "$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/PlugIns/"

  echo "Updating plist with dock tile plugin entries"
  plutil -insert NSDockTilePlugIn -string "WarpDockTilePlugin.docktileplugin" "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/Info.plist
  plutil -insert MainAppBundleIdentifier -string "$BUNDLE_ID" "$BUNDLE_DIR"/$WARP_APP_NAME.app/Contents/PlugIns/WarpDockTilePlugin.docktileplugin/Contents/Info.plist

  BUNDLED_RESOURCES_DIR="$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/Resources"
  echo "Preparing bundled resources..."
  "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$CARGO_PROFILE"

  "$WORKSPACE_ROOT_DIR/script/compile_icon" "$RELEASE_CHANNEL" "$BUNDLE_DIR/$WARP_APP_NAME.app"

  HELPERS_DIR="$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/Helpers"
  if [[ ",$FEATURES," =~ ",heap_usage_tracking," ]]; then
    echo "Bundling pprof..."
    "$WORKSPACE_ROOT_DIR/script/prepare_bundled_pprof" "$HELPERS_DIR"
  fi

  # Determine CLI wrapper script path based on release channel.  Each channel's
  # value here must match `Channel::cli_command_name` in the Rust source.
  if [[ $RELEASE_CHANNEL = "stable" ]]; then
    CLI_SCRIPT_PATH="$BUNDLED_RESOURCES_DIR/bin/oz"
  elif [[ $RELEASE_CHANNEL = "oss" ]]; then
    CLI_SCRIPT_PATH="$BUNDLED_RESOURCES_DIR/bin/warp-oss"
  else
    CLI_SCRIPT_PATH="$BUNDLED_RESOURCES_DIR/bin/oz-$RELEASE_CHANNEL"
  fi

  echo "Creating Resources/bin directory and CLI wrapper script..."
  mkdir -p "$BUNDLED_RESOURCES_DIR/bin"
  
  cat > "$CLI_SCRIPT_PATH" << 'EOF'
#!/bin/bash
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec -a "$0" "$script_dir/../../MacOS/WARP_BIN_PLACEHOLDER" "$@"
EOF
  
  # Replace the placeholder with the actual binary name
  sed -i '' "s/WARP_BIN_PLACEHOLDER/$WARP_BIN/" "$CLI_SCRIPT_PATH"
  
  # Make the script executable
  chmod +x "$CLI_SCRIPT_PATH"

  # Store the built artifact locations for GitHub Actions outputs.
  BINARY_PATH="target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN"
  DMG_PATH="$OUT_DIR/$FINAL_DMG_NAME"
elif [[ "$ARTIFACT" == "cli" ]]; then
  if [[ $BUILD_BINARY == true ]]; then
    # Create Info.plist before building the app, since it's embedded at build time.
    # Apple's codesigning tools will detect Info.plist files in the same directory as an executable.
    # This breaks the code signature, so we must use a different location for the file.
    mkdir -p "$BUNDLE_DIR"
    export WARP_PLIST_PATH="$BUNDLE_DIR/cli-info.plist"
    cp app/assets/resources/mac/CLI-Info.plist "$WARP_PLIST_PATH"

    export WARP_PLIST_NO_FILE_TYPES=true
    ./script/update_plist
    plutil -insert CFBundleIdentifier -string "$BUNDLE_ID" "$WARP_PLIST_PATH"
    plutil -insert CFBundleName -string "$WARP_BIN" "$WARP_PLIST_PATH"
    plutil -insert CFBundleExecutable -string "$WARP_BIN" "$WARP_PLIST_PATH"

    export "INFO_PLIST_PATH=$(realpath "$WARP_PLIST_PATH")"
    pushd app > /dev/null
    echo "Building $DEFAULT_TARGET for channel $RELEASE_CHANNEL with profile $CARGO_PROFILE"
    cargo build --profile "$CARGO_PROFILE" --bin "$WARP_BIN" --target "$DEFAULT_TARGET" --features "$FEATURES"
    popd > /dev/null
  else
    echo "Skipping binary build due to --skip-build flag"
  fi

  echo "Copying binary into $OUT_DIR/$WARP_BIN"
  cp "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN" "$OUT_DIR/$WARP_BIN"

  if [[ -n "$TARGET_ARCH" && -e "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM" ]]; then
    echo "Copying .dSYM into $OUT_DIR/$WARP_BIN.dSYM"
    cp -HR "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM" "$OUT_DIR/"
  fi

  echo "Preparing CLI resources directory"
  BUNDLED_RESOURCES_DIR="$OUT_DIR/resources"
  "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$CARGO_PROFILE"


  # Set the primary binary path to output.
  BINARY_PATH="$OUT_DIR/$WARP_BIN"
else
  echo "Unsupported artifact: $ARTIFACT" >&2
  exit 1
fi

##########################################
## Step 2: Create code-signing keychain ##
##########################################

if [[ $READ_PASSWORDS_FROM_ENV != true ]]; then
  echo "Skipping code-signing because passwords are not available in environment."
  CODESIGN=false
fi

if [[ $CODESIGN = true ]]; then
  # TODO - does this need to change per user?  Seems like it's tied to the WARP_NOTARIZATION_PASSWORD password.
  APPLE_TEAM_ID="2BBY89MBSN"
  CODESIGN_KEYCHAIN_NAME="warp-codesign-keychain"

  echo "Starting codesigning..."

  if [[ $READ_PASSWORDS_FROM_ENV = true ]]; then
    if [ -z "$WARP_NOTARIZATION_PASSWORD" ] ; then
      echo "WARP_NOTARIZATION_PASSWORD must be set for code signing"
      exit 1
    fi

    if [ -z "$WARP_DEVELOPER_ID_CERT_PASSWORD" ] ; then
      echo "WARP_DEVELOPER_ID_CERT_PASSWORD must be set for code signing"
      exit 1
    fi

    if [ -z "$WARP_CODESIGN_KEYCHAIN_PASSWORD" ] ; then
      echo "WARP_CODESIGN_KEYCHAIN_PASSWORD must be set for code signing"
      exit 1
    fi
  fi

  security delete-keychain $CODESIGN_KEYCHAIN_NAME || echo "No existing keychain to clean up".

  echo "Creating $CODESIGN_KEYCHAIN_NAME keychain."
  security create-keychain -p "$WARP_CODESIGN_KEYCHAIN_PASSWORD" $CODESIGN_KEYCHAIN_NAME
  security list-keychains -s $CODESIGN_KEYCHAIN_NAME
  security set-keychain-settings -t 3600 -u $CODESIGN_KEYCHAIN_NAME

  echo "Unlocking keychain and setting cert."
  security unlock-keychain -p "$WARP_CODESIGN_KEYCHAIN_PASSWORD" $CODESIGN_KEYCHAIN_NAME
  security import <(echo "$WARP_DEVELOPER_ID_CERT" | base64 -d) -f pkcs12 -P "$WARP_DEVELOPER_ID_CERT_PASSWORD" -k $CODESIGN_KEYCHAIN_NAME -T /usr/bin/codesign
  security set-key-partition-list -S "apple-tool:,apple:" -s -k "$WARP_CODESIGN_KEYCHAIN_PASSWORD" $CODESIGN_KEYCHAIN_NAME
fi

##############################
## Step 3: Codesign the app ##
##############################

if [[ $SELFSIGN = true ]]; then
  SIGNING_CERT="$(security find-identity -p codesigning -v | grep "Apple Development" | awk '{print $2}' | head -1)"
  if [[ -z "$SIGNING_CERT" ]]; then
    echo "No Apple Development cert found, falling back to ad-hoc signing."
    SIGNING_CERT="-"
  else
    echo "Found Apple Development certificate"
  fi
  if [[ "$ARTIFACT" == app ]]; then
    echo "Self-signing $BUNDLE_DIR/$WARP_APP_NAME.app with ${SIGNING_CERT}..."
    codesign --force --deep --options runtime --sign "$SIGNING_CERT" "$BUNDLE_DIR/$WARP_APP_NAME.app" --entitlements script/Debug-Entitlements.plist
  elif [[ "$ARTIFACT" == cli ]]; then
    echo "Self-signing $OUT_DIR/$WARP_BIN with ${SIGNING_CERT}..."
    codesign --force --options runtime --sign "$SIGNING_CERT" "$OUT_DIR/$WARP_BIN" --entitlements script/Debug-Entitlements.plist
  fi
elif [[ $CODESIGN = true ]]; then
  if [[ "$ARTIFACT" == app ]]; then
    echo "Codesigning $BUNDLE_DIR/$WARP_APP_NAME.app..."
    # Use --deep so we sign bundled frameworks as well
    codesign --deep -f -o runtime --timestamp -s "$APPLE_TEAM_ID" "$BUNDLE_DIR/$WARP_APP_NAME.app" --entitlements script/Entitlements.plist
  elif [[ "$ARTIFACT" == cli ]]; then
    echo "Codesigning $OUT_DIR/$WARP_BIN..."
    codesign -f -o runtime --timestamp -s "$APPLE_TEAM_ID" "$OUT_DIR/$WARP_BIN" --entitlements script/Entitlements.plist

    # Create the .zip for notarization in a separate location - otherwise, Apple's codesigning
    # tools decide that it's a sealed resource that belongs to the binary and needs to also be signed.
    NOTARIZATION_ARTIFACT="$BUNDLE_DIR/${WARP_BIN}_notarize.zip"
    if [[ -e "$NOTARIZATION_ARTIFACT" ]]; then
      echo "Removing old notarization artifact..."
      rm "$NOTARIZATION_ARTIFACT"
    fi

    # Create a .zip archive to notarize.
    ditto -c -k "$OUT_DIR/$WARP_BIN" "$NOTARIZATION_ARTIFACT"
    
    # It's not possible to staple notarization tickets to standalone binaries:
    # https://developer.apple.com/documentation/security/customizing-the-notarization-workflow?language=objc#Staple-the-ticket-to-your-distribution
    STAPLE_TICKET=false
  fi
fi

########################
## Step 4: Create DMG ##
########################

if [[ "$ARTIFACT" = app ]]; then
  function create_warp_dmg() {
    echo "Creating $DMG_DIR/$DMG_NAME..."
    rm "$DMG_DIR/$DMG_NAME" || true
    local source_folder="$1"

    local args=(
      --volname Warp
      # For --no-internet-enable, see https://github.com/create-dmg/create-dmg/issues/179
      --no-internet-enable
      --background app/assets/resources/mac/warp_install_image.png
      --icon-size 128
      --window-size 700 500
      --format UDZO
      --app-drop-link 550 250
      --icon "$WARP_APP_NAME.app" 150 250
      # macOS 26.4 Beta has issues with mounting HFS+ DMGs, so we're using APFS instead.
      # APFS has been supported as a DMG filesystem since macOS 10.13, and we target 10.14
      # as our minimum version.
      #
      # See: https://developer.apple.com/documentation/macos-release-notes/macos-26_4-release-notes#External-Media
      --filesystem APFS
    )

    # Skip running an AppleScript to format the DMG contents if running in Namespace - this consistently times out.
    # See https://github.com/create-dmg/create-dmg/issues/72
    if [[ "${RUNNER_NAME:-}" == nsc-* ]]; then
      args+=(--skip-jenkins)
    fi

    args+=("$DMG_DIR/$DMG_NAME" "$source_folder")

    create-dmg "${args[@]}"
  }

  # If we're codesigning, stage the signed app and use that to create the DMG.
  # Otherwise, create a DMG from the bundle dir directly.
  if [[ $CODESIGN = true ]]; then
    if test -d "$DMG_DIR"; then
      echo "Clearing old dmg directory $DMG_DIR"
      rm -r "$DMG_DIR"
    fi
    echo "Creating $DMG_DIR"
    mkdir -p "$DMG_DIR"
    cp -R "$BUNDLE_DIR/$WARP_APP_NAME.app" "$DMG_DIR"

    create_warp_dmg "$DMG_DIR"

    echo "Codesigning $DMG_DIR/$DMG_NAME..."
    codesign -s "$APPLE_TEAM_ID" --timestamp "$DMG_DIR/$DMG_NAME"

    NOTARIZATION_ARTIFACT="$DMG_DIR/$DMG_NAME"
    STAPLE_TICKET=true
  else
    echo "Creating $DMG_DIR"
    mkdir -p "$DMG_DIR"

    echo "Cleaning up any existing DMG files before creating new ones..."
    cleanup_dmg_files "$DMG_DIR"

    create_warp_dmg "$BUNDLE_DIR"
  fi
fi

##############################
## Step 5: Notarize the app ##
##############################

if [[ $CODESIGN = true ]]; then
  echo "Uploading $NOTARIZATION_ARTIFACT to Apple for notarization..."
  xcrun notarytool submit "$NOTARIZATION_ARTIFACT" --apple-id "$WARP_NOTARIZATION_APPLE_ID" --password "$WARP_NOTARIZATION_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait

  if [[ $STAPLE_TICKET = true ]]; then
    echo "Attempting to staple the notarization ticket to $NOTARIZATION_ARTIFACT..."
    xcrun stapler staple "$NOTARIZATION_ARTIFACT"
  fi

  if [[ $? != 0 ]]; then
    echo "Notarization failed; see above output for details."
    exit 1
  fi

  # Verify the notarization results (this is format-dependent)
  echo "Verifying notarization ticket..."
  if [[ "$ARTIFACT" = app ]]; then
    xcrun stapler validate "$DMG_DIR/$DMG_NAME"
  elif [[ "$ARTIFACT" = cli ]]; then
    spctl -a -t open --context context:primary-signature -vv "$OUT_DIR/$WARP_BIN"
  fi
fi


#######################################
## Step 6: Copy and output artifacts ##
#######################################

if [[ "$ARTIFACT" = app ]]; then
  echo "Copying dmg and app to $OUT_DIR"
  cp -R "$BUNDLE_DIR/$WARP_APP_NAME.app" "$OUT_DIR"
  cp "$DMG_DIR/$DMG_NAME" "$OUT_DIR/$FINAL_DMG_NAME"
fi

# If this is being run within a GitHub action, set an output variable with the
# location of the artifacts so they can be referenced by subsequent actions.
if [ "${GITHUB_ACTIONS}" == "true" ]; then
  echo "::echo::on"

  # For individual architecture builds, output binary information
  if [[ -n "$TARGET_ARCH" ]]; then
    echo "binary_path=$BINARY_PATH" >> "$GITHUB_OUTPUT"
    echo "target_arch=$TARGET_ARCH" >> "$GITHUB_OUTPUT"
    echo "rust_target=$DEFAULT_TARGET" >> "$GITHUB_OUTPUT"
    
    # If the dSYM is available, output both its path and its realpath.
    # The realpath is needed because the dSYM is a symlink, and when preserving
    # the build artifacts, we want to preserve both the symlink and its target.
    DSYM_PATH="${BINARY_PATH}.dSYM"
    if [[ -d "$DSYM_PATH" ]]; then
      echo "dsym_path=$DSYM_PATH" >> "$GITHUB_OUTPUT"

      # If the dSYM is a symlink, output its real path.
      if [[ -L "$DSYM_PATH" ]]; then
        echo "dsym_realpath=$(relpath $(realpath $DSYM_PATH))" >> "$GITHUB_OUTPUT"
      fi
    fi
  fi

  echo "dock_tile_plugin_dir=$DOCK_TILE_PLUGIN_DIR" >> "$GITHUB_OUTPUT"
  echo "frameworks_dir=app/frameworks/${FRAMEWORK_OVERRIDE:-default}" >> "$GITHUB_OUTPUT"
  
  # For full bundles, output DMG information
  if [[ -f "$OUT_DIR/$FINAL_DMG_NAME" ]]; then
    echo "dmg_name=$FINAL_DMG_NAME" >> "$GITHUB_OUTPUT"
    echo "dmg_path=$DMG_PATH" >> "$GITHUB_OUTPUT"
    echo "dsym_folder_path=$OUT_DIR" >> "$GITHUB_OUTPUT"
  fi

  if [[ -n "$BUNDLED_RESOURCES_DIR" ]]; then
    echo "bundled_resources_dir=$BUNDLED_RESOURCES_DIR" >> "$GITHUB_OUTPUT"
  fi
  
  echo "::echo::off"
fi

if [[ $OPEN_AFTER_BUNDLE = true ]]; then
    open "$OUT_DIR"
fi
