mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-20 01:01:53 +00:00
287 lines
7.7 KiB
JavaScript
287 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { Readable } from 'node:stream';
|
|
import { pipeline } from 'node:stream/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { TARGETS, writeSha256Sums } from './create-standalone-package.js';
|
|
import { isStandaloneArchiveName } from './release-asset-config.js';
|
|
import {
|
|
fail,
|
|
isMainModule,
|
|
parseCliArgs,
|
|
parseSha256Sums,
|
|
sha256File,
|
|
} from './release-script-utils.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
|
|
const RELEASE_TARGETS = [
|
|
{
|
|
qwenTarget: 'darwin-arm64',
|
|
nodeTarget: 'darwin-arm64',
|
|
nodeArchiveExtension: 'tar.gz',
|
|
},
|
|
{
|
|
qwenTarget: 'darwin-x64',
|
|
nodeTarget: 'darwin-x64',
|
|
nodeArchiveExtension: 'tar.gz',
|
|
},
|
|
{
|
|
qwenTarget: 'linux-arm64',
|
|
nodeTarget: 'linux-arm64',
|
|
nodeArchiveExtension: 'tar.xz',
|
|
},
|
|
{
|
|
qwenTarget: 'linux-x64',
|
|
nodeTarget: 'linux-x64',
|
|
nodeArchiveExtension: 'tar.xz',
|
|
},
|
|
{ qwenTarget: 'win-x64', nodeTarget: 'win-x64', nodeArchiveExtension: 'zip' },
|
|
];
|
|
const EXPECTED_ARCHIVE_COUNT = RELEASE_TARGETS.length;
|
|
const CLI_OPTIONS = {
|
|
'--help': { name: 'help', type: 'boolean' },
|
|
'-h': { name: 'help', type: 'boolean' },
|
|
'--node-version': { name: 'nodeVersion' },
|
|
'--out-dir': { name: 'outDir' },
|
|
'--runtime-dir': { name: 'runtimeDir' },
|
|
'--version': { name: 'version' },
|
|
};
|
|
|
|
if (isMainModule(import.meta.url)) {
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
|
|
help: false,
|
|
nodeVersion: undefined,
|
|
outDir: undefined,
|
|
runtimeDir: undefined,
|
|
version: undefined,
|
|
});
|
|
if (args.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
const nodeVersion = args.nodeVersion || process.versions.node;
|
|
const outDir = path.resolve(
|
|
args.outDir || path.join(rootDir, 'dist', 'standalone'),
|
|
);
|
|
const runtimeParent = path.resolve(
|
|
args.runtimeDir || process.env.RUNNER_TEMP || os.tmpdir(),
|
|
);
|
|
fs.mkdirSync(runtimeParent, { recursive: true });
|
|
const runtimeDir = fs.mkdtempSync(
|
|
path.join(runtimeParent, 'qwen-node-runtime-'),
|
|
);
|
|
const nodeDistUrl = `https://nodejs.org/dist/v${nodeVersion}`;
|
|
|
|
try {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const checksumsPath = path.join(runtimeDir, 'SHASUMS256.txt');
|
|
await downloadFile(`${nodeDistUrl}/SHASUMS256.txt`, checksumsPath);
|
|
const checksums = parseChecksums(fs.readFileSync(checksumsPath, 'utf8'));
|
|
|
|
const targetResults = await Promise.allSettled(
|
|
RELEASE_TARGETS.map(async (target) => {
|
|
await packageTarget({
|
|
...target,
|
|
nodeDistUrl,
|
|
nodeVersion,
|
|
outDir,
|
|
releaseVersion: args.version,
|
|
runtimeDir,
|
|
checksums,
|
|
});
|
|
return target.qwenTarget;
|
|
}),
|
|
);
|
|
const failures = targetResults.flatMap((result, index) =>
|
|
result.status === 'rejected'
|
|
? [
|
|
`${RELEASE_TARGETS[index].qwenTarget}: ${formatErrorReason(
|
|
result.reason,
|
|
)}`,
|
|
]
|
|
: [],
|
|
);
|
|
|
|
if (failures.length > 0) {
|
|
fail(`Failed to package standalone target(s): ${failures.join('; ')}`);
|
|
}
|
|
|
|
await writeSha256Sums(outDir);
|
|
assertStandaloneOutput(outDir);
|
|
} finally {
|
|
fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function packageTarget({
|
|
qwenTarget,
|
|
nodeTarget,
|
|
nodeArchiveExtension,
|
|
nodeDistUrl,
|
|
nodeVersion,
|
|
outDir,
|
|
releaseVersion,
|
|
runtimeDir,
|
|
checksums,
|
|
}) {
|
|
const archiveName = `node-v${nodeVersion}-${nodeTarget}.${nodeArchiveExtension}`;
|
|
const archivePath = path.join(runtimeDir, archiveName);
|
|
|
|
await downloadFile(`${nodeDistUrl}/${archiveName}`, archivePath);
|
|
await verifyNodeArchive(archivePath, archiveName, checksums);
|
|
|
|
const args = [
|
|
'scripts/create-standalone-package.js',
|
|
'--target',
|
|
qwenTarget,
|
|
'--node-archive',
|
|
archivePath,
|
|
'--out-dir',
|
|
outDir,
|
|
'--skip-checksums',
|
|
];
|
|
if (releaseVersion) {
|
|
args.push('--version', releaseVersion);
|
|
}
|
|
|
|
execFileSync(process.execPath, args, {
|
|
cwd: rootDir,
|
|
stdio: 'inherit',
|
|
});
|
|
}
|
|
|
|
function formatErrorReason(reason) {
|
|
if (reason instanceof Error) {
|
|
return reason.message;
|
|
}
|
|
return String(reason);
|
|
}
|
|
|
|
async function downloadFile(url, destination) {
|
|
console.log(`Downloading ${url}`);
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
fail(
|
|
`Failed to download ${url}: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
if (!response.body) {
|
|
fail(`Failed to download ${url}: response body was empty`);
|
|
}
|
|
await pipeline(
|
|
Readable.fromWeb(response.body),
|
|
fs.createWriteStream(destination),
|
|
);
|
|
}
|
|
|
|
function parseChecksums(content) {
|
|
return parseSha256Sums(content);
|
|
}
|
|
|
|
async function verifyNodeArchive(archivePath, archiveName, checksums) {
|
|
const expected = checksums.get(archiveName);
|
|
if (!expected) {
|
|
fail(`Node.js SHASUMS256.txt does not list ${archiveName}`);
|
|
}
|
|
|
|
const actual = await sha256File(archivePath);
|
|
if (actual !== expected) {
|
|
fail(`Checksum verification failed for ${archiveName}`);
|
|
}
|
|
|
|
console.log(`Verified Node.js runtime checksum for ${archiveName}`);
|
|
}
|
|
|
|
function assertStandaloneOutput(outDir) {
|
|
const checksumPath = path.join(outDir, 'SHA256SUMS');
|
|
if (!fs.existsSync(checksumPath)) {
|
|
fail(`Standalone SHA256SUMS was not created at ${checksumPath}`);
|
|
}
|
|
|
|
const archiveNames = fs
|
|
.readFileSync(checksumPath, 'utf8')
|
|
.split(/\r?\n/)
|
|
.filter((line) => /^[0-9a-f]{64}\s+/.test(line))
|
|
.map((line) => line.trim().split(/\s+/, 2)[1]?.replace(/^\*/, ''))
|
|
.filter(Boolean)
|
|
.filter(isStandaloneArchiveName)
|
|
.sort();
|
|
const expectedArchiveNames = RELEASE_TARGETS.map(({ qwenTarget }) =>
|
|
standaloneArchiveName(qwenTarget),
|
|
).sort();
|
|
const missing = expectedArchiveNames.filter(
|
|
(archiveName) => !archiveNames.includes(archiveName),
|
|
);
|
|
const extra = archiveNames.filter(
|
|
(archiveName) => !expectedArchiveNames.includes(archiveName),
|
|
);
|
|
|
|
if (
|
|
archiveNames.length !== EXPECTED_ARCHIVE_COUNT ||
|
|
missing.length > 0 ||
|
|
extra.length > 0
|
|
) {
|
|
fail(
|
|
[
|
|
`Expected standalone checksums for ${expectedArchiveNames.join(', ')}`,
|
|
`found ${archiveNames.join(', ') || 'none'}.`,
|
|
missing.length > 0 ? `Missing: ${missing.join(', ')}.` : '',
|
|
extra.length > 0 ? `Extra: ${extra.join(', ')}.` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' '),
|
|
);
|
|
}
|
|
|
|
console.log(`Verified ${archiveNames.length} standalone release checksums.`);
|
|
}
|
|
|
|
function standaloneArchiveName(qwenTarget) {
|
|
const targetConfig = TARGETS.get(qwenTarget);
|
|
if (!targetConfig) {
|
|
fail(`No standalone package target config found for ${qwenTarget}`);
|
|
}
|
|
return `qwen-code-${qwenTarget}.${targetConfig.outputExtension}`;
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Usage:
|
|
npm run package:standalone:release -- [OPTIONS]
|
|
|
|
Options:
|
|
--version VERSION Release version written to standalone manifests.
|
|
--out-dir PATH Output directory. Defaults to dist/standalone.
|
|
--runtime-dir PATH Temporary Node.js runtime download directory.
|
|
--node-version VERSION Node.js version to download. Defaults to current Node.
|
|
|
|
Host requirements:
|
|
Linux Node.js runtimes are downloaded as tar.xz archives, so the host
|
|
needs xz support (Ubuntu/Debian: xz-utils; Alpine: xz; macOS/Windows: built-in).
|
|
`);
|
|
}
|
|
|
|
export { assertStandaloneOutput, parseChecksums, RELEASE_TARGETS };
|