Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/release-cef-host.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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"
2 changes: 1 addition & 1 deletion example/macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral

SPEC CHECKSUMS:
flutter_cef_macos: f4ec14a9d75c0a198b6c8ba620ec4e56d4ad22f0
flutter_cef_macos: 663fd8075939eb7efa312c8900630e16bc3c0063
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1

PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_cef_macos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
native/cef_host/prebuilt/
12 changes: 12 additions & 0 deletions packages/flutter_cef_macos/cef_host_prebuilt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,16 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin {
return env
}
let inner = "/cef_host.app/Contents/MacOS/cef_host"
// The plugin podspec's :after_compile script phase embeds cef_host.app into THIS framework's
// Versions/A/Frameworks (a pod build phase can't reach the app's top-level Contents/Frameworks —
// it runs before the app bundle exists). The framework exposes no top-level `Frameworks` symlink,
// so reach it through `Versions/Current/Frameworks`. This makes a prebuilt-bundled host resolve
// with zero consumer wiring. The Contents/Frameworks + Contents/Helpers probes remain for a
// consumer that copies the host to the app's top level itself.
let fwFrameworks = Bundle(for: FlutterCefPlugin.self).bundleURL
.appendingPathComponent("Versions/Current/Frameworks").path
for base in [
fwFrameworks,
Bundle(for: FlutterCefPlugin.self).resourceURL?.path,
Bundle.main.bundlePath + "/Contents/Frameworks",
Bundle.main.bundlePath + "/Contents/Helpers",
Expand Down
42 changes: 38 additions & 4 deletions packages/flutter_cef_macos/macos/flutter_cef_macos.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,42 @@ rendering when off-screen. macOS only.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
s.resource_bundles = {'flutter_cef_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
# cef_host.app is bundled into the host .app by the app target, not the pod
# (a pod script-phase runs before the app bundle exists). For dev, point the
# app at it via $FLUTTER_CEF_HOST; for a distributable build, add a Run Script
# phase calling tool/bundle_cef_host.sh — see the README.

# Fetch the prebuilt, version-matched cef_host.app at `pod install` (downloads + SHA256-verifies
# the artifact named in cef_host_prebuilt.json into native/cef_host/prebuilt/). Fail-open + cached;
# FLUTTER_CEF_FROM_SOURCE=1 skips it for co-dev. The :after_compile phase below embeds whatever
# lands there, so `flutter pub get` + `flutter build macos` is turnkey with no make/host steps.
s.prepare_command = 'bash ../tool/fetch_cef_host.sh'

# Auto-embed cef_host.app into the consuming app's Contents/Frameworks. cef_host.app is a
# nested SIGNED app (Chromium + 5 helper apps) — CocoaPods can't auto-embed a nested .app the
# way it does a .framework (resource_bundles would nest it inside this pod's framework and break
# its seal; vendored_frameworks only embeds .framework). So we copy it ourselves in an
# :after_compile script phase, which runs DURING `flutter build macos` AFTER the app bundle +
# `[CP] Embed Pods Frameworks` exist and BEFORE Xcode's codesign — the moment the destination
# Contents/Frameworks is real (the old "a pod script-phase runs before the app bundle exists"
# was only true for :before_compile). ditto (never cp -R) preserves the prebuilt's inside-out
# signatures. The prebuilt is fetched at `pod install` by prepare_command (see fetch_cef_host.sh)
# into native/cef_host/prebuilt/. When absent (co-dev from-source, or FLUTTER_CEF_HOST set) this
# is a clean no-op — the runtime resolver falls back to FLUTTER_CEF_HOST / a make-built host.
s.script_phase = {
:name => 'Embed cef_host.app',
:execution_position => :after_compile,
:script => <<-SCRIPT
set -e
PREBUILT="${PODS_TARGET_SRCROOT}/../native/cef_host/prebuilt/cef_host.app"
DEST_DIR="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
echo "[flutter_cef] embed: PODS_TARGET_SRCROOT=${PODS_TARGET_SRCROOT}"
echo "[flutter_cef] embed: prebuilt=${PREBUILT}"
echo "[flutter_cef] embed: dest=${DEST_DIR}/cef_host.app"
if [ -d "${PREBUILT}" ]; then
mkdir -p "${DEST_DIR}"
rm -rf "${DEST_DIR}/cef_host.app"
ditto "${PREBUILT}" "${DEST_DIR}/cef_host.app"
echo "[flutter_cef] embedded cef_host.app into Contents/Frameworks"
else
echo "[flutter_cef] no prebuilt cef_host.app; skipping (co-dev from-source / FLUTTER_CEF_HOST path)"
fi
SCRIPT
}
end
78 changes: 78 additions & 0 deletions packages/flutter_cef_macos/tool/fetch_cef_host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/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.
#
# 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

if [ -n "${FLUTTER_CEF_FROM_SOURCE:-}" ]; then
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; }

case "$(uname -m)" in
arm64) arch=arm64 ;;
x86_64) arch=x86_64 ;;
*) echo "[flutter_cef] unsupported arch $(uname -m) — skipping fetch"; 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 <<EOF
$(python3 - "$MANIFEST" "$key" <<'PY'
import json, sys
m = json.load(open(sys.argv[1])); art = m.get("artifacts", {}).get(sys.argv[2])
if not art:
print("NONE NONE NONE NONE NONE")
else:
print(m["base_url"], art["file"], art["sha256"], m.get("source_sha", ""), m.get("cef_version", ""))
PY
)
EOF

if [ "$base" = "NONE" ] || [ -z "$base" ]; then
echo "[flutter_cef] no prebuilt for $key in manifest — skipping (build from source)"
exit 0
fi

# Already current? (the tarball carries cef_host_source_sha.txt next to cef_host.app)
if [ -d "$DEST/cef_host.app" ] && [ -f "$DEST/cef_host_source_sha.txt" ] \
&& [ "$(cat "$DEST/cef_host_source_sha.txt" 2>/dev/null)" = "$src" ]; then
echo "[flutter_cef] prebuilt cef_host already current ($src) — skipping fetch"
exit 0
fi

CACHE="${FLUTTER_CEF_CACHE:-$HOME/.cache/flutter_cef}/prebuilt/$src/$arch"
mkdir -p "$CACHE"
tarball="$CACHE/$file"

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
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
fi
mv "$tarball.part" "$tarball"
fi

echo "[flutter_cef] extracting prebuilt cef_host -> $DEST"
mkdir -p "$DEST"
rm -rf "$DEST/cef_host.app"
tar -xzf "$tarball" -C "$DEST"
echo "[flutter_cef] prebuilt cef_host ready ($src)"
74 changes: 74 additions & 0 deletions specs/prebuilt-cef-host/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Prebuilt, auto-bundled cef_host — make flutter_cef "just a Flutter package"

## 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
**build it from source** (cmake/ninja + a CEF SDK fetch) and **manually copy + sign** it
into `Contents/Frameworks` via make/scripts. A plain `flutter build macos` produces a
broken app (no cef_host → crashes ~30s in). This is the recurring "bundle the right cef"
pain, and it created a silent pin-drift class.

## Goal
A consumer gets a working app from `flutter pub get` + a normal `flutter build macos`:
- **debug/local**: zero extra steps, no `make cef-host`, no `FLUTTER_CEF_HOST`.
- **release**: one inside-out Developer-ID re-sign (consumer identity — irreducible).
- no pin-drift: one plugin version ⇒ exactly one cef_host.

## Mechanism (the two load-bearing decisions)
1. **EMBED** — a podspec `script_phase` with `execution_position: :after_compile` that
`ditto`s the prebuilt into `${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/cef_host.app`.
This runs DURING `flutter build macos`, AFTER `[CP] Embed Pods Frameworks` creates
`Contents/Frameworks`, BEFORE Xcode's codesign. (`resource_bundles` nests it inside the
pod framework — wrong place + breaks the framework seal; `vendored_frameworks` only
embeds `.framework`, not a nested `.app`. So `:after_compile` script_phase it is.)
The runtime resolver already probes `Contents/Frameworks` (FlutterCefPlugin.swift:691).
2. **FETCH** — a podspec `prepare_command` (runs at `pod install`) downloads + SHA256-verifies
a version-matched prebuilt from a GitHub release asset, driven by a committed manifest
`cef_host_prebuilt.json` (tag, urls, the four sha256s, source_sha, cef_version).

