chore(installer): narrow hosted release diff

This commit is contained in:
yiliang114 2026-05-13 17:26:05 +08:00
parent b8e54900f2
commit fedcbae1c9
7 changed files with 489 additions and 1203 deletions

View file

@ -6,16 +6,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
import {
fail,
isMainModule,
parseCliArgs,
parseSha256Sums,
sha256File,
} from './release-script-utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -55,12 +50,6 @@ const HOSTED_INSTALLATION_OUTPUT_NAMES = new Set([
'SHA256SUMS',
]);
const CLI_OPTIONS = {
'--help': { name: 'help', type: 'boolean' },
'-h': { name: 'help', type: 'boolean' },
'--out-dir': { name: 'outDir' },
};
if (isMainModule(import.meta.url)) {
try {
await main();
@ -71,10 +60,7 @@ if (isMainModule(import.meta.url)) {
}
async function main() {
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
help: false,
outDir: undefined,
});
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printUsage();
return;
@ -97,6 +83,31 @@ Options:
`);
}
function parseArgs(argv) {
const args = {
help: false,
outDir: undefined,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '--help':
case '-h':
args.help = true;
break;
case '--out-dir':
args.outDir = readOptionValue(argv, index, arg);
index += 1;
break;
default:
fail(`Unknown option: ${arg}`);
}
}
return args;
}
async function buildHostedInstallationAssets(outDir, options = {}) {
const root = options.root || rootDir;
fs.mkdirSync(outDir, { recursive: true });
@ -189,11 +200,49 @@ async function assertHostedInstallationAssetChecksums(outDir) {
}
}
function isMainModule(importMetaUrl) {
const filename = fileURLToPath(importMetaUrl);
return process.argv[1] && path.resolve(process.argv[1]) === filename;
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith('-')) {
fail(`${optionName} requires a value`);
}
return value;
}
function parseSha256Sums(content) {
const checksums = new Map();
for (const [index, line] of content.split(/\r?\n/).entries()) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = /^([0-9a-fA-F]{64})\s+\*?(.+)$/.exec(trimmed);
if (!match) {
fail(`Malformed SHA256SUMS line ${index + 1}: ${trimmed}`);
}
checksums.set(match[2], match[1].toLowerCase());
}
return checksums;
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream(filePath), hash);
return hash.digest('hex');
}
function fail(message) {
throw new Error(`ERROR: ${message}`);
}
export {
HOSTED_INSTALLATION_ASSETS,
HOSTED_INSTALLATION_ASSET_NAMES,
HOSTED_INSTALLER_DEFAULT_VERSION_PATTERNS,
HOSTED_INSTALLER_REQUIRED_FRAGMENTS,
assertHostedInstallationAssetChecksums,
buildHostedInstallationAssets,
};

View file

@ -7,28 +7,19 @@
*/
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
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';
import { writeSha256Sums } from './create-standalone-package.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
// RELEASE_TARGETS must stay in sync with TARGETS in create-standalone-package.js;
// every release qwenTarget should map to a package target and output extension.
const RELEASE_TARGETS = [
{
qwenTarget: 'darwin-arm64',
@ -53,16 +44,8 @@ const RELEASE_TARGETS = [
{ 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)) {
if (isMainModule()) {
try {
await main();
} catch (error) {
@ -72,21 +55,13 @@ if (isMainModule(import.meta.url)) {
}
async function main() {
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
help: false,
nodeVersion: undefined,
outDir: undefined,
runtimeDir: undefined,
version: undefined,
});
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printUsage();
return;
}
const nodeVersion = normalizeNodeVersion(
args.nodeVersion || process.versions.node,
);
const nodeVersion = args.nodeVersion || process.versions.node;
const outDir = path.resolve(
args.outDir || path.join(rootDir, 'dist', 'standalone'),
);
@ -100,37 +75,21 @@ async function main() {
const nodeDistUrl = `https://nodejs.org/dist/v${nodeVersion}`;
try {
cleanOutputDirectory(outDir);
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('; ')}`);
for (const target of RELEASE_TARGETS) {
await packageTarget({
...target,
nodeDistUrl,
nodeVersion,
outDir,
releaseVersion: args.version,
runtimeDir,
checksums,
});
}
await writeSha256Sums(outDir);
@ -140,8 +99,8 @@ async function main() {
}
}
function normalizeNodeVersion(version) {
return version.replace(/^v/i, '');
function isMainModule() {
return process.argv[1] && path.resolve(process.argv[1]) === __filename;
}
async function packageTarget({
@ -181,18 +140,6 @@ async function packageTarget({
});
}
function formatErrorReason(reason) {
if (reason instanceof Error) {
return reason.message;
}
return String(reason);
}
function cleanOutputDirectory(outDir) {
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(outDir, { recursive: true });
}
async function downloadFile(url, destination) {
console.log(`Downloading ${url}`);
const response = await fetch(url);
@ -211,7 +158,14 @@ async function downloadFile(url, destination) {
}
function parseChecksums(content) {
return parseSha256Sums(content);
const checksums = new Map();
for (const line of content.split(/\r?\n/)) {
const [hash, fileName] = line.trim().split(/\s+/, 2);
if (hash && fileName) {
checksums.set(fileName.replace(/^\*/, ''), hash);
}
}
return checksums;
}
async function verifyNodeArchive(archivePath, archiveName, checksums) {
@ -228,19 +182,28 @@ async function verifyNodeArchive(archivePath, archiveName, checksums) {
console.log(`Verified Node.js runtime checksum for ${archiveName}`);
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream(filePath), hash);
return hash.digest('hex');
}
function assertStandaloneOutput(outDir) {
const checksumPath = path.join(outDir, 'SHA256SUMS');
if (!fs.existsSync(checksumPath)) {
fail(`Standalone SHA256SUMS was not created at ${checksumPath}`);
}
const archiveNames = Array.from(
parseSha256Sums(fs.readFileSync(checksumPath, 'utf8')).keys(),
)
.filter(isStandaloneArchiveName)
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)
.sort();
const expectedArchiveNames = RELEASE_TARGETS.map(({ qwenTarget }) =>
standaloneArchiveName(qwenTarget),
const expectedArchiveNames = RELEASE_TARGETS.map(
({ qwenTarget }) =>
`qwen-code-${qwenTarget}.${qwenTarget === 'win-x64' ? 'zip' : 'tar.gz'}`,
).sort();
const missing = expectedArchiveNames.filter(
(archiveName) => !archiveNames.includes(archiveName),
@ -269,12 +232,52 @@ function assertStandaloneOutput(outDir) {
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}`);
function parseArgs(argv) {
const args = {
help: false,
nodeVersion: undefined,
outDir: undefined,
runtimeDir: undefined,
version: undefined,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '--help':
case '-h':
args.help = true;
break;
case '--node-version':
args.nodeVersion = readOptionValue(argv, index, arg);
index += 1;
break;
case '--out-dir':
args.outDir = readOptionValue(argv, index, arg);
index += 1;
break;
case '--runtime-dir':
args.runtimeDir = readOptionValue(argv, index, arg);
index += 1;
break;
case '--version':
args.version = readOptionValue(argv, index, arg);
index += 1;
break;
default:
fail(`Unknown option: ${arg}`);
}
}
return `qwen-code-${qwenTarget}.${targetConfig.outputExtension}`;
return args;
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith('-')) {
fail(`${optionName} requires a value`);
}
return value;
}
function printUsage() {
@ -287,17 +290,11 @@ Options:
--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,
normalizeNodeVersion,
parseChecksums,
RELEASE_TARGETS,
standaloneArchiveName,
};
function fail(message) {
throw new Error(`ERROR: ${message}`);
}
export { assertStandaloneOutput, parseChecksums, RELEASE_TARGETS };

