Merge pull request #931 from morluto/fix/apple-bridge-roots
fix(apple): pair bridge hypotheses within one application root
This commit is contained in:
commit
7acf4e741a
|
|
@ -96,9 +96,11 @@ The components also list:
|
|||
|
||||
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
|
||||
so a versioned framework is described through its real `Versions/<version>/`
|
||||
directories rather than `Versions/Current`. When a framework has zero or several
|
||||
`Versions/*/Resources/Info.plist` files, its `info_plist_path` is `null` and a
|
||||
limitation explains why.
|
||||
directories rather than `Versions/Current`. A framework's `info_plist_path` is
|
||||
`null`, with a limitation explaining why, when it has more than one real
|
||||
`Versions/<version>/` directory (even if only one of them holds a plist,
|
||||
because `Versions/Current` decides which one applies) or when its single
|
||||
version directory has no `Resources/Info.plist`.
|
||||
|
||||
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
|
||||
describe neighbouring files. They are excluded from roots, bundle roles, and the
|
||||
|
|
|
|||
|
|
@ -309,6 +309,29 @@ describe("Apple bundle layouts and containers", () => {
|
|||
expect(
|
||||
result.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBeNull();
|
||||
// Files beside the only real version are not versions.
|
||||
const withFiles = project(
|
||||
inventoryEvidence("zip", "Fixture.zip", [
|
||||
macho("Fixture.app/Contents/MacOS/Fixture"),
|
||||
{ path: `${framework}/Versions/A/Resources/Info.plist` },
|
||||
{ path: `${framework}/Versions/.DS_Store` },
|
||||
{ path: `${framework}/Versions/Current`, kind: "symlink" },
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
withFiles.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBe(`${framework}/Versions/A/Resources/Info.plist`);
|
||||
// Only A has a plist, but Versions/Current may select B.
|
||||
const onlyA = project(
|
||||
inventoryEvidence("zip", "Fixture.zip", [
|
||||
macho("Fixture.app/Contents/MacOS/Fixture"),
|
||||
{ path: `${framework}/Versions/A/Resources/Info.plist` },
|
||||
macho(`${framework}/Versions/B/Core`),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
onlyA.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBeNull();
|
||||
expect(result.limitations).toContainEqual(
|
||||
expect.stringContaining("Versions/Current selects is unknown"),
|
||||
);
|
||||
|
|
@ -438,3 +461,37 @@ describe("macOS application scope", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple application bridge candidates", () => {
|
||||
it("pairs scripts and native code only within one application root", () => {
|
||||
const result = project(
|
||||
inventoryEvidence("zip", "Suite.zip", [
|
||||
macho("First.app/Contents/MacOS/First"),
|
||||
{
|
||||
path: "First.app/Contents/Resources/first.js",
|
||||
format: "javascript-bundle",
|
||||
},
|
||||
macho("Second.app/Contents/MacOS/Second"),
|
||||
{
|
||||
path: "Second.app/Contents/Resources/second.js",
|
||||
format: "javascript-bundle",
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.application_roots).toEqual(["First.app", "Second.app"]);
|
||||
expect(
|
||||
result.bridge_candidates.map(
|
||||
({ source_path: source, native_path: target }) => [source, target],
|
||||
),
|
||||
).toEqual([
|
||||
[
|
||||
"First.app/Contents/Resources/first.js",
|
||||
"First.app/Contents/MacOS/First",
|
||||
],
|
||||
[
|
||||
"Second.app/Contents/Resources/second.js",
|
||||
"Second.app/Contents/MacOS/Second",
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export const projectAppleApplication = (
|
|||
...classified.executables,
|
||||
...classified.native_libraries,
|
||||
]),
|
||||
roots,
|
||||
);
|
||||
const uninventoriedImage =
|
||||
rootFormat === "dmg" && inventory.occurrences.length === 1;
|
||||
|
|
@ -305,18 +306,27 @@ const identifyRuntimeFamilies = (all: readonly Component[]) => {
|
|||
return [...families].sort(compare);
|
||||
};
|
||||
|
||||
/** Pair scripts and native code only within the same application root. */
|
||||
const identifyBridgeCandidates = (
|
||||
scripts: readonly Component[],
|
||||
native: readonly Component[],
|
||||
) => ({
|
||||
candidates: scripts.flatMap((script) =>
|
||||
native.map((item) => ({
|
||||
source_path: script.path,
|
||||
native_path: item.path,
|
||||
basis: bridgeBasis(item.path),
|
||||
})),
|
||||
),
|
||||
});
|
||||
roots: readonly string[],
|
||||
) => {
|
||||
const owner = (path: string): string | null =>
|
||||
roots.find((root) => isWithin(path, root)) ?? null;
|
||||
return {
|
||||
candidates: scripts.flatMap((script) => {
|
||||
const root = owner(script.path);
|
||||
return native
|
||||
.filter(({ path }) => owner(path) === root)
|
||||
.map((item) => ({
|
||||
source_path: script.path,
|
||||
native_path: item.path,
|
||||
basis: bridgeBasis(item.path),
|
||||
}));
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const bridgeBasis = (
|
||||
path: string,
|
||||
|
|
@ -360,7 +370,7 @@ const projectionLimitations = (facts: {
|
|||
layout === "versioned-framework" && plist === null,
|
||||
)
|
||||
? [
|
||||
"A versioned framework has zero or several Versions/*/Resources/Info.plist files; the one Versions/Current selects is unknown because symlink targets are not inventoried.",
|
||||
"A versioned framework has several version directories or no Versions/*/Resources/Info.plist; the Info.plist that Versions/Current selects is unknown because symlink targets are not inventoried.",
|
||||
]
|
||||
: []),
|
||||
...(facts.symlinks
|
||||
|
|
|
|||
|
|
@ -141,14 +141,18 @@ export const detectBundles = (
|
|||
interface Tree {
|
||||
readonly children: ReadonlyMap<string, ReadonlySet<string>>;
|
||||
readonly files: ReadonlyMap<string, Component>;
|
||||
/** Paths observed as directories, or implied by a deeper path. */
|
||||
readonly directories: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
const indexTree = (entries: readonly Entry[]): Tree => {
|
||||
const children = new Map<string, Set<string>>();
|
||||
const files = new Map<string, Component>();
|
||||
const directories = new Set<string>();
|
||||
for (const { path, kind, component } of entries) {
|
||||
if (isSidecar(path)) continue;
|
||||
if (kind === "file" && component !== undefined) files.set(path, component);
|
||||
if (kind === "directory") directories.add(path);
|
||||
const segments = path.split("/");
|
||||
let parent = "";
|
||||
for (const segment of segments) {
|
||||
|
|
@ -156,9 +160,10 @@ const indexTree = (entries: readonly Entry[]): Tree => {
|
|||
names.add(segment);
|
||||
children.set(parent, names);
|
||||
parent = joinPath(parent, segment);
|
||||
if (parent !== path) directories.add(parent);
|
||||
}
|
||||
}
|
||||
return { children, files };
|
||||
return { children, files, directories };
|
||||
};
|
||||
|
||||
const joinPath = (directory: string, name: string): string =>
|
||||
|
|
@ -223,9 +228,12 @@ const contentDirectories = (
|
|||
if (layout === "macos-deep") return [joinPath(directory, "Contents")];
|
||||
if (layout === "shallow") return [directory];
|
||||
const versions = joinPath(directory, "Versions");
|
||||
// Only real version directories count: Current is a symlink, and files
|
||||
// such as .DS_Store are not versions.
|
||||
return [...(tree.children.get(versions) ?? [])]
|
||||
.filter((version) => version !== "Current")
|
||||
.map((version) => joinPath(versions, version))
|
||||
.filter((version) => tree.directories.has(version))
|
||||
.sort(compare);
|
||||
};
|
||||
|
||||
|
|
@ -234,7 +242,11 @@ const infoPlistPath = (
|
|||
layout: Bundle["layout"],
|
||||
tree: Tree,
|
||||
): string | null => {
|
||||
const candidates = contentDirectories(bundle, layout, tree)
|
||||
const directories = contentDirectories(bundle, layout, tree);
|
||||
// With several real versions, Versions/Current (a symlink outside the
|
||||
// inventory) decides which plist applies, so the plist stays unknown.
|
||||
if (directories.length !== 1) return null;
|
||||
const candidates = directories
|
||||
.map((directory) =>
|
||||
joinPath(
|
||||
directory,
|
||||
|
|
|
|||
Loading…
Reference in New Issue