mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
fix(apple): derive bundle anatomy from directory facts
Build separate directory and authenticated-file indexes before applying anatomy name preferences. Share those facts across application roots, nested bundles, layout, metadata, executable candidates and signing paths. Remove mixed child-name interpretation and downstream membership repairs. A leaf Contents/Versions resource no longer changes a shallow layout, and leaf files/symlinks cannot manufacture application roots. Real explicit and implied directories retain their existing semantics and spelling. Adapt the directory-identity proposal and source regressions from PR #3505 by @bnkaiteam, extending it to metadata file selection and root admission. Validation: 57 focused checks, build, check:fast and docs:check pass. 30 tiny completed source projection cost controls preserve ordinary deep and versioned replies byte for byte. No real-provider/target execution, heavy profiling or published npm verification. Fixes #3359 Fixes #4779
This commit is contained in:
1 parent
c192c58815
commit
149a0fc06c
5 files changed
+338
-72
No files matched your search
@@ -77,6 +77,12 @@ including when empty, so Apple projection identifiers change.
|
||||
| `.app` directory | `.`, the inventoried bundle itself |
|
||||
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
|
||||
|
||||
Root and layout discovery use known directories and ancestors implied by deeper
|
||||
inventory paths. A leaf file or symlink named `*.app`, `Contents`, or `Versions`
|
||||
does not establish that directory. Directory and authenticated-file identity are
|
||||
selected before applying anatomy case preferences, so a file cannot shadow a
|
||||
folded directory name or a directory shadow a metadata/signature file.
|
||||
|
||||
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
|
||||
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
|
||||
macOS mounting adapter. When a DMG has only its root identity, the projection is
|
||||
|
||||
@@ -294,6 +294,91 @@ describe("macOS application roots and bundle roles", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple bundle directory identity", () => {
|
||||
it.each([
|
||||
["Contents", "file"],
|
||||
["Contents", "symlink"],
|
||||
["Versions", "file"],
|
||||
["Versions", "symlink"],
|
||||
] as const)("does not derive a layout from a %s %s", (name, kind) => {
|
||||
const root = "Payload/Mobile.app";
|
||||
const result = project(
|
||||
inventoryEvidence("ipa", "Mobile.ipa", [
|
||||
{ path: `${root}/Info.plist`, format: "plist" },
|
||||
{ path: `${root}/${name}`, kind },
|
||||
]),
|
||||
);
|
||||
expect(result.platforms).toEqual(["ios"]);
|
||||
expect(result.bundles).toEqual([
|
||||
expect.objectContaining({
|
||||
path: root,
|
||||
layout: "shallow",
|
||||
info_plist_path: `${root}/Info.plist`,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Contents", "macos-deep"],
|
||||
["Versions", "versioned-framework"],
|
||||
] as const)(
|
||||
"derives a layout from an explicit empty %s directory",
|
||||
(name, layout) => {
|
||||
const root = "Payload/Mobile.app";
|
||||
const result = project(
|
||||
inventoryEvidence("ipa", "Mobile.ipa", [
|
||||
{ path: `${root}/${name}`, kind: "directory" },
|
||||
]),
|
||||
);
|
||||
expect(result.bundles[0]).toMatchObject({ path: root, layout });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["Contents", "contents/Info.plist", "macos-deep"],
|
||||
["Versions", "versions/A/Resources/Info.plist", "versioned-framework"],
|
||||
] as const)(
|
||||
"keeps a real %s directory distinct from a same-named file",
|
||||
(name, plist, layout) => {
|
||||
const root = "Payload/Mobile.app";
|
||||
const result = project(
|
||||
inventoryEvidence("ipa", "Mobile.ipa", [
|
||||
{ path: `${root}/${name}` },
|
||||
{ path: `${root}/${plist}`, format: "plist" },
|
||||
]),
|
||||
);
|
||||
expect(result.bundles[0]).toMatchObject({
|
||||
path: root,
|
||||
layout,
|
||||
info_plist_path: `${root}/${plist}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps files from shadowing anatomy directories inside bundles", () => {
|
||||
const framework = "Contents/Frameworks/Core.framework";
|
||||
const result = project(
|
||||
inventoryEvidence("directory", "Fixture.app", [
|
||||
{ path: "Contents/Info.plist" },
|
||||
{ path: "Contents/MacOS" },
|
||||
macho("Contents/macos/Fixture"),
|
||||
{ path: "Contents/_CodeSignature" },
|
||||
{ path: "Contents/_codesignature/CodeResources" },
|
||||
{ path: `${framework}/Versions/A/Resources` },
|
||||
{ path: `${framework}/Versions/A/resources/Info.plist` },
|
||||
]),
|
||||
);
|
||||
expect(result.bundles[0]).toMatchObject({
|
||||
executable_candidates: ["Contents/macos/Fixture"],
|
||||
signing_paths: ["Contents/_codesignature/CodeResources"],
|
||||
});
|
||||
expect(result.bundles[1]).toMatchObject({
|
||||
path: framework,
|
||||
info_plist_path: `${framework}/Versions/A/resources/Info.plist`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple bundle layouts and containers", () => {
|
||||
it("follows versioned frameworks without resolving Versions/Current", () => {
|
||||
const framework = "Fixture.app/Contents/Frameworks/Core.framework";
|
||||
@@ -782,3 +867,41 @@ it.each(["file", "slice"] as const)(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("selects metadata and signature files before applying anatomy case preferences", () => {
|
||||
const result = project(
|
||||
inventoryEvidence("directory", "Fixture.app", [
|
||||
{ path: "Contents/Info.plist", kind: "directory" },
|
||||
{ path: "Contents/info.plist", format: "plist" },
|
||||
{ path: "Contents/_CodeSignature/CodeResources", kind: "directory" },
|
||||
{ path: "Contents/_CodeSignature/coderesources" },
|
||||
{ path: "Contents/embedded.mobileprovision", kind: "directory" },
|
||||
{ path: "Contents/Embedded.mobileprovision" },
|
||||
]),
|
||||
);
|
||||
expect(result.bundles[0]).toMatchObject({
|
||||
layout: "macos-deep",
|
||||
info_plist_path: "Contents/info.plist",
|
||||
signing_paths: [
|
||||
"Contents/Embedded.mobileprovision",
|
||||
"Contents/_CodeSignature/coderesources",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Payload/Fake.app", "file"],
|
||||
["Payload/Fake.app", "symlink"],
|
||||
["Fake.app/Contents", "file"],
|
||||
["Fake.app/Contents", "symlink"],
|
||||
] as const)(
|
||||
"does not invent an application root from a %s %s",
|
||||
(path, kind) => {
|
||||
const result = project(
|
||||
inventoryEvidence("zip", "Fixture.zip", [{ path, kind }]),
|
||||
);
|
||||
expect(result.application_roots).toEqual([]);
|
||||
expect(result.bundles).toEqual([]);
|
||||
expect(result.platforms).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
anatomyNameIs,
|
||||
anatomyTailMatches,
|
||||
appleBundleSchema,
|
||||
applicationRoots,
|
||||
detectBundles,
|
||||
projectAppleBundleAnatomy,
|
||||
isMachOFormat,
|
||||
isSidecar,
|
||||
isWithin,
|
||||
@@ -123,7 +122,7 @@ export const projectAppleApplication = (
|
||||
const inventoried = entries.flatMap(({ component }) =>
|
||||
component === undefined ? [] : [component],
|
||||
);
|
||||
const roots = applicationRoots(entries, {
|
||||
const { roots, bundles } = projectAppleBundleAnatomy(entries, {
|
||||
name: evidence[0]?.subject?.name ?? "",
|
||||
format: rootFormat,
|
||||
});
|
||||
@@ -142,7 +141,6 @@ export const projectAppleApplication = (
|
||||
roots,
|
||||
rootFormat,
|
||||
);
|
||||
const bundles = detectBundles(entries, roots);
|
||||
const classified = classifyComponents(entries, all, roots);
|
||||
const runtimeFamilies = identifyRuntimeFamilies(all);
|
||||
const bridgeProjection = identifyBridgeCandidates(
|
||||
|
||||
@@ -65,7 +65,7 @@ export const anatomyNameIs = (actual: string, expected: string): boolean =>
|
||||
|
||||
/** Directory entry that folds to one vocabulary word. The exact spelling wins. */
|
||||
export const anatomyChild = (
|
||||
names: ReadonlySet<string> | undefined,
|
||||
names: Iterable<string> | undefined,
|
||||
expected: string,
|
||||
): string | null => {
|
||||
if (names === undefined) return null;
|
||||
@@ -100,12 +100,22 @@ export const anatomyTailMatches = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Interpret bundle roots and anatomy from the same directory and file indexes. */
|
||||
export const projectAppleBundleAnatomy = (
|
||||
entries: readonly Entry[],
|
||||
subject: { readonly name: string; readonly format: string },
|
||||
) => {
|
||||
const tree = indexTree(entries);
|
||||
const roots = applicationRoots(tree, subject);
|
||||
return { roots, bundles: detectBundles(tree, roots) };
|
||||
};
|
||||
|
||||
/**
|
||||
* Outermost iOS (`Payload/X.app`) and macOS (`X.app/Contents/…`) applications.
|
||||
* An inventoried `.app` directory is its own root, `.`.
|
||||
*/
|
||||
export const applicationRoots = (
|
||||
entries: readonly Entry[],
|
||||
const applicationRoots = (
|
||||
tree: Tree,
|
||||
subject: { readonly name: string; readonly format: string },
|
||||
): string[] => {
|
||||
// A selected `.app` directory is the application, even when its inventory
|
||||
@@ -113,8 +123,7 @@ export const applicationRoots = (
|
||||
if (subject.format === "directory" && /\.app$/iu.test(subject.name))
|
||||
return ["."];
|
||||
const found = new Set<string>();
|
||||
for (const { path } of entries) {
|
||||
if (isSidecar(path)) continue;
|
||||
for (const path of directoryPaths(tree)) {
|
||||
const segments = path.split("/");
|
||||
const app = segments[1];
|
||||
if (
|
||||
@@ -140,26 +149,15 @@ export const applicationRoots = (
|
||||
.sort(compare);
|
||||
};
|
||||
|
||||
export const detectBundles = (
|
||||
entries: readonly Entry[],
|
||||
roots: readonly string[],
|
||||
): Bundle[] => {
|
||||
const detectBundles = (tree: Tree, roots: readonly string[]): Bundle[] => {
|
||||
const paths = new Set<string>(roots);
|
||||
for (const { path, kind } of entries) {
|
||||
if (isSidecar(path)) continue;
|
||||
const root = roots.find((candidate) => isWithin(path, candidate));
|
||||
if (root === undefined) continue;
|
||||
const segments = path.split("/");
|
||||
const start = root === "." ? 0 : root.split("/").length;
|
||||
for (let index = start; index < segments.length; index++) {
|
||||
const segment = segments[index] ?? "";
|
||||
const leaf = index === segments.length - 1;
|
||||
// A symlink or file named like a bundle has no observed contents.
|
||||
if (BUNDLE_EXTENSION.test(segment) && (!leaf || kind === "directory"))
|
||||
paths.add(segments.slice(0, index + 1).join("/"));
|
||||
}
|
||||
for (const path of directoryPaths(tree)) {
|
||||
if (
|
||||
BUNDLE_EXTENSION.test(path.split("/").at(-1) ?? "") &&
|
||||
roots.some((root) => isWithin(path, root))
|
||||
)
|
||||
paths.add(path);
|
||||
}
|
||||
const tree = indexTree(entries);
|
||||
return [...paths].sort(compare).map((path) => {
|
||||
const layout = bundleLayout(path, tree);
|
||||
return {
|
||||
@@ -175,35 +173,41 @@ export const detectBundles = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Directory children (including implied ancestors) and files, indexed once. */
|
||||
/** Keep directory identity separate from authenticated file content before name selection. */
|
||||
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>;
|
||||
readonly directories: ReadonlyMap<string, ReadonlySet<string>>;
|
||||
readonly files: ReadonlyMap<string, ReadonlyMap<string, Component>>;
|
||||
}
|
||||
|
||||
const indexTree = (entries: readonly Entry[]): Tree => {
|
||||
const children = new Map<string, Set<string>>();
|
||||
const files = new Map<string, Component>();
|
||||
const directories = new Set<string>();
|
||||
const directories = new Map<string, Set<string>>();
|
||||
const files = new Map<string, Map<string, Component>>();
|
||||
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) {
|
||||
const names = children.get(parent) ?? new Set<string>();
|
||||
names.add(segment);
|
||||
children.set(parent, names);
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
const leaf = index === segments.length - 1;
|
||||
if (!leaf || kind === "directory") {
|
||||
const names = directories.get(parent) ?? new Set<string>();
|
||||
names.add(segment);
|
||||
directories.set(parent, names);
|
||||
} else if (kind === "file" && component !== undefined) {
|
||||
const children = files.get(parent) ?? new Map<string, Component>();
|
||||
children.set(segment, component);
|
||||
files.set(parent, children);
|
||||
}
|
||||
parent = joinPath(parent, segment);
|
||||
if (parent !== path) directories.add(parent);
|
||||
}
|
||||
}
|
||||
return { children, files, directories };
|
||||
return { directories, files };
|
||||
};
|
||||
|
||||
function* directoryPaths(tree: Tree): Iterable<string> {
|
||||
for (const [parent, names] of tree.directories)
|
||||
for (const name of names) yield joinPath(parent, name);
|
||||
}
|
||||
|
||||
const joinPath = (directory: string, name: string): string =>
|
||||
directory === "" ? name : `${directory}/${name}`;
|
||||
|
||||
@@ -223,9 +227,10 @@ const enclosingBundle = (
|
||||
};
|
||||
|
||||
const bundleLayout = (bundle: string, tree: Tree): Bundle["layout"] => {
|
||||
const names = tree.children.get(directoryOf(bundle));
|
||||
if (anatomyChild(names, "Contents") !== null) return "macos-deep";
|
||||
if (anatomyChild(names, "Versions") !== null) return "versioned-framework";
|
||||
const directory = directoryOf(bundle);
|
||||
if (namedDirectory(tree, directory, "Contents") !== null) return "macos-deep";
|
||||
if (namedDirectory(tree, directory, "Versions") !== null)
|
||||
return "versioned-framework";
|
||||
return "shallow";
|
||||
};
|
||||
|
||||
@@ -255,11 +260,10 @@ const bundleRole = (bundle: string): Bundle["role"] => {
|
||||
};
|
||||
|
||||
/** Files directly inside one directory; nested bundles are deeper paths. */
|
||||
const directFiles = (directory: string, tree: Tree): string[] =>
|
||||
[...(tree.children.get(directory) ?? [])]
|
||||
.map((name) => joinPath(directory, name))
|
||||
.filter((path) => tree.files.has(path))
|
||||
.sort(compare);
|
||||
const directFiles = (directory: string, tree: Tree): Component[] =>
|
||||
[...(tree.files.get(directory)?.values() ?? [])].sort((left, right) =>
|
||||
compare(left.path, right.path),
|
||||
);
|
||||
|
||||
/** Content directories; `Versions/Current` is a symlink outside the inventory. */
|
||||
const contentDirectories = (
|
||||
@@ -269,28 +273,36 @@ const contentDirectories = (
|
||||
): string[] => {
|
||||
const directory = directoryOf(bundle);
|
||||
if (layout === "macos-deep") {
|
||||
const contents = namedChild(tree, directory, "Contents");
|
||||
const contents = namedDirectory(tree, directory, "Contents");
|
||||
return contents === null ? [] : [contents];
|
||||
}
|
||||
if (layout === "shallow") return [directory];
|
||||
const versions = namedChild(tree, directory, "Versions");
|
||||
const versions = namedDirectory(tree, directory, "Versions");
|
||||
if (versions === null) return [];
|
||||
// Only real version directories count: Current is a symlink, and files
|
||||
// such as .DS_Store are not versions.
|
||||
return [...(tree.children.get(versions) ?? [])]
|
||||
return [...(tree.directories.get(versions) ?? [])]
|
||||
.filter((version) => !anatomyNameIs(version, "Current"))
|
||||
.map((version) => joinPath(versions, version))
|
||||
.filter((version) => tree.directories.has(version))
|
||||
.sort(compare);
|
||||
};
|
||||
|
||||
/** Child path using the archive's spelling of one anatomy name. */
|
||||
const namedChild = (
|
||||
/** Select applicable entry kinds before applying Apple's case preference. */
|
||||
const namedDirectory = (
|
||||
tree: Tree,
|
||||
directory: string,
|
||||
expected: string,
|
||||
): string | null => {
|
||||
const name = anatomyChild(tree.children.get(directory), expected);
|
||||
const name = anatomyChild(tree.directories.get(directory), expected);
|
||||
return name === null ? null : joinPath(directory, name);
|
||||
};
|
||||
|
||||
const namedFile = (
|
||||
tree: Tree,
|
||||
directory: string,
|
||||
expected: string,
|
||||
): string | null => {
|
||||
const name = anatomyChild(tree.files.get(directory)?.keys(), expected);
|
||||
return name === null ? null : joinPath(directory, name);
|
||||
};
|
||||
|
||||
@@ -307,11 +319,10 @@ const infoPlistPath = (
|
||||
if (directory === undefined) return null;
|
||||
const parent =
|
||||
layout === "versioned-framework"
|
||||
? namedChild(tree, directory, "Resources")
|
||||
? namedDirectory(tree, directory, "Resources")
|
||||
: directory;
|
||||
if (parent === null) return null;
|
||||
const info = namedChild(tree, parent, "Info.plist");
|
||||
return info !== null && tree.files.has(info) ? info : null;
|
||||
return namedFile(tree, parent, "Info.plist");
|
||||
};
|
||||
|
||||
const executableCandidates = (
|
||||
@@ -323,15 +334,14 @@ const executableCandidates = (
|
||||
.flatMap((directory) => {
|
||||
const executableDirectory =
|
||||
layout === "macos-deep"
|
||||
? namedChild(tree, directory, "MacOS")
|
||||
? namedDirectory(tree, directory, "MacOS")
|
||||
: directory;
|
||||
return executableDirectory === null
|
||||
? []
|
||||
: directFiles(executableDirectory, tree);
|
||||
})
|
||||
.filter((path) =>
|
||||
MACH_O_FORMATS.includes(tree.files.get(path)?.format ?? ""),
|
||||
);
|
||||
.filter(({ format }) => isMachOFormat(format))
|
||||
.map(({ path }) => path);
|
||||
|
||||
const signingPaths = (
|
||||
bundle: string,
|
||||
@@ -340,16 +350,14 @@ const signingPaths = (
|
||||
): string[] =>
|
||||
contentDirectories(bundle, layout, tree)
|
||||
.flatMap((directory) => {
|
||||
const signature = namedChild(tree, directory, "_CodeSignature");
|
||||
const signature = namedDirectory(tree, directory, "_CodeSignature");
|
||||
return [
|
||||
signature === null
|
||||
? null
|
||||
: namedChild(tree, signature, "CodeResources"),
|
||||
namedChild(tree, directory, "embedded.provisionprofile"),
|
||||
namedChild(tree, directory, "embedded.mobileprovision"),
|
||||
signature === null ? null : namedFile(tree, signature, "CodeResources"),
|
||||
namedFile(tree, directory, "embedded.provisionprofile"),
|
||||
namedFile(tree, directory, "embedded.mobileprovision"),
|
||||
];
|
||||
})
|
||||
.filter((path): path is string => path !== null && tree.files.has(path))
|
||||
.filter((path): path is string => path !== null)
|
||||
.sort(compare);
|
||||
|
||||
/** Platforms of application roots whose layout was observed, not assumed. */
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js";
|
||||
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const rootPath = "Payload/Fixture.app";
|
||||
cliTest.for([
|
||||
{
|
||||
name: "regular Contents resource",
|
||||
resource: "Contents",
|
||||
layout: "shallow",
|
||||
plist: "Info.plist",
|
||||
platforms: ["ios"],
|
||||
},
|
||||
{
|
||||
name: "regular Versions resource",
|
||||
resource: "Versions",
|
||||
layout: "shallow",
|
||||
plist: "Info.plist",
|
||||
platforms: ["ios"],
|
||||
},
|
||||
{
|
||||
name: "implied Contents directory",
|
||||
resource: "Resource",
|
||||
layout: "macos-deep",
|
||||
plist: "Contents/Info.plist",
|
||||
platforms: ["macos"],
|
||||
},
|
||||
{
|
||||
name: "file shadowing a folded directory",
|
||||
resource: "Contents",
|
||||
layout: "macos-deep",
|
||||
plist: "contents/Info.plist",
|
||||
platforms: ["macos"],
|
||||
},
|
||||
])(
|
||||
"Apple CLI respects directory identity with $name",
|
||||
async (fixture, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-apple-directory-cli-");
|
||||
const path = join(root, "Fixture.ipa");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add(
|
||||
`${rootPath}/${fixture.resource}`,
|
||||
new TextReader("ordinary resource"),
|
||||
);
|
||||
await writer.add(
|
||||
`${rootPath}/${fixture.plist}`,
|
||||
new TextReader("<plist/>"),
|
||||
);
|
||||
await writeFile(path, await writer.close());
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", path, "--json"],
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const evidence = parseEvidence(inspected.json);
|
||||
const inventory = artifactInspectionResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
).substeps[0]?.evidence;
|
||||
expect(inventory?.normalized_result.occurrences).toContainEqual(
|
||||
expect.objectContaining({
|
||||
logical_path: `${rootPath}/${fixture.resource}`,
|
||||
entry_kind: "file",
|
||||
}),
|
||||
);
|
||||
const input = join(root, "projection.json");
|
||||
await writeFile(input, JSON.stringify({ inventory_evidence: [evidence] }));
|
||||
const output = await cli.run({
|
||||
arguments: ["project-apple-application-graph", input, "--json"],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const projection = appleApplicationProjectionResultSchema.parse(
|
||||
parseEvidence(output.json).normalized_result,
|
||||
);
|
||||
expect(projection.bundles).toEqual([
|
||||
expect.objectContaining({
|
||||
path: rootPath,
|
||||
layout: fixture.layout,
|
||||
info_plist_path: `${rootPath}/${fixture.plist}`,
|
||||
}),
|
||||
]);
|
||||
expect(projection.platforms).toEqual(fixture.platforms);
|
||||
expect(projection.coverage).toEqual({
|
||||
status: "complete-within-inventory",
|
||||
inventory_complete: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"Apple CLI does not invent an application from a leaf Contents file",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-apple-false-root-cli-");
|
||||
const path = join(root, "Fixture.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("Fake.app/Contents", new TextReader("ordinary resource"));
|
||||
await writeFile(path, await writer.close());
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", path, "--json"],
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const source = parseEvidence(inspected.json);
|
||||
const inventory = artifactInspectionResultSchema.parse(
|
||||
source.normalized_result,
|
||||
).substeps[0]?.evidence;
|
||||
expect(inventory?.normalized_result.occurrences).toContainEqual(
|
||||
expect.objectContaining({
|
||||
logical_path: "Fake.app/Contents",
|
||||
entry_kind: "file",
|
||||
}),
|
||||
);
|
||||
const input = join(root, "projection.json");
|
||||
await writeFile(input, JSON.stringify({ inventory_evidence: [source] }));
|
||||
const output = await cli.run({
|
||||
arguments: ["project-apple-application-graph", input, "--json"],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const projection = appleApplicationProjectionResultSchema.parse(
|
||||
parseEvidence(output.json).normalized_result,
|
||||
);
|
||||
expect(projection.application_roots).toEqual([]);
|
||||
expect(projection.bundles).toEqual([]);
|
||||
expect(projection.platforms).toEqual([]);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user