ByteNoteByteNote

字节笔记本

2026年6月4日

Swift CLI 项目改成 npm 分发的完整方案

API中转
¥120

可以,Swift CLI 项目完全可以改成 npm 分发

推荐路线是:

Swift 负责真正的 CLI 能力,npm 只负责安装入口和分发。

也就是说,用户最终这样用:

bash
npm i -g your-cli
your-cli

或者:

bash
npx your-cli

npm 的关键能力是 package.json 里的 bin 字段,它可以把某个本地文件注册成命令行命令;全局安装后,这个命令会被链接到系统可执行路径里。Swift 这边继续用 Swift Package Manager 构建 executable target 即可,Swift 官方也支持 executable target 这种命令行目标。


最小可行方案

项目结构可以改成这样:

txt
your-cli/
├── Package.swift
├── Sources/
│   └── YourCLI/
│       └── main.swift
├── bin/
│   └── your-cli.js
├── scripts/
│   └── build-swift.mjs
├── package.json
└── README.md

核心思路:

txt
npm install
   ↓
执行 postinstall
   ↓
swift build -c release
   ↓
生成 .build/release/your-cli
   ↓
bin/your-cli.js 调用这个 Swift 二进制

npm 支持 postinstallprepare 等生命周期脚本,可以在安装或发布阶段自动执行构建逻辑。


package.json 示例

json
{
  "name": "your-cli",
  "version": "0.1.0",
  "description": "A Swift CLI distributed by npm",
  "type": "module",
  "bin": {
    "your-cli": "bin/your-cli.js"
  },
  "scripts": {
    "build": "swift build -c release",
    "postinstall": "node scripts/build-swift.mjs",
    "prepublishOnly": "npm run build"
  },
  "files": [
    "bin",
    "scripts",
    "Sources",
    "Package.swift",
    "README.md"
  ],
  "engines": {
    "node": ">=18"
  },
  "os": [
    "darwin",
    "linux"
  ],
  "license": "MIT"
}

这里的 bin 就是 npm 分发 CLI 的关键。


bin/your-cli.js

这个 JS 文件只是一个壳,负责找到 Swift 编译出来的二进制并执行。

js
#!/usr/bin/env node

import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";

const __dirname = dirname(fileURLToPath(import.meta.url));

const binaryPath = resolve(
  __dirname,
  "../.build/release/your-cli"
);

if (!fs.existsSync(binaryPath)) {
  console.error("Swift binary not found. Try reinstalling the package.");
  process.exit(1);
}

const result = spawnSync(binaryPath, process.argv.slice(2), {
  stdio: "inherit"
});

process.exit(result.status ?? 1);

注意文件要加执行权限:

bash
chmod +x bin/your-cli.js

scripts/build-swift.mjs

js
import { spawnSync } from "node:child_process";

const result = spawnSync("swift", ["build", "-c", "release"], {
  stdio: "inherit"
});

if (result.status !== 0) {
  console.error("\nFailed to build Swift CLI.");
  console.error("Please make sure Swift is installed:");
  console.error("https://www.swift.org/install/");
  process.exit(result.status ?? 1);
}

这个方案的优点是简单,缺点是用户机器上必须安装 Swift。


Package.swift 示例

swift
// swift-tools-version: 5.10

import PackageDescription

let package = Package(
    name: "YourCLI",
    platforms: [
        .macOS(.v13)
    ],
    products: [
        .executable(
            name: "your-cli",
            targets: ["YourCLI"]
        )
    ],
    targets: [
        .executableTarget(
            name: "YourCLI"
        )
    ]
)

Swift CLI 的入口仍然是:

txt
Sources/YourCLI/main.swift

比如:

swift
import Foundation

print("Hello from Swift CLI")

发布流程

先登录 npm:

bash
npm login

本地测试:

bash
npm pack
npm i -g ./your-cli-0.1.0.tgz
your-cli

没问题后发布:

bash
npm publish

之后用户就可以:

bash
npm i -g your-cli
your-cli

或者:

bash
npx your-cli

更推荐的正式方案:预编译二进制

上面的方案适合 MVP,但正式分发最好不要让用户本地编译 Swift。

更好的方式是:

txt
npm install your-cli
   ↓
根据系统判断平台
   ↓
下载或选择对应二进制
   ↓
直接运行

例如拆成:

txt
your-cli
@your-scope/your-cli-darwin-arm64
@your-scope/your-cli-darwin-x64
@your-scope/your-cli-linux-x64

主包 your-cli 只负责入口,真正的 Swift 二进制放在不同平台包里。

这种方式体验最好:

bash
npm i -g your-cli

用户不需要安装 Swift,也不需要本地编译。


我建议你这样选

如果只是先跑通:

txt
方案一:npm postinstall 里执行 swift build

优点是最快,半小时就能改完。

如果准备公开发布:

txt
方案二:GitHub Actions 编译多平台二进制,再用 npm 分发

优点是用户体验接近 Node CLI,安装快,不依赖 Swift 环境。

你的项目如果主要面向 macOS 用户,我建议先只做:

txt
darwin-arm64
darwin-x64

也就是 Apple Silicon 和 Intel Mac 两个包。Linux 可以后面再加,Windows 暂时不建议优先支持,Swift CLI 在 Windows 上分发成本会明显高一些。


一句话总结:

Swift CLI 改 npm 分发,本质不是重写项目,而是给 Swift 二进制套一层 npm 安装壳。先用 postinstall 跑通,再升级到预编译二进制分发。

分享: