authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-19 00:47:02-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-19 00:47:02-04:00
log67e2fe71d937b45384a53c4e0addd9a167aed885
tree28db89d13db7dbdfea3af102172a78c3bb1ed851
parent3b8e7bbbe0c49b00630f0c89e931056df679a650
signature Signed by PGP key B5690EEEBB952194

fix(marketing): never serve the Intel build to Apple Silicon Macs (#7477)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

4 files changed, 30 insertions(+), 69 deletions(-)

apps/marketing/src/lib/macArch.test.ts deleted-23
......@@ -1,23 +0,0 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { macArchFromGpuRenderer } from "./macArch";
4
5describe("macArchFromGpuRenderer", () => {
6 it("detects explicit Apple Silicon renderers", () => {
7 expect(macArchFromGpuRenderer("Apple M4 Pro")).toBe("arm64");
8 });
9
10 it("prefers an Intel GPU marker even when the renderer also mentions Apple", () => {
11 expect(macArchFromGpuRenderer("ANGLE Metal Renderer: Apple, Intel Iris Plus Graphics")).toBe(
12 "x64",
13 );
14 });
15
16 it("detects Safari's generic Apple Silicon renderer", () => {
17 expect(macArchFromGpuRenderer("Apple GPU")).toBe("arm64");
18 });
19
20 it("uses x64 when the renderer is unavailable", () => {
21 expect(macArchFromGpuRenderer("")).toBe("x64");
22 });
23});
apps/marketing/src/lib/macArch.ts deleted-14
......@@ -1,14 +0,0 @@
1const INTEL_GPU_PATTERN = /intel|amd|radeon|nvidia|geforce/i;
2const APPLE_SILICON_GPU_PATTERN = /\bapple\s+(?:m\d|gpu)\b/i;
3
4export function macArchFromGpuRenderer(renderer: string): "arm64" | "x64" {
5 if (INTEL_GPU_PATTERN.test(renderer)) {
6 return "x64";
7 }
8 if (APPLE_SILICON_GPU_PATTERN.test(renderer)) {
9 return "arm64";
10 }
11
12 // Keep the fallback compatible with Intel Macs when WebGL is unavailable.
13 return "x64";
14}
apps/marketing/src/pages/download.astro+23-6
......@@ -24,11 +24,10 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
2424 <span class="card-arch">Apple Silicon (arm64)</span>
2525 <span class="card-format">.dmg</span>
2626 </a>
27 <a class="download-card" data-asset="x64.dmg" href={RELEASES_URL}>
28 <span class="card-arch">Intel (x64)</span>
29 <span class="card-format">.dmg</span>
30 </a>
3127 </div>
28 <p class="intel-note">
29 On an Intel Mac? <a data-asset="x64.dmg" href={RELEASES_URL}>Download the x64 build</a>.
30 </p>
3231 </section>
3332
3433 <!-- Windows -->
......@@ -92,8 +91,8 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
9291
9392 async function init() {
9493 const versionLabel = document.getElementById("version-label");
95 // Only release-asset cards; mobile store cards have no data-asset and keep their href.
96 const cards = document.querySelectorAll<HTMLAnchorElement>(".download-card[data-asset]");
94 // Only release-asset links; mobile store cards have no data-asset and keep their href.
95 const cards = document.querySelectorAll<HTMLAnchorElement>("a[data-asset]");
9796
9897 try {
9998 const release = await fetchLatestRelease();
......@@ -257,6 +256,24 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
257256 color: var(--fg-dim);
258257 }
259258
259 .intel-note {
260 font-size: 0.8rem;
261 color: var(--fg-dim);
262 }
263
264 .intel-note a {
265 color: var(--fg-muted);
266 text-decoration: underline;
267 text-decoration-color: rgba(161, 161, 170, 0.4);
268 text-underline-offset: 3px;
269 transition: color 0.3s ease, text-decoration-color 0.3s ease;
270 }
271
272 .intel-note a:hover {
273 color: var(--fg);
274 text-decoration-color: var(--fg);
275 }
276
260277 /* ── Releases link ── */
261278
262279 .releases-link {
apps/marketing/src/pages/index.astro+7-26
......@@ -391,46 +391,27 @@ const mobileEndorsementRows = [
391391
392392<script>
393393 import { fetchLatestRelease, RELEASES_URL, type ReleaseAsset } from "../lib/releases";
394 import { macArchFromGpuRenderer } from "../lib/macArch";
395394
396 type Platform = { os: "mac" | "win" | "linux"; label: string; arch?: string };
397
398 // navigator.userAgent reports "Intel Mac OS X" on both Intel and Apple Silicon
399 // Macs, so probe the GPU renderer instead: Apple Silicon exposes an "Apple" GPU.
400 function detectMacArch(): "arm64" | "x64" {
401 try {
402 const canvas = document.createElement("canvas");
403 const gl = (canvas.getContext("webgl") ||
404 canvas.getContext("experimental-webgl")) as WebGLRenderingContext | null;
405 const debugInfo = gl?.getExtension("WEBGL_debug_renderer_info");
406 if (gl && debugInfo) {
407 const renderer = String(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) ?? "");
408 return macArchFromGpuRenderer(renderer);
409 }
410 } catch {
411 // fall through to the default below
412 }
413 return macArchFromGpuRenderer("");
414 }
395 type Platform = { os: "mac" | "win" | "linux"; label: string };
415396
416397 function detectPlatform(): Platform | null {
417398 const ua = navigator.userAgent;
418399 if (/Win/i.test(ua)) return { os: "win", label: "Download for Windows" };
419 if (/Mac/i.test(ua)) {
420 return { os: "mac", label: "Download for macOS", arch: detectMacArch() };
421 }
400 if (/Mac/i.test(ua)) return { os: "mac", label: "Download for macOS" };
422401 if (/Linux/i.test(ua)) return { os: "linux", label: "Download for Linux" };
423402 return null;
424403 }
425404
405 // Macs always get the arm64 build. Browsers cannot reliably tell Apple Silicon
406 // from Intel (the UA lies, GPU strings lie), and a wrong guess hands an Apple
407 // Silicon user a broken app. Intel users pick their build on /download.
408 // Do NOT add arch detection back here.
426409 function pickAsset(assets: ReleaseAsset[], platform: Platform): string | null {
427410 if (platform.os === "win") {
428411 return assets.find((a) => a.name.endsWith("-x64.exe"))?.browser_download_url ?? null;
429412 }
430413 if (platform.os === "mac") {
431 const preferred = assets.find((a) => a.name.endsWith(`-${platform.arch}.dmg`));
432 const fallback = assets.find((a) => a.name.endsWith(".dmg"));
433 return (preferred ?? fallback)?.browser_download_url ?? null;
414 return assets.find((a) => a.name.endsWith("-arm64.dmg"))?.browser_download_url ?? null;
434415 }
435416 if (platform.os === "linux") {
436417 return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null;