Skip to content
Merged
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
25 changes: 21 additions & 4 deletions scripts/create-standalone-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ const TARGET_PREBUILD_DIR = new Map([

const DIST_REQUIRED_PATHS = [
'cli.js',
'cli-entry.js',
'chunks',
'vendor',
'bundled/qc-helper/docs',
];
const DIST_ALLOWED_ENTRIES = new Set([
'cli.js',
// bin wrapper emitted by prepare-package.js that re-spawns `node --expose-gc
// cli.js`; ships in dist/ as the package `bin` entry (#4914).
// bin wrapper emitted by prepare-package.js. Standalone shims use it for
// `qwen serve` so daemon startup gets the same fast path as npm installs.
'cli-entry.js',
// fzf fuzzy-search worker; esbuild emits it as a standalone entry that must
// sit next to cli.js so `new URL('./fzfWorker.js', ...)` resolves at runtime.
Expand All @@ -79,6 +80,9 @@ const DIST_ALLOWED_ENTRIES = new Set([
const DIST_ALLOWED_ENTRY_PATTERNS = [
/^sandbox-macos-(permissive|restrictive)-(open|closed|proxied)\.sb$/,
];
// Emitted into dist/ by prepare-package.js for npm publishing only;
// standalone archives must not copy them into lib/.
const DIST_NPM_PACKAGE_ONLY_ENTRIES = new Set(['postinstall.js', 'patches']);
Comment thread
doudouOUC marked this conversation as resolved.
const ROOT_REQUIRED_PATHS = ['README.md', 'LICENSE'];

if (isMainModule()) {
Expand Down Expand Up @@ -240,7 +244,10 @@ function assertRequiredInputs() {
for (const relativePath of DIST_REQUIRED_PATHS) {
const fullPath = path.join(distDir, relativePath);
if (!fs.existsSync(fullPath)) {
fail(`Required dist asset missing: ${fullPath}`);
fail(
`Required dist asset missing: ${fullPath}. ` +
'Run "npm run bundle" and "npm run prepare:package" first.',
);
}
}

Expand Down Expand Up @@ -271,7 +278,8 @@ function copyRuntimeAssets(packageRoot, outDir) {
if (
entry === skippedDistEntry ||
entry === '.DS_Store' ||
entry === 'node_modules'
entry === 'node_modules' ||
DIST_NPM_PACKAGE_ONLY_ENTRIES.has(entry)
) {
continue;
}
Expand Down Expand Up @@ -599,6 +607,9 @@ function writeShims(packageRoot) {
const unixShim = `#!/usr/bin/env sh
set -e
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
if [ "\${1:-}" = "serve" ]; then
exec "$ROOT/node/bin/node" "$ROOT/lib/cli-entry.js" "$@"
fi
exec "$ROOT/node/bin/node" --expose-gc "$ROOT/lib/cli.js" "$@"
`;
const unixShimPath = path.join(binDir, 'qwen');
Expand All @@ -608,7 +619,13 @@ exec "$ROOT/node/bin/node" --expose-gc "$ROOT/lib/cli.js" "$@"
const windowsShim = `@echo off
setlocal
set "ROOT=%~dp0.."
if "%~1"=="serve" goto serve
"%ROOT%\\node\\node.exe" --expose-gc "%ROOT%\\lib\\cli.js" %*
exit /b %ERRORLEVEL%

:serve
"%ROOT%\\node\\node.exe" "%ROOT%\\lib\\cli-entry.js" %*
exit /b %ERRORLEVEL%
`;
fs.writeFileSync(path.join(binDir, 'qwen.cmd'), windowsShim);
}
Expand Down
137 changes: 136 additions & 1 deletion scripts/tests/install-script.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,36 @@ describe('standalone release packaging', () => {
}
});

it('requires the standalone cli-entry wrapper in dist', () => {
const createdDist = ensureMinimalDist({ includeCliEntry: false });
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));

try {
expect(() =>
execFileSync(
'node',
[
'scripts/create-standalone-package.js',
'--target',
'win-x64',
'--node-archive',
createFakeWindowsNodeArchive(tmpDir),
'--out-dir',
path.join(tmpDir, 'out'),
'--version',
'0.0.0-test',
],
{ stdio: 'pipe' },
),
).toThrow(
/Required dist asset missing: .*cli-entry\.js[\s\S]*npm run prepare:package/,
);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
restoreMinimalDist(createdDist);
}
});

it('packages a win-x64 standalone archive', () => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));
Expand Down Expand Up @@ -1677,9 +1707,23 @@ describe('standalone release packaging', () => {
expect(
existsSync(path.join(extractDir, 'qwen-code', 'bin', 'qwen.cmd')),
).toBe(true);
expect(
existsSync(path.join(extractDir, 'qwen-code', 'lib', 'cli-entry.js')),
).toBe(true);
expect(
existsSync(path.join(extractDir, 'qwen-code', 'node', 'node.exe')),
).toBe(true);
const shim = readScript(
path.join(extractDir, 'qwen-code', 'bin', 'qwen.cmd'),
);
expect(shim).toContain('if "%~1"=="serve" goto serve');
expect(shim).toContain(
'"%ROOT%\\node\\node.exe" --expose-gc "%ROOT%\\lib\\cli.js" %*',
);
expect(shim).toContain(
'"%ROOT%\\node\\node.exe" "%ROOT%\\lib\\cli-entry.js" %*',
);
expect((shim.match(/exit \/b %ERRORLEVEL%/g) || []).length).toBe(2);
expect(readScript(path.join(outDir, 'SHA256SUMS'))).toContain(
Comment thread
doudouOUC marked this conversation as resolved.
'qwen-code-win-x64.zip',
);
Expand All @@ -1689,6 +1733,49 @@ describe('standalone release packaging', () => {
}
}, 30_000);

it('skips npm-only artifacts staged in dist', () => {
const createdDist = ensureMinimalDist({
includeNpmPackageArtifacts: true,
});
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));

try {
const outDir = path.join(tmpDir, 'out');
execFileSync(
'node',
[
'scripts/create-standalone-package.js',
'--target',
'win-x64',
'--node-archive',
createFakeWindowsNodeArchive(tmpDir),
'--out-dir',
outDir,
'--version',
'0.0.0-test',
],
{ stdio: 'pipe' },
);

const extractDir = path.join(tmpDir, 'extract');
mkdirSync(extractDir, { recursive: true });
extractZipForTest(path.join(outDir, 'qwen-code-win-x64.zip'), extractDir);

expect(
existsSync(path.join(extractDir, 'qwen-code', 'lib', 'cli-entry.js')),
).toBe(true);
expect(
existsSync(path.join(extractDir, 'qwen-code', 'lib', 'postinstall.js')),
).toBe(false);
expect(
existsSync(path.join(extractDir, 'qwen-code', 'lib', 'patches')),
).toBe(false);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
restoreMinimalDist(createdDist);
}
}, 30_000);

it('requires the native audio prebuild when release packaging opts in', () => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));
Expand Down Expand Up @@ -1783,6 +1870,37 @@ describe('standalone release packaging', () => {
}
});

itOnUnix('packages a Unix standalone archive with a serve fast path shim', () => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));

try {
const archive = packageFakeStandalone(tmpDir);
const extractDir = path.join(tmpDir, 'extract');
mkdirSync(extractDir, { recursive: true });
execFileSync('tar', ['-xzf', archive, '-C', extractDir], {
stdio: 'ignore',
});

expect(
existsSync(path.join(extractDir, 'qwen-code', 'lib', 'cli-entry.js')),
).toBe(true);
const shim = readScript(
path.join(extractDir, 'qwen-code', 'bin', 'qwen'),
);
expect(shim).toContain('if [ "${1:-}" = "serve" ]; then');
expect(shim).toContain(
'exec "$ROOT/node/bin/node" "$ROOT/lib/cli-entry.js" "$@"',
);
expect(shim).toContain(
'exec "$ROOT/node/bin/node" --expose-gc "$ROOT/lib/cli.js" "$@"',
);
} finally {
restoreMinimalDist(createdDist);
rmSync(tmpDir, { recursive: true, force: true });
}
});

