diff --git a/.github/scripts/smoke-warp-macos-app.sh b/.github/scripts/smoke-warp-macos-app.sh new file mode 100644 index 000000000..c87297612 --- /dev/null +++ b/.github/scripts/smoke-warp-macos-app.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euxo pipefail + +app="${1:?usage: smoke-warp-macos-app.sh [label]}" +label="${2:-warp-oss}" +slug="$(printf '%s' "$label" | tr -c '[:alnum:]._-' '-')" +binary="$app/Contents/MacOS/warp-oss" +stdout="${RUNNER_TEMP:?}/${slug}.stdout" +stderr="${RUNNER_TEMP:?}/${slug}.stderr" +log="${HOME:?}/Library/Logs/warp-oss.log" + +dump_stream() { + local title="$1" + local path="$2" + + echo "=== $title ===" + sed -n '1,200p' "$path" || true +} + +test -d "$app" +test -x "$binary" +test -s "$app/Contents/Info.plist" +test -s "$app/Contents/Resources/settings_schema.json" + +mkdir -p "$HOME/Library/Logs" +"$binary" >"$stdout" 2>"$stderr" & +pid="$!" +echo "Started $label with pid $pid" + +runtime_seen=0 +for _ in $(seq 1 20); do + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + + runtime_seen=1 + if [ -s "$log" ]; then + break + fi + sleep 1 +done + +if kill -0 "$pid" 2>/dev/null; then + kill "$pid" || true +fi +wait "$pid" || true + +dump_stream "$label stdout" "$stdout" +dump_stream "$label stderr" "$stderr" +echo "=== $label log ===" +if [ -s "$log" ]; then + tail -n 200 "$log" +else + echo "No warp-oss.log was written." +fi + +if [ "$runtime_seen" -ne 1 ] && [ ! -s "$log" ]; then + echo "$label neither stayed alive briefly nor wrote a log file" >&2 + exit 1 +fi diff --git a/.github/workflows/verify-macos-flake-gha.yml b/.github/workflows/verify-macos-flake-gha.yml new file mode 100644 index 000000000..390236c46 --- /dev/null +++ b/.github/workflows/verify-macos-flake-gha.yml @@ -0,0 +1,127 @@ +name: Verify macOS Warp flake + +on: + push: + branches: + - verify-macos-flake-gha + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-launch: + name: Build and launch ${{ matrix.system }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + system: aarch64-darwin + - runner: macos-15-intel + system: x86_64-darwin + + steps: + - name: Checkout sources + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Show macOS toolchain + run: | + set -euxo pipefail + sw_vers + xcodebuild -version + xcode-select -p + xcrun --sdk macosx --find metal + xcrun --sdk macosx --find metallib + + - name: Install Nix + uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31.7.0 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Warp macOS app + id: build + run: | + set -euxo pipefail + nix --version + nix config show system + out="$(nix build ".#packages.${{ matrix.system }}.warp-terminal" --print-out-paths -L --accept-flake-config)" + echo "out=$out" >> "$GITHUB_OUTPUT" + + - name: Inspect app bundle + run: | + set -euxo pipefail + out="${{ steps.build.outputs.out }}" + app="$out/Applications/WarpOss.app" + binary="$app/Contents/MacOS/warp-oss" + + test -d "$app" + test -x "$binary" + test -s "$app/Contents/Info.plist" + test -s "$app/Contents/Resources/settings_schema.json" + + file "$binary" + plutil -p "$app/Contents/Info.plist" + otool -L "$binary" | sed -n '1,160p' + + - name: Launch app binary briefly + env: + HOME: ${{ runner.temp }}/warp-home + run: | + bash .github/scripts/smoke-warp-macos-app.sh \ + "${{ steps.build.outputs.out }}/Applications/WarpOss.app" \ + "warp-oss" + + - name: Package DMG + id: package + run: | + set -euxo pipefail + out="${{ steps.build.outputs.out }}" + app="$out/Applications/WarpOss.app" + dmg_dir="$RUNNER_TEMP/warp-dmg" + staging="$RUNNER_TEMP/warp-dmg-staging" + dmg="$dmg_dir/WarpOss-${{ matrix.system }}.dmg" + + rm -rf "$dmg_dir" "$staging" + mkdir -p "$dmg_dir" "$staging" + ditto "$app" "$staging/WarpOss.app" + ln -s /Applications "$staging/Applications" + + hdiutil create \ + -volname "WarpOss ${{ matrix.system }}" \ + -srcfolder "$staging" \ + -ov \ + -format UDZO \ + "$dmg" + ls -lh "$dmg" + echo "dmg=$dmg" >> "$GITHUB_OUTPUT" + + - name: Validate DMG contents and launch + env: + HOME: ${{ runner.temp }}/warp-dmg-home + run: | + set -euxo pipefail + dmg="${{ steps.package.outputs.dmg }}" + mount="$RUNNER_TEMP/warp-dmg-mount" + + rm -rf "$mount" + mkdir -p "$mount" + hdiutil attach -nobrowse -readonly -mountpoint "$mount" "$dmg" + trap 'hdiutil detach "$mount" || true' EXIT + + bash .github/scripts/smoke-warp-macos-app.sh \ + "$mount/WarpOss.app" \ + "DMG warp-oss" + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: warp-oss-dmg-${{ matrix.system }} + path: ${{ steps.package.outputs.dmg }} + if-no-files-found: error diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..0a9baf3be --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "warpProtoApis": "warpProtoApis", + "warpWorkflows": "warpWorkflows" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777432579, + "narHash": "sha256-Ce11TStDsqCge2vAAfLKe2+4lDI5cSX5ZYZOuKJBKKQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "3ecb5e6ab380ced3272ef7fcfe398bffbcc0f152", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "warpProtoApis": { + "flake": false, + "locked": { + "lastModified": 1776900698, + "narHash": "sha256-8bB/tCLIzRCofMK1rYCe8bizUr1U4A6f6uVeckJJKI4=", + "owner": "warpdotdev", + "repo": "warp-proto-apis", + "rev": "78a78f21a75432bf0141e396fb318bf1694e47f0", + "type": "github" + }, + "original": { + "owner": "warpdotdev", + "repo": "warp-proto-apis", + "type": "github" + } + }, + "warpWorkflows": { + "flake": false, + "locked": { + "lastModified": 1776965078, + "narHash": "sha256-ICgkxlUUIfyhr0agZEk3KtGHX0uNRlRCKtz0iF2jd7o=", + "owner": "warpdotdev", + "repo": "workflows", + "rev": "793a98ddda6ef19682aed66364faebd2829f0e01", + "type": "github" + }, + "original": { + "owner": "warpdotdev", + "repo": "workflows", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..206229231 --- /dev/null +++ b/flake.nix @@ -0,0 +1,309 @@ +{ + description = "Warp is an agentic development environment, born out of the terminal (Experimental Nix Support, Linux-only)."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + warpProtoApis = { + url = "github:warpdotdev/warp-proto-apis"; + flake = false; + }; + warpWorkflows = { + url = "github:warpdotdev/workflows"; + flake = false; + }; + }; + + outputs = + { + self, + nixpkgs, + rust-overlay, + warpProtoApis, + warpWorkflows, + ... + }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + lib = pkgs.lib; + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + rustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + appCargoToml = builtins.fromTOML (builtins.readFile ./app/Cargo.toml); + version = "${appCargoToml.package.version}+${self.shortRev or "dirty"}"; + cargoDeps = pkgs.runCommand "warp-terminal-experimental-${version}-vendor" { } '' + cp -R ${ + rustPlatform.fetchCargoVendor { + src = self; + name = "warp-terminal-experimental-${version}"; + hash = "sha256-TzYSC82HVRhCxBHLmHw8BIZ4hJKCZfp+s/mfbeAjdQ4="; + } + }/. "$out" + chmod -R u+w "$out" + + # warp_multi_agent_api expects sibling .proto files from a full + # checkout, so point it at the pinned source tree fetched by Nix. + protoCrate="$(dirname "$(find "$out" -path '*/warp_multi_agent_api-0.0.0/Cargo.toml' -print -quit)")" + if [ -z "$protoCrate" ] || [ "$protoCrate" = "." ]; then + echo "could not find vendored warp_multi_agent_api crate" >&2 + exit 1 + fi + substituteInPlace "$protoCrate/build.rs" \ + --replace-fail \ + 'let proto_path = manifest_dir.parent().unwrap().parent().unwrap();' \ + 'let proto_path = std::path::PathBuf::from("${warpProtoApis}/apis/multi_agent/v1");' + + # warp-workflows expects ../specs from a full checkout, so point it + # at the pinned source tree fetched by Nix. + workflowCrate="$(dirname "$(find "$out" -path '*/warp-workflows-0.1.0/Cargo.toml' -print -quit)")" + if [ -z "$workflowCrate" ] || [ "$workflowCrate" = "." ]; then + echo "could not find vendored warp-workflows crate" >&2 + exit 1 + fi + substituteInPlace "$workflowCrate/build.rs" \ + --replace-fail \ + 'println!("cargo:rerun-if-changed=../specs");' \ + 'println!("cargo:rerun-if-changed=${warpWorkflows}/specs");' \ + --replace-fail \ + 'for entry in WalkDir::new("../specs") {' \ + 'for entry in WalkDir::new("${warpWorkflows}/specs") {' + ''; + + linuxRuntimeLibraries = with pkgs; [ + alsa-lib + curl + dbus + expat + fontconfig + freetype + libGL + libgit2 + libxkbcommon + openssl + stdenv.cc.cc.lib + udev + vulkan-loader + wayland + libx11 + libxscrnsaver + libxcursor + libxext + libxfixes + libxi + libxrandr + libxrender + libxcb + zlib + ]; + + buildFeatures = [ + "release_bundle" + "gui" + "nld_improvements" + ]; + + warp-terminal-experimental = rustPlatform.buildRustPackage { + pname = "warp-terminal-experimental"; + inherit version; + + src = self; + inherit cargoDeps; + + nativeBuildInputs = with pkgs; [ + brotli + cargo-about + clang + cmake + jq + makeWrapper + patchelf + pkg-config + protobuf + python3 + ]; + + buildInputs = linuxRuntimeLibraries; + + cargoBuildFlags = [ + "-p" + "warp" + "--bin" + "warp-oss" + "--bin" + "generate_settings_schema" + ]; + inherit buildFeatures; + + # The application test suite is large and GUI/integration-heavy; this + # flake's package check is the Nix build plus a launch smoke test. + doCheck = false; + + env = { + APPIMAGE_NAME = "WarpOss-${pkgs.stdenv.hostPlatform.parsed.cpu.name}.AppImage"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + PROTOC = "${pkgs.protobuf}/bin/protoc"; + PROTOC_INCLUDE = "${pkgs.protobuf}/include"; + CARGO_PROFILE_RELEASE_DEBUG = "false"; + }; + postInstall = + let + installDir = "$out/opt/warpdotdev/warp-terminal-experimental"; + resourcesDir = "${installDir}/resources"; + releaseChannel = "stable"; + libraryPath = lib.makeLibraryPath linuxRuntimeLibraries; + executablePath = lib.makeBinPath (with pkgs; [ xdg-utils ]); + in + '' + install -Dm755 "$out/bin/warp-oss" "${installDir}/warp-oss" + rm -f "$out/bin/warp-oss" + + patchShebangs \ + ./script/prepare_bundled_resources \ + ./script/copy_conditional_skills + + SKIP_SETTINGS_SCHEMA=1 ./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" + + install -Dm644 \ + "${resourcesDir}/THIRD_PARTY_LICENSES.txt" \ + "$out/share/licenses/warp-terminal-experimental/THIRD_PARTY_LICENSES.txt" + + install -Dm644 LICENSE-AGPL "$out/share/licenses/warp-terminal-experimental/LICENSE-AGPL" + install -Dm644 LICENSE-MIT "$out/share/licenses/warp-terminal-experimental/LICENSE-MIT" + + install -Dm644 app/channels/oss/dev.warp.WarpOss.desktop \ + "$out/share/applications/dev.warp.WarpOss.desktop" + substituteInPlace "$out/share/applications/dev.warp.WarpOss.desktop" \ + --replace-fail "Exec=warp-oss %U" "Exec=warp-terminal-experimental %U" + + for size in 16x16 32x32 64x64 128x128 256x256 512x512; do + icon="app/channels/oss/icon/no-padding/$size.png" + if [ -f "$icon" ]; then + install -Dm644 "$icon" \ + "$out/share/icons/hicolor/$size/apps/dev.warp.WarpOss.png" + fi + done + + wrapProgram "${installDir}/warp-oss" \ + --prefix LD_LIBRARY_PATH : "${libraryPath}" \ + --prefix PATH : "${executablePath}" + + mkdir -p "$out/bin" + ln -s "${installDir}/warp-oss" "$out/bin/warp-oss" + ln -s "${installDir}/warp-oss" "$out/bin/warp-terminal-experimental" + ''; + + postFixup = lib.optionalString pkgs.stdenv.isLinux '' + wrapped="/opt/warpdotdev/warp-terminal-experimental/.warp-oss-wrapped" + if [ -e "$out$wrapped" ] && ! patchelf --print-needed "$out$wrapped" | grep -q '^libfontconfig\.so\.1$'; then + patchelf --add-needed libfontconfig.so.1 "$out$wrapped" + fi + ''; + + meta = { + description = "Warp is an agentic development environment, born out of the terminal (Experimental Nix Support, Linux-only)."; + homepage = "https://www.warp.dev"; + license = lib.licenses.agpl3Only; + mainProgram = "warp-terminal-experimental"; + platforms = systems; + sourceProvenance = with lib.sourceTypes; [ fromSource ]; + }; + }; + in + { + inherit warp-terminal-experimental; + } + ); + + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + lib = pkgs.lib; + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + nativeBuildInputs = with pkgs; [ + brotli + cargo-about + cargo-nextest + clang + cmake + jq + lld + makeWrapper + patchelf + pkg-config + protobuf + python3 + rustToolchain + rust-analyzer + ]; + buildInputs = with pkgs; [ + alsa-lib + curl + dbus + expat + fontconfig + freetype + libGL + libgit2 + libxkbcommon + openssl + stdenv.cc.cc.lib + udev + vulkan-loader + wayland + libx11 + libxscrnsaver + libxcursor + libxext + libxfixes + libxi + libxrandr + libxrender + libxcb + zlib + ]; + in + { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + APPIMAGE_NAME = "WarpOss-${pkgs.stdenv.hostPlatform.parsed.cpu.name}.AppImage"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + PROTOC = "${pkgs.protobuf}/bin/protoc"; + PROTOC_INCLUDE = "${pkgs.protobuf}/include"; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + }; + } + ); + + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt); + }; +}