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";...@@ -24,11 +24,10 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
24 <span class="card-arch">Apple Silicon (arm64)</span>24 <span class="card-arch">Apple Silicon (arm64)</span>
25 <span class="card-format">.dmg</span>25 <span class="card-format">.dmg</span>
26 </a>26 </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>
31 </div>27 </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>
32 </section>31 </section>
3332
34 <!-- Windows -->33 <!-- Windows -->
...@@ -92,8 +91,8 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";...@@ -92,8 +91,8 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
9291
93 async function init() {92 async function init() {
94 const versionLabel = document.getElementById("version-label");93 const versionLabel = document.getElementById("version-label");
95 // Only release-asset cards; mobile store cards have no data-asset and keep their href.94 // Only release-asset links; mobile store cards have no data-asset and keep their href.
96 const cards = document.querySelectorAll<HTMLAnchorElement>(".download-card[data-asset]");95 const cards = document.querySelectorAll<HTMLAnchorElement>("a[data-asset]");
9796
98 try {97 try {
99 const release = await fetchLatestRelease();98 const release = await fetchLatestRelease();
...@@ -257,6 +256,24 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";...@@ -257,6 +256,24 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
257 color: var(--fg-dim);256 color: var(--fg-dim);
258 }257 }
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
260 /* ── Releases link ── */277 /* ── Releases link ── */
261278
262 .releases-link {279 .releases-link {
apps/marketing/src/pages/index.astro+7-26
...@@ -391,46 +391,27 @@ const mobileEndorsementRows = [...@@ -391,46 +391,27 @@ const mobileEndorsementRows = [
391391
392<script>392<script>
393 import { fetchLatestRelease, RELEASES_URL, type ReleaseAsset } from "../lib/releases";393 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 };395 type Platform = { os: "mac" | "win" | "linux"; label: 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 }
415396
416 function detectPlatform(): Platform | null {397 function detectPlatform(): Platform | null {
417 const ua = navigator.userAgent;398 const ua = navigator.userAgent;
418 if (/Win/i.test(ua)) return { os: "win", label: "Download for Windows" };399 if (/Win/i.test(ua)) return { os: "win", label: "Download for Windows" };
419 if (/Mac/i.test(ua)) {400 if (/Mac/i.test(ua)) return { os: "mac", label: "Download for macOS" };
420 return { os: "mac", label: "Download for macOS", arch: detectMacArch() };
421 }
422 if (/Linux/i.test(ua)) return { os: "linux", label: "Download for Linux" };401 if (/Linux/i.test(ua)) return { os: "linux", label: "Download for Linux" };
423 return null;402 return null;
424 }403 }
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.
426 function pickAsset(assets: ReleaseAsset[], platform: Platform): string | null {409 function pickAsset(assets: ReleaseAsset[], platform: Platform): string | null {
427 if (platform.os === "win") {410 if (platform.os === "win") {
428 return assets.find((a) => a.name.endsWith("-x64.exe"))?.browser_download_url ?? null;411 return assets.find((a) => a.name.endsWith("-x64.exe"))?.browser_download_url ?? null;
429 }412 }
430 if (platform.os === "mac") {413 if (platform.os === "mac") {
431 const preferred = assets.find((a) => a.name.endsWith(`-${platform.arch}.dmg`));414 return assets.find((a) => a.name.endsWith("-arm64.dmg"))?.browser_download_url ?? null;
432 const fallback = assets.find((a) => a.name.endsWith(".dmg"));
433 return (preferred ?? fallback)?.browser_download_url ?? null;
434 }415 }
435 if (platform.os === "linux") {416 if (platform.os === "linux") {
436 return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null;417 return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null;