mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-19 16:28:28 +00:00
chore(installer): narrow hosted release diff
This commit is contained in:
parent
b8e54900f2
commit
fedcbae1c9
7 changed files with 489 additions and 1203 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue