[build] move CLI platform packages to chunkdash scope

This commit is contained in:
dijunkun
2026-10-30 15:58:27 +08:00
parent e6e7e4c0f0
commit d984aa55de
6 changed files with 106 additions and 32 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ npx crossdesk-cli@latest doctor --json
npx crossdesk-cli@latest 123456789
```
**当前尚未发布到 npm,也尚未完成三端实机验收。** 已加入平台包生成、自动选包入口和五个平台的 CI 构建配置;运行要求、打包与本地安装见 [npm 分发](docs/npm-distribution.md)。
统一入口为 `crossdesk-cli`,五个平台依赖使用 `@chunkdash/crossdesk-cli-*`,由启动器自动选择。旧的无 scope 平台包不属于这套依赖;仅安装平台包不会注册 `crossdesk-cli` 命令。是否可安装以 npm 上完整版本的发布结果为准,三端实机验收尚未全部完成。运行要求、打包、本地安装与发布步骤见 [npm 分发](docs/npm-distribution.md)。
在 CrossDesk 仓库根目录执行以下命令构建 CLI:
+66 -15
View File
@@ -2,7 +2,7 @@
## 使用方式
发布后的统一入口如下;本仓库当前尚未向 npm 发布,不能把这些命令当成已经上线的服务。
完整版本发布后的统一入口如下。构建产物或部分平台包上传成功,不代表入口包和全部平台依赖已经可用;发布者须完成本文的注册表检查。
```sh
npx crossdesk-cli@latest doctor --json
@@ -11,17 +11,21 @@ npm install -g crossdesk-cli
crossdesk-cli 123456789
```
需要 Node.js 22 或以上和 npm。启动器根据 Node 的 `process.platform` / `process.arch` 选择同版本的平台包,直接启动原生 C++ 程序,继承终端、工作目录、环境变量和命令参数。安装过程不下载外部可执行文件、不执行 postinstall、不在用户机器编译。不要使用 `--omit=optional`,否则对应平台包不会安装。
需要 Node.js 22 或以上和 npm。启动器根据 Node 的 `process.platform` / `process.arch` 选择同版本的 `@chunkdash` 平台包,直接启动原生 C++ 程序,继承终端、工作目录、环境变量和命令参数。安装过程不下载外部可执行文件、不执行 postinstall、不在用户机器编译。不要使用 `--omit=optional`,否则对应平台包不会安装。
平台包仅提供原生二进制,不声明 `bin` 命令。`npm install @chunkdash/crossdesk-cli-linux-x64` 不会让 shell 出现 `crossdesk-cli`;普通用户应安装上面的通用入口。入口包的本地安装将命令放在 `node_modules/.bin`,全局安装才会放入 npm 的全局命令目录。
## 平台与终端
| 平台 | 原生包 | 终端适配与运行边界 |
| --- | --- | --- |
| macOS Apple Silicon | `crossdesk-cli-darwin-arm64` | POSIX TTY;具体最低系统版本写入包内 `build-info.json` |
| macOS Intel | `crossdesk-cli-darwin-x64` | POSIX TTY;最低系统版本同上 |
| Linux x64 | `crossdesk-cli-linux-x64` | glibc、libstdc++、zlib;所需 GLIBC / GLIBCXX 版本写入 `build-info.json` |
| Linux arm64 | `crossdesk-cli-linux-arm64` | 同上 |
| Windows x64 | `crossdesk-cli-win32-x64` | Windows 10 或以上,原生 Console / ConPTY 与 VT 输出 |
| macOS Apple Silicon | `@chunkdash/crossdesk-cli-darwin-arm64` | POSIX TTY;具体最低系统版本写入包内 `build-info.json` |
| macOS Intel | `@chunkdash/crossdesk-cli-darwin-x64` | POSIX TTY;最低系统版本同上 |
| Linux x64 | `@chunkdash/crossdesk-cli-linux-x64` | glibc、libstdc++、zlib;所需 GLIBC / GLIBCXX 版本写入 `build-info.json` |
| Linux arm64 | `@chunkdash/crossdesk-cli-linux-arm64` | 同上 |
| Windows x64 | `@chunkdash/crossdesk-cli-windows-x64` | Windows 10 或以上,原生 Console / ConPTY 与 VT 输出 |
`apps/cli/npm/platforms.json` 的 `packageName` 是打包脚本、入口包依赖和启动器共同使用的名称来源。Windows 的内部目标键仍为 `win32-x64`,包内 `os` 仍为 `win32`;只将 npm 包名写为 `windows-x64`,不能把 Node 平台标识也改成 `windows`。Scope 用于明确组织归属,不保证免于 npm 的名称或反垃圾审核。
这些是构建与分发目标,不代表均已实机通过。当前完整构建与 npm 运行验证只覆盖 macOS arm64;CLI 主程序与平台 I/O 已通过 Windows x64 与 Linux x64、arm64 的交叉编译检查。其他平台的完整 MiniRTC 链接、实际视频连接、键鼠、终端恢复仍需对应机器验收。
@@ -51,7 +55,9 @@ npm --prefix apps/cli run pack:npm
npm --prefix apps/cli run pack:npm -- --binary /absolute/path/crossdesk-cli --out-dir /absolute/path/output
```
每台构建机生成当前平台的原生包与相同版本的通用启动包。`apps/cli/npm/platforms.json` 集中定义五个平台;启动包通过精确版本的 `optionalDependencies` 让 npm 仅安装匹配系统和 CPU 的原生包。原生包包含二进制、运行要求、SHA-256,以及当前控制端和已应用修补的 MiniRTC 源码快照。外部 Xmake 依赖的许可证、对应源码和重链接材料需要随正式版本一并提供,参见 [源码来源](source-provenance.md)。源码快照已含 MiniRTC,解包后无需运行 `prepare-deps.sh`;在 `source/` 目录按上述构建说明显式关闭桌面、开启 CLI,再构建 `crossdesk-cli`。源码包仅包含 CLI 及其依赖,不包含桌面或移动应用。
每台构建机生成当前平台的原生包与相同版本的通用启动包。以 macOS arm64、版本 0.1.0 为例,文件名为 `chunkdash-crossdesk-cli-darwin-arm64-0.1.0.tgz` 和 `crossdesk-cli-0.1.0.tgz`;npm 将 `@scope/name` 的 tarball 文件名写作 `scope-name-version.tgz`。五个 CI artifact 各含一份入口包,最终需要五个平台包和一份入口包,共六个 tarball。
`apps/cli/npm/platforms.json` 集中定义五个平台;脚本校验平台包名合法且不重复,启动包通过精确版本的 `optionalDependencies` 让 npm 仅安装匹配系统和 CPU 的原生包。生成的六个包都包含作者、源码仓库、主页、问题反馈地址,以及面向 npm 官方注册表的公开发布配置。原生包包含二进制、运行要求、SHA-256,以及当前控制端和已应用修补的 MiniRTC 源码快照。外部 Xmake 依赖的许可证、对应源码和重链接材料需要随正式版本一并提供,参见 [源码来源](source-provenance.md)。源码快照已含 MiniRTC,解包后无需运行 `prepare-deps.sh`;在 `source/` 目录按上述构建说明显式关闭桌面、开启 CLI,再构建 `crossdesk-cli`。源码包仅包含 CLI 及其依赖,不包含桌面或移动应用。
`apps/cli/package.json` 的 `private: true` 表示源码目录不是发布产物。只发布打包脚本生成的 tarball;生成的包不带 private 标记。
@@ -61,7 +67,7 @@ npm --prefix apps/cli run pack:npm -- --binary /absolute/path/crossdesk-cli --ou
```sh
npx --yes --offline \
--package /absolute/path/crossdesk-cli-darwin-arm64-0.1.0.tgz \
--package /absolute/path/chunkdash-crossdesk-cli-darwin-arm64-0.1.0.tgz \
--package /absolute/path/crossdesk-cli-0.1.0.tgz \
-- crossdesk-cli doctor --json
```
@@ -74,14 +80,59 @@ Pi 等宿主应直接启动原生程序以可靠控制进程和专用密码描
`.github/workflows/build-cli.yml` 配置五个平台的原生 release 构建、doctor 和打包,并上传 tarball;该工作流不执行 npm publish。更新 MiniRTC 引用时,必须先将对应提交推送到子模块远端,再推送 CrossDesk,否则全新检出和 CI 会在拉取子模块时失败。
正式发布时,先发布全部对应版本的原生包,再发布通用 `crossdesk-cli` 包。例如:
首次使用 `@chunkdash` 前,确认当前 npm 账号属于该组织且有目标包的发布权限。本地发布需要已启用的 2FA 或符合 npm 要求的发布凭据;组织 owner 身份不代替发布认证。
```sh
npm publish crossdesk-cli-darwin-arm64-0.1.0.tgz --access public
# 同样发布其余四个平台的原生包后,再发布入口包。
npm publish crossdesk-cli-0.1.0.tgz --access public
npm whoami --registry=https://registry.npmjs.org/
npm org ls chunkdash --json --registry=https://registry.npmjs.org/
```
发布需要目标 npm 包的权限;当前名称只是配置值,查询时注册表返回未找到,不等于已经获得名称所有权。每次版本发布同步更新 `apps/cli/package.json` 和 `apps/cli/xmake.lua`。缺失平台包时启动器明确报错,不下载别的平台程序或回退为源码编译。
先重新构建五个平台的包;旧的 `crossdesk-cli-<平台>` tarball 不会自动变成 scoped 包,仅重命名文件也不会改变包内名称。已经发布的旧平台包保持原样,新入口只依赖 `@chunkdash` 包。每次版本发布同步更新 `apps/cli/package.json` 和 `apps/cli/xmake.lua`;若某个目标包的版本已经发布,不能用新的内容覆盖该版本。
依据:[npm bin / optionalDependencies / os / cpu](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/)、[npx](https://docs.npmjs.com/cli/v11/commands/npx/)、[GitHub runner 平台](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)、[Windows Console 模式](https://learn.microsoft.com/en-us/windows/console/getconsolemode)。
下载并解出同一提交、同一版本的六个 tarball,在其目录内执行发布。以下以 0.1.0 为例,先检查打包内容,再逐个发布五个平台包到 `next`:
```sh
(
set -e
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64 windows-x64; do
npm publish "./chunkdash-crossdesk-cli-${target}-0.1.0.tgz" \
--dry-run --ignore-scripts --access public --tag next --registry=https://registry.npmjs.org/
done
npm publish ./crossdesk-cli-0.1.0.tgz \
--dry-run --ignore-scripts --access public --tag next --registry=https://registry.npmjs.org/
)
```
`--dry-run` 只做本地发布预演,不能证明包名会被注册表接受。检查通过后发布平台包:
```sh
(
set -e
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64 windows-x64; do
npm publish "./chunkdash-crossdesk-cli-${target}-0.1.0.tgz" \
--ignore-scripts --access public --tag next --registry=https://registry.npmjs.org/
done
)
```
若 npm 返回正在处理(HTTP 202),等待对应的确切版本可查询,不要立即重复上传。遇到 `403 Package name triggered spam detection` 时停止,保留已成功的包,向 [npm 支持](https://www.npmjs.com/support)提供包名、仓库地址、版本及错误日志申请复核;不要自动重试整轮发布。恢复时先查询各包的确切版本,核对注册表的 `dist.integrity` 与 `npm pack <本地.tgz> --dry-run --json --ignore-scripts` 的 `integrity`,只补发未成功的包;同版本内容不同则停止并提升整套版本。
全部原生包可查询、完整性一致后,才发布通用入口:
```sh
(
set -e
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64 windows-x64; do
npm view "@chunkdash/crossdesk-cli-${target}@0.1.0" version dist.integrity \
--registry=https://registry.npmjs.org/
done
npm publish ./crossdesk-cli-0.1.0.tgz \
--ignore-scripts --access public --tag next --registry=https://registry.npmjs.org/
)
```
再确认 `crossdesk-cli@0.1.0` 的依赖和完整性,并在五个平台用 `npx crossdesk-cli@0.1.0 doctor --json` 做安装验证;交互连接仍需实机验收。验收后按需将六个包的 `latest` 指向该版本。不要只凭 `--tag next` 假定首次发布的包不会出现 `latest`,应查询实际 `dist-tags`。缺失平台包时启动器明确报错,不下载别的平台程序或回退为源码编译。
后续接入 GitHub Actions Trusted Publishing 时,需要在入口包和五个 scoped 平台包各自的 npm Settings 中配置同一发布工作流;它解决发布身份认证,不豁免包名审核。当前构建工作流仍只生成 artifact,不会因为采用 scope 而自动发布。
依据:[npm bin / optionalDependencies / os / cpu](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/)、[npm scopes](https://docs.npmjs.com/about-scopes/)、[npm publish](https://docs.npmjs.com/cli/v11/commands/npm-publish/)、[Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)、[npx](https://docs.npmjs.com/cli/v11/commands/npx/)、[GitHub runner 平台](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)、[Windows Console 模式](https://learn.microsoft.com/en-us/windows/console/getconsolemode)。
+3 -2
View File
@@ -9,13 +9,14 @@ const { version } = require('../package.json');
function main() {
const target = `${process.platform}-${process.arch}`;
if (!platforms[target]) {
const platform = platforms[target];
if (!platform) {
throw new Error(`No crossdesk-cli build for ${target}. Supported targets: ${Object.keys(platforms).join(', ')}.`);
}
if (process.platform === 'linux' && !process.report.getReport().header.glibcVersionRuntime) {
throw new Error('This release requires glibc Linux. Alpine/musl needs a separate native build.');
}
const packageName = `crossdesk-cli-${target}`;
const packageName = platform.packageName;
let manifestPath;
try {
manifestPath = require.resolve(`${packageName}/package.json`);
+5 -5
View File
@@ -1,7 +1,7 @@
{
"darwin-arm64": { "os": "darwin", "cpu": "arm64", "xmakePlatform": "macosx", "xmakeArch": "arm64" },
"darwin-x64": { "os": "darwin", "cpu": "x64", "xmakePlatform": "macosx", "xmakeArch": "x86_64" },
"linux-arm64": { "os": "linux", "cpu": "arm64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "arm64" },
"linux-x64": { "os": "linux", "cpu": "x64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "x86_64" },
"win32-x64": { "os": "win32", "cpu": "x64", "xmakePlatform": "windows", "xmakeArch": "x64" }
"darwin-arm64": { "packageName": "@chunkdash/crossdesk-cli-darwin-arm64", "os": "darwin", "cpu": "arm64", "xmakePlatform": "macosx", "xmakeArch": "arm64" },
"darwin-x64": { "packageName": "@chunkdash/crossdesk-cli-darwin-x64", "os": "darwin", "cpu": "x64", "xmakePlatform": "macosx", "xmakeArch": "x86_64" },
"linux-arm64": { "packageName": "@chunkdash/crossdesk-cli-linux-arm64", "os": "linux", "cpu": "arm64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "arm64" },
"linux-x64": { "packageName": "@chunkdash/crossdesk-cli-linux-x64", "os": "linux", "cpu": "x64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "x86_64" },
"win32-x64": { "packageName": "@chunkdash/crossdesk-cli-windows-x64", "os": "win32", "cpu": "x64", "xmakePlatform": "windows", "xmakeArch": "x64" }
}
+7
View File
@@ -5,6 +5,13 @@
"description": "CrossDesk remote desktop controller for the terminal",
"license": "GPL-3.0-only",
"author": "DI JUNKUN",
"repository": {
"type": "git",
"url": "git+https://github.com/kunkundi/crossdesk.git",
"directory": "apps/cli"
},
"homepage": "https://github.com/kunkundi/crossdesk#readme",
"bugs": { "url": "https://github.com/kunkundi/crossdesk/issues" },
"engines": { "node": ">=22" },
"scripts": {
"pack:npm": "node scripts/pack-npm.cjs"
+24 -9
View File
@@ -22,6 +22,10 @@ function copy(from, to) {
function json(file, value) {
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function tarballName(name, version) {
// npm pack writes @scope/name as scope-name-version.tgz.
return `${name.replace(/^@/, '').replace('/', '-')}-${version}.tgz`;
}
function inspect(binary, target) {
const data = fs.readFileSync(binary);
const info = { target, sha256: crypto.createHash('sha256').update(data).digest('hex') };
@@ -84,7 +88,7 @@ function inspect(binary, target) {
}
function pack(directory, output) {
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
const expected = `${manifest.name}-${manifest.version}.tgz`;
const expected = tarballName(manifest.name, manifest.version);
if (fs.existsSync(path.join(output, expected))) throw new Error(`Output already exists: ${expected}`);
const args = ['pack', '--json', '--ignore-scripts', '--pack-destination', output];
if (process.platform === 'win32' && !process.env.npm_execpath)
@@ -93,6 +97,7 @@ function pack(directory, output) {
? run(process.execPath, [process.env.npm_execpath, ...args], { cwd: directory })
: run('npm', args, { cwd: directory });
const [{ filename, size }] = JSON.parse(result);
if (filename !== expected) throw new Error(`Unexpected npm tarball name: ${filename}; expected ${expected}`);
return { filename, size };
}
@@ -111,10 +116,16 @@ try {
const target = `${process.platform}-${process.arch}`;
const platform = platforms[target];
if (!platform) throw new Error(`No packaging target for ${target}`);
const packageNames = Object.values(platforms).map(({ packageName }) => packageName);
if (packageNames.some((name) => typeof name !== 'string' || name.length > 214 ||
!/^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(name)))
throw new Error('Every npm platform must declare a valid scoped packageName.');
if (new Set(packageNames).size !== packageNames.length)
throw new Error('Each npm platform must have a distinct packageName.');
const version = fs.readFileSync(path.join(cliRoot, 'xmake.lua'), 'utf8').match(/set_version\("([^"]+)"\)/)?.[1];
if (version !== project.version) throw new Error('apps/cli/package.json and apps/cli/xmake.lua versions must match.');
for (const name of [project.name, `${project.name}-${target}`]) {
const filename = `${name}-${version}.tgz`;
for (const name of [project.name, platform.packageName]) {
const filename = tarballName(name, version);
if (fs.existsSync(path.join(output, filename))) throw new Error(`Output already exists: ${filename}`);
}
run('git', ['-C', path.join(root, 'deps/submodules/minirtc'), 'apply', '--reverse', '--check',
@@ -142,10 +153,15 @@ try {
'LICENSE', '.gitmodules'])
copy(entry, path.join(native, 'source', entry));
json(path.join(native, 'build-info.json'), { version, ...info });
fs.writeFileSync(path.join(native, 'README.md'), `# ${project.name}-${target}\n\nNative package for crossdesk-cli ${version}.\n\nRuntime requirements and linked libraries: [build-info.json](build-info.json).\n\nController and patched MiniRTC sources are included in source/. From source/, configure with xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release, then run xmake build crossdesk-cli. External dependency notices and corresponding sources must accompany the product release; see source/apps/cli/docs/source-provenance.md.\n`);
fs.writeFileSync(path.join(native, 'README.md'), `# ${platform.packageName}\n\nNative ${target} dependency for [${project.name}](https://www.npmjs.com/package/${project.name}) ${version}, published under the ${platform.packageName.split('/')[0]} scope.\n\nInstall the cross-platform entry package with \`npm install -g ${project.name}\` or run \`npx ${project.name} doctor --json\`. This platform package contains the native binary and does not register a shell command.\n\nSource repository: [CrossDesk](${project.repository.url.replace(/^git\+/, '').replace(/\.git$/, '')}). Report problems at ${project.bugs.url}.\n\nRuntime requirements and linked libraries: [build-info.json](build-info.json).\n\nController and patched MiniRTC sources are included in source/. From source/, configure with xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release, then run xmake build crossdesk-cli. External dependency notices and corresponding sources must accompany the product release; see source/apps/cli/docs/source-provenance.md.\n`);
const metadata = {
license: project.license, author: project.author, repository: project.repository,
homepage: project.homepage, bugs: project.bugs,
publishConfig: { access: 'public', registry: 'https://registry.npmjs.org/' },
};
json(path.join(native, 'package.json'), {
name: `${project.name}-${target}`, version, description: `${project.description} (${target})`,
license: project.license, os: [platform.os], cpu: [platform.cpu],
...metadata, name: platform.packageName, version, description: `${project.description} (${target})`,
os: [platform.os], cpu: [platform.cpu],
...(platform.libc ? { libc: [platform.libc] } : {}),
files: ['bin', 'build-info.json', 'source', 'LICENSE', 'README.md'],
});
@@ -155,12 +171,11 @@ try {
copy('LICENSE', path.join(launcher, 'LICENSE'));
fs.chmodSync(path.join(launcher, 'npm', 'cli.cjs'), 0o755);
json(path.join(launcher, 'package.json'), {
name: project.name, version, description: project.description, license: project.license,
author: project.author, engines: project.engines,
...metadata, name: project.name, version, description: project.description, engines: project.engines,
bin: { 'crossdesk-cli': 'npm/cli.cjs' },
os: ['darwin', 'linux', 'win32'], cpu: ['arm64', 'x64'],
files: ['npm', 'docs', 'README.md', 'LICENSE'],
optionalDependencies: Object.fromEntries(Object.keys(platforms).map((key) => [`${project.name}-${key}`, version])),
optionalDependencies: Object.fromEntries(packageNames.map((name) => [name, version])),
});
// Finish both archives before publishing either one to the output directory.
const archives = path.join(staging, 'archives');