diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index f4d06ae84..4fb002a92 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -7,7 +7,7 @@ import { inspectAppleDispatchMetadata } from "./AppleDispatchMetadata.js"; import { observeNativeUi } from "./NativeUiObservation.js"; import { LldbCallTracer, type NativeCallTracer } from "./LldbCallTracer.js"; import { observeNativeCalls } from "./NativeCallObservation.js"; -import { dirname, isAbsolute, resolve } from "node:path"; +import { basename, dirname, isAbsolute, resolve } from "node:path"; import { open, realpath, stat } from "node:fs/promises"; import { z } from "zod"; @@ -1094,7 +1094,7 @@ const unreadablePlist = ( ): AnalysisError => { const sentence = reason.endsWith(".") ? reason : `${reason}.`; // Name the default that is missing and the selection that still works. - const missingDefault = `The target has no readable Contents/Info.plist at ${path}: ${sentence} Select another plist in the target with the path input (CLI: --relative-path).`; + const missingDefault = `The target has no readable default plist at ${path}: ${sentence} Select another plist in the target with the path input (CLI: --relative-path).`; return requested === undefined ? new AnalysisCapabilityUnavailableError( IDENTITY.id, @@ -1150,6 +1150,15 @@ const requireRegularPlist = async ( } }; +/** The bundle whose Contents/MacOS directory directly holds `path`. */ +const macOSBundleRoot = (path: string): string | undefined => { + const programs = dirname(path); + const contents = dirname(programs); + return basename(programs) === "MacOS" && basename(contents) === "Contents" + ? dirname(contents) + : undefined; +}; + const resolvePlistPath = async ( target: BinaryTarget, requested: string | undefined, @@ -1159,8 +1168,13 @@ const resolvePlistPath = async ( if (requested !== undefined && isAbsolute(requested)) return ok(resolve(requested)); const sourceMetadata = await stat(source); - const root = sourceMetadata.isDirectory() ? source : dirname(target.path); + const root = sourceMetadata.isDirectory() + ? source + : (macOSBundleRoot(target.path) ?? dirname(target.path)); if (requested !== undefined) return ok(resolve(root, requested)); + // A selected plist file is itself the plist to inspect. + if (target.kind === "artifact" && target.format === "plist") + return ok(target.path); // A bundle target records the Info.plist its program file was declared // in: Contents/Info.plist, or the root plist of an iOS-style bundle. return ok(target.bundleInfoPlist ?? resolve(root, "Contents/Info.plist")); diff --git a/tests/boundary/cli/nativePlistCli.test.ts b/tests/boundary/cli/nativePlistCli.test.ts index 50ed030f3..324d4e20c 100644 --- a/tests/boundary/cli/nativePlistCli.test.ts +++ b/tests/boundary/cli/nativePlistCli.test.ts @@ -12,13 +12,22 @@ import { cliTest } from "../../support/cli/cliFixture.js"; describe.skipIf(process.platform !== "darwin")( "native plist CLI selection", () => { + // Targets are app-relative: the bundle, its program file, or a plist file. cliTest.for([ - ["default", undefined, "default"], - ["alternate", "Contents/Alternate.plist", "alternate"], - ["missing", "Contents/Missing.plist", undefined], + ["default", "", undefined, "default"], + ["alternate", "", "Contents/Alternate.plist", "alternate"], + ["missing", "", "Contents/Missing.plist", undefined], + ["program default", "Contents/MacOS/App", undefined, "default"], + [ + "program alternate", + "Contents/MacOS/App", + "Contents/Alternate.plist", + "alternate", + ], + ["plist file", "Contents/Alternate.plist", undefined, "alternate"], ] as const)( "honors the $0 plist selection", - async ([, selected, identifier], { cli }) => { + async ([, target, selected, identifier], { cli }) => { const directory = await createTestTempDirectory("rea-plist-cli-"); const app = join(directory, "Example.app"); const contents = join(app, "Contents"); @@ -39,7 +48,7 @@ describe.skipIf(process.platform !== "darwin")( const result = await cli.run({ arguments: [ "inspect-plist", - app, + join(app, target), ...(selected === undefined ? [] : ["--relative-path", selected]), "--json", ], @@ -55,7 +64,11 @@ describe.skipIf(process.platform !== "darwin")( expect(result.exitCode).toBe(0); expect(result.json).toMatchObject({ normalized_result: { - source_path: join(app, selected ?? "Contents/Info.plist"), + source_path: join( + app, + selected ?? + (target.endsWith(".plist") ? target : "Contents/Info.plist"), + ), value: { CFBundleIdentifier: identifier }, }, }); diff --git a/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts b/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts index e14ca4cc3..640e12b63 100644 --- a/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts +++ b/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts @@ -122,7 +122,7 @@ describe.skipIf(process.platform !== "darwin")( details: { operation: "inspect_plist", reason: expect.stringContaining( - `no readable Contents/Info.plist at ${join(directory, "Contents/Info.plist")}: no file exists there.`, + `no readable default plist at ${join(directory, "Contents/Info.plist")}: no file exists there.`, ), }, });