diff --git a/.github/actions/bundle_arch_package/action.yml b/.github/actions/bundle_arch_package/action.yml index 2b3db2ee19d..87e079846e0 100644 --- a/.github/actions/bundle_arch_package/action.yml +++ b/.github/actions/bundle_arch_package/action.yml @@ -17,6 +17,9 @@ inputs: description: The artifact type to build (app or cli) required: false default: app + settings-schema-source: + description: The path to the settings schema to package. + required: true runs: using: composite @@ -27,16 +30,20 @@ runs: - name: Run in container shell: bash + env: + SETTINGS_SCHEMA_SOURCE: ${{ inputs.settings-schema-source }} run: | + settings_schema_source="$(realpath "$SETTINGS_SCHEMA_SOURCE")" docker run --rm \ -v "${{ github.workspace }}:/github/workspace" \ -v "${{ github.workspace }}/target:/github/workspace/target" \ + -v "$settings_schema_source:/tmp/settings_schema.json:ro" \ -w /github/workspace \ -v "${CARGO_HOME:-$HOME/.cargo}/git:/home/build/.cargo/git" \ -v "${CARGO_HOME:-$HOME/.cargo}/registry:/home/build/.cargo/registry" \ -e GIT_RELEASE_TAG="$GIT_RELEASE_TAG" \ -e GITHUB_ACTIONS="$GITHUB_ACTIONS" \ -e GITHUB_OUTPUT="/dev/null" \ - ${SETTINGS_SCHEMA_CACHE:+-e SETTINGS_SCHEMA_CACHE=/github/workspace/.settings_schema_cache.json} \ + -e SETTINGS_SCHEMA_SOURCE="/tmp/settings_schema.json" \ arch-bundle-builder \ ${{ inputs.channel }} ${{ inputs.release-tag }} ${{ inputs.arch }} ${{ inputs.artifact }} diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 97893d8d611..d65fcdbe46f 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -81,7 +81,6 @@ jobs: with: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} - - name: Set publish flag based on the workflow trigger id: set_publish run: | @@ -167,12 +166,6 @@ jobs: run: rustup target add ${{ matrix.arch }}-apple-darwin shell: bash - # Install go toolchain, as it may be needed for some build steps. - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 - with: - go-version: stable - - name: Get channel configuration id: get-config uses: ./.github/actions/get_channel_config/ @@ -200,6 +193,13 @@ jobs: WARP_NOTARIZATION_APPLE_ID: ${{ secrets.WARP_NOTARIZATION_APPLE_ID }} WARP_NOTARIZATION_PASSWORD: ${{ secrets.WARP_NOTARIZATION_PASSWORD }} + - name: Archive ${{ matrix.arch }} macOS settings schema + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: settings-schema-macos-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: ${{ steps.bundle_app.outputs.settings_schema_path }} + - name: Add DMG to GitHub release assets if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 @@ -283,12 +283,6 @@ jobs: install_release_deps: true ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} - # Install go toolchain, as it may be needed for some build steps. - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 - with: - go-version: stable - - name: Get channel configuration id: get-config uses: ./.github/actions/get_channel_config/ @@ -314,6 +308,25 @@ jobs: run: tar xvfz build-artifacts.tar.gz shell: bash + - name: Download canonical aarch64 macOS settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-macos-aarch64-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-macos + + - name: Download x86_64 macOS settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-macos-x86_64-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-macos-x86_64 + + # These two schemas are expected to be byte-identical: no setting is currently scoped to + # a specific macOS architecture. A mismatch here means that assumption no longer holds, + # and this step needs to become architecture-aware rather than simply failing the release. + - name: Verify macOS settings schemas match + run: cmp .release-artifacts/settings-schema-macos/settings_schema.json .release-artifacts/settings-schema-macos-x86_64/settings_schema.json + shell: bash + - name: Install cargo-bundle run: script/install_cargo_bundle @@ -333,6 +346,7 @@ jobs: WARP_CODESIGN_KEYCHAIN_PASSWORD: ${{ secrets.WARP_CODESIGN_KEYCHAIN_PASSWORD }} WARP_NOTARIZATION_APPLE_ID: ${{ secrets.WARP_NOTARIZATION_APPLE_ID }} WARP_NOTARIZATION_PASSWORD: ${{ secrets.WARP_NOTARIZATION_PASSWORD }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-macos/settings_schema.json - name: Set up Sentry CLI if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -407,8 +421,8 @@ jobs: name: release-macos-universal-${{ steps.get-config.outputs.channel }} path: ${{ steps.bundle_app.outputs.dmg_path }} - release_macos_cli: - name: Build Release (macOS CLI ${{ matrix.arch }}) + build_macos_cli_binaries: + name: Build Release Binary (macOS CLI ${{ matrix.arch }}) runs-on: macos-26-xlarge needs: prepare_release if: ${{ inputs.build_macos != false }} @@ -434,6 +448,85 @@ jobs: run: rustup target add ${{ matrix.arch }}-apple-darwin shell: bash + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ + with: + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + # Only the binary matters here; this job's own bundled resources are + # discarded, so skip signing/notarization and the (otherwise mandatory) + # schema, both of which belong to the packaging step below. + - name: Build ${{ matrix.arch }} binary + id: build_cli + run: | + script/bundle --channel $CHANNEL --arch ${{ matrix.arch }} --artifact cli --nosign + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + NO_LICENSES: "1" + SKIP_SETTINGS_SCHEMA: "1" + + # binary_path is already copied out of the per-target-triple Cargo output + # directory; --skip-build (used by release_macos_cli below) re-derives + # that copy from the triple directory, so preserve it here instead. The + # dSYM Cargo produces there is itself a symlink into target/.../deps, so + # (as with the app archive above) also include its resolved target; + # otherwise the link dangles once extracted into a fresh job. + - name: Create tar archive with build artifacts + run: | + BINARY_PATH="${{ steps.build_cli.outputs.binary_path }}" + PROFILE_DIR="${BINARY_PATH#target/}" + PROFILE_DIR="${PROFILE_DIR%/bundle/osx/*}" + RAW_BINARY_PATH="target/${{ steps.build_cli.outputs.rust_target }}/$PROFILE_DIR/$(basename "$BINARY_PATH")" + RAW_DSYM_PATH="$RAW_BINARY_PATH.dSYM" + TAR_PATHS=("$RAW_BINARY_PATH") + if [[ -e "$RAW_DSYM_PATH" ]]; then + TAR_PATHS+=("$RAW_DSYM_PATH") + if [[ -L "$RAW_DSYM_PATH" ]]; then + REAL_DSYM_PATH="$(python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1]))' "$(realpath "$RAW_DSYM_PATH")")" + TAR_PATHS+=("$REAL_DSYM_PATH") + fi + fi + tar czfv build-artifacts.tar.gz "${TAR_PATHS[@]}" + shell: bash + + - name: Archive ${{ matrix.arch }} CLI build artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: macos-cli-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + # We compress the tar archive, so no need to re-compress. + compression-level: 0 + path: build-artifacts.tar.gz + + # Packages the CLI binary build_macos_cli_binaries built separately (in + # parallel), then adds the canonical schema and signs/notarizes. + release_macos_cli: + name: Build Release (macOS CLI ${{ matrix.arch }}) + runs-on: macos-26-xlarge + needs: [prepare_release, release_macos_single_arch, build_macos_cli_binaries] + if: ${{ inputs.build_macos != false }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - arch: aarch64 + - arch: x86_64 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: macos + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} + # Install go toolchain, as it may be needed for some build steps. - name: Setup Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 @@ -447,10 +540,25 @@ jobs: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} - - name: Build ${{ matrix.arch }} binary + - name: Download ${{ matrix.arch }} CLI build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: macos-cli-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + + - name: Extract CLI build artifacts + run: tar xvfz build-artifacts.tar.gz + shell: bash + + - name: Download canonical macOS settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-macos-aarch64-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-macos + + - name: Bundle ${{ matrix.arch }} binary id: bundle_cli run: | - script/bundle --read-passwords-from-env --channel $CHANNEL --arch ${{ matrix.arch }} --artifact cli + script/bundle --skip-build --read-passwords-from-env --channel $CHANNEL --arch ${{ matrix.arch }} --artifact cli shell: bash env: CHANNEL: ${{ steps.get-config.outputs.channel }} @@ -460,6 +568,7 @@ jobs: WARP_CODESIGN_KEYCHAIN_PASSWORD: ${{ secrets.WARP_CODESIGN_KEYCHAIN_PASSWORD }} WARP_NOTARIZATION_APPLE_ID: ${{ secrets.WARP_NOTARIZATION_APPLE_ID }} WARP_NOTARIZATION_PASSWORD: ${{ secrets.WARP_NOTARIZATION_PASSWORD }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-macos/settings_schema.json - name: Package CLI binary run: | @@ -511,8 +620,8 @@ jobs: env: DEBUG_FILE_OR_FOLDER_PATH: ${{ steps.bundle_cli.outputs.dsym_path }} - release_macos_tui: - name: Build Release (macOS TUI ${{ matrix.arch }}) + build_macos_tui_binaries: + name: Build Release Binary (macOS TUI ${{ matrix.arch }}) runs-on: macos-26-xlarge needs: prepare_release if: ${{ inputs.build_macos != false }} @@ -538,11 +647,84 @@ jobs: run: rustup target add ${{ matrix.arch }}-apple-darwin shell: bash - # Install go toolchain, as it may be needed for some build steps. - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ with: - go-version: stable + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + # Only the binary matters here; this job's own bundled resources are + # discarded, so skip signing/notarization and the (otherwise mandatory) + # schema, both of which belong to the packaging step below. + - name: Build ${{ matrix.arch }} binary + id: build_tui + run: | + script/bundle --channel $CHANNEL --arch ${{ matrix.arch }} --artifact tui --nosign + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + NO_LICENSES: "1" + SKIP_SETTINGS_SCHEMA: "1" + + # binary_path is already copied out of the per-target-triple Cargo output + # directory; --skip-build (used by release_macos_tui below) re-derives + # that copy from the triple directory, so preserve it here instead. The + # dSYM Cargo produces there is itself a symlink into target/.../deps, so + # (as with the app archive above) also include its resolved target; + # otherwise the link dangles once extracted into a fresh job. + - name: Create tar archive with build artifacts + run: | + BINARY_PATH="${{ steps.build_tui.outputs.binary_path }}" + PROFILE_DIR="${BINARY_PATH#target/}" + PROFILE_DIR="${PROFILE_DIR%/bundle/osx/*}" + RAW_BINARY_PATH="target/${{ steps.build_tui.outputs.rust_target }}/$PROFILE_DIR/$(basename "$BINARY_PATH")" + RAW_DSYM_PATH="$RAW_BINARY_PATH.dSYM" + TAR_PATHS=("$RAW_BINARY_PATH") + if [[ -e "$RAW_DSYM_PATH" ]]; then + TAR_PATHS+=("$RAW_DSYM_PATH") + if [[ -L "$RAW_DSYM_PATH" ]]; then + REAL_DSYM_PATH="$(python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1]))' "$(realpath "$RAW_DSYM_PATH")")" + TAR_PATHS+=("$REAL_DSYM_PATH") + fi + fi + tar czfv build-artifacts.tar.gz "${TAR_PATHS[@]}" + shell: bash + + - name: Archive ${{ matrix.arch }} TUI build artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: macos-tui-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + # We compress the tar archive, so no need to re-compress. + compression-level: 0 + path: build-artifacts.tar.gz + + # Packages the TUI binary build_macos_tui_binaries built separately (in + # parallel), then adds the canonical schema and signs/notarizes. + release_macos_tui: + name: Build Release (macOS TUI ${{ matrix.arch }}) + runs-on: macos-26-xlarge + needs: [prepare_release, release_macos_single_arch, build_macos_tui_binaries] + if: ${{ inputs.build_macos != false }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - arch: aarch64 + - arch: x86_64 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: macos + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} - name: Get channel configuration id: get-config @@ -551,10 +733,25 @@ jobs: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} - - name: Build ${{ matrix.arch }} binary + - name: Download ${{ matrix.arch }} TUI build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: macos-tui-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + + - name: Extract TUI build artifacts + run: tar xvfz build-artifacts.tar.gz + shell: bash + + - name: Download canonical macOS settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-macos-aarch64-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-macos + + - name: Bundle ${{ matrix.arch }} binary id: bundle_tui run: | - script/bundle --read-passwords-from-env --channel $CHANNEL --arch ${{ matrix.arch }} --artifact tui + script/bundle --skip-build --read-passwords-from-env --channel $CHANNEL --arch ${{ matrix.arch }} --artifact tui shell: bash env: CHANNEL: ${{ steps.get-config.outputs.channel }} @@ -564,6 +761,7 @@ jobs: WARP_CODESIGN_KEYCHAIN_PASSWORD: ${{ secrets.WARP_CODESIGN_KEYCHAIN_PASSWORD }} WARP_NOTARIZATION_APPLE_ID: ${{ secrets.WARP_NOTARIZATION_APPLE_ID }} WARP_NOTARIZATION_PASSWORD: ${{ secrets.WARP_NOTARIZATION_PASSWORD }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-macos/settings_schema.json - name: Set up Sentry CLI if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -626,10 +824,97 @@ jobs: name: release-macos-tui-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} path: warp-tui-${{ steps.get-config.outputs.channel }}-macos-${{ matrix.arch }}.tar.gz + build_linux_tui_binaries: + name: Build Release Binary (Linux TUI ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + needs: prepare_release + if: ${{ inputs.build_linux != false }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: namespace-profile-ubuntu-20-04 + - arch: aarch64 + runner: namespace-profile-ubuntu-20-04-arm + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: linux + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} + + # Install gcc 10, as gcc 9.5 has a bug with memcmp that is incompatible + # with aws-lc-rs (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95189). + - name: Install and configure gcc-10 + run: | + sudo apt update + sudo apt install -y gcc-10 g++-10 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 60 --slave /usr/bin/g++ g++ /usr/bin/g++-10 + gcc --version + g++ --version + + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ + with: + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + # Remove cached _bundle_ output that can be shared across different _jobs_. This won't + # remove any incremental build artifacts, only the final bundled output. + - name: Clean stale bundle output + run: | + if [[ -d target ]]; then + find target -path '*/bundle/linux' -type d -prune -exec rm -rf {} + + fi + shell: bash + + # Only the binary matters here; this job's own bundled resources are + # discarded, so skip the (otherwise mandatory) schema and third-party + # license generation, both of which belong to the packaging step below. + - name: Build TUI binary + id: build_tui + run: script/bundle --channel "$CHANNEL" --artifact tui --arch "${{ matrix.arch }}" + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + NO_LICENSES: "1" + SKIP_SETTINGS_SCHEMA: "1" + + # executable_path is the copy already staged into bundle/linux; + # --skip-build (used by release_linux_tui below) instead re-copies from + # the raw target// Cargo output, which debug_executable_path + # (that raw path plus ".debug") lets us recover without duplicating the + # channel/profile-naming logic that lives in script/linux/bundle. + - name: Create tar archive with build artifacts + run: | + DEBUG_EXECUTABLE_PATH="$(realpath --relative-to=$(pwd) "$DEBUG_EXECUTABLE_PATH")" + RAW_EXECUTABLE_PATH="${DEBUG_EXECUTABLE_PATH%.debug}" + tar czvf build-artifacts.tar.gz "$RAW_EXECUTABLE_PATH" + shell: bash + env: + DEBUG_EXECUTABLE_PATH: ${{ steps.build_tui.outputs.debug_executable_path }} + + - name: Archive ${{ matrix.arch }} TUI build artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: linux-tui-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: build-artifacts.tar.gz + + # Packages the TUI binary build_linux_tui_binaries built separately (in + # parallel), then adds the canonical schema. release_linux_tui: name: Build Release (Linux TUI ${{ matrix.arch }}) runs-on: ${{ matrix.runner }} - needs: prepare_release + needs: [prepare_release, release_linux_x86, build_linux_tui_binaries] if: ${{ inputs.build_linux != false }} timeout-minutes: 180 strategy: @@ -668,7 +953,23 @@ jobs: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} - # Namespace's User Bundled Cache persists target/ between jobs on the same profile. + - name: Download ${{ matrix.arch }} TUI build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: linux-tui-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + + - name: Extract TUI build artifacts + run: tar xzvf build-artifacts.tar.gz + shell: bash + + - name: Download canonical Linux settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-linux-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-linux + + # Remove cached _bundle_ output that can be shared across different _jobs_. This won't + # remove any incremental build artifacts, only the final bundled output. - name: Clean stale bundle output run: | if [[ -d target ]]; then @@ -678,11 +979,12 @@ jobs: - name: Bundle TUI id: bundle_tui - run: script/bundle --channel "$CHANNEL" --artifact tui --arch "${{ matrix.arch }}" + run: script/bundle --channel "$CHANNEL" --artifact tui --arch "${{ matrix.arch }}" --skip-build shell: bash env: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-linux/settings_schema.json - name: Package and validate TUI tarball run: | @@ -746,9 +1048,6 @@ jobs: # them with FUSE, which isn't available on GitHub runners (and this is # easier and less error-prone than trying to install it). APPIMAGE_EXTRACT_AND_RUN: "1" - # Cache the generated settings schema so prepare_bundled_resources only - # compiles and runs the generator once per job instead of per-package. - SETTINGS_SCHEMA_CACHE: ${{ github.workspace }}/.settings_schema_cache.json steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -835,12 +1134,20 @@ jobs: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + - name: Archive canonical Linux settings schema + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: settings-schema-linux-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: ${{ steps.bundle_app.outputs.settings_schema_path }} + - name: Bundle Arch Linux package uses: ./.github/actions/bundle_arch_package with: channel: ${{ steps.get-config.outputs.channel }} release-tag: ${{ needs.prepare_release.outputs.release_tag }} arch: x86_64 + settings-schema-source: ${{ steps.bundle_app.outputs.settings_schema_path }} - name: Sign Arch Linux packages run: | @@ -896,16 +1203,93 @@ jobs: name: release-linux-x86_64-${{ steps.get-config.outputs.channel }} path: ${{ steps.bundle_app.outputs.packages_dir }} + build_linux_cli_x86_binaries: + name: Build Release Binary (Linux CLI x86_64) + runs-on: namespace-profile-ubuntu-22-04 + needs: prepare_release + if: ${{ inputs.build_linux != false }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: linux + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} + + # Install gcc 10, as gcc 9.5 has a bug with memcmp that is incompatible + # with aws-lc-rs (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95189). + - name: Install and configure gcc-10 + run: | + sudo apt update + sudo apt install -y gcc-10 g++-10 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 60 --slave /usr/bin/g++ g++ /usr/bin/g++-10 + gcc --version + g++ --version + + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ + with: + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + # Remove cached _bundle_ output that can be shared across different _jobs_. This won't + # remove any incremental build artifacts, only the final bundled output. + - name: Clean stale bundle output + run: | + if [[ -d target ]]; then + find target -path '*/bundle/linux' -type d -prune -exec rm -rf {} + + fi + shell: bash + + # Only the binary matters here; this job's own bundled resources are + # discarded, so skip the (otherwise mandatory) schema and third-party + # license generation, both of which belong to the packaging step below. + # --packages none avoids building the (app-only) AppImage that + # --packages otherwise defaults to. + - name: Build CLI binary + id: build_cli + run: script/bundle --channel "$CHANNEL" --artifact cli --packages none + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + NO_LICENSES: "1" + SKIP_SETTINGS_SCHEMA: "1" + + # Unlike the TUI job above, the cli artifact doesn't stage a copy into + # bundle/linux, so executable_path and debug_executable_path are + # already the raw target//[.debug] Cargo output paths; + # no derivation is needed to satisfy --skip-build below. + - name: Create tar archive with build artifacts + run: | + EXECUTABLE_PATH="$(realpath --relative-to=$(pwd) "$EXECUTABLE_PATH")" + DEBUG_EXECUTABLE_PATH="$(realpath --relative-to=$(pwd) "$DEBUG_EXECUTABLE_PATH")" + tar czvf build-artifacts.tar.gz "$EXECUTABLE_PATH" "$DEBUG_EXECUTABLE_PATH" + shell: bash + env: + EXECUTABLE_PATH: ${{ steps.build_cli.outputs.executable_path }} + DEBUG_EXECUTABLE_PATH: ${{ steps.build_cli.outputs.debug_executable_path }} + + - name: Archive Linux CLI build artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: linux-cli-x86_64-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: build-artifacts.tar.gz + + # Packages the CLI binary build_linux_cli_x86_binaries built separately (in + # parallel), then adds the canonical schema. release_linux_cli_x86: name: Build Release (Linux CLI x86_64) runs-on: namespace-profile-ubuntu-22-04 - needs: prepare_release + needs: [prepare_release, release_linux_x86, build_linux_cli_x86_binaries] if: ${{ inputs.build_linux != false }} timeout-minutes: 90 - env: - # Cache the generated settings schema so prepare_bundled_resources only - # compiles and runs the generator once per job instead of per-package. - SETTINGS_SCHEMA_CACHE: ${{ github.workspace }}/.settings_schema_cache.json steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -978,15 +1362,31 @@ jobs: fi shell: bash + - name: Download Linux CLI build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: linux-cli-x86_64-${{ steps.get-config.outputs.channel }} + + - name: Extract Linux CLI build artifacts + run: tar xzvf build-artifacts.tar.gz + shell: bash + + - name: Download canonical Linux settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-linux-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-linux + - name: Bundle CLI id: bundle_cli run: | - # Build CLI only - script/bundle --channel $CHANNEL --artifact cli --packages deb,rpm + # Package the already-built CLI binary + script/bundle --channel $CHANNEL --skip-build --artifact cli --packages deb,rpm shell: bash env: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-linux/settings_schema.json - name: Package CLI tar.gz run: | @@ -1000,6 +1400,7 @@ jobs: release-tag: ${{ needs.prepare_release.outputs.release_tag }} arch: x86_64 artifact: cli + settings-schema-source: .release-artifacts/settings-schema-linux/settings_schema.json - name: Sign Arch Linux packages run: | @@ -1085,9 +1486,6 @@ jobs: # them with FUSE, which isn't available on GitHub runners (and this is # easier and less error-prone than trying to install it). APPIMAGE_EXTRACT_AND_RUN: "1" - # Cache the generated settings schema so prepare_bundled_resources only - # compiles and runs the generator once per job instead of per-package. - SETTINGS_SCHEMA_CACHE: ${{ github.workspace }}/.settings_schema_cache.json steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -1157,12 +1555,14 @@ jobs: EXECUTABLE_PATH="$(realpath --relative-to=$(pwd) $EXECUTABLE_PATH)" DEBUG_EXECUTABLE_PATH="$(realpath --relative-to=$(pwd) $DEBUG_EXECUTABLE_PATH)" PACKAGES_DIR="$(realpath --relative-to=$(pwd) $PACKAGES_DIR)" - tar czvf binaries.tar.gz $EXECUTABLE_PATH $DEBUG_EXECUTABLE_PATH $PACKAGES_DIR + cp "$SETTINGS_SCHEMA_PATH" settings_schema.json + tar czvf binaries.tar.gz $EXECUTABLE_PATH $DEBUG_EXECUTABLE_PATH $PACKAGES_DIR settings_schema.json shell: bash env: EXECUTABLE_PATH: ${{ steps.build_app.outputs.executable_path }} DEBUG_EXECUTABLE_PATH: ${{ steps.build_app.outputs.debug_executable_path }} PACKAGES_DIR: ${{steps.build_app.outputs.packages_dir}} + SETTINGS_SCHEMA_PATH: ${{ steps.build_app.outputs.settings_schema_path }} - name: Archive build files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1215,6 +1615,10 @@ jobs: run: rm -rf target/*/bundle/linux shell: bash + # This cross-compiles aarch64 on an x86_64 host, so the just-built binary + # cannot be executed here to generate a schema. release_linux_arm, which + # packages this binary, supplies the canonical SETTINGS_SCHEMA_SOURCE + # itself, so this job never needs one. - name: Build CLI id: build_cli run: | @@ -1224,6 +1628,7 @@ jobs: env: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SKIP_SETTINGS_SCHEMA: "1" - name: Package binaries id: package_binaries @@ -1251,13 +1656,9 @@ jobs: release_linux_arm: name: Bundle Release (Linux ARM) runs-on: namespace-profile-ubuntu-20-04 - needs: [ prepare_release, build_linux_arm_binaries, build_linux_cli_arm_binaries ] + needs: [ prepare_release, release_linux_x86, build_linux_arm_binaries, build_linux_cli_arm_binaries ] if: ${{ inputs.build_linux != false }} timeout-minutes: 60 - env: - # Cache the generated settings schema so prepare_bundled_resources only - # compiles and runs the generator once per job instead of per-package. - SETTINGS_SCHEMA_CACHE: ${{ github.workspace }}/.settings_schema_cache.json steps: - name: Checkout sources uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -1271,6 +1672,12 @@ jobs: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} + - name: Download canonical Linux settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-linux-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-linux + - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} @@ -1293,12 +1700,8 @@ jobs: gcc --version g++ --version - # Install Linux build dependencies (protoc, libssl-dev, etc.). Although this job - # runs with --skip-build, script/prepare_bundled_resources still invokes - # `cargo run --bin generate_settings_schema` and `cargo about generate`, which - # compile workspace crates whose build scripts depend on protoc and other - # system libraries. The four sibling Linux jobs get these via prepare_environment; - # this job doesn't use prepare_environment, so install them explicitly here. + # Install Linux build dependencies required by cargo-about while preparing + # the bundled third-party license resources. - name: Install Linux build dependencies shell: bash run: ./script/linux/install_build_deps @@ -1359,6 +1762,10 @@ jobs: tar xzvf binaries.tar.gz shell: bash + - name: Verify Linux settings schemas match + run: cmp .release-artifacts/settings-schema-linux/settings_schema.json settings_schema.json + shell: bash + - name: Bundle deb/rpm app packages id: bundle_app run: | @@ -1369,6 +1776,7 @@ jobs: env: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-linux/settings_schema.json - name: Bundle Arch Linux app package uses: ./.github/actions/bundle_arch_package @@ -1376,6 +1784,7 @@ jobs: channel: ${{ steps.get-config.outputs.channel }} release-tag: ${{ needs.prepare_release.outputs.release_tag }} arch: aarch64 + settings-schema-source: .release-artifacts/settings-schema-linux/settings_schema.json - name: Upload app debugging symbols to Sentry if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -1407,6 +1816,7 @@ jobs: env: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-linux/settings_schema.json - name: Package CLI tar.gz run: | @@ -1420,6 +1830,7 @@ jobs: release-tag: ${{ needs.prepare_release.outputs.release_tag }} arch: aarch64 artifact: cli + settings-schema-source: .release-artifacts/settings-schema-linux/settings_schema.json - name: Upload CLI debugging symbols to Sentry if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -1623,10 +2034,83 @@ jobs: name: release-web-${{ steps.get-config.outputs.channel }} path: ${{ steps.bundle_app.outputs.packages_dir }} + build_windows_tui_binaries: + name: Build Release Binary (Windows TUI ${{ matrix.arch }}) + runs-on: windows-latest-large + needs: prepare_release + if: ${{ inputs.build_windows != false }} + timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + - arch: arm64 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: windows + cache_key: tui-${{ matrix.arch }} + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} + + - name: Add arm64 target + if: ${{ matrix.arch == 'arm64' }} + run: rustup target add aarch64-pc-windows-msvc + shell: bash + + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ + with: + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + - name: Build Warp Agent CLI + id: build_tui + run: script/bundle --artifact tui --channel "$CHANNEL" --skip_build_installer --arch ${{ matrix.arch }} + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + + # actions/upload-artifact stores paths relative to the least common + # ancestor of everything it's given; binary_path and pdb_file_path live + # in the same directory, so uploading them directly would collapse the + # LCA to that directory and strip the target// prefix + # that release_windows_tui's own script/bundle invocation expects to + # find them at. Tar with explicit relative paths instead, so the + # structure is guaranteed rather than an emergent property of which + # paths happen to be listed. + - name: Create tar archive with build artifacts + run: | + BINARY_PATH="$(realpath --relative-to="$(pwd)" "$(cygpath -u "$BINARY_PATH")")" + PDB_FILE_PATH="$(realpath --relative-to="$(pwd)" "$(cygpath -u "$PDB_FILE_PATH")")" + tar czfv build-artifacts.tar.gz "$BINARY_PATH" "$PDB_FILE_PATH" + shell: bash + env: + BINARY_PATH: ${{ steps.build_tui.outputs.binary_path }} + PDB_FILE_PATH: ${{ steps.build_tui.outputs.pdb_file_path }} + + - name: Archive Windows TUI binary build + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: windows-tui-binary-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + # We compress the tar archive, so no need to re-compress. + compression-level: 0 + path: build-artifacts.tar.gz + + # Packages the TUI binary build_windows_tui_binaries built separately (in + # parallel), then adds the canonical schema and signs it. release_windows_tui: name: Build Release (Windows TUI ${{ matrix.arch }}) runs-on: windows-latest-large - needs: prepare_release + needs: [prepare_release, build_windows_binaries, build_windows_tui_binaries] if: ${{ inputs.build_windows != false }} timeout-minutes: 150 strategy: @@ -1652,10 +2136,6 @@ jobs: ref: ${{ needs.prepare_release.outputs.release_branch }} install_release_deps: true ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} - - name: Add arm64 target - if: ${{ matrix.arch == 'arm64' }} - run: rustup target add aarch64-pc-windows-msvc - shell: bash - name: Get channel configuration id: get-config @@ -1664,14 +2144,29 @@ jobs: config_file: ${{ env.CONFIG_FILE }} channel: ${{ inputs.channel }} + - name: Download Windows TUI binary build + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-tui-binary-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + + - name: Extract Windows TUI binary build + run: tar xzvf build-artifacts.tar.gz + shell: bash + + - name: Download canonical Windows settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-windows-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-windows + - name: Test Windows TUI installer if: ${{ matrix.arch == 'x64' }} run: ./script/windows/test_tui_installer.ps1 shell: pwsh - - name: Build Warp Agent CLI + - name: Locate TUI binary build outputs id: build_tui - run: script/bundle --artifact tui --channel "$CHANNEL" --skip_build_installer --arch ${{ matrix.arch }} + run: script/bundle --artifact tui --channel "$CHANNEL" --skip_build_binary --skip_build_installer --arch ${{ matrix.arch }} shell: bash env: CHANNEL: ${{ steps.get-config.outputs.channel }} @@ -1710,6 +2205,8 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} SIGN_TOOL_CMD: ${{ steps.setup_signing.outputs.sign_tool_cmd }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-windows/settings_schema.json + - name: Add TUI installer to GitHub release assets if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 @@ -1754,8 +2251,8 @@ jobs: ${{ steps.bundle_tui.outputs.pdb_file_path }} if-no-files-found: error - release_windows: - name: Build Release (Windows ${{ matrix.arch }}) + build_windows_binaries: + name: Build Release Binary (Windows ${{ matrix.arch }}) runs-on: ${{ matrix.runner }} needs: prepare_release if: ${{ inputs.build_windows != false }} @@ -1767,10 +2264,6 @@ jobs: runner: windows-latest-large - arch: arm64 runner: windows-latest-large - env: - TRUSTED_SIGNING_ENDPOINT: https://eus.codesigning.azure.net/ - TRUSTED_SIGNING_ACCOUNT: warpdotdev - TRUSTED_SIGNING_CERT_PROFILE: warpterminal steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -1803,6 +2296,92 @@ jobs: CHANNEL: ${{ steps.get-config.outputs.channel }} GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + - name: Generate canonical Windows settings schema + if: ${{ matrix.arch == 'x64' }} + shell: pwsh + run: | + $Process = Start-Process ` + -FilePath "${{ steps.build_binary.outputs.binary_path }}" ` + -ArgumentList @('dump-settings-schema', "${{ runner.temp }}\settings_schema.json") ` + -NoNewWindow ` + -PassThru ` + -Wait + if ($Process.ExitCode -ne 0) { throw "Failed to generate the Windows settings schema: exit code $($Process.ExitCode)." } + + - name: Archive canonical Windows settings schema + if: ${{ matrix.arch == 'x64' }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: settings-schema-windows-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: ${{ runner.temp }}\settings_schema.json + + - name: Archive Windows binary build + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: windows-binary-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + retention-days: 1 + path: | + ${{ steps.build_binary.outputs.binary_path }} + ${{ steps.build_binary.outputs.pdb_file_path }} + app/assets/windows/${{ matrix.arch }} + + release_windows: + name: Bundle Release (Windows ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + needs: [prepare_release, build_windows_binaries] + if: ${{ inputs.build_windows != false }} + timeout-minutes: 150 + strategy: + matrix: + include: + - arch: x64 + runner: windows-latest-large + - arch: arm64 + runner: windows-latest-large + env: + TRUSTED_SIGNING_ENDPOINT: https://eus.codesigning.azure.net/ + TRUSTED_SIGNING_ACCOUNT: warpdotdev + TRUSTED_SIGNING_CERT_PROFILE: warpterminal + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: ./.github/actions/prepare_environment + with: + target_os: windows + cache_key: ${{ matrix.arch }} + is_self_hosted: false + ref: ${{ needs.prepare_release.outputs.release_branch }} + install_release_deps: true + ssh_key: ${{ secrets.WARP_CHANNEL_CONFIG_ACCESS_SSH_KEY }} + + - name: Get channel configuration + id: get-config + uses: ./.github/actions/get_channel_config/ + with: + config_file: ${{ env.CONFIG_FILE }} + channel: ${{ inputs.channel }} + + - name: Download Windows binary build + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-binary-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} + path: . + + - name: Download canonical Windows settings schema + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: settings-schema-windows-${{ steps.get-config.outputs.channel }} + path: .release-artifacts/settings-schema-windows + + - name: Locate binary build outputs + id: binary_paths + run: script/bundle -Channel $CHANNEL -skip_build_binary -skip_build_installer --arch ${{ matrix.arch }} + shell: bash + env: + CHANNEL: ${{ steps.get-config.outputs.channel }} + GIT_RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + - name: Sign the executables with Azure Trusted Signing uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0 with: @@ -1813,7 +2392,7 @@ jobs: trusted-signing-account-name: ${{ env.TRUSTED_SIGNING_ACCOUNT }} certificate-profile-name: ${{ env.TRUSTED_SIGNING_CERT_PROFILE }} files: | - ${{ steps.build_binary.outputs.binary_path }} + ${{ steps.binary_paths.outputs.binary_path }} ${{ github.workspace }}\app\assets\bundled\bootstrap\pwsh.ps1 files-folder: ${{ github.workspace }}\app\assets\windows\${{ matrix.arch }} files-folder-filter: dll,exe @@ -1844,6 +2423,7 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} SIGN_TOOL_CMD: ${{ steps.setup_signing.outputs.sign_tool_cmd }} + SETTINGS_SCHEMA_SOURCE: ${{ github.workspace }}/.release-artifacts/settings-schema-windows/settings_schema.json - name: Set up Sentry CLI if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -1859,7 +2439,7 @@ jobs: script/sentry_upload_dif.sh shell: bash env: - DEBUG_FILE_OR_FOLDER_PATH: ${{ steps.build_binary.outputs.binary_path }} + DEBUG_FILE_OR_FOLDER_PATH: ${{ steps.binary_paths.outputs.binary_path }} - name: Upload debugging symbols to Sentry if: ${{ needs.prepare_release.outputs.should_publish == 'true' }} @@ -1902,8 +2482,19 @@ jobs: name: release-windows-${{ matrix.arch }}-${{ steps.get-config.outputs.channel }} path: ${{ steps.bundle_app.outputs.installer_path }} - send_slack_notif_if_unsuccessful: - name: Send Slack notification if any release jobs were unsuccessful + # Aggregates the result of every build/release job into a single place, so + # downstream jobs don't each need their own copy of the same "which + # platforms actually matter, and did they succeed" logic. A platform whose + # build was deliberately disabled via a workflow input (build_macos, + # build_linux, build_windows, build_web, or web-on-preview) is excluded + # entirely, rather than being treated as a failure. Anything else that + # isn't a plain `success` -- a genuine failure, a cancellation, or a job + # skipped because one of ITS OWN dependencies failed (e.g. a build-only + # job) -- counts as a failure. `if: always()` is required so this job + # still runs, and still reports failures, even when one of the jobs it + # depends on did not succeed. + collect_results: + name: Collect release job results runs-on: ubuntu-latest needs: - prepare_release @@ -1918,55 +2509,95 @@ jobs: - release_web - release_windows_tui - release_windows - if: ${{ always() && needs.prepare_release.outputs.should_publish == 'true' }} + if: always() + outputs: + failed: ${{ steps.collect.outputs.failed }} steps: - - name: Figure out which releases failed, if any - id: failed_releases + # This job intentionally fails (after recording `failed` as an output) + # when any non-disabled platform did not succeed. That failure is the + # publish gate: generate_changelogs needs this job with default + # (success-required) semantics, so it is skipped whenever this step + # fails, exactly like any other failed dependency. Jobs that only need + # to observe the failure list regardless of outcome (e.g. the Slack + # notification below) declare `if: always()` on themselves instead. + - name: Determine which release jobs genuinely failed + id: collect shell: bash + env: + BUILD_MACOS: ${{ inputs.build_macos }} + BUILD_LINUX: ${{ inputs.build_linux }} + BUILD_WINDOWS: ${{ inputs.build_windows }} + BUILD_WEB: ${{ inputs.build_web }} + CHANNEL: ${{ inputs.channel }} + MACOS_SINGLE_ARCH_RESULT: ${{ needs.release_macos_single_arch.result }} + MACOS_UNIVERSAL_RESULT: ${{ needs.release_macos_universal.result }} + MACOS_CLI_RESULT: ${{ needs.release_macos_cli.result }} + MACOS_TUI_RESULT: ${{ needs.release_macos_tui.result }} + LINUX_TUI_RESULT: ${{ needs.release_linux_tui.result }} + LINUX_X86_RESULT: ${{ needs.release_linux_x86.result }} + LINUX_ARM_RESULT: ${{ needs.release_linux_arm.result }} + LINUX_CLI_X86_RESULT: ${{ needs.release_linux_cli_x86.result }} + WEB_RESULT: ${{ needs.release_web.result }} + WINDOWS_TUI_RESULT: ${{ needs.release_windows_tui.result }} + WINDOWS_RESULT: ${{ needs.release_windows.result }} run: | FAILED=() - if [[ ${{ needs.release_macos_single_arch.result }} != 'success' ]]; then - FAILED+=("MacOS Single Arch") - fi - if [[ ${{ needs.release_macos_universal.result }} != 'success' ]]; then - FAILED+=("MacOS Universal") - fi - if [[ ${{ needs.release_macos_cli.result }} != 'success' ]]; then - FAILED+=('MacOS CLI') - fi - if [[ ${{ needs.release_macos_tui.result }} != 'success' ]]; then - FAILED+=('MacOS TUI') - fi - if [[ ${{ needs.release_linux_tui.result }} != 'success' ]]; then - FAILED+=('Linux TUI') - fi - if [[ ${{ needs.release_linux_x86.result }} != 'success' ]]; then - FAILED+=("Linux x86_64") - fi - if [[ ${{ needs.release_linux_arm.result }} != 'success' ]]; then - FAILED+=("Linux ARM64") - fi - if [[ ${{ needs.release_linux_cli_x86.result }} != 'success' ]]; then - FAILED+=("Linux CLI x86_64") - fi - if [[ ${{ needs.release_web.result }} != 'success' && ${{ inputs.channel }} != 'preview' ]]; then - FAILED+=("WASM") - fi - if [[ ${{ needs.release_windows.result }} != 'success' ]]; then - FAILED+=("Windows") - fi - if [[ ${{ needs.release_windows_tui.result }} != 'success' ]]; then - FAILED+=("Windows TUI") - fi + + # A disabled platform is excluded outright, regardless of its + # result (which will be "skipped" anyway, since that's what a + # false job-level `if:` produces). Otherwise, anything other than + # "success" -- including "skipped", which for a non-disabled + # platform only happens because one of its own dependencies + # failed -- is a genuine failure. + check() { + local name="$1" result="$2" disabled="$3" + if [[ "$disabled" == "true" ]]; then + return + fi + if [[ "$result" != "success" ]]; then + FAILED+=("$name") + fi + } + + MACOS_DISABLED=false + [[ "$BUILD_MACOS" == "false" ]] && MACOS_DISABLED=true + LINUX_DISABLED=false + [[ "$BUILD_LINUX" == "false" ]] && LINUX_DISABLED=true + WINDOWS_DISABLED=false + [[ "$BUILD_WINDOWS" == "false" ]] && WINDOWS_DISABLED=true + WEB_DISABLED=false + [[ "$BUILD_WEB" == "false" || "$CHANNEL" == "preview" ]] && WEB_DISABLED=true + + check "MacOS Single Arch" "$MACOS_SINGLE_ARCH_RESULT" "$MACOS_DISABLED" + check "MacOS Universal" "$MACOS_UNIVERSAL_RESULT" "$MACOS_DISABLED" + check "MacOS CLI" "$MACOS_CLI_RESULT" "$MACOS_DISABLED" + check "MacOS TUI" "$MACOS_TUI_RESULT" "$MACOS_DISABLED" + check "Linux TUI" "$LINUX_TUI_RESULT" "$LINUX_DISABLED" + check "Linux x86_64" "$LINUX_X86_RESULT" "$LINUX_DISABLED" + check "Linux ARM64" "$LINUX_ARM_RESULT" "$LINUX_DISABLED" + check "Linux CLI x86_64" "$LINUX_CLI_X86_RESULT" "$LINUX_DISABLED" + check "WASM" "$WEB_RESULT" "$WEB_DISABLED" + check "Windows TUI" "$WINDOWS_TUI_RESULT" "$WINDOWS_DISABLED" + check "Windows" "$WINDOWS_RESULT" "$WINDOWS_DISABLED" if (( ${#FAILED[@]} )); then printf -v FORMATTED "%s, " "${FAILED[@]}" - echo "failed=$(echo "{${FORMATTED%, }}")" >> $GITHUB_OUTPUT + echo "failed=$(echo "{${FORMATTED%, }}")" >> "$GITHUB_OUTPUT" + echo "::error::The following release jobs did not succeed: ${FAILED[*]}" + exit 1 fi + send_slack_notif_if_unsuccessful: + name: Send Slack notification if any release jobs were unsuccessful + runs-on: ubuntu-latest + needs: + - prepare_release + - collect_results + if: ${{ always() && needs.prepare_release.outputs.should_publish == 'true' }} + steps: - name: Send Slack notification if unsuccessful uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 - if: ${{ steps.failed_releases.outputs.failed != '' }} + if: ${{ needs.collect_results.outputs.failed != '' }} with: # Fail the step when Slack rejects the message. errors: true @@ -1976,35 +2607,7 @@ jobs: channel: "#oncall-client" # Follows https://api.slack.com/reference/surfaces/formatting#mentioning-users. # S06N82RS7FA is the user group ID for @oncall-client-eng. - text: "Failed to create ${{ steps.failed_releases.outputs.failed }} release artifacts for ${{ inputs.channel }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}. cc: " - - gate_tui_release_artifacts: - name: Gate changelog publication on TUI release artifacts - runs-on: ubuntu-latest - needs: - - prepare_release - - release_macos_tui - - release_linux_tui - - release_windows_tui - if: ${{ always() }} - steps: - - name: Verify TUI release artifacts - shell: bash - env: - SHOULD_PUBLISH: ${{ needs.prepare_release.outputs.should_publish }} - MACOS_TUI_RESULT: ${{ needs.release_macos_tui.result }} - LINUX_TUI_RESULT: ${{ needs.release_linux_tui.result }} - WINDOWS_TUI_RESULT: ${{ needs.release_windows_tui.result }} - run: | - if [[ "$SHOULD_PUBLISH" != "true" ]]; then - exit 0 - fi - if [[ "$MACOS_TUI_RESULT" != "success" || - "$LINUX_TUI_RESULT" != "success" || - "$WINDOWS_TUI_RESULT" != "success" ]]; then - echo "::error::TUI release artifacts were not published successfully (macOS: $MACOS_TUI_RESULT, Linux: $LINUX_TUI_RESULT, Windows: $WINDOWS_TUI_RESULT)" - exit 1 - fi + text: "Failed to create ${{ needs.collect_results.outputs.failed }} release artifacts for ${{ inputs.channel }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}. cc: " generate_changelogs: name: Generate Changelogs @@ -2012,15 +2615,7 @@ jobs: runs-on: ubuntu-latest needs: - prepare_release - - release_macos_single_arch - - release_macos_universal - - release_macos_cli - - release_linux_x86 - - release_linux_arm - - release_linux_cli_x86 - - release_web - - release_windows - - gate_tui_release_artifacts + - collect_results steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 name: Checkout sources diff --git a/app/Cargo.toml b/app/Cargo.toml index 7dfd14d8806..dd270cc7eba 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -48,11 +48,6 @@ path = "src/bin/preview.rs" required-features = ["preview_channel"] test = false -[[bin]] -name = "generate_settings_schema" -path = "src/bin/generate_settings_schema.rs" -test = false - [dependencies] addr = "0.15.6" ai.workspace = true diff --git a/app/src/bin/generate_settings_schema.rs b/app/src/bin/generate_settings_schema.rs deleted file mode 100644 index 969da71722e..00000000000 --- a/app/src/bin/generate_settings_schema.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Generates a JSON Schema file describing Warp's user-facing settings. -//! -//! Usage: -//! ``` -//! cargo run --bin generate_settings_schema -- [--channel dev|preview|stable] [output_path] -//! ``` - -use std::collections::HashSet; -use std::io::Write; - -use schemars::SchemaGenerator; -use serde_json::{Map, Value}; -use settings::schema::SettingSchemaEntry; -use settings::{SettingSurfaces, SettingsMode}; -use warp_core::features::{DEBUG_FLAGS, DOGFOOD_FLAGS, FeatureFlag, PREVIEW_FLAGS, RELEASE_FLAGS}; - -/// Ensures all `inventory::submit!` registrations from the app crate's -/// dependency tree are linked into the binary. -/// -/// Binary targets only link crate code that is transitively referenced. -/// Without an explicit reference to the `warp` library, the linker will -/// not include most of the app's object files and the `inventory` -/// submissions they contain. -fn ensure_settings_linked() { - let _ = std::hint::black_box(warp::settings::RESTORE_SESSION); -} - -/// Recursively strips `minimum`, `maximum`, and `format` from integer and -/// number schemas. schemars derives these from Rust type bounds (e.g. `u8` -/// → `minimum: 0, maximum: 255, format: "uint8"`), which are misleading -/// for settings whose valid domain is narrower than the type allows. -fn strip_numeric_metadata(value: &mut Value) { - match value { - Value::Object(map) => { - let is_numeric = map - .get("type") - .and_then(Value::as_str) - .is_some_and(|t| t == "integer" || t == "number"); - - if is_numeric { - map.remove("minimum"); - map.remove("maximum"); - map.remove("format"); - } - - for val in map.values_mut() { - strip_numeric_metadata(val); - } - } - Value::Array(arr) => { - for val in arr { - strip_numeric_metadata(val); - } - } - _ => {} - } -} - -/// Removes `{"enum": [], "type": "string"}` entries from `oneOf` arrays. -/// schemars emits an empty enum bucket for externally-tagged enums when all -/// unit variants have individual descriptions (and are therefore promoted to -/// separate `oneOf` branches with `const`). The empty bucket is unreachable -/// and confuses schema consumers. -fn strip_empty_enum_entries(value: &mut Value) { - match value { - Value::Object(map) => { - if let Some(Value::Array(one_of)) = map.get_mut("oneOf") { - one_of.retain(|entry| { - !matches!(entry, Value::Object(obj) - if obj.get("enum").is_some_and(|e| e.as_array().is_some_and(|a| a.is_empty())) - ) - }); - } - - for val in map.values_mut() { - strip_empty_enum_entries(val); - } - } - Value::Array(arr) => { - for val in arr { - strip_empty_enum_entries(val); - } - } - _ => {} - } -} - -fn active_flags_for_channel(channel: &str) -> HashSet { - let mut flags = HashSet::new(); - - let flag_lists: &[&[FeatureFlag]] = match channel { - "stable" => &[RELEASE_FLAGS], - "preview" => &[RELEASE_FLAGS, PREVIEW_FLAGS], - "dev" => &[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS], - other => { - eprintln!("Unknown channel '{other}', defaulting to dev"); - &[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS] - } - }; - - for list in flag_lists { - for flag in *list { - flags.insert(*flag); - } - } - - flags -} - -/// Creates intermediate hierarchy objects so that a setting at e.g. -/// `appearance.text` is nested under `properties.appearance.properties.text.properties`. -fn ensure_hierarchy<'a>( - root_properties: &'a mut Map, - hierarchy: &str, -) -> &'a mut Map { - let segments: Vec<&str> = hierarchy.split('.').collect(); - let mut current = root_properties; - - for segment in segments { - // Ensure the segment object exists - let entry = current.entry(segment.to_string()).or_insert_with(|| { - Value::Object({ - let mut m = Map::new(); - m.insert("type".to_string(), Value::String("object".to_string())); - m.insert("properties".to_string(), Value::Object(Map::new())); - m - }) - }); - - // Navigate into its properties - current = entry - .as_object_mut() - .expect("hierarchy node should be an object") - .entry("properties") - .or_insert_with(|| Value::Object(Map::new())) - .as_object_mut() - .expect("properties should be an object"); - } - - current -} - -fn setting_surface_names(surfaces: SettingSurfaces) -> Vec { - [(SettingsMode::Gui, "gui"), (SettingsMode::Tui, "tui")] - .into_iter() - .filter(|(mode, _)| surfaces.includes(*mode)) - .map(|(_, name)| Value::String(name.to_owned())) - .collect() -} -fn main() { - ensure_settings_linked(); - - let args: Vec = std::env::args().collect(); - - let mut channel = "dev"; - let mut output_path: Option<&str> = None; - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--channel" => { - i += 1; - if i < args.len() { - channel = &args[i]; - } - } - arg if !arg.starts_with('-') => { - output_path = Some(arg); - } - other => { - eprintln!("Unknown argument: {other}"); - std::process::exit(1); - } - } - i += 1; - } - - let active_flags = active_flags_for_channel(channel); - let mut generator = SchemaGenerator::default(); - let mut root_properties = Map::new(); - let mut entry_count = 0; - - for entry in inventory::iter:: { - // Skip private settings - if entry.is_private { - continue; - } - - // Skip settings whose feature flag is not active - if let Some(flag) = entry.feature_flag - && !active_flags.contains(&flag) - { - continue; - } - - let type_schema = (entry.schema_fn)(&mut generator); - - let mut schema_value: Value = type_schema.to_value(); - schema_value - .as_object_mut() - .expect("setting schema should be an object") - .insert( - "x-warp-surfaces".to_string(), - Value::Array(setting_surface_names((entry.surfaces_fn)())), - ); - - // Compute default value — prefer file default over serde default - let default_json = (entry.file_default_value_fn)(); - - if let Ok(default_value) = serde_json::from_str::(&default_json) - && let Some(obj) = schema_value.as_object_mut() - { - obj.insert("default".to_string(), default_value); - } - - // Always overwrite description with the macro-provided one - if !entry.description.is_empty() - && let Some(obj) = schema_value.as_object_mut() - { - obj.insert( - "description".to_string(), - Value::String(entry.description.to_string()), - ); - } - - // Place the setting in the hierarchy - let target = if let Some(hierarchy) = entry.hierarchy { - ensure_hierarchy(&mut root_properties, hierarchy) - } else { - &mut root_properties - }; - - target.insert(entry.storage_key.to_string(), schema_value); - entry_count += 1; - } - - // Collect $defs from the generator - let defs_map = generator.take_definitions(true); - - // Assemble the root document - let mut root = Map::new(); - root.insert( - "$schema".to_string(), - Value::String("https://json-schema.org/draft/2020-12/schema".to_string()), - ); - root.insert( - "title".to_string(), - Value::String("Warp Settings".to_string()), - ); - root.insert( - "description".to_string(), - Value::String(format!( - "JSON Schema for Warp settings ({channel} channel, {entry_count} settings)" - )), - ); - root.insert("type".to_string(), Value::String("object".to_string())); - root.insert("properties".to_string(), Value::Object(root_properties)); - - if !defs_map.is_empty() { - root.insert("$defs".to_string(), Value::Object(defs_map)); - } - - // Strip type-derived numeric metadata (minimum, maximum, format) that - // schemars emits from Rust primitive bounds (e.g. u8 → max 255). - // These leak implementation details rather than semantic constraints. - let mut root_value = Value::Object(root); - strip_numeric_metadata(&mut root_value); - strip_empty_enum_entries(&mut root_value); - - let output = serde_json::to_string_pretty(&root_value).expect("schema should serialize"); - - if let Some(path) = output_path { - let mut file = std::fs::File::create(path) - .unwrap_or_else(|e| panic!("Failed to create output file '{path}': {e}")); - file.write_all(output.as_bytes()) - .unwrap_or_else(|e| panic!("Failed to write to '{path}': {e}")); - eprintln!("Wrote {entry_count} settings to {path}"); - } else { - println!("{output}"); - } -} - -#[cfg(test)] -#[path = "generate_settings_schema_tests.rs"] -mod tests; diff --git a/app/src/bin/generate_settings_schema_tests.rs b/app/src/bin/generate_settings_schema_tests.rs deleted file mode 100644 index 7c4b069cf32..00000000000 --- a/app/src/bin/generate_settings_schema_tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::*; - -#[test] -fn surface_annotation_matches_setting_schema_entry_metadata() { - ensure_settings_linked(); - - for entry in inventory::iter:: { - let surfaces = (entry.surfaces_fn)(); - let annotation = setting_surface_names(surfaces); - let annotation_names: HashSet<&str> = annotation.iter().filter_map(Value::as_str).collect(); - - assert_eq!( - annotation_names.contains("gui"), - surfaces.includes(SettingsMode::Gui), - "GUI surface mismatch for {}", - entry.storage_key - ); - assert_eq!( - annotation_names.contains("tui"), - surfaces.includes(SettingsMode::Tui), - "TUI surface mismatch for {}", - entry.storage_key - ); - assert_eq!( - annotation_names.len(), - usize::from(surfaces.includes(SettingsMode::Gui)) - + usize::from(surfaces.includes(SettingsMode::Tui)), - "unexpected surface annotation for {}", - entry.storage_key - ); - } -} diff --git a/app/src/lib.rs b/app/src/lib.rs index cabaaf71647..d049dcff98d 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -815,6 +815,10 @@ pub fn run() -> Result<()> { return debug_dump::run(); } #[cfg(not(target_family = "wasm"))] + warp_cli::Command::DumpSettingsSchema { output_path } => { + return settings::schema_generation::dump_settings_schema(output_path.as_deref()); + } + #[cfg(not(target_family = "wasm"))] warp_cli::Command::PrintTelemetryEvents => { return TelemetryEvent::print_telemetry_events_json(); } diff --git a/app/src/settings/mod.rs b/app/src/settings/mod.rs index 72609bbd507..7f5cb5a08e2 100644 --- a/app/src/settings/mod.rs +++ b/app/src/settings/mod.rs @@ -28,6 +28,8 @@ mod onboarding; mod pane; mod privacy; mod same_line_prompt_block; +#[cfg(not(target_family = "wasm"))] +pub(crate) mod schema_generation; mod scroll; mod select; mod shared_object_limit_banner; @@ -66,6 +68,8 @@ pub use onboarding::*; pub use pane::*; pub use privacy::*; pub use same_line_prompt_block::*; +#[cfg(not(target_family = "wasm"))] +pub use schema_generation::dump_settings_schema; pub use scroll::*; pub use select::*; pub use shared_object_limit_banner::*; diff --git a/app/src/settings/schema_generation.rs b/app/src/settings/schema_generation.rs new file mode 100644 index 00000000000..e4d52f57d3f --- /dev/null +++ b/app/src/settings/schema_generation.rs @@ -0,0 +1,221 @@ +use std::io::Write as _; +use std::path::Path; + +use anyhow::{Context as _, Result}; +use schemars::SchemaGenerator; +use serde_json::{Map, Value}; +use settings::schema::SettingSchemaEntry; +use settings::{SettingSurfaces, SettingsMode}; +use tempfile::NamedTempFile; +use warp_core::channel::ChannelState; +use warp_core::features::FeatureFlag; + +/// Writes the settings schema to a file or prints it to standard output. +pub fn dump_settings_schema(output_path: Option<&Path>) -> Result<()> { + let output = settings_schema_json(|flag| flag.is_enabled())?; + + if let Some(path) = output_path { + write_atomically(path, output.as_bytes())?; + eprintln!("Wrote settings schema to {}", path.display()); + } else { + println!("{output}"); + } + + Ok(()) +} + +fn settings_schema_json(is_flag_enabled: impl Fn(FeatureFlag) -> bool) -> Result { + let mut generator = SchemaGenerator::default(); + let mut root_properties = Map::new(); + let mut entry_count = 0; + + for entry in inventory::iter:: { + if entry.is_private { + continue; + } + + if entry + .feature_flag + .is_some_and(|flag| !is_flag_enabled(flag)) + { + continue; + } + + let type_schema = (entry.schema_fn)(&mut generator); + let mut schema_value = type_schema.to_value(); + schema_value + .as_object_mut() + .expect("setting schema should be an object") + .insert( + "x-warp-surfaces".to_string(), + Value::Array(setting_surface_names((entry.surfaces_fn)())), + ); + let default_json = (entry.file_default_value_fn)(); + + if let Ok(default_value) = serde_json::from_str::(&default_json) + && let Some(object) = schema_value.as_object_mut() + { + object.insert("default".to_string(), default_value); + } + + if !entry.description.is_empty() + && let Some(object) = schema_value.as_object_mut() + { + object.insert( + "description".to_string(), + Value::String(entry.description.to_string()), + ); + } + + let target = if let Some(hierarchy) = entry.hierarchy { + ensure_hierarchy(&mut root_properties, hierarchy) + } else { + &mut root_properties + }; + + target.insert(entry.storage_key.to_string(), schema_value); + entry_count += 1; + } + + let definitions = generator.take_definitions(true); + let mut root = Map::new(); + root.insert( + "$schema".to_string(), + Value::String("https://json-schema.org/draft/2020-12/schema".to_string()), + ); + root.insert( + "title".to_string(), + Value::String("Warp Settings".to_string()), + ); + root.insert( + "description".to_string(), + Value::String(format!( + "JSON Schema for Warp settings ({} channel, {entry_count} settings)", + ChannelState::channel() + )), + ); + root.insert("type".to_string(), Value::String("object".to_string())); + root.insert("properties".to_string(), Value::Object(root_properties)); + + if !definitions.is_empty() { + root.insert("$defs".to_string(), Value::Object(definitions)); + } + + let mut root_value = Value::Object(root); + strip_numeric_metadata(&mut root_value); + strip_empty_enum_entries(&mut root_value); + + serde_json::to_string_pretty(&root_value).context("settings schema should serialize") +} + +fn setting_surface_names(surfaces: SettingSurfaces) -> Vec { + [(SettingsMode::Gui, "gui"), (SettingsMode::Tui, "tui")] + .into_iter() + .filter(|(mode, _)| surfaces.includes(*mode)) + .map(|(_, name)| Value::String(name.to_owned())) + .collect() +} + +fn ensure_hierarchy<'a>( + root_properties: &'a mut Map, + hierarchy: &str, +) -> &'a mut Map { + let mut current = root_properties; + + for segment in hierarchy.split('.') { + let entry = current.entry(segment.to_string()).or_insert_with(|| { + Value::Object({ + let mut object = Map::new(); + object.insert("type".to_string(), Value::String("object".to_string())); + object.insert("properties".to_string(), Value::Object(Map::new())); + object + }) + }); + + current = entry + .as_object_mut() + .expect("hierarchy node should be an object") + .entry("properties") + .or_insert_with(|| Value::Object(Map::new())) + .as_object_mut() + .expect("properties should be an object"); + } + + current +} + +fn strip_numeric_metadata(value: &mut Value) { + match value { + Value::Object(map) => { + let is_numeric = map + .get("type") + .and_then(Value::as_str) + .is_some_and(|value_type| value_type == "integer" || value_type == "number"); + + if is_numeric { + map.remove("minimum"); + map.remove("maximum"); + map.remove("format"); + } + + for value in map.values_mut() { + strip_numeric_metadata(value); + } + } + Value::Array(array) => { + for value in array { + strip_numeric_metadata(value); + } + } + _ => {} + } +} + +fn strip_empty_enum_entries(value: &mut Value) { + match value { + Value::Object(map) => { + if let Some(Value::Array(one_of)) = map.get_mut("oneOf") { + one_of.retain(|entry| { + !matches!(entry, Value::Object(object) + if object.get("enum").is_some_and(|value| value.as_array().is_some_and(Vec::is_empty)) + ) + }); + } + + for value in map.values_mut() { + strip_empty_enum_entries(value); + } + } + Value::Array(array) => { + for value in array { + strip_empty_enum_entries(value); + } + } + _ => {} + } +} + +fn write_atomically(path: &Path, contents: &[u8]) -> Result<()> { + let parent = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + + let mut temporary_file = NamedTempFile::new_in(parent) + .with_context(|| format!("failed to create a temporary file in {}", parent.display()))?; + temporary_file + .write_all(contents) + .with_context(|| format!("failed to write temporary schema for {}", path.display()))?; + temporary_file + .persist(path) + .map_err(|error| error.error) + .with_context(|| format!("failed to persist settings schema to {}", path.display()))?; + + Ok(()) +} + +#[cfg(test)] +#[path = "schema_generation_tests.rs"] +mod tests; diff --git a/app/src/settings/schema_generation_tests.rs b/app/src/settings/schema_generation_tests.rs new file mode 100644 index 00000000000..059c4eb09e5 --- /dev/null +++ b/app/src/settings/schema_generation_tests.rs @@ -0,0 +1,108 @@ +use std::collections::HashSet; + +use serde_json::json; +use settings::SettingsMode; +use settings::schema::SettingSchemaEntry; + +use super::{ + setting_surface_names, settings_schema_json, strip_empty_enum_entries, strip_numeric_metadata, +}; + +#[test] +fn strips_numeric_metadata_recursively() { + let mut schema = json!({ + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "format": "uint8" + } + } + }); + + strip_numeric_metadata(&mut schema); + + assert_eq!( + schema, + json!({ + "type": "object", + "properties": { + "count": { + "type": "integer" + } + } + }) + ); +} + +#[test] +fn strips_empty_enum_entries() { + let mut schema = json!({ + "oneOf": [ + { + "enum": [], + "type": "string" + }, + { + "const": "kept" + } + ] + }); + + strip_empty_enum_entries(&mut schema); + + assert_eq!( + schema, + json!({ + "oneOf": [ + { + "const": "kept" + } + ] + }) + ); +} + +#[test] +fn generates_a_settings_schema() { + let schema = settings_schema_json(|_| false).unwrap(); + let schema: serde_json::Value = serde_json::from_str(&schema).unwrap(); + + assert_eq!(schema["title"], "Warp Settings"); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"].is_object()); +} + +#[test] +fn surface_annotation_matches_setting_schema_entry_metadata() { + for entry in inventory::iter:: { + let surfaces = (entry.surfaces_fn)(); + let annotation = setting_surface_names(surfaces); + let annotation_names: HashSet<&str> = annotation + .iter() + .filter_map(|value| value.as_str()) + .collect(); + + assert_eq!( + annotation_names.contains("gui"), + surfaces.includes(SettingsMode::Gui), + "GUI surface mismatch for {}", + entry.storage_key + ); + assert_eq!( + annotation_names.contains("tui"), + surfaces.includes(SettingsMode::Tui), + "TUI surface mismatch for {}", + entry.storage_key + ); + assert_eq!( + annotation_names.len(), + usize::from(surfaces.includes(SettingsMode::Gui)) + + usize::from(surfaces.includes(SettingsMode::Tui)), + "unexpected surface annotation for {}", + entry.storage_key + ); + } +} diff --git a/crates/warp_cli/src/lib.rs b/crates/warp_cli/src/lib.rs index af2c05ce31e..dfb1f54ad66 100644 --- a/crates/warp_cli/src/lib.rs +++ b/crates/warp_cli/src/lib.rs @@ -683,6 +683,12 @@ pub enum Command { /// Print debugging information and exit. #[clap(long_flag = "dump-debug-info")] DumpDebugInfo, + /// Print the JSON schema for the current Warp channel's settings and exit. + #[cfg(not(target_family = "wasm"))] + DumpSettingsSchema { + /// Write the schema to this path instead of standard output. + output_path: Option, + }, /// Print telemetry events in production and exit. #[clap(long_flag = "print-telemetry-events", hide = true)] @@ -698,6 +704,8 @@ impl Command { Command::CommandLine(_) | Command::DumpDebugInfo => true, Command::Completions { .. } => true, #[cfg(not(target_family = "wasm"))] + Command::DumpSettingsSchema { output_path } => output_path.is_none(), + #[cfg(not(target_family = "wasm"))] Command::PrintTelemetryEvents => true, } } diff --git a/crates/warp_tui/src/session.rs b/crates/warp_tui/src/session.rs index 041544b3b10..a6f7f018b9c 100644 --- a/crates/warp_tui/src/session.rs +++ b/crates/warp_tui/src/session.rs @@ -6,12 +6,13 @@ //! allowing authentication to complete in the background. use std::io::{self, IsTerminal as _, Read as _}; +use std::path::PathBuf; use ai::LLMProvider; use ai::api_keys::ApiKeyManager; use anyhow::{Context, Result, anyhow}; -use clap::Parser; use clap::error::ErrorKind; +use clap::{Parser, Subcommand}; use inquire::{InquireError, Password, PasswordDisplayMode}; use warp::settings::{TuiThemeSettings, TuiZeroStateSettings, TuiZeroStateSettingsChangedEvent}; #[cfg(feature = "voice_input")] @@ -50,6 +51,9 @@ const CLI_VERSION: &str = match option_env!("GIT_RELEASE_TAG") { #[derive(Debug, Parser)] #[command(name = "warp", version = CLI_VERSION)] struct TuiArgs { + #[command(subcommand)] + command: Option, + /// Resume an Oz/Warp conversation by server token. #[arg(long)] resume: Option, @@ -91,6 +95,20 @@ enum ProviderApiKeyCommand { }, } +/// One-shot commands available on the standalone TUI binaries. The full app +/// binary implements the same `dump-settings-schema` contract (see +/// `warp_cli::Command::DumpSettingsSchema`); this local copy lets +/// `script/run-tui` seed a real settings schema from whichever TUI binary is +/// about to run, without building the full GUI app binary just for that. +#[derive(Debug, Subcommand)] +enum TuiCommand { + /// Print the JSON schema for the current Warp channel's settings and exit. + DumpSettingsSchema { + /// Write the schema to this path instead of standard output. + output_path: Option, + }, +} + /// Reads a provider API key from a masked TTY prompt or, when stdin is piped, /// from stdin. Empty input and interactive cancellation return `Ok(None)`. fn read_provider_api_key() -> Result> { @@ -144,6 +162,10 @@ pub fn run() -> Result<()> { } Err(error) => return Err(anyhow::Error::new(error)), }; + if let Some(TuiCommand::DumpSettingsSchema { output_path }) = args.command { + warp::features::init_feature_flags(); + return warp::settings::dump_settings_schema(output_path.as_deref()); + } let provider_api_key_command = if let Some(provider) = args.set_provider_api_key { if !provider.supports_pasted_api_key() { return Err(anyhow!( diff --git a/flake.nix b/flake.nix index 2dc81648b58..ddc7ba3abff 100644 --- a/flake.nix +++ b/flake.nix @@ -155,8 +155,6 @@ "warp" "--bin" "warp-oss" - "--bin" - "generate_settings_schema" ]; inherit buildFeatures; @@ -175,7 +173,7 @@ let installDir = "$out/opt/warpdotdev/warp-terminal-experimental"; resourcesDir = "${installDir}/resources"; - releaseChannel = "stable"; + releaseChannel = "oss"; libraryPath = lib.makeLibraryPath linuxRuntimeLibraries; executablePath = lib.makeBinPath (with pkgs; [ xdg-utils ]); in @@ -187,15 +185,9 @@ ./script/prepare_bundled_resources \ ./script/copy_conditional_skills - SKIP_SETTINGS_SCHEMA=1 ./script/prepare_bundled_resources \ + SETTINGS_SCHEMA_EXECUTABLE="${installDir}/warp-oss" ./script/prepare_bundled_resources \ "${resourcesDir}" \ - "${releaseChannel}" \ - release - - "$out/bin/generate_settings_schema" \ - --channel "${releaseChannel}" \ - "${resourcesDir}/settings_schema.json" - rm -f "$out/bin/generate_settings_schema" + "${releaseChannel}" install -Dm644 \ "${resourcesDir}/THIRD_PARTY_LICENSES.txt" \ diff --git a/script/deploy_remote_server b/script/deploy_remote_server index 0e7fd415cee..2608c71d04c 100755 --- a/script/deploy_remote_server +++ b/script/deploy_remote_server @@ -147,10 +147,9 @@ echo "==> Build complete ($BINARY_SIZE)" # Prepare the bundled resources tree (skills, settings schema) deployed to # the global, version-independent location the daemon reads. rm -rf "$LOCAL_RESOURCES_DIR" -"$WORKSPACE_ROOT/script/prepare_bundled_resources" \ +SKIP_SETTINGS_SCHEMA=1 "$WORKSPACE_ROOT/script/prepare_bundled_resources" \ "$LOCAL_RESOURCES_DIR" \ - local \ - "$CARGO_PROFILE" + local # Resolve $HOME on the remote so our upload lands in the same directory the # Warp client checks. The client runs `test -x ~/.warp-local/...` and lets @@ -194,6 +193,9 @@ echo "==> Uploading bundled resources to $HOST:$REMOTE_ABS_DIR/$BUNDLED_RESOURCE rsync -z -rlt --delete --partial --progress \ "$LOCAL_RESOURCES_DIR/" \ "$HOST:$REMOTE_ABS_DIR/$BUNDLED_RESOURCES_DIR_NAME/" +echo "==> Generating Linux settings schema on $HOST" +ssh "$HOST" \ + "$REMOTE_ABS_DIR/$BINARY_NAME dump-settings-schema $REMOTE_ABS_DIR/$BUNDLED_RESOURCES_DIR_NAME/settings_schema.json" echo "==> Stopping stale remote-server daemons on $HOST" ssh "$HOST" bash <<'EOF' diff --git a/script/deploy_remote_server_to_test_vm b/script/deploy_remote_server_to_test_vm index c938ebdb092..f2a5d8ac412 100755 --- a/script/deploy_remote_server_to_test_vm +++ b/script/deploy_remote_server_to_test_vm @@ -98,10 +98,9 @@ echo "==> Build complete ($BINARY_SIZE)" rm -rf "$LOCAL_BUNDLE_DIR" mkdir -p "$LOCAL_BUNDLE_DIR" cp "$BUILT_BINARY" "$LOCAL_BUNDLE_DIR/$BUNDLE_BINARY_NAME" -"$WORKSPACE_ROOT/script/prepare_bundled_resources" \ +SKIP_SETTINGS_SCHEMA=1 "$WORKSPACE_ROOT/script/prepare_bundled_resources" \ "$LOCAL_BUNDLE_DIR/resources" \ - integration \ - "$CARGO_PROFILE" + integration tar -czf "$LOCAL_BUNDLE_TARBALL" -C "$LOCAL_BUNDLE_DIR" . # ── Upload via SCP through GCP IAP tunnel ───────────────────────── @@ -197,6 +196,7 @@ mv resources bundled_resources chmod 755 "$3" mv -f "$3" "$4" +"./$4" dump-settings-schema bundled_resources/settings_schema.json "./$4" --version REMOTE_SCRIPT done diff --git a/script/linux/bundle b/script/linux/bundle index d9274ea7a3a..696c8e0f911 100755 --- a/script/linux/bundle +++ b/script/linux/bundle @@ -120,23 +120,27 @@ done # set positional arguments in their proper place eval set -- "$PARAMS" +# Linux has no transparent cross-arch execution (unlike macOS's Rosetta), so a +# binary built for a different architecture than the host can never be run +# here. Resolve the host architecture once and reuse it both for the TUI +# native-build restriction below and for deciding whether the just-built +# binary can stand in for a settings-schema executable further down. +case "$(uname -m)" in + arm64|aarch64) HOST_ARCH="aarch64" ;; + x86_64|amd64) HOST_ARCH="x86_64" ;; + *) + echo "Error: Unsupported host architecture: $(uname -m)" >&2 + exit 1 + ;; +esac + # TUI Linux bundles use the native GNU target because their clipboard backends # link against host Wayland/X11 libraries. Refuse to compile on a different # architecture so an x86 binary can never be published under an ARM filename # (or vice versa). Packaging-only runs may still stage a prebuilt native binary. -if [[ "$ARTIFACT" == "tui" && "$BUILD" == "true" ]]; then - case "$(uname -m)" in - arm64|aarch64) HOST_ARCH="aarch64" ;; - x86_64|amd64) HOST_ARCH="x86_64" ;; - *) - echo "Error: Unsupported native TUI build architecture: $(uname -m)" >&2 - exit 1 - ;; - esac - if [[ "$BUILD_ARCH" != "$HOST_ARCH" ]]; then - echo "Error: TUI bundles must be built natively; requested '$BUILD_ARCH' on '$HOST_ARCH'." >&2 - exit 1 - fi +if [[ "$ARTIFACT" == "tui" && "$BUILD" == "true" && "$BUILD_ARCH" != "$HOST_ARCH" ]]; then + echo "Error: TUI bundles must be built natively; requested '$BUILD_ARCH' on '$HOST_ARCH'." >&2 + exit 1 fi # Statically compile the CLI and warpctrl artifacts so they can run on older Linux distros. @@ -344,11 +348,43 @@ elif [[ "$ARTIFACT" == "tui" ]]; then BINARY_PATH="$OUT_DIR/$WARP_BIN" fi +if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -z "${SETTINGS_SCHEMA_EXECUTABLE:-}" && -z "${SETTINGS_SCHEMA_SOURCE:-}" ]]; then + if [[ "$ARTIFACT" == "tui" ]]; then + echo "Error: TUI bundles require SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + elif [[ "$BUILD" != "true" ]]; then + echo "Error: --skip-build requires SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + elif [[ "$BUILD_ARCH" != "$HOST_ARCH" ]]; then + # The CLI/warpctrl artifacts can be cross-compiled (see the musl toolchain + # setup above); the resulting binary cannot then be executed on this host + # to generate a schema, so require the caller to supply one explicitly + # instead of failing later with an opaque exec format error. + echo "Error: Cannot execute the cross-compiled '$BUILD_ARCH' binary on this '$HOST_ARCH' host to generate a settings schema; pass SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + else + export SETTINGS_SCHEMA_EXECUTABLE="$EXECUTABLE_PATH" + fi +fi + +if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -n "${SETTINGS_SCHEMA_EXECUTABLE:-}" && -n "${SETTINGS_SCHEMA_SOURCE:-}" ]]; then + echo "Error: SETTINGS_SCHEMA_EXECUTABLE and SETTINGS_SCHEMA_SOURCE are mutually exclusive." >&2 + exit 1 +fi + +if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -n "${SETTINGS_SCHEMA_EXECUTABLE:-}" ]]; then + GENERATED_SETTINGS_SCHEMA="$CARGO_TARGET_OUTPUT_DIR/settings_schema.json" + echo "Generating reusable settings schema at $GENERATED_SETTINGS_SCHEMA" + "$SETTINGS_SCHEMA_EXECUTABLE" dump-settings-schema "$GENERATED_SETTINGS_SCHEMA" + unset SETTINGS_SCHEMA_EXECUTABLE + export SETTINGS_SCHEMA_SOURCE="$GENERATED_SETTINGS_SCHEMA" +fi + # Prepare bundled resources for standalone builds. if [[ "$ARTIFACT" == "cli" || "$ARTIFACT" == "warpctrl" || "$ARTIFACT" == "tui" ]]; then echo "Preparing standalone resources directory" BUNDLED_RESOURCES_DIR="$OUT_DIR/resources" - "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$CARGO_PROFILE" + "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" fi @@ -361,6 +397,7 @@ if [ "${GITHUB_ACTIONS}" == "true" ]; then echo "debug_executable_path=$DEBUG_EXECUTABLE_PATH" >> "$GITHUB_OUTPUT" echo "packages_dir=$OUT_DIR" >> "$GITHUB_OUTPUT" echo "bundled_resources_dir=${BUNDLED_RESOURCES_DIR:-}" >> "$GITHUB_OUTPUT" + echo "settings_schema_path=${SETTINGS_SCHEMA_SOURCE:-}" >> "$GITHUB_OUTPUT" echo "::echo::off" fi diff --git a/script/linux/bundle_install b/script/linux/bundle_install index 818d596338a..78a72e53fc7 100755 --- a/script/linux/bundle_install +++ b/script/linux/bundle_install @@ -44,4 +44,4 @@ if [[ "$ARTIFACT" == "app" ]]; then fi # Prepare the bundled resources directory. -"$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "${TARGET_DIR}/${OPT_DIR}/resources" "$RELEASE_CHANNEL" "$CARGO_PROFILE" +"$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "${TARGET_DIR}/${OPT_DIR}/resources" "$RELEASE_CHANNEL" diff --git a/script/macos/bundle b/script/macos/bundle index 98ab3262052..aa1a141f749 100755 --- a/script/macos/bundle +++ b/script/macos/bundle @@ -467,6 +467,31 @@ else fi fi +# 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 + ;; + esac +fi + # Set artifact-specific configuration. if [[ "$ARTIFACT" == cli || "$ARTIFACT" == warpctrl ]]; then UNIVERSAL_BINARY=false @@ -495,6 +520,15 @@ elif [[ "$ARTIFACT" == app ]]; then FEATURES="$FEATURES,gui,nld_classifier_v3,nld_heuristic_v2" fi +# A universal binary always has a native slice, so it is unaffected by the +# host/target mismatch below. This must run after the artifact-specific +# configuration above, since that's what gives UNIVERSAL_BINARY its final +# value (e.g. cli/warpctrl/tui always force it to false). +CAN_EXECUTE_DEFAULT_TARGET=true +if [[ "$UNIVERSAL_BINARY" != true && "$HOST_ARCH" == "x86_64" && "$DEFAULT_TARGET" == "$ARM_TARGET" ]]; then + CAN_EXECUTE_DEFAULT_TARGET=false +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" @@ -662,7 +696,18 @@ if [[ "$ARTIFACT" == "app" ]]; then 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" + if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -z "${SETTINGS_SCHEMA_EXECUTABLE:-}" && -z "${SETTINGS_SCHEMA_SOURCE:-}" ]]; then + if [[ $BUILD_BINARY != true ]]; then + echo "Error: --skip-build requires SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + elif [[ "$CAN_EXECUTE_DEFAULT_TARGET" != true ]]; then + echo "Error: Cannot execute the just-built $DEFAULT_TARGET binary on this $HOST_ARCH host to generate a settings schema; pass SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + else + export SETTINGS_SCHEMA_EXECUTABLE="$BUNDLE_DIR/$WARP_APP_NAME.app/Contents/MacOS/$WARP_BIN" + fi + fi + "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$WORKSPACE_ROOT_DIR/script/compile_icon" "$RELEASE_CHANNEL" "$BUNDLE_DIR/$WARP_APP_NAME.app" @@ -762,7 +807,18 @@ elif [[ "$ARTIFACT" == "cli" || "$ARTIFACT" == "warpctrl" ]]; then 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" + if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -z "${SETTINGS_SCHEMA_EXECUTABLE:-}" && -z "${SETTINGS_SCHEMA_SOURCE:-}" ]]; then + if [[ $BUILD_BINARY != true ]]; then + echo "Error: --skip-build requires SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + elif [[ "$CAN_EXECUTE_DEFAULT_TARGET" != true ]]; then + echo "Error: Cannot execute the just-built $DEFAULT_TARGET binary on this $HOST_ARCH host to generate a settings schema; pass SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + else + export SETTINGS_SCHEMA_EXECUTABLE="$OUT_DIR/$WARP_BIN" + fi + fi + "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" # Set the primary binary path to output. @@ -792,7 +848,11 @@ elif [[ "$ARTIFACT" == "tui" ]]; then # Fonts are compiled into the binary, so they are intentionally absent here. echo "Preparing TUI resources directory" BUNDLED_RESOURCES_DIR="$OUT_DIR/resources" - "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$CARGO_PROFILE" + if [[ "${SKIP_SETTINGS_SCHEMA:-}" != "1" && -z "${SETTINGS_SCHEMA_EXECUTABLE:-}" && -z "${SETTINGS_SCHEMA_SOURCE:-}" ]]; then + echo "Error: TUI bundles require SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." >&2 + exit 1 + fi + "$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" BINARY_PATH="$OUT_DIR/$WARP_BIN" else @@ -1030,6 +1090,9 @@ if [ "${GITHUB_ACTIONS}" == "true" ]; then if [[ -n "$BUNDLED_RESOURCES_DIR" ]]; then echo "bundled_resources_dir=$BUNDLED_RESOURCES_DIR" >> "$GITHUB_OUTPUT" fi + if [[ -f "$BUNDLED_RESOURCES_DIR/settings_schema.json" ]]; then + echo "settings_schema_path=$BUNDLED_RESOURCES_DIR/settings_schema.json" >> "$GITHUB_OUTPUT" + fi echo "::echo::off" fi diff --git a/script/macos/run b/script/macos/run index f2172a53afc..8fb85216a4b 100755 --- a/script/macos/run +++ b/script/macos/run @@ -123,6 +123,8 @@ export WARP_PLIST_PATH="$WARP_APP_PATH/Contents/Info.plist" echo "Preparing bundled resources..." if [ "${GENERATE_SCHEMA:-false}" != "true" ]; then export SKIP_SETTINGS_SCHEMA=1 +else + export SETTINGS_SCHEMA_EXECUTABLE="$WARP_APP_PATH/Contents/MacOS/$WARP_BIN_NAME" fi NO_LICENSES=1 "${REPO_ROOT}/script/prepare_bundled_resources" "$WARP_APP_PATH/Contents/Resources" "$WARP_CHANNEL" diff --git a/script/prepare_bundled_resources b/script/prepare_bundled_resources index deb9d6979ed..2c48dea1688 100755 --- a/script/prepare_bundled_resources +++ b/script/prepare_bundled_resources @@ -6,7 +6,7 @@ # destination directory. It is used by macOS and Linux build scripts. # # Usage: -# prepare_bundled_resources [channel] [cargo_profile] +# prepare_bundled_resources [channel] # # Arguments: # destination_directory: The directory where resources should be installed. @@ -14,32 +14,28 @@ # this path (e.g., $DEST_DIR/skills). # channel: (Optional) Release channel (local, dev, preview, # stable). Used to include channel-gated skills. -# cargo_profile: (Optional) Cargo build profile to use when -# generating the settings schema. Reusing the same -# profile as the main build avoids recompiling deps. # # Environment variables: # SKIP_SETTINGS_SCHEMA: Set to "1" to skip generating the JSON settings # schema. By default the schema is always generated # so that production bundles never accidentally omit it. -# SETTINGS_SCHEMA_CACHE: Path to a cache file for the generated schema. -# When set, the first invocation generates the schema -# and saves a copy to this path; subsequent invocations -# copy from the cache instead of regenerating. This -# avoids redundant compilations when bundling multiple -# package formats for the same channel. +# SETTINGS_SCHEMA_EXECUTABLE: Path to a runnable compiled Warp executable. +# The script invokes its dump-settings-schema +# subcommand to generate the schema. +# SETTINGS_SCHEMA_SOURCE: Path to an existing settings schema to copy. +# This is required when the compiled executable cannot +# run on the current host. set -e -if [ $# -lt 1 ] || [ $# -gt 3 ]; then - echo "Error: Expected 1-3 arguments but received $#" >&2 - echo "Usage: $0 [channel] [cargo_profile]" >&2 +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "Error: Expected 1-2 arguments but received $#" >&2 + echo "Usage: $0 [channel]" >&2 exit 1 fi DEST_DIR="$1" CHANNEL="${2:-}" -CARGO_PROFILE="${3:-}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" RESOURCES_SRC="$REPO_ROOT/resources" @@ -130,29 +126,31 @@ fi # Generate settings JSON schema unless explicitly skipped. if [ "${SKIP_SETTINGS_SCHEMA:-}" != "1" ]; then SCHEMA_OUTPUT="$DEST_DIR/settings_schema.json" - - if [ -n "${SETTINGS_SCHEMA_CACHE:-}" ] && [ -f "$SETTINGS_SCHEMA_CACHE" ]; then - echo "Copying cached settings schema to $SCHEMA_OUTPUT" - cp "$SETTINGS_SCHEMA_CACHE" "$SCHEMA_OUTPUT" - else - echo "Generating settings schema at $SCHEMA_OUTPUT" - - SCHEMA_CMD=(cargo run) - if [ -n "$CARGO_PROFILE" ]; then - SCHEMA_CMD+=(--profile "$CARGO_PROFILE") + if [ -n "${SETTINGS_SCHEMA_EXECUTABLE:-}" ] && [ -n "${SETTINGS_SCHEMA_SOURCE:-}" ]; then + echo "Error: SETTINGS_SCHEMA_EXECUTABLE and SETTINGS_SCHEMA_SOURCE are mutually exclusive." >&2 + exit 1 + elif [ -n "${SETTINGS_SCHEMA_EXECUTABLE:-}" ]; then + if [ ! -x "$SETTINGS_SCHEMA_EXECUTABLE" ]; then + echo "Error: Settings schema executable is not runnable: $SETTINGS_SCHEMA_EXECUTABLE" >&2 + exit 1 fi - SCHEMA_CMD+=(--manifest-path "$REPO_ROOT/Cargo.toml" --bin generate_settings_schema --) - if [ -n "$CHANNEL" ]; then - SCHEMA_CMD+=(--channel "$CHANNEL") + echo "Generating settings schema at $SCHEMA_OUTPUT" + "$SETTINGS_SCHEMA_EXECUTABLE" dump-settings-schema "$SCHEMA_OUTPUT" + elif [ -n "${SETTINGS_SCHEMA_SOURCE:-}" ]; then + if [ ! -f "$SETTINGS_SCHEMA_SOURCE" ]; then + echo "Error: Settings schema source does not exist: $SETTINGS_SCHEMA_SOURCE" >&2 + exit 1 fi - SCHEMA_CMD+=("$SCHEMA_OUTPUT") - - "${SCHEMA_CMD[@]}" + echo "Copying settings schema to $SCHEMA_OUTPUT" + cp "$SETTINGS_SCHEMA_SOURCE" "$SCHEMA_OUTPUT" + else + echo "Error: Set SETTINGS_SCHEMA_EXECUTABLE or SETTINGS_SCHEMA_SOURCE when preparing a settings schema." >&2 + exit 1 + fi - if [ -n "${SETTINGS_SCHEMA_CACHE:-}" ]; then - echo "Caching settings schema at $SETTINGS_SCHEMA_CACHE" - cp "$SCHEMA_OUTPUT" "$SETTINGS_SCHEMA_CACHE" - fi + if [ ! -s "$SCHEMA_OUTPUT" ]; then + echo "Error: Settings schema was not written to $SCHEMA_OUTPUT" >&2 + exit 1 fi fi diff --git a/script/run-tui b/script/run-tui index f6bedf28346..7aa4f230ee1 100755 --- a/script/run-tui +++ b/script/run-tui @@ -111,18 +111,16 @@ fi BUILD_DIR="${BUILD_DIR}/${PROFILE_DIR}" # Stage bundled skills and the settings schema next to the binary so the -# standalone TUI can resolve them at runtime. License generation remains skipped -# for local runs. Reuse the selected Cargo profile for schema generation so its -# dependencies do not need to be rebuilt in a separate profile. +# standalone TUI can resolve them at runtime, including activating the +# modify-settings skill. The schema comes from the just-built TUI binary +# itself and is for local development only; packaged TUI builds still +# consume the canonical schema generated from the full app binary. License +# generation remains skipped for local runs. prepare_tui_bundled_resources() { echo "Preparing bundled resources at ${BUILD_DIR}/resources (channel: ${CHANNEL})..." - if [ "${PROFILE_DIR}" = "debug" ]; then - NO_LICENSES=1 "${REPO_ROOT}/script/prepare_bundled_resources" \ - "${BUILD_DIR}/resources" "${CHANNEL}" - else - NO_LICENSES=1 "${REPO_ROOT}/script/prepare_bundled_resources" \ - "${BUILD_DIR}/resources" "${CHANNEL}" "${PROFILE_DIR}" - fi + NO_LICENSES=1 SETTINGS_SCHEMA_EXECUTABLE="${BUILD_DIR}/${BIN}" \ + "${REPO_ROOT}/script/prepare_bundled_resources" \ + "${BUILD_DIR}/resources" "${CHANNEL}" } if [[ "$(uname -s)" = "Darwin" ]]; then diff --git a/script/test_prepare_bundled_resources b/script/test_prepare_bundled_resources new file mode 100755 index 00000000000..e9a8dad1bcc --- /dev/null +++ b/script/test_prepare_bundled_resources @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -e + +WORKSPACE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT + +FAKE_EXECUTABLE="$TEMP_DIR/warp" +cat > "$FAKE_EXECUTABLE" <<'EOF' +#!/usr/bin/env bash +if [[ "$1" != "dump-settings-schema" || -z "$2" ]]; then + exit 1 +fi +printf '{"source":"executable"}' > "$2" +EOF +chmod +x "$FAKE_EXECUTABLE" + +EXECUTABLE_DESTINATION="$TEMP_DIR/from-executable" +NO_LICENSES=1 \ + SETTINGS_SCHEMA_EXECUTABLE="$FAKE_EXECUTABLE" \ + "$WORKSPACE_ROOT/script/prepare_bundled_resources" "$EXECUTABLE_DESTINATION" dev +grep -qx '{"source":"executable"}' "$EXECUTABLE_DESTINATION/settings_schema.json" + +SOURCE_SCHEMA="$TEMP_DIR/source-schema.json" +printf '{"source":"file"}' > "$SOURCE_SCHEMA" +SOURCE_DESTINATION="$TEMP_DIR/from-source" +NO_LICENSES=1 \ + SETTINGS_SCHEMA_SOURCE="$SOURCE_SCHEMA" \ + "$WORKSPACE_ROOT/script/prepare_bundled_resources" "$SOURCE_DESTINATION" dev +cmp "$SOURCE_SCHEMA" "$SOURCE_DESTINATION/settings_schema.json" + +if NO_LICENSES=1 "$WORKSPACE_ROOT/script/prepare_bundled_resources" "$TEMP_DIR/missing-input" dev; then + echo "Expected resource preparation without a schema input to fail." >&2 + exit 1 +fi + +if NO_LICENSES=1 \ + SETTINGS_SCHEMA_EXECUTABLE="$FAKE_EXECUTABLE" \ + SETTINGS_SCHEMA_SOURCE="$SOURCE_SCHEMA" \ + "$WORKSPACE_ROOT/script/prepare_bundled_resources" "$TEMP_DIR/conflicting-inputs" dev; then + echo "Expected conflicting schema inputs to fail." >&2 + exit 1 +fi + +# SKIP_SETTINGS_SCHEMA=1 lets build-only jobs defer schema generation to a later packaging step. +SKIPPED_DESTINATION="$TEMP_DIR/skipped-schema" +NO_LICENSES=1 \ + SKIP_SETTINGS_SCHEMA=1 \ + "$WORKSPACE_ROOT/script/prepare_bundled_resources" "$SKIPPED_DESTINATION" dev +if [ -e "$SKIPPED_DESTINATION/settings_schema.json" ]; then + echo "Expected no settings schema to be written when SKIP_SETTINGS_SCHEMA=1." >&2 + exit 1 +fi diff --git a/script/windows/bundle.ps1 b/script/windows/bundle.ps1 index ea0efe6ac50..fe18894a882 100644 --- a/script/windows/bundle.ps1 +++ b/script/windows/bundle.ps1 @@ -60,6 +60,31 @@ if ($ARCH -eq 'arm64') { $PLATFORM_TARGET = 'x86_64-pc-windows-msvc' } +# Windows-on-ARM64 hosts can run x64 binaries via built-in emulation, but x64 +# hosts cannot run arm64 binaries at all, so only that direction is unsafe. +# Resolve it once so the settings-schema default below (which assumes the +# just-built binary is executable) can fail clearly instead of the process +# simply failing to start. +# +# PROCESSOR_ARCHITECTURE reports the architecture of the running process, not +# the host: an x64 PowerShell process running under WOW64 on a Windows-on-ARM +# machine reports AMD64 even though the host is natively ARM64. +# PROCESSOR_ARCHITEW6432 carries the true native host architecture in that +# case, so prefer it when present. +$NATIVE_PROCESSOR_ARCHITECTURE = if ($env:PROCESSOR_ARCHITEW6432) { + $env:PROCESSOR_ARCHITEW6432 +} else { + $env:PROCESSOR_ARCHITECTURE +} +$HOST_ARCH = if ($NATIVE_PROCESSOR_ARCHITECTURE -eq 'AMD64') { + 'x64' +} elseif ($NATIVE_PROCESSOR_ARCHITECTURE -eq 'ARM64') { + 'arm64' +} else { + throw "Unsupported host architecture: $NATIVE_PROCESSOR_ARCHITECTURE" +} +$CAN_EXECUTE_ARCH = -not ($ARCH -eq 'arm64' -and $HOST_ARCH -eq 'x64') + $ErrorActionPreference = 'Stop' function Assert-ValidSignature { @@ -249,9 +274,22 @@ if ($SKIP_BUILD_INSTALLER) { Write-Output "Built for $ARCH with executable at $BINARY_PATH" # Prepare bundled resources +if ($env:SKIP_SETTINGS_SCHEMA -ne '1' -and -not $env:SETTINGS_SCHEMA_EXECUTABLE -and -not $env:SETTINGS_SCHEMA_SOURCE) { + if ($IS_TUI) { + Write-Error 'TUI bundles require SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE.' + exit 1 + } elseif ($SKIP_BUILD_BINARY) { + Write-Error '-skip_build_binary requires SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE.' + exit 1 + } elseif (-not $CAN_EXECUTE_ARCH) { + Write-Error "Cannot execute the just-built $ARCH binary on this $HOST_ARCH host to generate a settings schema; pass SETTINGS_SCHEMA_SOURCE or SETTINGS_SCHEMA_EXECUTABLE." + exit 1 + } + $env:SETTINGS_SCHEMA_EXECUTABLE = $BINARY_PATH +} $BUNDLED_RESOURCES_DIR = "$CARGO_TARGET_OUTPUT_DIR\resources" Write-Output 'Preparing bundled resources...' -& "$WINDOWS_INSTALLER_DIR\prepare_bundled_resources.ps1" -DestinationDir "$BUNDLED_RESOURCES_DIR" -Channel "$CHANNEL" -CargoProfile "$CARGO_PROFILE" +& "$WINDOWS_INSTALLER_DIR\prepare_bundled_resources.ps1" -DestinationDir "$BUNDLED_RESOURCES_DIR" -Channel "$CHANNEL" if (-Not $?) { Write-Error 'Failed to prepare bundled resources' exit 1 diff --git a/script/windows/prepare_bundled_resources.ps1 b/script/windows/prepare_bundled_resources.ps1 index 1ac5b9897af..6e0183e4921 100644 --- a/script/windows/prepare_bundled_resources.ps1 +++ b/script/windows/prepare_bundled_resources.ps1 @@ -17,10 +17,7 @@ Param( [String]$DestinationDir, [Parameter(Mandatory = $false)] - [String]$Channel = '', - - [Parameter(Mandatory = $false)] - [String]$CargoProfile = '' + [String]$Channel = '' ) $ErrorActionPreference = 'Stop' @@ -162,21 +159,40 @@ foreach ($entry in $AdditionalLicenses) { # Generate settings JSON schema unless explicitly skipped. if ($env:SKIP_SETTINGS_SCHEMA -ne '1') { $SchemaOutput = Join-Path $DestinationDir 'settings_schema.json' - Write-Output "Generating settings schema at $SchemaOutput" - $SchemaCmd = @('run') - if ($CargoProfile) { - $SchemaCmd += @('--profile', $CargoProfile) - } - $SchemaCmd += @('--manifest-path', (Join-Path $RepoRoot 'Cargo.toml'), '--bin', 'generate_settings_schema', '--') - if ($Channel) { - $SchemaCmd += @('--channel', $Channel) + if ($env:SETTINGS_SCHEMA_EXECUTABLE -and $env:SETTINGS_SCHEMA_SOURCE) { + Write-Error 'SETTINGS_SCHEMA_EXECUTABLE and SETTINGS_SCHEMA_SOURCE are mutually exclusive.' + exit 1 + } elseif ($env:SETTINGS_SCHEMA_EXECUTABLE) { + if (-Not (Test-Path $env:SETTINGS_SCHEMA_EXECUTABLE -PathType Leaf)) { + Write-Error "Settings schema executable does not exist: $env:SETTINGS_SCHEMA_EXECUTABLE" + exit 1 + } + Write-Output "Generating settings schema at $SchemaOutput" + $Process = Start-Process ` + -FilePath $env:SETTINGS_SCHEMA_EXECUTABLE ` + -ArgumentList @('dump-settings-schema', $SchemaOutput) ` + -NoNewWindow ` + -PassThru ` + -Wait + if ($Process.ExitCode -ne 0) { + Write-Error "Failed to generate settings schema: executable exited with code $($Process.ExitCode)" + exit 1 + } + } elseif ($env:SETTINGS_SCHEMA_SOURCE) { + if (-Not (Test-Path $env:SETTINGS_SCHEMA_SOURCE -PathType Leaf)) { + Write-Error "Settings schema source does not exist: $env:SETTINGS_SCHEMA_SOURCE" + exit 1 + } + Write-Output "Copying settings schema to $SchemaOutput" + Copy-Item -Path $env:SETTINGS_SCHEMA_SOURCE -Destination $SchemaOutput -Force + } else { + Write-Error 'Set SETTINGS_SCHEMA_EXECUTABLE or SETTINGS_SCHEMA_SOURCE when preparing a settings schema.' + exit 1 } - $SchemaCmd += $SchemaOutput - & cargo @SchemaCmd - if (-Not $?) { - Write-Error 'Failed to generate settings schema' + if (-Not (Test-Path $SchemaOutput -PathType Leaf) -or (Get-Item $SchemaOutput).Length -eq 0) { + Write-Error "Settings schema was not written to $SchemaOutput" exit 1 } }