View file

@ -7,25 +7,18 @@
*/
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
import { isStandaloneArchiveName } from './release-asset-config.js';
import {
fail,
isMainModule,
parseCliArgs,
sha256File,
} from './release-script-utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const distDir = path.join(rootDir, 'dist');
// TARGETS must stay in sync with RELEASE_TARGETS in build-standalone-release.js;
// every release target should have a package target and output extension here.
const TARGETS = new Map([
[
'darwin-arm64',
@ -57,19 +50,9 @@ const DIST_ALLOWED_ENTRIES = new Set([
const DIST_ALLOWED_ENTRY_PATTERNS = [
/^sandbox-macos-(permissive|restrictive)-(open|closed|proxied)\.sb$/,
];
const DIST_IGNORED_ENTRIES = new Set(['.DS_Store', 'esbuild.json']);
const ROOT_REQUIRED_PATHS = ['README.md', 'LICENSE'];
const CLI_OPTIONS = {
'--help': { name: 'help', type: 'boolean' },
'-h': { name: 'help', type: 'boolean' },
'--target': { name: 'target' },
'--node-archive': { name: 'nodeArchive' },
'--out-dir': { name: 'outDir' },
'--version': { name: 'version' },
'--skip-checksums': { name: 'skipChecksums', type: 'boolean' },
};
if (isMainModule(import.meta.url)) {
if (isMainModule()) {
try {
await main();
} catch (error) {
@ -79,14 +62,7 @@ if (isMainModule(import.meta.url)) {
}
async function main() {
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
help: false,
nodeArchive: undefined,
outDir: undefined,
skipChecksums: false,
target: undefined,
version: undefined,
});
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printUsage();
@ -155,6 +131,62 @@ async function main() {
}
}
function isMainModule() {
return process.argv[1] && path.resolve(process.argv[1]) === __filename;
}
function parseArgs(argv) {
const args = {
help: false,
outDir: undefined,
nodeArchive: undefined,
skipChecksums: false,
target: undefined,
version: undefined,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '--help':
case '-h':
args.help = true;
break;
case '--target':
args.target = readOptionValue(argv, index, arg);
index += 1;
break;
case '--node-archive':
args.nodeArchive = readOptionValue(argv, index, arg);
index += 1;
break;
case '--out-dir':
args.outDir = readOptionValue(argv, index, arg);
index += 1;
break;
case '--version':
args.version = readOptionValue(argv, index, arg);
index += 1;
break;
case '--skip-checksums':
args.skipChecksums = true;
break;
default:
fail(`Unknown option: ${arg}`);
}
}
return args;
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith('-')) {
fail(`${optionName} requires a value`);
}
return value;
}
function printUsage() {
console.log(`Qwen Code standalone package builder
@ -202,7 +234,7 @@ function copyRuntimeAssets(packageRoot, outDir) {
fs.mkdirSync(libDir, { recursive: true });
for (const entry of fs.readdirSync(distDir)) {
if (entry === skippedDistEntry || DIST_IGNORED_ENTRIES.has(entry)) {
if (entry === skippedDistEntry || entry === '.DS_Store') {
continue;
}
if (!isAllowedDistEntry(entry)) {
@ -223,10 +255,10 @@ function copyRuntimeAssets(packageRoot, outDir) {
);
}
const packageJsonPath = fs.existsSync(path.join(distDir, 'package.json'))
? path.join(distDir, 'package.json')
: path.join(rootDir, 'package.json');
fs.copyFileSync(packageJsonPath, path.join(packageRoot, 'package.json'));
fs.copyFileSync(
path.join(rootDir, 'package.json'),
path.join(packageRoot, 'package.json'),
);
}
function topLevelDistEntryForPath(candidatePath) {
@ -491,57 +523,11 @@ function createArchive(outputExtension, outputPath, cwd) {
return;
}
// On macOS Sequoia+, every file inherits an immovable `com.apple.provenance`
// xattr that bsdtar embeds into pax extended headers. Linux GNU tar then
// emits one `Ignoring unknown extended header keyword` warning per file at
// extract time. bsdtar's `--no-mac-metadata` is silently ignored in older
// libarchive (3.5.x), and `xattr -d com.apple.provenance` is rejected by
// SIP. The reliable fix is to use GNU tar, which does not write xattrs
// unless `--xattrs` is passed.
const tarBin = pickTarBinary();
run(tarBin, ['-czf', outputPath, '-C', cwd, 'qwen-code']);
}
function pickTarBinary() {
if (process.platform !== 'darwin') return 'tar';
// Try common gtar paths (homebrew arm/intel + gnubin shim).
const candidates = [
'/opt/homebrew/bin/gtar',
'/usr/local/bin/gtar',
'/opt/homebrew/opt/gnu-tar/libexec/gnubin/tar',
];
for (const candidate of candidates) {
try {
if (fs.statSync(candidate).isFile()) return candidate;
} catch {
// continue
}
}
// PATH lookup via /bin/sh -c "command -v gtar".
try {
const out = execFileSync('/bin/sh', ['-c', 'command -v gtar'], {
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
}).trim();
if (out) return out;
} catch {
// not found
}
console.warn(
'WARNING: GNU tar (gtar) not found on macOS. Falling back to bsdtar; ' +
'archives will include com.apple.provenance pax headers that emit ' +
'noisy warnings on Linux extract. Install with: brew install gnu-tar',
);
return 'tar';
run('tar', ['-czf', outputPath, '-C', cwd, 'qwen-code']);
}
function createZipArchive(outputPath, cwd) {
if (process.platform === 'win32') {
// Use [IO.Compression.ZipFile]::CreateFromDirectory rather than
// Compress-Archive: the latter writes Windows-style backslash
// separators into ZIP entry names, which then trip the .bat
// installer's path-traversal guard against backslashes.
// CreateFromDirectory writes spec-compliant forward slashes.
run(
'powershell',
[
@ -549,7 +535,7 @@ function createZipArchive(outputPath, cwd) {
'-ExecutionPolicy',
'Bypass',
'-Command',
'Add-Type -AssemblyName System.IO.Compression.FileSystem; if (Test-Path -LiteralPath $env:QWEN_OUTPUT_PATH) { Remove-Item -LiteralPath $env:QWEN_OUTPUT_PATH -Force }; [IO.Compression.ZipFile]::CreateFromDirectory($env:QWEN_PACKAGE_ROOT, $env:QWEN_OUTPUT_PATH, [IO.Compression.CompressionLevel]::Optimal, $true)',
'Compress-Archive -LiteralPath $env:QWEN_PACKAGE_ROOT -DestinationPath $env:QWEN_OUTPUT_PATH -Force',
],
{
env: {
@ -565,31 +551,38 @@ function createZipArchive(outputPath, cwd) {
run('zip', ['-qr', outputPath, 'qwen-code'], { cwd });
}
/**
* Rebuild SHA256SUMS from scratch by scanning outDir for standalone release
* archives. This overwrites any existing SHA256SUMS, so callers must ensure
* all desired archives are present in outDir before calling.
*/
async function writeSha256Sums(outDir) {
const entries = fs.readdirSync(outDir).filter(isStandaloneArchiveName).sort();
const entries = fs
.readdirSync(outDir)
.filter(
(entry) =>
entry.startsWith('qwen-code-') &&
(entry.endsWith('.tar.gz') || entry.endsWith('.zip')),
)
.sort();
if (entries.length === 0) {
fail(
`No standalone archive files found in ${outDir}; refusing to write empty SHA256SUMS.`,
`No qwen-code archives found in ${outDir}; refusing to write empty SHA256SUMS.`,
);
}
const lines = await Promise.all(
entries.map(async (entry) => {
const filePath = path.join(outDir, entry);
const hash = await sha256File(filePath);
return `${hash} ${entry}`;
}),
);
const lines = [];
for (const entry of entries) {
const filePath = path.join(outDir, entry);
const hash = await sha256File(filePath);
lines.push(`${hash} ${entry}`);
}
fs.writeFileSync(path.join(outDir, 'SHA256SUMS'), `${lines.join('\n')}\n`);
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream(filePath), hash);
return hash.digest('hex');
}
function run(command, args, options = {}) {
try {
execFileSync(command, args, {
@ -605,4 +598,8 @@ function run(command, args, options = {}) {
}
}
export { TARGETS, writeSha256Sums };
function fail(message) {
throw new Error(`Error: ${message}`);
}
export { writeSha256Sums };

View file

@ -1,21 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
const STANDALONE_ARCHIVE_PREFIX = 'qwen-code-';
// Keep this extension allowlist in sync with the standalone packager target
// output extensions and the release workflow upload globs.
const STANDALONE_ARCHIVE_EXTENSIONS = ['.tar.gz', '.zip'];
function isStandaloneArchiveName(fileName) {
return (
fileName.startsWith(STANDALONE_ARCHIVE_PREFIX) &&
STANDALONE_ARCHIVE_EXTENSIONS.some((extension) =>
fileName.endsWith(extension),
)
);
}
export { isStandaloneArchiveName };

View file

@ -1,108 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
function isMainModule(importMetaUrl) {
const filename = fileURLToPath(importMetaUrl);
return process.argv[1] && path.resolve(process.argv[1]) === filename;
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith('-')) {
fail(`${optionName} requires a value`);
}
return value;
}
function parseCliArgs(argv, options, defaults = {}) {
const args = { ...defaults };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
let key = arg;
let inlineValue;
if (arg.startsWith('--')) {
const equalsIndex = arg.indexOf('=');
if (equalsIndex > -1) {
key = arg.slice(0, equalsIndex);
inlineValue = arg.slice(equalsIndex + 1);
}
}
const option = options[key];
if (!option) {
fail(`Unknown option: ${arg}`);
}
if (option.type === 'boolean') {
if (inlineValue !== undefined) {
fail(`${key} does not accept a value`);
}
args[option.name] = true;
continue;
}
let value;
if (inlineValue !== undefined) {
if (inlineValue === '') {
fail(`${key} requires a value`);
}
value = inlineValue;
} else {
value = readOptionValue(argv, index, key);
index += 1;
}
if (option.validate) {
option.validate(value);
}
args[option.name] = value;
}
return args;
}
function parseSha256Sums(content) {
const checksums = new Map();
for (const [index, line] of content.split(/\r?\n/).entries()) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = /^([0-9a-fA-F]{64})\s+\*?(.+)$/.exec(trimmed);
if (!match) {
fail(`Malformed SHA256SUMS line ${index + 1}: ${trimmed}`);
}
checksums.set(match[2], match[1].toLowerCase());
}
return checksums;
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream(filePath), hash);
return hash.digest('hex');
}
function fail(message) {
throw new Error(`ERROR: ${message}`);
}
export {
fail,
isMainModule,
parseCliArgs,
parseSha256Sums,
readOptionValue,
sha256File,
};

File diff suppressed because it is too large Load diff

View file

@ -6,30 +6,23 @@
* SPDX-License-Identifier: Apache-2.0
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
import {
RELEASE_TARGETS,
standaloneArchiveName,
} from './build-standalone-release.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, '..');
// This import-time computation intentionally asserts release/package target
// consistency via standaloneArchiveName(); keep RELEASE_TARGETS backed by TARGETS.
const EXPECTED_STANDALONE_ARCHIVE_NAMES = RELEASE_TARGETS.map(
({ qwenTarget }) => standaloneArchiveName(qwenTarget),
);
const EXPECTED_STANDALONE_ARCHIVE_NAMES = [
'qwen-code-darwin-arm64.tar.gz',
'qwen-code-darwin-x64.tar.gz',
'qwen-code-linux-arm64.tar.gz',
'qwen-code-linux-x64.tar.gz',
'qwen-code-win-x64.zip',
];
// Release artifacts that the installer chain expects in a GitHub Release.
// Hosted installer scripts (install-qwen.sh / install-qwen.bat) are served
// from a separate hosted endpoint and are
@ -40,13 +33,6 @@ const EXPECTED_RELEASE_ASSET_NAMES = [
'SHA256SUMS',
];
const CLI_OPTIONS = {
'--help': { name: 'help', type: 'boolean' },
'-h': { name: 'help', type: 'boolean' },
'--dir': { name: 'dir' },
'--base-url': { name: 'baseUrl' },
};
if (isMainModule(import.meta.url)) {
try {
await main();
@ -57,11 +43,7 @@ if (isMainModule(import.meta.url)) {
}
async function main() {
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
help: false,
dir: undefined,
baseUrl: undefined,
});
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printUsage();
return;
@ -94,6 +76,36 @@ Options:
`);
}
function parseArgs(argv) {
const args = {
help: false,
dir: undefined,
baseUrl: undefined,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '--help':
case '-h':
args.help = true;
break;
case '--dir':
args.dir = readOptionValue(argv, index, arg);
index += 1;
break;
case '--base-url':
args.baseUrl = readOptionValue(argv, index, arg);
index += 1;
break;
default:
fail(`Unknown option: ${arg}`);
}
}
return args;
}
async function verifyReleaseDirectory(dir) {
const checksums = readReleaseChecksums(dir);
assertExpectedChecksumEntries(checksums);
@ -172,25 +184,20 @@ function assertExpectedArchiveFiles(dir) {
}
async function assertRemoteAssetsAvailable(normalizedBaseUrl, fetchImpl) {
const results = await Promise.allSettled(
EXPECTED_STANDALONE_ARCHIVE_NAMES.map(async (assetName) => {
const failures = [];
for (const assetName of EXPECTED_STANDALONE_ARCHIVE_NAMES) {
try {
await assertRemoteAssetAvailable(
new URL(assetName, normalizedBaseUrl).toString(),
fetchImpl,
);
return assetName;
}),
);
const failures = results.flatMap((result, index) =>
result.status === 'rejected'
? [
{
assetName: EXPECTED_STANDALONE_ARCHIVE_NAMES[index],
reason: formatErrorReason(result.reason),
},
]
: [],
);
} catch (reason) {
failures.push({
assetName,
reason: formatErrorReason(reason),
});
}
}
if (failures.length === 0) {
return;
@ -265,6 +272,46 @@ function normalizeHttpsBaseUrl(baseUrl) {
return parsed.toString();
}
function isMainModule(importMetaUrl) {
const filename = fileURLToPath(importMetaUrl);
return process.argv[1] && path.resolve(process.argv[1]) === filename;
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith('-')) {
fail(`${optionName} requires a value`);
}
return value;
}
function parseSha256Sums(content) {
const checksums = new Map();
for (const [index, line] of content.split(/\r?\n/).entries()) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = /^([0-9a-fA-F]{64})\s+\*?(.+)$/.exec(trimmed);
if (!match) {
fail(`Malformed SHA256SUMS line ${index + 1}: ${trimmed}`);
}
checksums.set(match[2], match[1].toLowerCase());
}
return checksums;
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream(filePath), hash);
return hash.digest('hex');
}
function fail(message) {
throw new Error(`ERROR: ${message}`);
}
export {
EXPECTED_STANDALONE_ARCHIVE_NAMES,
verifyReleaseBaseUrl,