openclaw/apps/macos/Sources/Clawdbot/CLIInstaller.swift
Rocuts c0d3eee83b fix(install): guard against Rosetta trap on Apple Silicon AI Assisted
## Description
This PR fixes installation instability on Apple Silicon (M1/M2/M3) Macs caused by the "Rosetta Trap".
The Problem:
When a terminal runs under Rosetta, scripts using `uname -m` incorrectly detect Intel. This leads to wrong binary downloads and Homebrew path issues.
The Fix:
1. scripts/postinstall.js: Added check for sysctl.proc_translated.
2. apps/macos/.../CLIInstaller.swift: Added native Swift guard.
3. scripts/check-env.sh: Added diagnostic utility.
## AI/Vibe-Coded 🤖
- [x] AI-Assisted: Marked in title/description.
- [x] Testing Level: Fully Tested (Verified on M1 environment with check-env.sh).
- [x] Transparency: Prompts derived from installation_debugging.md analysis.
- [x] I understand what this code does
2026-01-26 12:06:09 -05:00

120 lines
4.1 KiB
Swift

import Foundation
@MainActor
enum CLIInstaller {
static func installedLocation() -> String? {
self.installedLocation(
searchPaths: CommandResolver.preferredPaths(),
fileManager: .default)
}
static func installedLocation(
searchPaths: [String],
fileManager: FileManager) -> String?
{
for basePath in searchPaths {
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("clawdbot").path
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
!isDirectory.boolValue
else {
continue
}
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
return candidate
}
return nil
}
static func isInstalled() -> Bool {
self.installedLocation() != nil
}
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async {
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
let prefix = Self.installPrefix()
await statusHandler("Installing clawdbot CLI…")
// 1. Architecture/Rosetta Guard
if self.isRosettaTranslated() {
await statusHandler("Warning: Rosetta detected. Attempting to force arm64…")
}
let cmd = self.installScriptCommand(version: expected, prefix: prefix)
let response = await ShellExecutor.runDetailed(command: cmd, cwd: nil, env: nil, timeout: 900)
if response.success {
let parsed = self.parseInstallEvents(response.stdout)
let installedVersion = parsed.last { $0.event == "done" }?.version
let summary = installedVersion.map { "Installed clawdbot \($0)." } ?? "Installed clawdbot."
await statusHandler(summary)
return
}
let parsed = self.parseInstallEvents(response.stdout)
if let error = parsed.last(where: { $0.event == "error" })?.message {
await statusHandler("Install failed: \(error)")
return
}
let detail = response.stderr.trimmingCharacters(in: .whitespacesAndNewlines)
let fallback = response.errorMessage ?? "install failed"
await statusHandler("Install failed: \(detail.isEmpty ? fallback : detail)")
}
private static func installPrefix() -> String {
FileManager().homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot")
.path
}
private static func installScriptCommand(version: String, prefix: String) -> [String] {
let escapedVersion = self.shellEscape(version)
let escapedPrefix = self.shellEscape(prefix)
let script = """
curl -fsSL https://clawd.bot/install-cli.sh | \
bash -s -- --json --no-onboard --prefix \(escapedPrefix) --version \(escapedVersion)
"""
return ["/bin/bash", "-lc", script]
}
private static func parseInstallEvents(_ output: String) -> [InstallEvent] {
let decoder = JSONDecoder()
let lines = output
.split(whereSeparator: \.isNewline)
.map { String($0) }
var events: [InstallEvent] = []
for line in lines {
guard let data = line.data(using: .utf8) else { continue }
if let event = try? decoder.decode(InstallEvent.self, from: data) {
events.append(event)
}
}
return events
}
private static func shellEscape(_ raw: String) -> String {
"'" + raw.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}
private static func isRosettaTranslated() -> Bool {
var ret = 0
var size = MemoryLayout.size(ofValue: ret)
// sysctl.proc_translated: 1 if translated (Rosetta), 0 if native
if sysctlbyname("sysctl.proc_translated", &ret, &size, nil, 0) == 0 {
return ret == 1
}
return false
}
}
private struct InstallEvent: Decodable {
let event: String
let version: String?
let message: String?
}