From 18a968c91914822600d68072b43a467465e7927b Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Tue, 30 Jun 2026 23:14:18 -0700 Subject: [PATCH 1/3] feat(cef_host): content-hash-keyed signed prebuilt on GCS (Codemagic) Replaces the ad-hoc GitHub-release + committed-manifest prebuilt with a content-hash-keyed, Developer-ID-signed cef_host published to public GCS by a private Codemagic workflow. - tool/cef_host_hash.sh: deterministic hash of the build inputs (native/cef_host + build_cef_host.sh), sourced by both fetch + publish so they can't drift. - tool/fetch_cef_host.sh: derive the GCS URL from the hash, download + sha256 verify, extract. No manifest. Fail-open on network, fail-closed on mismatch. - tool/publish-cef-host.sh: build the SANDBOXED (CEF_HOST_ADHOC=OFF, Developer-ID) variant -- the one that renders agent_ui -- hash it, idempotently upload to gs://flutterflow-downloads/campus_prebuilt_cef_host//. - codemagic.yaml: private publish pipeline (push-to-main + cef-host-v* tags); signing material + GCS service account live only in Codemagic, not the repo. - Delete cef_host_prebuilt.json + .github/workflows/release-cef-host.yml (the ad-hoc CI that shipped the agent_ui-breaking host). Keying by build-input hash makes the artifact release-model-agnostic (SHA, branch, or a future release tag all resolve to the same object) and rebuilds only when the host actually changes. Requires the `cef_host_publish` Codemagic env group (GCP SA + Developer-ID P12) -- see specs/prebuilt-cef-host/PLAN.md. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-cef-host.yml | 88 -------------- codemagic.yaml | 96 +++++++++++++++ .../flutter_cef_macos/cef_host_prebuilt.json | 12 -- .../flutter_cef_macos/tool/cef_host_hash.sh | 42 +++++++ .../flutter_cef_macos/tool/fetch_cef_host.sh | 115 ++++++++++-------- .../tool/publish-cef-host.sh | 77 ++++++++++++ specs/prebuilt-cef-host/PLAN.md | 20 +++ 7 files changed, 299 insertions(+), 151 deletions(-) delete mode 100644 .github/workflows/release-cef-host.yml create mode 100644 codemagic.yaml delete mode 100644 packages/flutter_cef_macos/cef_host_prebuilt.json create mode 100755 packages/flutter_cef_macos/tool/cef_host_hash.sh create mode 100755 packages/flutter_cef_macos/tool/publish-cef-host.sh diff --git a/.github/workflows/release-cef-host.yml b/.github/workflows/release-cef-host.yml deleted file mode 100644 index ca22a13..0000000 --- a/.github/workflows/release-cef-host.yml +++ /dev/null @@ -1,88 +0,0 @@ -# Publish a prebuilt cef_host.app when a `cef-host-v*` tag is pushed, and update the committed -# manifest (cef_host_prebuilt.json) so consumers' `pod install` fetches it. Uses only GITHUB_TOKEN -# — NO signing secrets: the published artifact is ad-hoc signed; consumers re-sign their own release -# builds with their own Developer-ID. See specs/prebuilt-cef-host/PLAN.md. -# -# TODO (follow-ups): add an x86_64 matrix leg (needs an Intel/Rosetta runner — cross-arch sign is -# unproven) and a hardened "release-compiled" variant (CEF_HOST_ADHOC=OFF drops the dev mock-keychain -# / Mach-port bypass) once the signing path is wired. -name: release-cef-host - -on: - push: - tags: ['cef-host-v*'] - workflow_dispatch: - inputs: - tag: - description: 'Existing cef-host-v* tag to (re)publish for' - required: true - -permissions: - contents: write # upload release assets + commit the manifest back - -jobs: - arm64: - runs-on: macos-14 # Apple Silicon - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.tag || github.ref_name }} - - - name: Build cef_host (ad-hoc, dev variant) - run: | - CEF_HOST_ADHOC=ON bash packages/flutter_cef_macos/native/build_cef_host.sh "$RUNNER_TEMP/out" - test -d "$RUNNER_TEMP/out/cef_host.app" - - - name: Package + checksum (with provenance) - id: pkg - run: | - TAG="${{ github.event.inputs.tag || github.ref_name }}" - SRC_SHA="$(git rev-parse HEAD)" - CEF_VER="$(grep '^CEF_VERSION=' packages/flutter_cef_macos/native/build_cef_host.sh | head -1 | cut -d'"' -f2)" - FILE="cef_host-macos-arm64-dev.tar.gz" - cd "$RUNNER_TEMP/out" - echo "$SRC_SHA" > cef_host_source_sha.txt - echo "$CEF_VER" > cef_version.txt - tar -czf "$RUNNER_TEMP/$FILE" cef_host.app cef_host_source_sha.txt cef_version.txt - SHA="$(shasum -a 256 "$RUNNER_TEMP/$FILE" | awk '{print $1}')" - echo "$SHA $FILE" > "$RUNNER_TEMP/$FILE.sha256" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "file=$FILE" >> "$GITHUB_OUTPUT" - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - echo "src=$SRC_SHA" >> "$GITHUB_OUTPUT" - echo "cef=$CEF_VER" >> "$GITHUB_OUTPUT" - - - name: Upload to the release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release upload "${{ steps.pkg.outputs.tag }}" \ - "$RUNNER_TEMP/${{ steps.pkg.outputs.file }}" \ - "$RUNNER_TEMP/${{ steps.pkg.outputs.file }}.sha256" \ - --repo "${{ github.repository }}" --clobber - - - name: Update committed manifest on the default branch - env: - GH_TOKEN: ${{ github.token }} - run: | - DEF="${{ github.event.repository.default_branch }}" - git fetch origin "$DEF" - git checkout "$DEF" - python3 - "$DEF" "${{ steps.pkg.outputs.tag }}" "${{ steps.pkg.outputs.file }}" \ - "${{ steps.pkg.outputs.sha }}" "${{ steps.pkg.outputs.src }}" "${{ steps.pkg.outputs.cef }}" <<'PY' - import json, sys - _def, tag, file, sha, src, cef = sys.argv[1:7] - p = "packages/flutter_cef_macos/cef_host_prebuilt.json" - m = json.load(open(p)) - m["version"] = tag - m["cef_version"] = cef - m["source_sha"] = src - m["base_url"] = f"https://github.com/${{ github.repository }}/releases/download/{tag}" - m.setdefault("artifacts", {})["macos-arm64-dev"] = {"file": file, "sha256": sha} - json.dump(m, open(p, "w"), indent=2); open(p, "a").write("\n") - PY - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add packages/flutter_cef_macos/cef_host_prebuilt.json - git commit -m "chore(cef): publish prebuilt cef_host ${{ steps.pkg.outputs.tag }} [skip ci]" || echo "manifest already current" - git push origin "$DEF" diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 0000000..6d50efc --- /dev/null +++ b/codemagic.yaml @@ -0,0 +1,96 @@ +# Private Codemagic pipeline for flutter_cef: builds the SANDBOXED, Developer-ID +# cef_host and publishes it to public GCS keyed by a content hash of the build +# inputs (packages/flutter_cef_macos/tool/publish-cef-host.sh). Consumers fetch +# it at `pod install` (fetch_cef_host.sh) — so `pub get` + `flutter build macos` +# is turnkey, no `make cef-host`, no FLUTTER_CEF_HOST, no drift. +# +# Signing material + the GCS service account live ONLY in Codemagic (the +# `cef_host_publish` env group), never in this public repo. Publish is +# idempotent (skips when the hashed object already exists), so a main push and a +# later tag over the same tree are safe no-ops after the first build. +workflows: + publish-cef-host: + name: Publish prebuilt cef_host (macOS arm64, Developer-ID) + instance_type: mac_mini_m2 + max_build_duration: 90 + environment: + groups: + - cef_host_publish # GOOGLE_CREDENTIALS_BODY, CAMPUS_CODESIGN_P12_BASE64, + # CAMPUS_CODESIGN_P12_PASSWORD, [SLACK_WEBHOOK_URL] + triggering: + events: + - push + - tag + branch_patterns: + - pattern: 'main' + include: true + tag_patterns: + - pattern: 'cef-host-v*' + include: true + cache: + cache_paths: + - $HOME/.cache/flutter_cef # CEF binary dist (pinned by CEF_VERSION) + scripts: + - name: Install build tools + script: | + set -euo pipefail + command -v cmake >/dev/null 2>&1 || brew install cmake + command -v ninja >/dev/null 2>&1 || brew install ninja + + - name: Set up GCP service account + script: | + set -euo pipefail + GCP_KEY_FILE="$HOME/.gcp-publisher-key.json" + printf '%s' "$GOOGLE_CREDENTIALS_BODY" > "$GCP_KEY_FILE" + chmod 600 "$GCP_KEY_FILE" + gcloud auth activate-service-account --key-file="$GCP_KEY_FILE" + echo "export GOOGLE_APPLICATION_CREDENTIALS=$GCP_KEY_FILE" >> "$CM_ENV" + + - name: Set up codesigning keychain (Developer ID) + script: | + set -euo pipefail + keychain initialize + CM_KEYCHAIN="$(security list-keychains -d user | awk -F\" 'NF>1 {print $2}' | grep codemagic-cli-tools | head -1)" + security unlock-keychain -p "" "$CM_KEYCHAIN" + security set-keychain-settings -lut 21600 "$CM_KEYCHAIN" + # Apple's Developer ID intermediates (so the imported cert chains to a trusted root). + for url in \ + https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ + https://www.apple.com/certificateauthority/DeveloperIDCA.cer; do + f="/tmp/$(basename "$url")" + curl -fsSL "$url" -o "$f" + security import "$f" -k "$CM_KEYCHAIN" 2>/dev/null || true + done + P12_PATH="$HOME/campus-developer-id.p12" + printf '%s' "$CAMPUS_CODESIGN_P12_BASE64" | base64 -D > "$P12_PATH" + security import "$P12_PATH" \ + -P "$CAMPUS_CODESIGN_P12_PASSWORD" \ + -k "$CM_KEYCHAIN" \ + -T /usr/bin/codesign -T /usr/bin/security + security unlock-keychain -p "" "$CM_KEYCHAIN" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$CM_KEYCHAIN" + rm -f "$P12_PATH" + IDENTITY="$(security find-identity -v -p codesigning | grep 'Developer ID Application' | head -1 | awk '{print $2}')" + [ -n "$IDENTITY" ] || { echo "::error:: Developer ID Application identity missing"; exit 1; } + echo "export CODESIGN_ID=$IDENTITY" >> "$CM_ENV" + + - name: Build + publish cef_host (idempotent) + script: | + set -euo pipefail + bash packages/flutter_cef_macos/tool/publish-cef-host.sh + touch "$HOME/.cef_host_publish_succeeded" + + publishing: + scripts: + - name: Notify Slack + script: | + [ -n "${SLACK_WEBHOOK_URL:-}" ] || exit 0 + BUILD_URL="https://codemagic.io/app/${CM_PROJECT_ID}/build/${CM_BUILD_ID}" + LABEL="${CM_TAG:-${CM_BRANCH:-manual}}" + if [ -f "$HOME/.cef_host_publish_succeeded" ]; then + MSG="✅ flutter_cef cef_host ${LABEL} published → ${BUILD_URL}" + else + MSG="🚨 flutter_cef cef_host ${LABEL} FAILED → ${BUILD_URL}" + fi + curl -fsS -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"${MSG}\"}" "${SLACK_WEBHOOK_URL}" || true diff --git a/packages/flutter_cef_macos/cef_host_prebuilt.json b/packages/flutter_cef_macos/cef_host_prebuilt.json deleted file mode 100644 index 081ca0b..0000000 --- a/packages/flutter_cef_macos/cef_host_prebuilt.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "v0.2.0", - "cef_version": "144.0.27+g3fae261+chromium-144.0.7559.254", - "source_sha": "3f38477928b6b6ce5831348fad2d3f06a3d58214", - "base_url": "https://github.com/FlutterFlow/flutter_cef/releases/download/cef-host-v0.2.0", - "artifacts": { - "macos-arm64-dev": { - "file": "cef_host-macos-arm64-dev.tar.gz", - "sha256": "a9eaceeb06a25097ddae8ee573b662b419753bc822ef06c15cc615c08d802f52" - } - } -} diff --git a/packages/flutter_cef_macos/tool/cef_host_hash.sh b/packages/flutter_cef_macos/tool/cef_host_hash.sh new file mode 100755 index 0000000..95db4ad --- /dev/null +++ b/packages/flutter_cef_macos/tool/cef_host_hash.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Deterministic content hash of the cef_host build inputs. +# +# Sourced by BOTH fetch_cef_host.sh (consumer, at pod install) and +# publish-cef-host.sh (CI) so they ALWAYS compute the same digest from the same +# source tree — that digest is the GCS object key, so any drift here is a silent +# cache miss. Prints a 64-hex digest to stdout. +# +# Inputs = native/build_cef_host.sh (carries CEF_VERSION + the CEF dist sha pin + +# the signing/adhoc defaults) + every source file under native/cef_host/ +# (excluding the prebuilt/ and build/ OUTPUT dirs). The huge CEF binary dist is +# NOT hashed — it is pinned transitively by build_cef_host.sh's CEF_VERSION. +# +# Usage: . cef_host_hash.sh ; cef_host_input_hash +# = .../packages/flutter_cef_macos/native +set -euo pipefail + +# sha256 of the given file(s), or of stdin when no args. Portable across bash/zsh +# (no reliance on word-splitting a command string) and macOS/Linux. +_cefhost_sha256() { + if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@"; else sha256sum "$@"; fi +} + +cef_host_input_hash() { + local native_dir="$1" + # Sorted, path-relative list of build-input files. LC_ALL=C makes the sort + # byte-stable across machines. We emit "\n\n" per file so + # BOTH content changes and path/renames move the final digest. + ( + cd "$native_dir" + export LC_ALL=C + { + printf '%s\n' "build_cef_host.sh" + find cef_host -type f \ + -not -path 'cef_host/prebuilt/*' \ + -not -path 'cef_host/build/*' + } | LC_ALL=C sort -u | while IFS= read -r f; do + printf '%s\n' "$f" + _cefhost_sha256 "$f" | awk '{print $1}' + done + ) | _cefhost_sha256 | awk '{print $1}' +} diff --git a/packages/flutter_cef_macos/tool/fetch_cef_host.sh b/packages/flutter_cef_macos/tool/fetch_cef_host.sh index 724cfe7..50ef9b8 100755 --- a/packages/flutter_cef_macos/tool/fetch_cef_host.sh +++ b/packages/flutter_cef_macos/tool/fetch_cef_host.sh @@ -1,72 +1,82 @@ #!/usr/bin/env bash -# Fetch the prebuilt, version-matched cef_host.app — run at `pod install` via the podspec's -# prepare_command. Downloads + SHA256-verifies the artifact named in cef_host_prebuilt.json, -# caches it, and extracts cef_host.app into native/cef_host/prebuilt/ where the :after_compile -# script phase embeds it. Self-locating (CWD-independent). Fail-OPEN: any problem leaves no -# prebuilt, and the build falls back to FLUTTER_CEF_HOST / build-from-source. +# Fetch a prebuilt, Developer-ID-signed cef_host.app keyed by a CONTENT HASH of +# the build inputs, from public GCS. Runs at `pod install` via the podspec's +# prepare_command; the :after_compile phase then embeds native/cef_host/prebuilt/ +# cef_host.app into the app's Contents/Frameworks. Self-locating (CWD-independent). # -# Escape hatch: FLUTTER_CEF_FROM_SOURCE=1 skips the fetch entirely (co-dev builds cef_host from -# source via native/build_cef_host.sh and points the app at it with $FLUTTER_CEF_HOST). -set -uo pipefail +# The hash (see cef_host_hash.sh) is derived from the checked-out native/cef_host +# sources + build_cef_host.sh, so it is identical to what the publisher computed — +# no committed manifest, no per-commit bookkeeping, release-model-agnostic (any +# SHA/branch/tag pin that checks out the same native sources resolves to the same +# object). Fail-OPEN on network/missing (co-dev + offline builds fall back to +# build-from-source / FLUTTER_CEF_HOST); fail-CLOSED on checksum mismatch. +set -euo pipefail +# Escape hatch: co-dev / build-from-source (native/build_cef_host.sh + a make host). if [ -n "${FLUTTER_CEF_FROM_SOURCE:-}" ]; then - echo "[flutter_cef] FLUTTER_CEF_FROM_SOURCE set — skipping prebuilt fetch (build-from-source)" + echo "[flutter_cef] FLUTTER_CEF_FROM_SOURCE set — skipping prebuilt fetch (build from source)." exit 0 fi -HERE="$(cd "$(dirname "$0")" && pwd)" # .../flutter_cef_macos/tool -PKG="$(cd "$HERE/.." && pwd)" # .../flutter_cef_macos -MANIFEST="$PKG/cef_host_prebuilt.json" -DEST="$PKG/native/cef_host/prebuilt" - -[ -f "$MANIFEST" ] || { echo "[flutter_cef] no $MANIFEST — skipping fetch"; exit 0; } +HERE="$(cd "$(dirname "$0")" && pwd)" # .../tool +PKG="$(cd "$HERE/.." && pwd)" # .../flutter_cef_macos +NATIVE="$PKG/native" +DEST="$NATIVE/cef_host/prebuilt" +# Only macos-arm64 is published today; x86_64 builds from source. case "$(uname -m)" in - arm64) arch=arm64 ;; - x86_64) arch=x86_64 ;; - *) echo "[flutter_cef] unsupported arch $(uname -m) — skipping fetch"; exit 0 ;; + arm64) arch=arm64 ;; + *) echo "[flutter_cef] arch $(uname -m) has no prebuilt cef_host — build from source."; exit 0 ;; esac -key="macos-${arch}-dev" -# Parse the manifest with python3 (present on every macOS dev box). Prints: base file sha src ver -read -r base file sha src ver </dev/null)" = "$src" ]; then - echo "[flutter_cef] prebuilt cef_host already current ($src) — skipping fetch" +# Already current? the extracted prebuilt carries the input hash it was built from. +STAMP="$DEST/cef_host_input_hash.txt" +if [ -d "$DEST/cef_host.app" ] && [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$HASH" ]; then + echo "[flutter_cef] prebuilt cef_host.app already current ($HASH) — skipping fetch." exit 0 fi -CACHE="${FLUTTER_CEF_CACHE:-$HOME/.cache/flutter_cef}/prebuilt/$src/$arch" +CACHE="${FLUTTER_CEF_CACHE:-$HOME/.cache/flutter_cef}/prebuilt/$HASH/$arch" mkdir -p "$CACHE" -tarball="$CACHE/$file" +tarball="$CACHE/$FILE" + +sha256_file() { + if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' + else sha256sum "$1" | awk '{print $1}'; fi +} + +# The expected tarball sha256 (transport integrity) lives beside the object. +# Fail-OPEN if unreachable: no published host for this hash yet (a fresh native +# change before CI publishes, or offline) -> build from source. +expected="" +if ! expected="$(curl -fsSL --retry 3 --retry-delay 1 "$SHA_URL" 2>/dev/null | awk '{print $1}')"; then + echo "[flutter_cef] no published cef_host for hash $HASH ($SHA_URL unreachable)." + echo "[flutter_cef] building from source (dev), or CI will publish it shortly." + exit 0 +fi -if [ ! -f "$tarball" ] || [ "$(shasum -a 256 "$tarball" 2>/dev/null | awk '{print $1}')" != "$sha" ]; then - echo "[flutter_cef] downloading prebuilt cef_host ($key, cef $ver)…" - if ! curl -fL --retry 3 "$base/$file" -o "$tarball.part"; then - echo "[flutter_cef] download failed — leaving no prebuilt (FLUTTER_CEF_HOST / from-source will be used)" >&2 - rm -f "$tarball.part"; exit 0 +# (Re)download on cache miss or a stale/corrupt cached tarball. +if [ ! -f "$tarball" ] || [ "$(sha256_file "$tarball")" != "$expected" ]; then + echo "[flutter_cef] downloading prebuilt cef_host: $URL" + if ! curl -fL --retry 3 --retry-delay 1 -o "$tarball.part" "$URL"; then + echo "[flutter_cef] download failed — building from source." >&2 + rm -f "$tarball.part" + exit 0 fi - got="$(shasum -a 256 "$tarball.part" | awk '{print $1}')" - if [ "$got" != "$sha" ]; then - echo "[flutter_cef] SHA256 mismatch (got $got, want $sha) — refusing the artifact" >&2 - rm -f "$tarball.part"; exit 1 + actual="$(sha256_file "$tarball.part")" + if [ "$actual" != "$expected" ]; then + echo "[flutter_cef] SHA256 mismatch for $FILE (expected $expected, got $actual) — refusing." >&2 + rm -f "$tarball.part" + exit 1 # fail-CLOSED: never embed an unverified host fi mv "$tarball.part" "$tarball" fi @@ -74,5 +84,8 @@ fi echo "[flutter_cef] extracting prebuilt cef_host -> $DEST" mkdir -p "$DEST" rm -rf "$DEST/cef_host.app" +# tar preserves the inside-out Developer-ID signature; the .app + provenance +# stamps (source_sha / version / input_hash) land in prebuilt/. tar -xzf "$tarball" -C "$DEST" -echo "[flutter_cef] prebuilt cef_host ready ($src)" +printf '%s\n' "$HASH" > "$STAMP" # stamp even if the tarball predates the field +echo "[flutter_cef] prebuilt cef_host ready ($HASH)." diff --git a/packages/flutter_cef_macos/tool/publish-cef-host.sh b/packages/flutter_cef_macos/tool/publish-cef-host.sh new file mode 100755 index 0000000..c053620 --- /dev/null +++ b/packages/flutter_cef_macos/tool/publish-cef-host.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Build the SANDBOXED (CEF_HOST_ADHOC=OFF, Developer-ID) cef_host, key it by a +# content hash of the build inputs, and idempotently publish it to public GCS. +# Run by the private flutter_cef Codemagic workflow (which holds the signing +# material + a GCS-writable service account) on push-to-main and cef-host-v* tags. +# +# The SANDBOXED variant is deliberate: the ad-hoc variant (get-task-allow + mock +# keychain + Mach-port bypass) fails to render agent_ui in a consuming app. The +# Developer-ID signature is inside-out; release consumers re-sign it with their +# own identity, so only the (rare) direct-run case depends on it. +# +# Requires: gsutil/gcloud authed with object-create on gs://$GCS_BUCKET, and a +# Developer-ID Application identity in the keychain named by $CODESIGN_ID. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" # .../tool +PKG="$(cd "$HERE/.." && pwd)" # .../flutter_cef_macos +NATIVE="$PKG/native" +REPO="$(cd "$PKG/../.." && pwd)" # repo root (git provenance) + +: "${CODESIGN_ID:?CODESIGN_ID (Developer ID Application identity) must be set}" +GCS_BUCKET="${GCS_BUCKET:-flutterflow-downloads}" +GCS_PREFIX="${GCS_PREFIX:-campus_prebuilt_cef_host}" +arch=arm64 +FILE="cef_host-macos-${arch}.tar.gz" + +# shellcheck source=cef_host_hash.sh +. "$HERE/cef_host_hash.sh" +HASH="$(cef_host_input_hash "$NATIVE")" +echo "[publish] cef_host input hash: $HASH" + +DST="gs://$GCS_BUCKET/$GCS_PREFIX/$HASH/$FILE" + +# Idempotency: this exact tree was already built + uploaded -> nothing to do. +if gsutil -q stat "$DST" 2>/dev/null; then + echo "[publish] $DST already exists — nothing to do." + exit 0 +fi + +# --- Build the sandboxed, Developer-ID-signed variant --- +OUT="$(mktemp -d)/out" +mkdir -p "$OUT" +CEF_HOST_ADHOC=OFF CODESIGN_ID="$CODESIGN_ID" bash "$NATIVE/build_cef_host.sh" "$OUT" +APP="$OUT/cef_host.app" +[ -d "$APP" ] || { echo "::error:: cef_host.app not produced by build" >&2; exit 1; } + +# Fail-fast: it must be Developer-ID signed and NOT ad-hoc. +codesign -dvv "$APP" 2>&1 | grep -q "Authority=Developer ID Application" \ + || { echo "::error:: cef_host.app is not Developer-ID signed (ad-hoc?)" >&2; exit 1; } + +# --- Provenance stamps beside the app (informational; the URL is the hash) --- +SRC_SHA="$(git -C "$REPO" rev-parse HEAD)" +CEF_VER="$(grep '^CEF_VERSION=' "$NATIVE/build_cef_host.sh" | head -1 | cut -d'"' -f2)" +printf '%s\n' "$SRC_SHA" > "$OUT/cef_host_source_sha.txt" +printf '%s\n' "$CEF_VER" > "$OUT/cef_version.txt" +printf '%s\n' "$HASH" > "$OUT/cef_host_input_hash.txt" + +# --- Tar + sha256 (COPYFILE_DISABLE keeps ._* AppleDouble junk out of the tar) --- +STAGE="$(mktemp -d)" +TARBALL="$STAGE/$FILE" +COPYFILE_DISABLE=1 tar -czf "$TARBALL" -C "$OUT" \ + cef_host.app cef_host_source_sha.txt cef_version.txt cef_host_input_hash.txt +if command -v shasum >/dev/null 2>&1; then + TAR_SHA="$(shasum -a 256 "$TARBALL" | awk '{print $1}')" +else + TAR_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')" +fi +printf '%s %s\n' "$TAR_SHA" "$FILE" > "$TARBALL.sha256" + +# --- Upload (re-check to close a publish race; objects are immutable) --- +if gsutil -q stat "$DST" 2>/dev/null; then + echo "[publish] $DST appeared during build — skipping upload." + exit 0 +fi +gsutil -h "Cache-Control:public,max-age=31536000,immutable" cp "$TARBALL" "$DST" +gsutil -h "Cache-Control:public,max-age=31536000,immutable" cp "$TARBALL.sha256" "$DST.sha256" +echo "[publish] uploaded $DST (tarball sha256 $TAR_SHA)" diff --git a/specs/prebuilt-cef-host/PLAN.md b/specs/prebuilt-cef-host/PLAN.md index 092753a..e2a7423 100644 --- a/specs/prebuilt-cef-host/PLAN.md +++ b/specs/prebuilt-cef-host/PLAN.md @@ -1,5 +1,25 @@ # Prebuilt, auto-bundled cef_host — make flutter_cef "just a Flutter package" +## Status — content-hash + GCS + Codemagic (supersedes the manifest/GitHub-release model) + +The prebuilt is now **keyed by a content hash of the build inputs** (`native/cef_host/` + +`build_cef_host.sh`) and served from **public GCS**, published by a **private Codemagic +workflow** — not by a committed manifest + ad-hoc GitHub release. Why: the artifact +decouples from how flutter_cef versions itself (any SHA/branch/tag pin that checks out the +same native sources resolves to the same object), a Dart-only change rebuilds nothing, and +the signing material stays in Codemagic (never in this public repo). The published variant +is the **sandboxed, Developer-ID** host (`CEF_HOST_ADHOC=OFF`) — the ad-hoc variant fails to +render `agent_ui` in a consumer, which is why the earlier manifest/ad-hoc approach was not +adopted downstream. + +- `tool/cef_host_hash.sh` — the deterministic hash, sourced by both sides so they can't drift. +- `tool/fetch_cef_host.sh` — consumer fetch: hash → `https://storage.googleapis.com/flutterflow-downloads/campus_prebuilt_cef_host//cef_host-macos-arm64.tar.gz`, sha256-verify, extract. Fail-open on network, fail-closed on mismatch. +- `tool/publish-cef-host.sh` — CI: build sandboxed+signed → hash → idempotent upload. +- `codemagic.yaml` — the private publish pipeline (push-to-main + `cef-host-v*` tags). + +The design detail below (embedding, signing, incremental rollout) still stands; only the +publish/fetch transport changed from "committed manifest + `gh release`" to "content-hash + GCS". + ## Problem The Dart/Swift half of flutter_cef is already a normal pod. The native `cef_host.app` (a nested SIGNED app: ~200MB Chromium + 5 helper apps) is NOT — every consumer must From fe8347ff81a8e975edc0b2ea15b272faaf959f8d Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Tue, 30 Jun 2026 23:30:39 -0700 Subject: [PATCH 2/3] feat(cef_host): add `make publish-cef-host` + robust signature check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Makefile target auto-resolves the Developer ID Application identity and runs tool/publish-cef-host.sh — the local zero-CI publish, run when cef_host changes. - Harden the publish signature check: capture codesign output and string-match instead of piping into grep under `set -o pipefail` (codesign -dvv can exit non-zero on a valid signature, which false-failed the check). Validated end-to-end against a staging GCS prefix: build sandboxed + Developer-ID host -> hash -> upload -> anonymous public-read fetch -> sha256-verify -> extract (Developer-ID confirmed); fetch and publish both idempotent. Co-Authored-By: Claude Opus 4.8 --- Makefile | 20 +++++++++++++++++++ .../tool/publish-cef-host.sh | 14 ++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0aedd4a --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# flutter_cef — developer convenience targets. +# +# publish-cef-host: build the SANDBOXED (CEF_HOST_ADHOC=OFF), Developer-ID cef_host +# and publish it to GCS keyed by a content hash of the build inputs. Run this when +# native/cef_host/ or the CEF version changes so consumers can fetch a matching host +# (fetch_cef_host.sh) instead of building from source. Idempotent — re-running with +# an unchanged host is a no-op. Needs a Developer ID Application identity in your +# keychain and gsutil write on gs://flutterflow-downloads (auto-resolves the +# identity; override CODESIGN_ID / GCS_PREFIX to customize). +# +# make publish-cef-host +# GCS_PREFIX=campus_prebuilt_cef_host-staging make publish-cef-host # dry-run to staging + +CODESIGN_ID ?= $(shell security find-identity -v -p codesigning | grep 'Developer ID Application' | head -1 | awk '{print $$2}') + +.PHONY: publish-cef-host +publish-cef-host: + @test -n "$(CODESIGN_ID)" || { echo "error: no 'Developer ID Application' identity in the keychain"; exit 1; } + @command -v gsutil >/dev/null 2>&1 || { echo "error: gsutil not found (install the Google Cloud SDK)"; exit 1; } + CODESIGN_ID="$(CODESIGN_ID)" bash packages/flutter_cef_macos/tool/publish-cef-host.sh diff --git a/packages/flutter_cef_macos/tool/publish-cef-host.sh b/packages/flutter_cef_macos/tool/publish-cef-host.sh index c053620..48710b7 100755 --- a/packages/flutter_cef_macos/tool/publish-cef-host.sh +++ b/packages/flutter_cef_macos/tool/publish-cef-host.sh @@ -44,9 +44,17 @@ CEF_HOST_ADHOC=OFF CODESIGN_ID="$CODESIGN_ID" bash "$NATIVE/build_cef_host.sh" " APP="$OUT/cef_host.app" [ -d "$APP" ] || { echo "::error:: cef_host.app not produced by build" >&2; exit 1; } -# Fail-fast: it must be Developer-ID signed and NOT ad-hoc. -codesign -dvv "$APP" 2>&1 | grep -q "Authority=Developer ID Application" \ - || { echo "::error:: cef_host.app is not Developer-ID signed (ad-hoc?)" >&2; exit 1; } +# Fail-fast: it must be Developer-ID signed and NOT ad-hoc. Capture the output +# (|| true) and string-match rather than piping into grep under `set -o pipefail` +# — codesign -dvv can exit non-zero on a perfectly valid signature, which would +# false-fail a `codesign … | grep` pipeline. +sig="$(codesign -dvv "$APP" 2>&1 || true)" +case "$sig" in + *"Developer ID Application"*) : ;; + *) echo "::error:: cef_host.app is not Developer-ID signed (ad-hoc?):" >&2 + printf '%s\n' "$sig" | head -3 >&2 + exit 1 ;; +esac # --- Provenance stamps beside the app (informational; the URL is the hash) --- SRC_SHA="$(git -C "$REPO" rev-parse HEAD)" From 09d7b9253328757f4d2395dc45cc0dc2549b55ac Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Tue, 30 Jun 2026 23:31:48 -0700 Subject: [PATCH 3/3] chore(cef_host): drop the inert codemagic.yaml; publish via `make publish-cef-host` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flutter_cef Codemagic app isn't available, so publishing is the local `make publish-cef-host` target (zero CI; the host changes rarely). Removed the non-functional codemagic.yaml — publish-cef-host.sh stays CI-agnostic for any future automation in a repo that holds the signing + GCS creds. Doc note updated. Co-Authored-By: Claude Opus 4.8 --- codemagic.yaml | 96 --------------------------------- specs/prebuilt-cef-host/PLAN.md | 22 ++++---- 2 files changed, 12 insertions(+), 106 deletions(-) delete mode 100644 codemagic.yaml diff --git a/codemagic.yaml b/codemagic.yaml deleted file mode 100644 index 6d50efc..0000000 --- a/codemagic.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# Private Codemagic pipeline for flutter_cef: builds the SANDBOXED, Developer-ID -# cef_host and publishes it to public GCS keyed by a content hash of the build -# inputs (packages/flutter_cef_macos/tool/publish-cef-host.sh). Consumers fetch -# it at `pod install` (fetch_cef_host.sh) — so `pub get` + `flutter build macos` -# is turnkey, no `make cef-host`, no FLUTTER_CEF_HOST, no drift. -# -# Signing material + the GCS service account live ONLY in Codemagic (the -# `cef_host_publish` env group), never in this public repo. Publish is -# idempotent (skips when the hashed object already exists), so a main push and a -# later tag over the same tree are safe no-ops after the first build. -workflows: - publish-cef-host: - name: Publish prebuilt cef_host (macOS arm64, Developer-ID) - instance_type: mac_mini_m2 - max_build_duration: 90 - environment: - groups: - - cef_host_publish # GOOGLE_CREDENTIALS_BODY, CAMPUS_CODESIGN_P12_BASE64, - # CAMPUS_CODESIGN_P12_PASSWORD, [SLACK_WEBHOOK_URL] - triggering: - events: - - push - - tag - branch_patterns: - - pattern: 'main' - include: true - tag_patterns: - - pattern: 'cef-host-v*' - include: true - cache: - cache_paths: - - $HOME/.cache/flutter_cef # CEF binary dist (pinned by CEF_VERSION) - scripts: - - name: Install build tools - script: | - set -euo pipefail - command -v cmake >/dev/null 2>&1 || brew install cmake - command -v ninja >/dev/null 2>&1 || brew install ninja - - - name: Set up GCP service account - script: | - set -euo pipefail - GCP_KEY_FILE="$HOME/.gcp-publisher-key.json" - printf '%s' "$GOOGLE_CREDENTIALS_BODY" > "$GCP_KEY_FILE" - chmod 600 "$GCP_KEY_FILE" - gcloud auth activate-service-account --key-file="$GCP_KEY_FILE" - echo "export GOOGLE_APPLICATION_CREDENTIALS=$GCP_KEY_FILE" >> "$CM_ENV" - - - name: Set up codesigning keychain (Developer ID) - script: | - set -euo pipefail - keychain initialize - CM_KEYCHAIN="$(security list-keychains -d user | awk -F\" 'NF>1 {print $2}' | grep codemagic-cli-tools | head -1)" - security unlock-keychain -p "" "$CM_KEYCHAIN" - security set-keychain-settings -lut 21600 "$CM_KEYCHAIN" - # Apple's Developer ID intermediates (so the imported cert chains to a trusted root). - for url in \ - https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ - https://www.apple.com/certificateauthority/DeveloperIDCA.cer; do - f="/tmp/$(basename "$url")" - curl -fsSL "$url" -o "$f" - security import "$f" -k "$CM_KEYCHAIN" 2>/dev/null || true - done - P12_PATH="$HOME/campus-developer-id.p12" - printf '%s' "$CAMPUS_CODESIGN_P12_BASE64" | base64 -D > "$P12_PATH" - security import "$P12_PATH" \ - -P "$CAMPUS_CODESIGN_P12_PASSWORD" \ - -k "$CM_KEYCHAIN" \ - -T /usr/bin/codesign -T /usr/bin/security - security unlock-keychain -p "" "$CM_KEYCHAIN" - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$CM_KEYCHAIN" - rm -f "$P12_PATH" - IDENTITY="$(security find-identity -v -p codesigning | grep 'Developer ID Application' | head -1 | awk '{print $2}')" - [ -n "$IDENTITY" ] || { echo "::error:: Developer ID Application identity missing"; exit 1; } - echo "export CODESIGN_ID=$IDENTITY" >> "$CM_ENV" - - - name: Build + publish cef_host (idempotent) - script: | - set -euo pipefail - bash packages/flutter_cef_macos/tool/publish-cef-host.sh - touch "$HOME/.cef_host_publish_succeeded" - - publishing: - scripts: - - name: Notify Slack - script: | - [ -n "${SLACK_WEBHOOK_URL:-}" ] || exit 0 - BUILD_URL="https://codemagic.io/app/${CM_PROJECT_ID}/build/${CM_BUILD_ID}" - LABEL="${CM_TAG:-${CM_BRANCH:-manual}}" - if [ -f "$HOME/.cef_host_publish_succeeded" ]; then - MSG="✅ flutter_cef cef_host ${LABEL} published → ${BUILD_URL}" - else - MSG="🚨 flutter_cef cef_host ${LABEL} FAILED → ${BUILD_URL}" - fi - curl -fsS -X POST -H 'Content-type: application/json' \ - --data "{\"text\":\"${MSG}\"}" "${SLACK_WEBHOOK_URL}" || true diff --git a/specs/prebuilt-cef-host/PLAN.md b/specs/prebuilt-cef-host/PLAN.md index e2a7423..3780b7f 100644 --- a/specs/prebuilt-cef-host/PLAN.md +++ b/specs/prebuilt-cef-host/PLAN.md @@ -3,19 +3,21 @@ ## Status — content-hash + GCS + Codemagic (supersedes the manifest/GitHub-release model) The prebuilt is now **keyed by a content hash of the build inputs** (`native/cef_host/` + -`build_cef_host.sh`) and served from **public GCS**, published by a **private Codemagic -workflow** — not by a committed manifest + ad-hoc GitHub release. Why: the artifact -decouples from how flutter_cef versions itself (any SHA/branch/tag pin that checks out the -same native sources resolves to the same object), a Dart-only change rebuilds nothing, and -the signing material stays in Codemagic (never in this public repo). The published variant -is the **sandboxed, Developer-ID** host (`CEF_HOST_ADHOC=OFF`) — the ad-hoc variant fails to -render `agent_ui` in a consumer, which is why the earlier manifest/ad-hoc approach was not -adopted downstream. +`build_cef_host.sh`) and served from **public GCS** — not by a committed manifest + ad-hoc +GitHub release. Why: the artifact decouples from how flutter_cef versions itself (any +SHA/branch/tag pin that checks out the same native sources resolves to the same object), a +Dart-only change rebuilds nothing, and the signing material never touches this public repo. +The published variant is the **sandboxed, Developer-ID** host (`CEF_HOST_ADHOC=OFF`) — the +ad-hoc variant fails to render `agent_ui` in a consumer, which is why the earlier +manifest/ad-hoc approach was not adopted downstream. - `tool/cef_host_hash.sh` — the deterministic hash, sourced by both sides so they can't drift. - `tool/fetch_cef_host.sh` — consumer fetch: hash → `https://storage.googleapis.com/flutterflow-downloads/campus_prebuilt_cef_host//cef_host-macos-arm64.tar.gz`, sha256-verify, extract. Fail-open on network, fail-closed on mismatch. -- `tool/publish-cef-host.sh` — CI: build sandboxed+signed → hash → idempotent upload. -- `codemagic.yaml` — the private publish pipeline (push-to-main + `cef-host-v*` tags). +- `tool/publish-cef-host.sh` — build sandboxed+signed → hash → idempotent GCS upload. CI-agnostic. +- `make publish-cef-host` — the current publisher: run it locally when `cef_host` changes + (auto-resolves the Developer-ID identity; needs `gsutil` write). Since the host changes + rarely, this is enough; `publish-cef-host.sh` is CI-ready if automation is wanted later + (e.g. a step in a repo you control that already holds signing + GCS creds). The design detail below (embedding, signing, incremental rollout) still stands; only the publish/fetch transport changed from "committed manifest + `gh release`" to "content-hash + GCS".