diff --git a/.agents/skills/apple-notarization-setup/SKILL.md b/.agents/skills/apple-notarization-setup/SKILL.md new file mode 100644 index 0000000..9188cc8 --- /dev/null +++ b/.agents/skills/apple-notarization-setup/SKILL.md @@ -0,0 +1,169 @@ +--- +name: apple-notarization-setup +description: Use when setting up or verifying Apple Developer ID signing credentials, Apple app-specific passwords, kinko secret storage, or local macOS notarization readiness for chilla without recording credential values. +--- + +# Apple Notarization Setup + +Use this for chilla macOS signing/notarization setup before local deploys or release builds. Keep all credential values out of logs, skill files, commits, and final responses. + +## Credential Safety + +- Never print, paste, commit, or summarize actual Apple passwords, app-specific passwords, certificate passwords, private keys, `.p12` contents, or kinko secret values. +- It is acceptable to mention secret key names such as `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, and `APPLE_SIGNING_IDENTITY`. +- When a private login, passkey, or 2FA step is needed, use Computer Use to navigate to the prompt, then ask the user to enter it directly in the browser or system dialog. +- Use `kinko exec --env ...` for commands that need secrets. Do not use commands that echo exported secret values. + +## Required Local Inputs + +The local notarization path expects: + +- A valid Developer ID Application certificate imported into the macOS login keychain. +- `APPLE_SIGNING_IDENTITY` stored in kinko. +- `APPLE_ID` stored in kinko. +- `APPLE_TEAM_ID` stored in kinko. +- `APPLE_PASSWORD` stored in kinko as an Apple app-specific password. + +Check presence only: + +```bash +kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc ' +for key in APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do + if [ -n "${!key:-}" ]; then echo "$key=present"; else echo "$key=missing"; fi +done +' +``` + +Check the local certificate: + +```bash +security find-identity -v -p codesigning +``` + +Expect a valid `Developer ID Application` identity matching `APPLE_SIGNING_IDENTITY`. + +## App-Specific Password Flow + +Use Computer Use for browser navigation when requested. + +1. Open `https://account.apple.com/account/manage`. +2. Ask the user to complete Apple Account login, passkey, and 2FA directly in Safari. +3. Open `Sign-In and Security`. +4. Open `App-Specific Passwords`. +5. Generate a password with a label such as `Chilla`. +6. Store it in kinko as `APPLE_PASSWORD`. +7. Do not include the generated password in chat, commits, or files. + +Use kinko storage: + +```bash +kinko set-key APPLE_PASSWORD --value '' +``` + +After storage, avoid showing the value again. If the browser dialog is still open, close it after confirming the password is stored. + +## Local Build And Notarization + +Prefer the existing macOS release skill and tasks for actual packaging. The common local command is: + +```bash +kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- task bundle-macos-dmg +``` + +This builds: + +- `target/release/bundle/macos/chilla.app` +- `target/release/bundle/dmg/*.dmg` after notarization and DMG packaging complete + +Cargo commands must use `CARGO_TERM_QUIET=true` when invoked directly. + +## Nix Environment Notarytool Workaround + +In this repository's Nix shell, `xcrun` may point at the Nix Apple SDK and fail with: + +```text +tool 'notarytool' not found +``` + +Do not replace the whole build environment with the system Xcode `DEVELOPER_DIR`; that can break Nix SDK linking. Instead, create a temporary `notarytool` wrapper and keep the Nix build environment intact: + +```bash +tmp_notary_dir=/tmp/chilla-notarytool-wrapper +mkdir -p "$tmp_notary_dir" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'exec /Applications/Xcode.app/Contents/Developer/usr/bin/notarytool "$@"' \ + > "$tmp_notary_dir/notarytool" +chmod 700 "$tmp_notary_dir/notarytool" + +kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc ' + export PATH="/tmp/chilla-notarytool-wrapper:$PATH" + task bundle-macos-dmg +' +``` + +If `/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool` is unavailable, check: + +```bash +/usr/bin/mdfind 'kMDItemFSName == "notarytool"' +``` + +## Notarization Status + +When Tauri submits notarization, record only the submission ID and status. To check status: + +```bash +kinko exec --env APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc ' +/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool info \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" +' +``` + +Look for: + +```text +status: Accepted +``` + +Recent submissions: + +```bash +kinko exec --env APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc ' +/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool history \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" +' +``` + +If a submission stays `In Progress`, do not claim deployment is complete. Report that Apple has not returned a decision yet. + +## Validation After Acceptance + +After notarization is accepted and artifacts exist: + +```bash +codesign --verify --deep --strict --verbose=2 target/release/bundle/macos/chilla.app +xcrun stapler validate target/release/bundle/macos/chilla.app +xcrun stapler validate target/release/bundle/dmg/*.dmg +spctl --assess --type execute --verbose=4 target/release/bundle/macos/chilla.app +spctl --assess --type open --context context:primary-signature --verbose=4 target/release/bundle/dmg/*.dmg +``` + +If `xcrun stapler` cannot find tools due to the Nix SDK, call the Xcode tool directly when appropriate: + +```bash +/Applications/Xcode.app/Contents/Developer/usr/bin/stapler validate target/release/bundle/macos/chilla.app +``` + +## Completion Criteria + +Local Apple setup is complete when: + +- kinko has all required Apple secret keys present. +- `security find-identity` reports a valid Developer ID Application identity. +- `task bundle-macos-dmg` or the wrapper-based equivalent signs the app. +- Notarization reaches `Accepted`. +- Stapler and Gatekeeper validation pass for the app and DMG. diff --git a/.agents/skills/apple-notarization-setup/agents/openai.yaml b/.agents/skills/apple-notarization-setup/agents/openai.yaml new file mode 100644 index 0000000..b662028 --- /dev/null +++ b/.agents/skills/apple-notarization-setup/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Apple Notarization Setup" + short_description: "Set up Apple signing and notarization prerequisites for chilla" + default_prompt: "Set up or verify Apple Developer ID signing, app-specific password storage, and local notarization readiness for chilla without exposing credential values." diff --git a/flake.lock b/flake.lock index b07ceed..aa3cfae 100644 --- a/flake.lock +++ b/flake.lock @@ -162,6 +162,22 @@ "type": "github" } }, + "nixpkgs-webkit": { + "locked": { + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_2": { "locked": { "lastModified": 1751290243, @@ -185,7 +201,8 @@ "fenix": "fenix", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs_2", - "nixpkgs-unstable": "nixpkgs-unstable" + "nixpkgs-unstable": "nixpkgs-unstable", + "nixpkgs-webkit": "nixpkgs-webkit" } }, "rust-analyzer-src": { diff --git a/flake.nix b/flake.nix index d4440c4..1eaf634 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/release-24.11"; + nixpkgs-webkit.url = "github:NixOS/nixpkgs/nixos-24.05"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; fenix = { @@ -20,6 +21,7 @@ { self, nixpkgs, + nixpkgs-webkit, nixpkgs-unstable, flake-utils, fenix, @@ -32,6 +34,7 @@ let overlays = [ fenix.overlays.default ]; pkgs = import nixpkgs { inherit system overlays; }; + pkgs-webkit = import nixpkgs-webkit { inherit system; }; pkgs-unstable = import nixpkgs-unstable { inherit system; }; lib = pkgs.lib; bun = pkgs-unstable.bun; @@ -59,7 +62,9 @@ ); }; - linuxGuiLibraries = with pkgs; [ + linuxGuiPkgs = if pkgs.stdenv.isLinux then pkgs-webkit else pkgs; + + linuxGuiLibraries = with linuxGuiPkgs; [ atk cairo gdk-pixbuf @@ -75,10 +80,10 @@ # - gstreamer itself for coreelements like typefind/fakesink # - base/good/bad/ugly/libav for container/codec support # - pipewire so autoaudiosink can resolve a compatible pipewiresink - linuxGStreamerCorePackage = pkgs.gst_all_1.gstreamer.out; + linuxGStreamerCorePackage = linuxGuiPkgs.gst_all_1.gstreamer.out; linuxGStreamerPluginPackages = - (with pkgs.gst_all_1; [ + (with linuxGuiPkgs.gst_all_1; [ gst-plugins-base gst-plugins-good gst-plugins-bad @@ -87,25 +92,26 @@ ]) ++ [ linuxGStreamerCorePackage - pkgs.pipewire + linuxGuiPkgs.pipewire ]; linuxGStreamerPluginPath = lib.makeSearchPath "lib/gstreamer-1.0" linuxGStreamerPluginPackages; linuxGStreamerPluginScanner = "${linuxGStreamerCorePackage}/libexec/gstreamer-1.0/gst-plugin-scanner"; - linuxWebkitMediaLibraries = with pkgs; [ + linuxWebkitMediaLibraries = with linuxGuiPkgs; [ ffmpeg libpulseaudio alsa-lib pipewire ]; + gitRuntimePath = lib.makeBinPath [ pkgs.git ]; linuxRuntimeLibraryPath = if pkgs.stdenv.isLinux then lib.makeLibraryPath ( linuxGuiLibraries - ++ (with pkgs.gst_all_1; [ + ++ (with linuxGuiPkgs.gst_all_1; [ linuxGStreamerCorePackage gst-plugins-base ]) @@ -113,11 +119,11 @@ ) else lib.makeLibraryPath linuxGuiLibraries; - linuxGioModulePath = lib.makeSearchPath "lib/gio/modules" (with pkgs; [ + linuxGioModulePath = lib.makeSearchPath "lib/gio/modules" (with linuxGuiPkgs; [ dconf.lib glib-networking ]); - linuxXdgDataDirs = lib.concatStringsSep ":" (with pkgs; [ + linuxXdgDataDirs = lib.concatStringsSep ":" (with linuxGuiPkgs; [ "${gsettings-desktop-schemas}/share/gsettings-schemas/${gsettings-desktop-schemas.name}" "${gtk3}/share/gsettings-schemas/${gtk3.name}" "${shared-mime-info}/share" @@ -188,7 +194,7 @@ src = tauriBuildSource; cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml"; buildInputs = commonBuildInputs; - nativeBuildInputs = with pkgs; [ pkg-config ]; + nativeBuildInputs = with pkgs; [ git pkg-config ]; }; chilla = craneLib.buildPackage { @@ -199,6 +205,7 @@ cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml"; buildInputs = commonBuildInputs; nativeBuildInputs = with pkgs; [ + git makeWrapper pkg-config ]; @@ -217,6 +224,7 @@ postFixup = lib.optionalString pkgs.stdenv.isLinux '' wrapProgram $out/bin/chilla \ + --prefix PATH : "${gitRuntimePath}" \ --prefix LD_LIBRARY_PATH : "${linuxRuntimeLibraryPath}" \ --set GIO_EXTRA_MODULES "${linuxGioModulePath}" \ --set XDG_DATA_DIRS "${linuxXdgDataDirs}" \ @@ -250,6 +258,7 @@ openssl pkg-config taplo + git gh go-task ] @@ -269,7 +278,19 @@ inherit cargoArtifacts; cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml"; buildInputs = commonBuildInputs; - nativeBuildInputs = with pkgs; [ pkg-config ]; + nativeBuildInputs = with pkgs; [ git pkg-config ]; + preBuild = '' + # Tauri caches build-script outputs with absolute OUT_DIR paths. + # When crane reuses cargoArtifacts across derivations, those paths + # point at the previous sandbox and break permission generation. + rm -rf target/release/build/tauri-* + rm -rf target/release/build/tauri-build-* + rm -rf target/release/build/tauri-plugin-* + rm -rf target/release/build/tauri-runtime-* + rm -rf target/release/build/tauri-runtime-wry-* + rm -rf target/release/build/tauri-utils-* + rm -rf target/release/build/chilla-* + ''; cargoClippyExtraArgs = "--all-targets -- -D warnings"; }; diff --git a/scripts/run-tauri-e2e-linux.sh b/scripts/run-tauri-e2e-linux.sh index 9f1a210..cdfc69b 100755 --- a/scripts/run-tauri-e2e-linux.sh +++ b/scripts/run-tauri-e2e-linux.sh @@ -22,8 +22,12 @@ fi export CARGO_TERM_QUIET=true export GDK_BACKEND="${GDK_BACKEND:-x11}" +export EGL_PLATFORM="${EGL_PLATFORM:-x11}" +export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}" export GSK_RENDERER="${GSK_RENDERER:-cairo}" export LIBGL_ALWAYS_SOFTWARE="${LIBGL_ALWAYS_SOFTWARE:-1}" +export LIBGL_KOPPER_DISABLE="${LIBGL_KOPPER_DISABLE:-true}" +export MESA_LOADER_DRIVER_OVERRIDE="${MESA_LOADER_DRIVER_OVERRIDE:-llvmpipe}" export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-1}" export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}" export CHILLA_TAURI_E2E_REPO_ROOT="$repo_root" diff --git a/tests/tauri/tauri-smoke.e2e.ts b/tests/tauri/tauri-smoke.e2e.ts index 4c89565..2292eed 100644 --- a/tests/tauri/tauri-smoke.e2e.ts +++ b/tests/tauri/tauri-smoke.e2e.ts @@ -39,8 +39,12 @@ const FIXTURE_MP4_NAME = "file_example_MP4_480_1_5MG.mp4"; const SHUTDOWN_TIMEOUT_MS = 5_000; const HEADLESS_RENDER_ENV_EXPORTS = [ 'export GDK_BACKEND="${GDK_BACKEND:-x11}"', + 'export EGL_PLATFORM="${EGL_PLATFORM:-x11}"', + 'export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"', 'export GSK_RENDERER="${GSK_RENDERER:-cairo}"', 'export LIBGL_ALWAYS_SOFTWARE="${LIBGL_ALWAYS_SOFTWARE:-1}"', + 'export LIBGL_KOPPER_DISABLE="${LIBGL_KOPPER_DISABLE:-true}"', + 'export MESA_LOADER_DRIVER_OVERRIDE="${MESA_LOADER_DRIVER_OVERRIDE:-llvmpipe}"', 'export NO_AT_BRIDGE="${NO_AT_BRIDGE:-1}"', 'export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-1}"', 'export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"', @@ -273,9 +277,14 @@ async function verifyWorkspaceLoads( until.elementLocated(By.css(".file-browser__path")), STARTUP_TIMEOUT_MS, ); - const pathText = await pathElement.getText(); + let pathText = ""; + await waitUntil(currentDriver, async () => { + pathText = await pathElement.getText(); + + return pathText.includes(expectedWorkspaceRoot); + }); - if (!pathText.includes(expectedWorkspaceRoot)) { + if (pathText.length === 0 || !pathText.includes(expectedWorkspaceRoot)) { throw new Error( `Expected workspace path to include ${expectedWorkspaceRoot}, got ${JSON.stringify(pathText)}`, );