Files
qwen-code/scripts/tests/copy-bundle-openTui-assets.test.js
易良 005fc97b33 fix(desktop): unblock macOS and Linux desktop packaging (#11518)
* fix(build): stop bundling the OpenTUI library for the other libc

The OpenTUI asset copier ships the native render library from every
installed `@opentui/core-*` platform package. On Linux that is two
packages, not one: the glibc and musl builds differ only by the `libc`
field, so a glibc host installs both and the bundle carries both ELF
libraries.

That breaks AppImage packaging. linuxdeploy walks every ELF file in the
AppDir to deploy its dependencies, and the musl library needs `libc.so`,
which no glibc system resolves; `ldd` exits non-zero and linuxdeploy
aborts with nothing but `failed to run linuxdeploy` reaching the log.
Desktop release 0.2.3-preview.0 failed this way on Linux, because the
desktop app copies this dist tree into its bundled runtime verbatim.

Skip the Linux package built for the libc this host does not use. The
remaining platforms are untouched: their libraries are not ELF, so the
Linux bundler ignores them and the completeness gate keeps working. It
also drops roughly 27 MB from every Linux bundle.

* fix(desktop): sign every Mach-O binary in the bundled runtime

The macOS signing step walks a list: the ripgrep vendor binaries and the
Node.js runtime. That list covered everything the bundled runtime carried
when it was written, but the runtime is a copy of the CLI's dist tree, so
its native payload changes whenever a CLI dependency does — with nothing
in this package changing.

A renderer library arrived that way and shipped unsigned. Release
0.2.3-preview.0 built for twenty minutes on both macOS targets and then
lost the archive to the notary service: "The binary is not signed with a
valid Developer ID certificate."

Enumerate the Mach-O files under the staged runtime and sign all of them,
keeping the JIT entitlement on the Node.js binary alone. Verify the
signatures in the same step so a gap fails the build in seconds instead
of surfacing from the notary service after the build.
2026-09-10 03:33:27 +00:00

190 lines
6.5 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Verifies copy_bundle_assets.js relocates the OpenTUI renderer runtime
* assets (parser worker, tree-sitter grammars, web-tree-sitter wasm and the
* native render library) into dist/opentui-assets under exactly the keys the
* runtime gate (packages/cli/src/ui/opentui/opentui-assets.ts) checks before
* pointing OTUI_ASSET_ROOT at them.
*/
import { describe, expect, it } from 'vitest';
import {
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { copyOpenTuiAssets } from '../copy_bundle_assets.js';
function seedOpentuiPackages(root) {
const coreDir = join(root, 'node_modules', '@opentui', 'core');
mkdirSync(coreDir, { recursive: true });
writeFileSync(
join(coreDir, 'package.json'),
JSON.stringify({ name: '@opentui/core', main: 'index.node.js' }),
);
writeFileSync(join(coreDir, 'index.node.js'), 'module.exports = {};');
writeFileSync(join(coreDir, 'parser.worker.js'), '// worker');
for (const [lang, files] of [
['javascript', ['highlights.scm', 'tree-sitter-javascript.wasm']],
[
'markdown',
['highlights.scm', 'injections.scm', 'tree-sitter-markdown.wasm'],
],
]) {
const langDir = join(coreDir, 'assets', lang);
mkdirSync(langDir, { recursive: true });
for (const file of files) {
writeFileSync(join(langDir, file), `${lang}/${file}`);
}
}
const platformDir = join(
root,
'node_modules',
'@opentui',
'core-darwin-arm64',
);
mkdirSync(platformDir, { recursive: true });
writeFileSync(
join(platformDir, 'package.json'),
JSON.stringify({ name: '@opentui/core-darwin-arm64' }),
);
writeFileSync(join(platformDir, 'libopentui.dylib'), 'native');
const treeSitterDir = join(root, 'node_modules', 'web-tree-sitter');
mkdirSync(treeSitterDir, { recursive: true });
writeFileSync(
join(treeSitterDir, 'package.json'),
JSON.stringify({ name: 'web-tree-sitter', main: 'tree-sitter.js' }),
);
writeFileSync(join(treeSitterDir, 'tree-sitter.js'), '');
writeFileSync(join(treeSitterDir, 'tree-sitter.wasm'), 'wasm');
}
function seedLinuxPlatformPackages(root) {
for (const name of ['core-linux-x64', 'core-linux-x64-musl']) {
const platformDir = join(root, 'node_modules', '@opentui', name);
mkdirSync(platformDir, { recursive: true });
writeFileSync(
join(platformDir, 'package.json'),
JSON.stringify({ name: `@opentui/${name}` }),
);
writeFileSync(join(platformDir, 'libopentui.so'), 'native');
}
}
describe('copyOpenTuiAssets', () => {
it('copies the runtime assets under the exact OTUI_ASSET_ROOT keys', () => {
const root = mkdtempSync(join(tmpdir(), 'opentui-bundle-assets-'));
try {
writeFileSync(join(root, 'package.json'), JSON.stringify({}));
seedOpentuiPackages(root);
const copied = copyOpenTuiAssets({ root });
const dest = join(root, 'dist', 'opentui-assets');
expect(copied).toContain('@opentui/core/parser.worker.js');
expect(copied).toContain(
'@opentui/core/assets/javascript/tree-sitter-javascript.wasm',
);
expect(copied).toContain('@opentui/core/assets/markdown/injections.scm');
expect(copied).toContain('web-tree-sitter/tree-sitter.wasm');
expect(copied).toContain('@opentui/core-darwin-arm64/libopentui.dylib');
for (const key of copied) {
expect(existsSync(join(dest, key)), key).toBe(true);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('warns and skips (never fails the bundle) without @opentui/core', () => {
const root = mkdtempSync(join(tmpdir(), 'opentui-bundle-assets-'));
try {
writeFileSync(join(root, 'package.json'), JSON.stringify({}));
const copied = copyOpenTuiAssets({ root });
expect(copied).toEqual([]);
expect(existsSync(join(root, 'dist', 'opentui-assets'))).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('drops a stale tree from an earlier bundle', () => {
const root = mkdtempSync(join(tmpdir(), 'opentui-bundle-assets-'));
try {
writeFileSync(join(root, 'package.json'), JSON.stringify({}));
seedOpentuiPackages(root);
// Simulate a leftover from an earlier bundle whose source set still
// included a platform this install no longer carries: the runtime
// gate checks key existence only, so it must not survive a re-copy.
const staleKey = '@opentui/core-linux-x64-musl/libopentui.so';
const stalePath = join(
root,
'dist',
'opentui-assets',
...staleKey.split('/'),
);
mkdirSync(dirname(stalePath), { recursive: true });
writeFileSync(stalePath, 'stale');
const copied = copyOpenTuiAssets({ root });
expect(copied).not.toContain(staleKey);
expect(existsSync(stalePath)).toBe(false);
expect(
existsSync(
join(
root,
'dist',
'opentui-assets',
'@opentui',
'core-darwin-arm64',
'libopentui.dylib',
),
),
).toBe(true);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
// npm installs both Linux flavors on a glibc host — only `libc` separates
// them — and the musl library then aborts AppImage packaging, because
// linuxdeploy runs `ldd` over every ELF in the tree and no glibc system
// resolves its `libc.so`.
it.each([
{ musl: false, kept: 'core-linux-x64', dropped: 'core-linux-x64-musl' },
{ musl: true, kept: 'core-linux-x64-musl', dropped: 'core-linux-x64' },
])(
'copies only the Linux library for this host libc (musl: $musl)',
({ musl, kept, dropped }) => {
const root = mkdtempSync(join(tmpdir(), 'opentui-bundle-assets-'));
try {
writeFileSync(join(root, 'package.json'), JSON.stringify({}));
seedOpentuiPackages(root);
seedLinuxPlatformPackages(root);
const copied = copyOpenTuiAssets({ root, musl });
expect(copied).toContain(`@opentui/${kept}/libopentui.so`);
expect(copied).not.toContain(`@opentui/${dropped}/libopentui.so`);
const dest = join(root, 'dist', 'opentui-assets', '@opentui');
expect(existsSync(join(dest, kept, 'libopentui.so'))).toBe(true);
expect(existsSync(join(dest, dropped))).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
},
);
});