itOnUnix('does not package audio-capture test artifacts', () => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-'));
Expand Down Expand Up @@ -3979,7 +4097,10 @@ describe('Windows PowerShell uninstaller end-to-end', () => {
});
});

function ensureMinimalDist() {
function ensureMinimalDist({
includeCliEntry = true,
includeNpmPackageArtifacts = false,
} = {}) {
const distPath = path.resolve('dist');
const backupPath = existsSync(distPath)
? path.join(
Expand All @@ -3999,6 +4120,20 @@ function ensureMinimalDist() {
recursive: true,
});
writeFileSync(path.join(distPath, 'cli.js'), 'console.log("qwen");\n');
if (includeCliEntry) {
writeFileSync(path.join(distPath, 'cli-entry.js'), 'import "./cli.js";\n');
}
if (includeNpmPackageArtifacts) {
writeFileSync(
path.join(distPath, 'postinstall.js'),
'console.log("postinstall");\n',
);
mkdirSync(path.join(distPath, 'patches'), { recursive: true });
writeFileSync(
path.join(distPath, 'patches', 'dependency.patch'),
'patch\n',
);
}
writeFileSync(path.join(distPath, 'chunks/index.js'), 'export {};\n');
writeFileSync(
path.join(distPath, 'package.json'),
Expand Down
Loading