Centralize canonical system/user and deterministic renamed-bundle path facts. Reuse the installer destination and honor the same local candidates in doctor and runtime; scan renamed bundles only after canonical candidates fail. Preserve explicit path spelling and exclusive selection. Homebrew command probing remains doctor-only with explicit runtime configuration guidance. Validation: owner, config, setup and doctor tests cover canonical priority and renamed paths; public CLI verifies explicit path preservation without launching Hopper. No real Hopper analysis claimed. Fixes #3054 Related: #3085 Co-authored-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
export const MAC_HOPPER_APPLICATION_DIRECTORY = "/Applications";
|
|
export const MAC_HOPPER_APPLICATION_NAME = "Hopper Disassembler.app";
|
|
export const MAC_HOPPER_USER_APPLICATIONS_DIRECTORY = "Applications";
|
|
|
|
export const MAC_HOPPER_SYSTEM_LAUNCHER = join(
|
|
MAC_HOPPER_APPLICATION_DIRECTORY,
|
|
MAC_HOPPER_APPLICATION_NAME,
|
|
"Contents/MacOS/hopper",
|
|
);
|
|
|
|
export const macHopperUserApplicationPath = (homeDirectory: string): string =>
|
|
join(
|
|
homeDirectory,
|
|
MAC_HOPPER_USER_APPLICATIONS_DIRECTORY,
|
|
MAC_HOPPER_APPLICATION_NAME,
|
|
);
|
|
|
|
export const macHopperApplicationRoots = (
|
|
homeDirectory: string,
|
|
): readonly string[] => [
|
|
MAC_HOPPER_APPLICATION_DIRECTORY,
|
|
join(homeDirectory, MAC_HOPPER_USER_APPLICATIONS_DIRECTORY),
|
|
];
|
|
|
|
export const macHopperCanonicalLauncherCandidates = (
|
|
homeDirectory: string,
|
|
): readonly string[] => [
|
|
MAC_HOPPER_SYSTEM_LAUNCHER,
|
|
join(macHopperUserApplicationPath(homeDirectory), "Contents/MacOS/hopper"),
|
|
];
|
|
|
|
/** Build deterministic candidates from the discovered app bundle names. */
|
|
export const macHopperRenamedLauncherCandidates = (
|
|
homeDirectory: string,
|
|
bundleNamesByRoot: ReadonlyMap<string, readonly string[]>,
|
|
): readonly string[] =>
|
|
macHopperApplicationRoots(homeDirectory).flatMap((root) =>
|
|
[...new Set(bundleNamesByRoot.get(root) ?? [])]
|
|
.filter(
|
|
(name) =>
|
|
/^Hopper.*\.app$/iu.test(name) &&
|
|
name.toLocaleLowerCase("en-US") !==
|
|
MAC_HOPPER_APPLICATION_NAME.toLocaleLowerCase("en-US"),
|
|
)
|
|
.sort()
|
|
.map((name) => join(root, name, "Contents/MacOS/hopper")),
|
|
);
|
|
|
|
const localBundleNames = (root: string): readonly string[] => {
|
|
try {
|
|
return readdirSync(root, { withFileTypes: true })
|
|
.filter(
|
|
(entry) => entry.isDirectory() && /^Hopper.*\.app$/iu.test(entry.name),
|
|
)
|
|
.map(({ name }) => name);
|
|
} catch (cause: unknown) {
|
|
// Missing or unreadable optional app directories contain no candidates.
|
|
void cause;
|
|
return [];
|
|
}
|
|
};
|
|
|
|
/** Yield canonical paths first; enumerate renamed bundles only if needed. */
|
|
export function* macHopperLauncherCandidates(
|
|
homeDirectory: string,
|
|
): Generator<string> {
|
|
yield* macHopperCanonicalLauncherCandidates(homeDirectory);
|
|
for (const root of macHopperApplicationRoots(homeDirectory))
|
|
yield* macHopperRenamedLauncherCandidates(
|
|
homeDirectory,
|
|
new Map([[root, localBundleNames(root)]]),
|
|
);
|
|
}
|