## Signing
- **dev**: embed the ad-hoc / get-task-allow variant; non-hardened debug runtime tolerates the
unsealed nested app. Do NOT re-sign the main app (strips the Firebase Auth keychain entitlement).
- **release**: consumer re-signs the embedded tree in place with its own Developer-ID, inside-out
(helpers → CEF framework Versions/A dylibs + Versions/A → root). Exactly today's
release-macos-firebase.sh:888-901, just no longer preceded by a build. The prebuilt's own
signature is throwaway (overwritten).

## Co-dev escape hatch (retained, additive — prebuilt is default)
`FLUTTER_CEF_HOST` env still wins over the bundled prebuilt (probed first). `build_cef_host.sh`,
`bundle_cef_host.sh`, and the consumer `make cef-host` loop all keep working for native hackers.

## Increments (each independently shippable + verifiable)
- **INC 0** — de-risk the embedding. Hand-build a cef_host.app → `native/cef_host/prebuilt/`,
add ONLY the `:after_compile` script_phase, `flutter build macos` the example, assert cef_host.app
lands at `Contents/Frameworks` (and exactly one, nowhere nested). Validate the actual phase order
in the generated Runner.xcodeproj; fall back to a consumer Run Script if CocoaPods won't guarantee
`:after_compile` after embed-frameworks. **This proves the whole approach.**
- **INC 1** — dev turnkey: add a dev (get-task-allow) variant; build work_canvas with a path-override,
no `make cef-host`, no `FLUTTER_CEF_HOST` → app survives past 30s, tiles render.
- **INC 2** — release: rewire release-macos-firebase.sh to delete the build+ditto block, keep only the
inside-out re-sign pointed at the embedded host; produce a notarized DMG + Gatekeeper-verify.
- **INC 3** — fetch automation: `tool/fetch_cef_host.sh` + `cef_host_prebuilt.json` + podspec
`prepare_command`; hand-upload tarballs to a `v0.2.x` GitHub release; verify turnkey from a clean
machine (no cmake/ninja). Escape hatches still win.
- **INC 4** — publishing CI: `.github/workflows/release-cef-host.yml` on `v*` — arm64 (+x86_64)
matrix, build both variants, inside-out sign + standalone-notarize the release variant, stamp
provenance, tar+sha256, `gh release upload` + GCS mirror, commit the manifest back.
- **INC 5** — consumer cleanup: delete the Makefile `cef-host` target/vars, the open-firebase manual
bundle, the release-script build block, the cmake/ninja preflight; retire cef-doctor ASSERT 2 and
repoint ASSERT 1's host-match to the fetched `cef_host_source_sha.txt`.

## Open risks (validate as we go)
- **CocoaPods phase ordering** (the linchpin): `:after_compile` must run after embed-frameworks +
before codesign. Validate in INC 0; fall back to a one-time consumer Run Script if not guaranteed.
- **resourceURL vs Frameworks**: resolver probes the pod resourceURL before Contents/Frameworks —
ensure no cef_host fragment leaks into the pod resource bundle.
- **git-dep prepare_command**: confirm it runs for a git-sourced flutter_cef and the fetched prebuilt
survives Flutter's `.symlinks/plugins` copy into the build.
- **x86_64 cross-build/sign/notarize** unproven — may ship arm64-only first, keep from-source for x64.
- **notarization** of the consumer-resigned host (prebuilt's stapled ticket is invalidated by the
re-sign — verify the whole-app notarize still passes in INC 2).
- **manifest source_sha staleness**: a consumer pinned to an untagged commit fetches the last tag's
prebuilt; cef-doctor's repointed assert must allow `tag <= resolved && IPC-compatible`, not strict sha.
- **artifact size**: ~293MB × 4 per tag — cache `~/.cache/flutter_cef/prebuilt/<tag>`; consider
arm64-only default.
Loading