Skip to content

fix(desktop): install native CLI on Apple Silicon - #2093

Open
ysfscream wants to merge 4 commits into
mainfrom
lukeyu/fix-cli-arm64
Open

ysfscream wants to merge 4 commits into
mainfrom
lukeyu/fix-cli-arm64

Conversation

@ysfscream

@ysfscream ysfscream commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

What is the current behavior?

On an Apple Silicon Mac running the native ARM64 Desktop app without Rosetta, Desktop's Install CLI action fails with ./applet: Bad CPU type in executable. The macOS permission helper bundled with sudo-prompt@9.2.1 contains i386 and x86_64 code, but no ARM64 code. In this case, Desktop already selects the ARM64 CLI; the failure occurs when starting the permission helper.

Separately, an Intel Desktop app running under Rosetta selects the x64 CLI because os.arch() reports the process architecture. The x64 CLI can run in this environment because Rosetta is present, but users receive a translated binary.

Issue Number

Reported by Ivan in Slack. Companion Homebrew fix: emqx/homebrew-mqttx#3 (Refs emqx/homebrew-mqttx#1).

What is the new behavior?

  • Fix the installation failure by replacing sudo-prompt with @vscode/sudo-prompt@9.3.2, whose permission helper contains both ARM64 and x86_64 code.
  • Prefer the native ARM64 CLI when Electron reports that the macOS Desktop app is running under ARM64 translation. This changes which binary is downloaded in the Rosetta case.

Native ARM64 Desktop apps and Intel Macs retain their existing CLI architecture selection. Windows and Linux behavior is unchanged.

Does this PR introduce a breaking change?

  • Yes
  • No

Specific Instructions

Verify the permission dialog and completed CLI installation using the native ARM64 Desktop app on an Apple Silicon Mac without Rosetta.

Other information

Validation:

  • Two direct getCLIArch unit tests passed through the normal yarn test:unit runner, covering Rosetta and native architectures.
  • TypeScript checks passed; the installer passes ESLint with --max-warnings 0.
  • Full desktop lint completed with zero errors and 176 warnings in unmodified files.
  • Unsigned ARM64 Desktop build passed.
  • Extracted the new permission helper and verified its ARM64/x86_64 slices, then confirmed the same helper is included in the built app.asar.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 05:55
@ysfscream ysfscream added this to the v1.13.2 milestone Oct 9, 2026
@ysfscream ysfscream added fix Fix bug or issues desktop MQTTX Desktop labels Oct 9, 2026
@ysfscream ysfscream self-assigned this Oct 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The native macOS permission dialog still requires verification on Apple Silicon without Rosetta.

0 open findings

What changed in this PR

Updates desktop CLI installation for Apple Silicon compatibility.

Changes:

  • Replaces sudo-prompt with the universal @vscode/sudo-prompt.
  • Detects Rosetta translation to download the ARM64 CLI.
  • Adds cross-platform architecture regression tests.
File Description
src/​main/​installCLI.ts Selects ARM64 under Rosetta and uses the new elevation package.
tests/​unit/​main/​installCLI.spec.js Covers macOS, Windows, and Linux architecture selection.
package.json Replaces the elevation dependency.
yarn.lock Locks the new dependency version.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ysfscream ysfscream left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks right to me. I checked both packages:

  • The helper app in sudo-prompt@9.2.1 only contains i386 and x86_64 code, so it can't run on an Apple Silicon Mac without Rosetta.
  • The helper app in @vscode/sudo-prompt@9.3.2 contains both x86_64 and arm64 code, and its API is the same.
  • app.runningUnderARM64Translation is a documented Electron API. The Electron docs suggest using it for exactly this case.

I left a few inline comments about simplifying the change. The main one is the test file: it is much more complex than the one line it tests.

One note on the PR description: the Rosetta case is not a crash. An Intel build can only run if Rosetta is installed, and then the x64 CLI also runs. That part only gets users a native binary. The crash only happens with the native arm64 desktop app on a Mac without Rosetta. Could you split these two cases in the description, so readers know which change fixes the reported error?

Comment thread tests/unit/main/installCLI.spec.js Outdated
const vm = require('vm')
const ts = require('typescript')

async function requestedBinary(platform, arch, translated = false) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test compiles the TypeScript file by hand, runs it in a vm sandbox, and fakes every import. That is a lot of setup for one line of logic. It will also break when someone adds any new import to installCLI.ts, because the fake require throws Unexpected import.

A simpler way: move the decision into a small exported function and test that function directly. The test config already replaces electron and electron-store with mocks (see vue.config.js), so a normal import should work:

import { expect } from 'chai'
import { getCLIArch } from '@/main/installCLI'

describe('getCLIArch', () => {
  it('uses arm64 when an x64 build runs under Rosetta', () => {
    expect(getCLIArch('x64', true)).to.equal('arm64')
  })
  it('keeps the process architecture otherwise', () => {
    expect(getCLIArch('x64', false)).to.equal('x64')
    expect(getCLIArch('arm64', false)).to.equal('arm64')
  })
})

That cuts about 78 lines down to about 12. The Windows and Linux URL checks can go too: this PR doesn't change how those URLs are built.

The line is simple enough that dropping the test file would also be fine. The real risk is the password dialog on actual hardware, and a unit test can't check that anyway.

Comment thread src/main/installCLI.ts Outdated
const platform = os.platform()
const isWindows = platform === 'win32'
const isMacOS = platform === 'darwin'
const arch = isMacOS && app.runningUnderARM64Translation ? 'arm64' : os.arch()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take the test suggestion, this becomes:

/**
 * An x64 build running under Rosetta is still on an ARM64 Mac, so it should get the native ARM64 CLI.
 */
export function getCLIArch(arch: string, isTranslated: boolean): string {
  return isTranslated ? 'arm64' : arch
}
const arch = getCLIArch(os.arch(), isMacOS && app.runningUnderARM64Translation)

The isMacOS && check stays here, so Windows and Linux never read the property. Otherwise, the current one-line version is fine as it is.

Comment thread src/main/installCLI.ts
platform: os.platform(),
arch: os.arch(),
}
export default async function installCLI(win: BrowserWindow): Promise<void> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the : Promise<void> return type and splitting up the old { platform, arch } object are unrelated to this fix. Both are harmless, but keeping the diff to the actual fix makes it easier to review and revert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop MQTTX Desktop fix Fix bug or issues

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants