Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
createStageWorkspaceConfig,
createStagePnpmConfig,
DESKTOP_ASAR_UNPACK,
parseFfiRsLockfileVersion,
resolveDesktopRuntimeDependencies,
resolveFfiRsNativeDependencies,
resolveFffNativeDependencies,
resolveBuildOptions,
resolveDesktopBuildIconAssets,
Expand Down Expand Up @@ -171,8 +173,55 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
});
});

it("unpacks the fff shared library for filesystem and FFI access", () => {
assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, ["node_modules/@ff-labs/fff-bin-*/**/*"]);
it("unpacks the fff shared library and ffi-rs native bindings for FFI access", () => {
assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, [
"node_modules/@ff-labs/fff-bin-*/**/*",
"node_modules/@yuuang/ffi-rs-*/**/*",
]);
});

it("promotes the target ffi-rs native binding to a direct staged dependency", () => {
assert.deepStrictEqual(resolveFfiRsNativeDependencies("mac", "arm64", "1.3.2"), {
"@yuuang/ffi-rs-darwin-arm64": "1.3.2",
});
assert.deepStrictEqual(resolveFfiRsNativeDependencies("mac", "universal", "1.3.2"), {
"@yuuang/ffi-rs-darwin-arm64": "1.3.2",
"@yuuang/ffi-rs-darwin-x64": "1.3.2",
});
assert.deepStrictEqual(resolveFfiRsNativeDependencies("win", "x64", "1.3.2"), {
"@yuuang/ffi-rs-win32-x64-msvc": "1.3.2",
});
assert.deepStrictEqual(resolveFfiRsNativeDependencies("win", "arm64", "1.3.2"), {
"@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2",
});
assert.deepStrictEqual(resolveFfiRsNativeDependencies("linux", "arm64", "1.3.2"), {
"@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2",
"@yuuang/ffi-rs-linux-arm64-musl": "1.3.2",
});
});

it("reads the single resolved ffi-rs version from the lockfile", () => {
const lockfile = [
"packages:",
" fetch-nodeshim@0.4.10: {}",
" ffi-rs@1.3.2:",
" resolution: {integrity: sha512-deadbeef==}",
" fill-range@7.1.1:",
"snapshots:",
" ffi-rs@1.3.2:",
" optionalDependencies:",
" '@yuuang/ffi-rs-win32-x64-msvc': 1.3.2",
"",
].join("\n");
assert.equal(parseFfiRsLockfileVersion(lockfile), "1.3.2");
});

it("fails when ffi-rs is absent or resolves to multiple versions", () => {
assert.throws(() => parseFfiRsLockfileVersion("packages:\n effect@4.0.0: {}\n"), /Could not find ffi-rs/);
assert.throws(
() => parseFfiRsLockfileVersion("packages:\n ffi-rs@1.3.2:\n ffi-rs@1.4.0:\n"),
/single ffi-rs version/,
);
});

it("promotes target fff binaries to direct staged dependencies", () => {
Expand Down
105 changes: 99 additions & 6 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,13 @@ interface StagePackageJson {
}

export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const;
export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const;
export const DESKTOP_ASAR_UNPACK = [
"node_modules/@ff-labs/fff-bin-*/**/*",
// ffi-rs (used by @ff-labs/fff-node) loads its prebuilt native addon from a
// platform-specific @yuuang/ffi-rs-* package. The `.node` must live outside
// the asar archive so Electron can dlopen it at runtime.
"node_modules/@yuuang/ffi-rs-*/**/*",
] as const;

export function resolveFffNativeDependencies(
platform: typeof BuildPlatform.Type,
Expand Down Expand Up @@ -319,6 +325,88 @@ export function resolveFffNativeDependencies(
);
}

export function resolveFfiRsNativeDependencies(
platform: typeof BuildPlatform.Type,
arch: typeof BuildArch.Type,
version: string,
): Record<string, string> {
const architectures = arch === "universal" ? (["arm64", "x64"] as const) : [arch];

if (platform === "mac") {
return Object.fromEntries(
architectures.map((architecture) => [`@yuuang/ffi-rs-darwin-${architecture}`, version]),
);
}

if (platform === "win") {
return Object.fromEntries(
architectures.map((architecture) => [`@yuuang/ffi-rs-win32-${architecture}-msvc`, version]),
);
}

return Object.fromEntries(
architectures.flatMap((architecture) =>
["gnu", "musl"].map((libc) => [`@yuuang/ffi-rs-linux-${architecture}-${libc}`, version]),
),
);
}

// ffi-rs is a transitive dependency (via @ff-labs/fff-node), so its version is
// not declared in any package.json. Its prebuilt @yuuang/ffi-rs-* bindings are
// published in lockstep with ffi-rs, so we read the resolved version from the
// lockfile to pin the binding we promote into the staged install.
export function parseFfiRsLockfileVersion(lockfileContents: string): string {
const versions = new Set<string>();
const pattern = /(?:^|\n) {2}ffi-rs@([^:\s(]+):/g;
for (const match of lockfileContents.matchAll(pattern)) {
const version = match[1];
if (version !== undefined) {
versions.add(version);
}
}

const uniqueVersions = [...versions];
if (uniqueVersions.length > 1) {
throw new Error(
`Expected a single ffi-rs version in pnpm-lock.yaml but found: ${uniqueVersions.sort().join(", ")}.`,
);
}

const [version] = uniqueVersions;
if (version === undefined) {
throw new Error("Could not find ffi-rs in pnpm-lock.yaml.");
}

return version;
}

const resolveFfiRsBindingVersion = Effect.fn("resolveFfiRsBindingVersion")(function* (
repoRoot: string,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const lockfileContents = yield* fs
.readFileString(path.join(repoRoot, "pnpm-lock.yaml"))
.pipe(
Effect.mapError(
(cause) =>
new BuildScriptError({
message: "Could not read pnpm-lock.yaml to resolve ffi-rs native bindings.",
cause,
}),
),
);

return yield* Effect.try({
try: () => parseFfiRsLockfileVersion(lockfileContents),
catch: (cause) =>
new BuildScriptError({
message: "Could not resolve the ffi-rs native binding version from pnpm-lock.yaml.",
cause,
}),
});
});

export function createStageWorkspaceConfig(
platform: typeof BuildPlatform.Type,
arch: typeof BuildArch.Type,
Expand Down Expand Up @@ -956,14 +1044,19 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
// electron-builder is filtering out stageResourcesDir directory in the AppImage for production
yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources"));

const fffNodeVersion = serverPackageJson.dependencies["@ff-labs/fff-node"];
const ffiRsNativeDependencies = fffNodeVersion
? resolveFfiRsNativeDependencies(
options.platform,
options.arch,
yield* resolveFfiRsBindingVersion(repoRoot),
)
: {};
const stageDependencies = {
...resolvedServerDependencies,
...resolvedDesktopRuntimeDependencies,
...resolveFffNativeDependencies(
options.platform,
options.arch,
serverPackageJson.dependencies["@ff-labs/fff-node"],
),
...resolveFffNativeDependencies(options.platform, options.arch, fffNodeVersion),
...ffiRsNativeDependencies,
};
const stagePnpmConfig = createStagePnpmConfig(workspacePatchedDependencies, stageDependencies);
const stagePackageJson: StagePackageJson = {
Expand Down
Loading