Fix packaged beta build resources

This commit is contained in:
PerishCode 2026-05-14 18:01:52 +08:00
parent cba8bf151d
commit e0c76a09f2
8 changed files with 209 additions and 5 deletions

View file

@ -1023,6 +1023,11 @@ const PROMPT_TEMPLATES_DIR = resolveDaemonResourceDir(
'prompt-templates',
path.join(PROJECT_ROOT, 'prompt-templates'),
);
const BUNDLED_PLUGINS_DIR = resolveDaemonResourceDir(
DAEMON_RESOURCE_ROOT,
path.join('plugins', '_official'),
defaultBundledRoot(PROJECT_ROOT),
);
export function resolveDataDir(raw, projectRoot) {
if (!raw) return path.join(projectRoot, '.od');
// expandHomePrefix is shared with media-config.ts so OD_DATA_DIR and
@ -2635,14 +2640,15 @@ export async function startServer({
}
// Plan §3.I3 / spec §23.3.5 — register every plugin under
// <projectRoot>/plugins/_official/** as a bundled plugin. The walker
// <resourceRoot>/plugins/_official/** in packaged runs, or
// <projectRoot>/plugins/_official/** in workspace runs, as bundled plugins. The walker
// is idempotent (upserts on every boot) so a daemon upgrade rotates
// the bundled set in lockstep with the code. ENOENT is silent —
// running the daemon outside the dev tree just skips this step.
try {
const result = await registerBundledPlugins({
db,
bundledRoot: defaultBundledRoot(PROJECT_ROOT),
bundledRoot: BUNDLED_PLUGINS_DIR,
});
if (result.registered.length > 0) {
console.log(`[plugins] registered ${result.registered.length} bundled plugin(s)`);

View file

@ -557,6 +557,73 @@ function isMacCodeBundle(name) {
return name.endsWith(".app") || name.endsWith(".framework");
}
async function ensureRelativeSymlink(linkPath, targetPath, type) {
if (await pathLstatExists(linkPath)) {
const metadata = await lstat(linkPath);
if (metadata.isSymbolicLink()) {
const existingTarget = await readlink(linkPath);
if (existingTarget === targetPath) return false;
}
await rm(linkPath, { force: true, recursive: true });
}
await symlink(targetPath, linkPath, type);
return true;
}
async function normalizeMacVersionedFramework(frameworkPath) {
const versionsRoot = path.join(frameworkPath, "Versions");
const entries = await readdir(versionsRoot, { withFileTypes: true }).catch(() => []);
const versionName = entries
.filter((entry) => entry.isDirectory() && entry.name !== "Current")
.map((entry) => entry.name)
.sort()[0];
if (versionName == null) return false;
const versionPath = path.join(versionsRoot, versionName);
await ensureRelativeSymlink(path.join(versionsRoot, "Current"), versionName, "dir");
const versionEntries = await readdir(versionPath, { withFileTypes: true }).catch(() => []);
let changed = false;
for (const entry of versionEntries) {
if (entry.name === "_CodeSignature") continue;
const targetPath = `Versions/Current/${entry.name}`;
const linkPath = path.join(frameworkPath, entry.name);
const type = entry.isDirectory() ? "dir" : "file";
changed = (await ensureRelativeSymlink(linkPath, targetPath, type)) || changed;
}
return changed;
}
async function normalizeMacVersionedFrameworks(appPath) {
const frameworksRoot = path.join(appPath, "Contents", "Frameworks");
async function visit(current) {
const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const entryPath = path.join(current, entry.name);
if (entry.name.endsWith(".framework")) {
await normalizeMacVersionedFramework(entryPath);
continue;
}
await visit(entryPath);
}
}
await visit(frameworksRoot);
}
async function resolveMacAdhocSignTarget(bundlePath, bundleName) {
if (!bundleName.endsWith(".framework")) return bundlePath;
const currentVersionPath = path.join(bundlePath, "Versions", "Current");
if (await pathExists(currentVersionPath)) return currentVersionPath;
return bundlePath;
}
async function collectMacAdhocSignTargets(appPath) {
const frameworksRoot = path.join(appPath, "Contents", "Frameworks");
const targets = [];
@ -567,7 +634,7 @@ async function collectMacAdhocSignTargets(appPath) {
if (!entry.isDirectory()) continue;
const entryPath = path.join(current, entry.name);
if (isMacCodeBundle(entry.name)) {
targets.push(entryPath);
targets.push(await resolveMacAdhocSignTarget(entryPath, entry.name));
continue;
}
await visit(entryPath);
@ -580,6 +647,7 @@ async function collectMacAdhocSignTargets(appPath) {
}
async function signMacAdhocBundle(appPath) {
await normalizeMacVersionedFrameworks(appPath);
const targets = await collectMacAdhocSignTargets(appPath);
for (const target of targets) {
await execFileAsync("codesign", ["--force", "--sign", "-", "--timestamp=none", target], {

View file

@ -14,6 +14,8 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise<void> {
await runPnpm(config, ["--filter", "@open-design/sidecar-proto", "build"]);
await runPnpm(config, ["--filter", "@open-design/sidecar", "build"]);
await runPnpm(config, ["--filter", "@open-design/platform", "build"]);
await runPnpm(config, ["--filter", "@open-design/agui-adapter", "build"]);
await runPnpm(config, ["--filter", "@open-design/plugin-runtime", "build"]);
await runPnpm(config, ["--filter", "@open-design/daemon", "build"]);
try {
await runPnpm(config, ["--filter", "@open-design/web", "build"], {

View file

@ -59,6 +59,7 @@ const BUNDLED_RESOURCE_TREES = [
{ from: "design-templates", to: "design-templates" },
{ from: "design-systems", to: "design-systems" },
{ from: "craft", to: "craft" },
{ from: join("plugins", "_official"), to: join("plugins", "_official") },
{ from: join("assets", "frames"), to: "frames" },
{ from: join("assets", "community-pets"), to: "community-pets" },
{ from: "prompt-templates", to: "prompt-templates" },

View file

@ -119,6 +119,8 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise<void> {
await runPnpm(config, ["--filter", "@open-design/sidecar-proto", "build"]);
await runPnpm(config, ["--filter", "@open-design/sidecar", "build"]);
await runPnpm(config, ["--filter", "@open-design/platform", "build"]);
await runPnpm(config, ["--filter", "@open-design/agui-adapter", "build"]);
await runPnpm(config, ["--filter", "@open-design/plugin-runtime", "build"]);
await runPnpm(config, ["--filter", "@open-design/daemon", "build"]);
try {
await runPnpm(config, ["--filter", "@open-design/web", "build"], { OD_WEB_OUTPUT_MODE: config.webOutputMode });

View file

@ -38,6 +38,9 @@ describe("copyBundledResourceTrees", () => {
recursive: true,
});
await mkdir(join(workspaceRoot, "craft", "sample"), { recursive: true });
await mkdir(join(workspaceRoot, "plugins", "_official", "sample"), {
recursive: true,
});
await mkdir(join(workspaceRoot, "assets", "frames"), { recursive: true });
await mkdir(join(workspaceRoot, "assets", "community-pets", "sample"), {
recursive: true,
@ -47,6 +50,11 @@ describe("copyBundledResourceTrees", () => {
});
await writeFile(promptTemplatePath, "{\"id\":\"sample\"}\n", "utf8");
await writeFile(communityPetPath, "{\"name\":\"sample\"}\n", "utf8");
await writeFile(
join(workspaceRoot, "plugins", "_official", "sample", "open-design.json"),
"{\"id\":\"sample\"}\n",
"utf8",
);
await copyBundledResourceTrees({ workspaceRoot, resourceRoot });
@ -62,6 +70,12 @@ describe("copyBundledResourceTrees", () => {
"utf8",
),
).resolves.toBe("{\"name\":\"sample\"}\n");
await expect(
readFile(
join(resourceRoot, "plugins", "_official", "sample", "open-design.json"),
"utf8",
),
).resolves.toBe("{\"id\":\"sample\"}\n");
} finally {
await rm(root, { force: true, recursive: true });
}

View file

@ -1,4 +1,4 @@
import { access, mkdir, mkdtemp, readFile, readlink, rm, symlink, writeFile } from "node:fs/promises";
import { access, chmod, mkdir, mkdtemp, readFile, readlink, rm, symlink, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import path, { join } from "node:path";
@ -89,11 +89,13 @@ async function writeStandaloneFixture(
async function runFixture(options: {
includeHoistedNext?: boolean;
includeWebNext: boolean;
macAdhocBundleSign?: boolean;
omitMacAdhocBundleSign?: boolean;
omitRootWebPackage?: boolean;
platformName?: "darwin" | "win32";
requireRootWebPackageAudit?: boolean;
useAbsolutePnpmSymlinks?: boolean;
writeMacCodeBundleFixture?: boolean;
}): Promise<{
appOutDir: string;
auditReportPath: string;
@ -112,11 +114,24 @@ async function runFixture(options: {
const resourcesRoot = platformName === "darwin"
? join(appOutDir, "Open Design.app", "Contents", "Resources")
: join(appOutDir, "resources");
const appPath = join(appOutDir, "Open Design.app");
const auditReportPath = join(root, "audit.json");
const configPath = join(root, "config.json");
const oldConfigEnv = process.env[CONFIG_ENV];
await mkdir(resourcesRoot, { recursive: true });
if (platformName === "darwin" && options.writeMacCodeBundleFixture) {
const frameworksRoot = join(appPath, "Contents", "Frameworks");
const electronFrameworkRoot = join(frameworksRoot, "Electron Framework.framework");
await mkdir(join(electronFrameworkRoot, "Resources"), { recursive: true });
await mkdir(join(electronFrameworkRoot, "Versions", "A", "Resources"), { recursive: true });
await mkdir(join(electronFrameworkRoot, "Versions", "Current", "Resources"), { recursive: true });
await writeFile(join(electronFrameworkRoot, "Electron Framework"), "binary\n", "utf8");
await writeFile(join(electronFrameworkRoot, "Versions", "A", "Electron Framework"), "binary\n", "utf8");
await writeFile(join(electronFrameworkRoot, "Versions", "Current", "Electron Framework"), "binary\n", "utf8");
await mkdir(join(frameworksRoot, "ReactiveObjC.framework"), { recursive: true });
await mkdir(join(frameworksRoot, "Open Design Helper.app"), { recursive: true });
}
if (options.omitRootWebPackage !== true) {
await writeRootWebPackage(resourcesRoot);
}
@ -125,7 +140,7 @@ async function runFixture(options: {
`${JSON.stringify(
{
auditReportPath,
...(options.omitMacAdhocBundleSign ? {} : { macAdhocBundleSign: false }),
...(options.omitMacAdhocBundleSign ? {} : { macAdhocBundleSign: options.macAdhocBundleSign ?? false }),
pruneCopiedSharp: false,
pruneRootNext: false,
pruneRootSharp: false,
@ -270,4 +285,98 @@ describe("web standalone afterPack hook", () => {
await rm(fixture.root, { force: true, recursive: true });
}
});
darwinSymlinkIt("signs versioned mac frameworks at their Current version path", async () => {
const codesignRoot = await mkdtemp(join(tmpdir(), "open-design-fake-codesign-"));
const codesignBin = join(codesignRoot, "bin");
const codesignLog = join(codesignRoot, "codesign.log");
const oldPath = process.env.PATH;
const oldCodesignLog = process.env.OD_FAKE_CODESIGN_LOG;
await mkdir(codesignBin, { recursive: true });
await writeFile(
join(codesignBin, "codesign"),
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$OD_FAKE_CODESIGN_LOG\"\n",
"utf8",
);
await chmod(join(codesignBin, "codesign"), 0o755);
process.env.PATH = `${codesignBin}${path.delimiter}${oldPath ?? ""}`;
process.env.OD_FAKE_CODESIGN_LOG = codesignLog;
let fixture: Awaited<ReturnType<typeof runFixture>> | null = null;
try {
fixture = await runFixture({
includeWebNext: true,
macAdhocBundleSign: true,
platformName: "darwin",
writeMacCodeBundleFixture: true,
});
const report = JSON.parse(await readFile(fixture.auditReportPath, "utf8")) as {
macAdhocBundleSign: string[];
};
const signedTargets = report.macAdhocBundleSign.map((target) => target.split(path.sep).join("/"));
const codesignInvocations = await readFile(codesignLog, "utf8");
expect(signedTargets).toEqual(
expect.arrayContaining([
expect.stringMatching(/Electron Framework\.framework\/Versions\/Current$/),
expect.stringMatching(/ReactiveObjC\.framework$/),
expect.stringMatching(/Open Design Helper\.app$/),
expect.stringMatching(/Open Design\.app$/),
]),
);
expect(signedTargets).not.toContainEqual(expect.stringMatching(/Electron Framework\.framework$/));
await expect(
readlink(join(
fixture.appOutDir,
"Open Design.app",
"Contents",
"Frameworks",
"Electron Framework.framework",
"Versions",
"Current",
)),
).resolves.toBe("A");
await expect(
readlink(join(
fixture.appOutDir,
"Open Design.app",
"Contents",
"Frameworks",
"Electron Framework.framework",
"Electron Framework",
)),
).resolves.toBe("Versions/Current/Electron Framework");
await expect(
readlink(join(
fixture.appOutDir,
"Open Design.app",
"Contents",
"Frameworks",
"Electron Framework.framework",
"Resources",
)),
).resolves.toBe("Versions/Current/Resources");
expect(codesignInvocations.split(path.sep).join("/")).toContain(
"Electron Framework.framework/Versions/Current",
);
} finally {
if (fixture != null) {
await rm(fixture.root, { force: true, recursive: true });
}
await rm(codesignRoot, { force: true, recursive: true });
if (oldPath == null) {
delete process.env.PATH;
} else {
process.env.PATH = oldPath;
}
if (oldCodesignLog == null) {
delete process.env.OD_FAKE_CODESIGN_LOG;
} else {
process.env.OD_FAKE_CODESIGN_LOG = oldCodesignLog;
}
}
});
});

View file

@ -12,6 +12,8 @@ const PACKAGE_DIRS = [
"packages/sidecar-proto",
"packages/sidecar",
"packages/platform",
"packages/agui-adapter",
"packages/plugin-runtime",
"apps/daemon",
"apps/web",
"apps/desktop",