一个 CLI 工具的开源迭代记录:从单二进制到全平台分发

张开发
2026/5/2 16:51:16 15 分钟阅读

分享文章

一个 CLI 工具的开源迭代记录:从单二进制到全平台分发
一个 CLI 工具的开源迭代记录从单二进制到全平台分发这不是一篇产品介绍。这是一个用 Go 写的 CLI 工具在 v0.2.x 阶段的工程迭代记录——怎么把一个能跑的二进制变成一个用户在任何平台都能用一行命令装上的工具。过程中用到的工具链、踩过的坑、做过的取舍可能对同样在做开源 CLI 的人有参考价值。背景Shield CLI 是一个用 Go 写的内网穿透工具核心功能是通过 Chisel 协议建立加密隧道支持 SSH/RDP/VNC/HTTP 等协议的浏览器内访问。v0.1.x 阶段做完了核心功能通过 GoReleaser 交叉编译出 macOS / Linux / Windows 的二进制放到 GitHub Release 上用户curl | sh或brew install能装上。但实际推出去之后发现能装和好装之间还差着不少工程量。v0.2.x 主要在填这个坑。一、Web UI 和系统服务从命令行工具到常驻服务问题CLI 工具默认是前台进程终端一关就断了。对于隧道这种需要长时间运行的服务这不够用。做法v0.2.0 加了一个内嵌的 Web UIshield start启动默认localhost:8181用浏览器管理多个应用连接。v0.2.1 接着做了系统服务注册# 注册为系统服务开机自启shieldinstall# 指定端口shieldinstall--port8182三个平台走的是不同的底层机制平台机制备注macOSlaunchd 用户代理不需要 sudoLinuxsystemd 服务标准做法WindowsWindows Service需要管理员权限同时 macOS 和 Windows 加了系统托盘图标点击可以快速打开 Dashboard、重启、退出。踩坑记录系统托盘依赖 CGO底层用到了各平台的原生 GUI 库但 Linux 服务器通常没有桌面环境也不需要托盘。所以 GoReleaser 配置拆成了两套桌面版macOS/WindowsCGO_ENABLED1和服务器版Linux纯 Go 编译。这是一个在 CI 里调了很久才跑通的东西——交叉编译 CGO 基本上是噩梦级别的组合最后 macOS 和 Windows 各自在对应平台的 runner 上原生编译才解决。二、Docker 容器化看似简单实际有坑为什么要做有用户反馈想在服务器上容器化部署和已有的 Docker Compose 栈统一管理。DockerfileFROM golang:1.23-alpine AS builder WORKDIR /app COPY . . RUN CGO_ENABLED0 go build -o shield-cli . FROM alpine:3.21 COPY --frombuilder /app/shield-cli /usr/local/bin/shield ENV SHIELD_LISTEN_HOST0.0.0.0 ENTRYPOINT [shield, start]多阶段构建最终镜像基于 Alpine很常规。但有两个细节不常规1. 必须用--network host一般 Web 应用容器-p 8080:8080就行了。但内网穿透工具的核心功能是访问宿主机网络和内网资源——10.0.0.0/24网段在 bridge 模式下不可达。--network host让容器共享宿主机网络栈这是这类工具容器化的必要条件。2. 监听地址的问题容器内127.0.0.1是容器自己的 loopback外部流量进不来。所以 Docker 镜像默认把SHIELD_LISTEN_HOST设为0.0.0.0。第一次上线时漏掉了这个导致好几个用户反馈容器启动了但访问不了。CI 自动构建GitHub Actions 里用docker/build-push-action做多架构构建amd64 arm64同时推到 Docker Hub 和 GHCR。语义化标签latest、0.2.2、0.2通过docker/metadata-action自动生成。这套流程现在是标准模板了任何 Go 项目都可以直接抄-uses:docker/metadata-actionv5with:images:|fengyily/shield-cli ghcr.io/fengyily/shield-clitags:|typesemver,pattern{{version}} typesemver,pattern{{major}}.{{minor}} typeraw,valuelatest三、Linux 包管理器APT 和 YUM 仓库搭建为什么要做用户已经可以通过curl | sh安装了但 Linux 运维习惯的是apt install/yum install。更重要的是包管理器支持apt upgrade自动更新不用手动重跑安装脚本。技术方案没有用第三方包管理托管服务Packagecloud 等要收费而是基于GitHub Pages自建仓库APT 仓库用dpkg-scanpackages生成Packages.gz用 GPG 签名Release文件YUM 仓库用createrepo生成repodata/RPM 包用 GPG 签名整个仓库托管在一个独立的 GitHub repo 的gh-pages分支上GitHub Actions 在每次 Release 时自动把新的 deb/rpm 包推进去并重新生成索引。用户配置仓库源# Debian / Ubuntucurl-fsSLhttps://cdn.jsdelivr.net/gh/fengyily/shield-climain/install.sh|shsudoaptupdatesudoaptinstallshield-cli# RHEL / CentOS / Fedorasudotee/etc/yum.repos.d/shield-cli.repoEOF [shield-cli] nameShield CLI Repository baseurlhttps://fengyily.github.io/linux-repo/yum enabled1 gpgcheck0 EOFsudoyuminstallshield-cli# 或 dnf install shield-cli还做了一个安装检测脚本install.sh加了--apt/--yum参数自动检测系统类型并配置对应的包管理器源curl-fsSLhttps://cdn.jsdelivr.net/gh/fengyily/shield-climain/install.sh|sh-s----apt四、TCP/UDP 端口代理协议层的扩展之前的状态Shield CLI 最初只支持 SSH、RDP、VNC、HTTP、HTTPS、Telnet 这些有明确语义的协议——它在网关侧做协议渲染比如 SSH 转成 Web TerminalRDP 转成 HTML5 Canvas所以每个协议需要对应的网关支持。新需求有用户需要代理 MySQL3306、Redis6379、PostgreSQL5432等 TCP 服务。这些不需要浏览器渲染纯端口转发就够了。还有少量 DNS53、Syslog 等 UDP 场景。实现# TCP 代理shield tcp3306# 本地 MySQLshield tcp10.0.0.5:6379# 远程 Redis# UDP 代理shield udp53# DNSshield udp10.0.0.5:514# Syslog技术上TCP 走 Chisel 的标准反向隧道UDP 用 Chisel 原生的/udp后缀做 UDP over WebSocket 转发。和 SSH/RDP 等协议的关键区别TCP/UDP没有默认端口所以 CLI 强制要求用户指定端口号。隧道建立后不会自动打开浏览器因为没有 Web UI 可看而是在终端打印连接指南 Connection Guide (TCP port proxy): gateway.example.com:48721 → 10.0.0.5:3306 Examples: mysql -h gateway.example.com -P 48721 -u root redis-cli -h gateway.example.com -p 48721五、分发矩阵总结做完 v0.2.x 这一轮之后Shield CLI 的安装方式变成了这样方式命令平台Homebrewbrew install shield-climacOSScoopscoop install shield-cliWindowsAPTapt install shield-cliDebian/UbuntuYUMyum install shield-cliRHEL/CentOS/FedoraDockerdocker run fengyily/shield-cli任意 Linuxcurlcurl ... | shmacOS/LinuxPowerShell一键脚本Windows二进制GitHub Release 下载全平台这些不是一次性做完的是在半个月内迭代加上去的。每一种安装方式背后都有对应的 CI 流水线在维护——GoReleaser 管二进制 Homebrew Scoop deb/rpm 包Docker 走单独的 workflowAPT/YUM 仓库也是独立的 workflow。开源工具链清单把这次用到的工具列一下都是开源的别的 Go 项目可以直接参考工具用途GoReleaser交叉编译 打包 Homebrew/Scoop/deb/rpmdocker/build-push-action多架构 Docker 构建和推送docker/metadata-actionDocker 标签自动生成nfpm不需要 dpkg-deb 也能打 deb/rpm 包GoReleaser 内置GitHub PagesAPT/YUM 仓库静态托管GitHub Actions SecretsGPG 密钥、Docker 凭证管理几个教训1. 不要低估分发的工作量。核心功能可能只占 40% 的工作量剩下的全在让用户装得上这件事上。Homebrew tap 配置、Scoop bucket manifest、deb/rpm 打包参数、Docker 多架构、APT/YUM 仓库签名、install.sh 的各种 edge case……每一个都不难但加起来很花时间。2. CGO 和交叉编译是两个互斥的目标。如果你的 Go 项目依赖 CGOGUI 库、SQLite 等老老实实在目标平台上原生编译。不要试图在 Linux CI 上交叉编译 macOS 的 CGO 项目那条路走不通。3. Docker 内网穿透 --network host。这个组合很违反直觉因为 Docker 的核心价值之一是网络隔离。但对于需要访问宿主机网络的工具host 模式是唯一选择。在文档里一定要把这个说清楚否则用户的第一反应永远是为什么容器里连不上。4. GitHub Pages 做包仓库足够用了。不需要 Packagecloud不需要 Artifactory。一个 gh-pages 分支 GitHub Actions 自动更新索引对于中小型开源项目完全够。省钱可控。最后这篇文章记录的不是 Shield CLI 本身的功能而是一个开源 CLI 工具在分发和部署层面的工程实践。如果你也在做一个 Go CLI 项目正在纠结怎么让用户更方便地安装和运行这里面的工具链和踩坑经验应该能帮上忙。所有构建配置和 CI 脚本都在仓库里可以直接参考GoReleaser 配置.goreleaser.yamlDocker 构建Dockerfile.github/workflows/docker.ymlAPT/YUM 仓库.github/workflows/update-repo.yml项目地址https://github.com/fengyily/shield-cli如果这篇文章对你有帮助去仓库点个 Star 就是最好的支持。用的过程中遇到问题或者有想法欢迎直接提 Issue。

更多文章