fix(cli): clean up legacy AppImage wrapper

This commit is contained in:
Neil
2026-09-01 22:40:17 -07:00
parent fd4d36f892
commit 5ab0c2f7fe
2 changed files with 69 additions and 30 deletions
+19 -30
View File
@@ -20,11 +20,9 @@ import {
} from './cli-command-filesystem-transaction'
import { DEV_LAUNCHER_DIR, LEGACY_LINUX_COMMAND_NAME } from './cli-install-constants'
import { buildWindowsForwarder } from './cli-dev-launcher'
import { isMissingError, isPermissionError } from './cli-install-errors'
import { isPermissionError } from './cli-install-errors'
import { isPathInsideOrEqual } from './cli-install-path-format'
const STABLE_LEGACY_INSPECTION_ATTEMPTS = 3
export class CliCommandInstallation extends CliCommandInspection {
protected async installSymlink(status: CliInstallStatus): Promise<void> {
const commandPath = status.commandPath
@@ -194,34 +192,25 @@ export class CliCommandInstallation extends CliCommandInspection {
})
| null
> {
for (let attempt = 0; attempt < STABLE_LEGACY_INSPECTION_ATTEMPTS; attempt += 1) {
const before = await readEntrySnapshot(commandPath)
if (!before) {
return null
}
let target: string | null = null
try {
target = before.isSymbolicLink ? await readlink(commandPath) : null
} catch (error) {
if (isMissingError(error)) {
continue
}
throw error
}
const after = await readEntrySnapshot(commandPath)
if (after && hasSameSnapshot(before, after)) {
const resolvedTarget = target ? resolve(dirname(commandPath), target) : null
return {
fileSha256: null,
rawSymlinkTarget: target,
snapshot: after,
managed: Boolean(
resolvedTarget && this.isManagedLegacyLinuxTarget(resolvedTarget, launcherPath)
)
}
}
const inspected = await inspectStableCommand(commandPath, () =>
this.inspectSymlink(commandPath, launcherPath)
)
if (!inspected.snapshot) {
return null
}
const resolvedTarget = inspected.rawSymlinkTarget
? resolve(dirname(commandPath), inspected.rawSymlinkTarget)
: inspected.status.currentTarget
return {
fileSha256: inspected.fileSha256,
rawSymlinkTarget: inspected.rawSymlinkTarget,
snapshot: inspected.snapshot,
managed: Boolean(
resolvedTarget &&
(this.isManagedLegacyLinuxTarget(resolvedTarget, launcherPath) ||
(this.appImagePath && resolve(resolvedTarget) === resolve(this.appImagePath)))
)
}
throw new Error(`The command at ${commandPath} changed while Orca inspected it.`)
}
private async restoreQuarantinedCommand(
+50
View File
@@ -370,6 +370,56 @@ describe('CliInstaller', () => {
}
)
it.skipIf(process.platform === 'win32')(
'removes a legacy AppImage wrapper only when it names the current AppImage',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const commandDir = join(homePath, '.local', 'bin')
const legacyCommandPath = join(commandDir, 'orca')
const appImagePath = join(fixture.root, 'Orca.AppImage')
const foreignAppImagePath = join(fixture.root, 'Other.AppImage')
const cacheRootPath = join(fixture.root, 'cache')
await mkdir(commandDir, { recursive: true })
await writeFile(appImagePath, '#!/usr/bin/env bash\n', {
encoding: 'utf8',
mode: 0o755
})
await writeFile(foreignAppImagePath, '#!/usr/bin/env bash\n', {
encoding: 'utf8',
mode: 0o755
})
await writeFile(legacyCommandPath, buildLegacyAppImageCliWrapper(appImagePath), {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'linux',
isPackaged: true,
userDataPath: fixture.userDataPath,
appPath: fixture.appPath,
appImagePath,
appImageCacheRootPath: cacheRootPath,
appImageExtractRunner: fakeAppImageExtractRunner,
homePath,
processPathEnv: commandDir
})
await installer.install()
await expect(lstat(legacyCommandPath)).rejects.toMatchObject({ code: 'ENOENT' })
await writeFile(legacyCommandPath, buildLegacyAppImageCliWrapper(foreignAppImagePath), {
encoding: 'utf8',
mode: 0o755
})
await installer.remove()
await expect(readFile(legacyCommandPath, 'utf8')).resolves.toBe(
buildLegacyAppImageCliWrapper(foreignAppImagePath)
)
}
)
// Why: the privilegedRunner is injectable so the EACCES→osascript path can be
// exercised in integration without spawning osascript in unit tests.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(