feat(mosh): integrate MoshCatty pure Rust client (no Cygwin legacy) (#2085)
* feat(mosh): default bundled client to MoshCatty releases Wire Netcatty packaging and npm run dev to pull pure Rust mosh-client binaries from binaricat/MoshCatty (moshcatty-* tags), document the cutover, and mark the old Cygwin Windows fetch path as legacy-only. * fix(mosh): accept pure MoshCatty packages (exe-only, no dlls/terminfo) Align fetch/packaging with MoshCatty pure-Rust releases: require only mosh-client[.exe], treat Cygwin dll bags and terminfo as optional legacy layout. Document moshcatty-0.1.1 (ConPTY Ctrl+C + static MSVC CRT). * refactor(mosh): drop Cygwin legacy; MoshCatty-only pipeline Remove FluentTerminal/mosh-bin fallbacks, build-mosh C++ CI, and dll/terminfo runtime helpers. Fetch accepts only moshcatty-* pure binaries; packaging ships a single mosh-client per platform. * docs: align ET checklist and et README with MoshCatty-only mosh Update mosh template references after removing Cygwin build pipeline and legacy fallbacks. Point ET packaging notes at fetch-mosh / MoshCatty instead of deleted build-mosh-binaries workflow. * docs(mosh): require MoshCatty 0.1.2+ for Linux glibc floors Codex review vs main: defaulting to MoshCatty Linux binaries built on ubuntu-latest required GLIBC 2.34, above Netcatty's package floors (x64 2.28 / arm64 2.31). Document the floors, prefer moshcatty-0.1.2+, and drop trailing whitespace in the design note. Upstream MoshCatty release CI now builds in almalinux:8 / debian:bullseye with assert. * fix(mosh): reject MoshCatty releases below 0.1.2 Codex re-review: docs alone still allowed MOSH_BIN_RELEASE=moshcatty-0.1.0/0.1.1, which produce packages that fail to start on Netcatty's Linux glibc floors. Validate the minimum tag in resolve and fetch, skip pre-0.1.2 when picking latest, and cover the reject path in unit tests. * fix(mosh): pin MoshCatty owner and reject floor prereleases Codex review vs main (P2): - Always default MOSH_BIN owner/repo to binaricat/MoshCatty; do not inherit fork owner from GITHUB_REPOSITORY in resolve or fetch. - Treat moshcatty-X.Y.Z-rcN as below final X.Y.Z so pins like moshcatty-0.1.2-rc1 cannot bypass the glibc floor. * fix(mosh): accept build metadata in MoshCatty release tags TAG_RE rejected '+' so moshcatty-0.1.2+build.1 failed validation even though the version parser and isAtLeastMinRelease accepted it. Align the regex and require a successful semver parse.
This commit is contained in:
19 files changed
+482
-1808
No files matched your search
@@ -1,234 +0,0 @@
|
||||
name: build-mosh-binaries
|
||||
|
||||
# Trigger philosophy (mirrors build.yml):
|
||||
# - Pushes that touch the mosh build pipeline + PRs run the matrix
|
||||
# so we can validate workflow / script changes without tagging.
|
||||
# Artifacts upload as workflow artifacts only; *no* release.
|
||||
# - Manual `workflow_dispatch` with `release_tag` publishes the
|
||||
# binaries + SHA256SUMS to the dedicated binary repository
|
||||
# (`binaricat/Netcatty-mosh-bin` by default).
|
||||
#
|
||||
# `paths` keeps unrelated commits (UI, bridges, etc) from rebuilding
|
||||
# or refreshing mosh binaries on every push.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mosh_ref:
|
||||
description: "mosh upstream git ref (tag/branch/commit) — see https://github.com/mobile-shell/mosh"
|
||||
type: string
|
||||
default: "mosh-1.4.0"
|
||||
release_tag:
|
||||
description: "Optional release tag to attach binaries to (e.g. mosh-bin-1.4.0-1). Empty = artifacts only."
|
||||
type: string
|
||||
default: ""
|
||||
release_repo:
|
||||
description: "Repository that stores mosh-client binary releases."
|
||||
type: string
|
||||
default: "binaricat/Netcatty-mosh-bin"
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- ".gitattributes"
|
||||
- ".github/workflows/build-mosh-binaries.yml"
|
||||
- "electron-builder.config.cjs"
|
||||
- "package.json"
|
||||
- "scripts/build-mosh/**"
|
||||
- "scripts/fetch-mosh-binaries.cjs"
|
||||
- "scripts/mosh-extra-resources.cjs"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".gitattributes"
|
||||
- ".github/workflows/build-mosh-binaries.yml"
|
||||
- "electron-builder.config.cjs"
|
||||
- "package.json"
|
||||
- "scripts/build-mosh/**"
|
||||
- "scripts/fetch-mosh-binaries.cjs"
|
||||
- "scripts/mosh-extra-resources.cjs"
|
||||
|
||||
# Cancel superseded branch / PR builds.
|
||||
concurrency:
|
||||
group: build-mosh-binaries-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MOSH_REF: ${{ inputs.mosh_ref || 'mosh-1.4.0' }}
|
||||
|
||||
jobs:
|
||||
# ------------------------------------------------------------------
|
||||
# Linux x64 (manylinux2014 / glibc 2.17, broad distro compatibility).
|
||||
# Static-links the heavy third-party deps where possible; the resulting
|
||||
# mosh-client still depends on baseline Linux system libraries.
|
||||
# ------------------------------------------------------------------
|
||||
build-linux-x64:
|
||||
name: build-linux-x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build mosh-client (linux-x64)
|
||||
run: |
|
||||
# Run only the compiler inside manylinux2014. JavaScript actions
|
||||
# need the host runner's newer glibc.
|
||||
docker run --rm \
|
||||
-e MOSH_REF="${MOSH_REF}" \
|
||||
-e OUT_DIR=/work/out \
|
||||
-e ARCH=x64 \
|
||||
-v "${GITHUB_WORKSPACE}:/work" \
|
||||
-w /work \
|
||||
quay.io/pypa/manylinux2014_x86_64 \
|
||||
bash scripts/build-mosh/build-linux.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mosh-client-linux-x64
|
||||
path: out/
|
||||
|
||||
build-linux-arm64:
|
||||
name: build-linux-arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build mosh-client (linux-arm64)
|
||||
run: |
|
||||
# Run only the compiler inside manylinux2014. JavaScript actions
|
||||
# need the host runner's newer glibc.
|
||||
docker run --rm \
|
||||
-e MOSH_REF="${MOSH_REF}" \
|
||||
-e OUT_DIR=/work/out \
|
||||
-e ARCH=arm64 \
|
||||
-v "${GITHUB_WORKSPACE}:/work" \
|
||||
-w /work \
|
||||
quay.io/pypa/manylinux2014_aarch64 \
|
||||
bash scripts/build-mosh/build-linux.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mosh-client-linux-arm64
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# macOS universal2 (arm64 + x86_64 lipo).
|
||||
# Min deployment target: macOS 11 (Big Sur) — covers arm64 hardware.
|
||||
# Static-links OpenSSL, protobuf, ncurses for both arches.
|
||||
# ------------------------------------------------------------------
|
||||
build-macos-universal:
|
||||
name: build-macos-universal
|
||||
runs-on: macos-15-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build mosh-client (darwin-universal)
|
||||
env:
|
||||
MOSH_REF: ${{ env.MOSH_REF }}
|
||||
OUT_DIR: ${{ github.workspace }}/out
|
||||
MACOSX_DEPLOYMENT_TARGET: "11.0"
|
||||
run: bash scripts/build-mosh/build-macos.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mosh-client-darwin-universal
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Windows x64 pinned runtime bundle.
|
||||
# Do not compile this in CI: rebuilding the upstream Cygwin client here
|
||||
# previously regressed Windows startup. Ship the SHA256-pinned Netcatty
|
||||
# bundle with DLLs + terminfo, plus the FluentTerminal standalone fallback,
|
||||
# verified by fetch-windows.sh.
|
||||
# ------------------------------------------------------------------
|
||||
fetch-windows-x64:
|
||||
name: fetch-windows-x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch pinned mosh-client bundle (win32-x64)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export OUT_DIR="${GITHUB_WORKSPACE}/out"
|
||||
mkdir -p "$OUT_DIR"
|
||||
bash scripts/build-mosh/fetch-windows.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mosh-client-win32-x64
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Windows arm64 — intentionally not built.
|
||||
# The pinned upstream source only provides x64. arm64 Windows builds
|
||||
# should be added only after we have a tested standalone arm64 client.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Aggregate + optional release to the dedicated binary repository.
|
||||
# ------------------------------------------------------------------
|
||||
release:
|
||||
name: release
|
||||
needs:
|
||||
- build-linux-x64
|
||||
- build-linux-arm64
|
||||
- build-macos-universal
|
||||
- fetch-windows-x64
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Stage release files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p release
|
||||
for d in artifacts/*/; do
|
||||
find "$d" -maxdepth 1 -type f -exec cp {} release/ \;
|
||||
done
|
||||
(cd release && find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%P\n' | sort | xargs sha256sum > SHA256SUMS)
|
||||
ls -la release
|
||||
cat release/SHA256SUMS
|
||||
- name: Determine tag
|
||||
id: tag
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
tag="${RELEASE_TAG}"
|
||||
if [[ ! "$tag" =~ ^mosh-bin-[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Invalid mosh binary release tag: $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'name=%s\n' "$tag" >> "$GITHUB_OUTPUT"
|
||||
- name: Create / update release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.MOSH_BIN_RELEASE_TOKEN }}
|
||||
RELEASE_REPO: ${{ inputs.release_repo }}
|
||||
RELEASE_TAG: ${{ steps.tag.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${GH_TOKEN:-}" ]]; then
|
||||
echo "::error::MOSH_BIN_RELEASE_TOKEN is required to publish into ${RELEASE_REPO}."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
printf '%s\n' 'Pre-built `mosh-client` binaries consumed by `scripts/fetch-mosh-binaries.cjs` during `npm run pack`.'
|
||||
printf 'Linux/macOS artifacts are built from `mobile-shell/mosh` upstream ref `%s`.\n' "${MOSH_REF}"
|
||||
printf '%s\n\n' 'Windows x64 is the SHA256-pinned Netcatty runtime bundle, with the FluentTerminal standalone `mosh-client.exe` kept as a fallback asset.'
|
||||
printf 'Source workflow: %s/%s/actions/runs/%s\n' "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}"
|
||||
printf 'Source commit: `%s`\n\n' "${GITHUB_SHA}"
|
||||
printf '%s\n' 'All artifacts are GPL-3.0; see `resources/mosh/README.md` for source provenance.'
|
||||
} > release-notes.md
|
||||
if gh release view "${RELEASE_TAG}" --repo "${RELEASE_REPO}" >/dev/null 2>&1; then
|
||||
gh release edit "${RELEASE_TAG}" \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--title "${RELEASE_TAG}" \
|
||||
--notes-file release-notes.md
|
||||
gh release upload "${RELEASE_TAG}" release/* \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--clobber
|
||||
else
|
||||
gh release create "${RELEASE_TAG}" release/* \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--title "${RELEASE_TAG}" \
|
||||
--notes-file release-notes.md
|
||||
fi
|
||||
+3
-8
@@ -77,16 +77,11 @@ Directory.Build.targets
|
||||
build_with_vs.bat
|
||||
build_with_vs2022.bat
|
||||
|
||||
# Bundled mosh-client binaries fetched at pack time by
|
||||
# scripts/fetch-mosh-binaries.cjs. resources/mosh/README.md is
|
||||
# committed; the actual binaries, the Cygwin DLL bundle (Windows),
|
||||
# and the bundled ncurses terminfo database are all pulled from the
|
||||
# dedicated mosh binary repository, never committed.
|
||||
# Bundled MoshCatty mosh-client binaries fetched at pack time by
|
||||
# scripts/fetch-mosh-binaries.cjs. resources/mosh/README.md is committed;
|
||||
# pure single-binary artifacts from binaricat/MoshCatty are never committed.
|
||||
/resources/mosh/*/mosh-client
|
||||
/resources/mosh/*/mosh-client.exe
|
||||
/resources/mosh/*/mosh-client-*-dlls/
|
||||
/resources/mosh/*/*.dll
|
||||
/resources/mosh/*/terminfo/
|
||||
|
||||
# Bundled EternalTerminal `et` client binaries fetched at pack time by
|
||||
# scripts/fetch-et-binaries.cjs. resources/et/README.md is committed; the
|
||||
|
||||
+28
-25
@@ -8,37 +8,42 @@
|
||||
> 后端 + UI 重新落到上游重构后的目录结构上,并让它启动**捆绑的** `et`。
|
||||
>
|
||||
> 旧实现参考:`git show 67e81616`(共 7 个 ET 提交,见 `feat/eternal-terminal`)。
|
||||
> Mosh 模板参考:`resources/mosh/README.md`、`scripts/*mosh*`、
|
||||
> `electron/bridges/terminalBridge/moshSession.cjs`、`.github/workflows/build-mosh-binaries.yml`。
|
||||
> Mosh 模板参考(**仅 MoshCatty 纯二进制路径**):`resources/mosh/README.md`、
|
||||
> `scripts/fetch-mosh-binaries.cjs`、`scripts/resolve-mosh-bin-release.cjs`、
|
||||
> `scripts/mosh-extra-resources.cjs`、`electron/bridges/terminalBridge/moshSession.cjs`。
|
||||
> 客户端本体在独立仓库 [binaricat/MoshCatty](https://github.com/binaricat/MoshCatty)
|
||||
> (`moshcatty-*` releases);Netcatty 内已无 Cygwin 构建流水线 / FluentTerminal 回退。
|
||||
|
||||
## 关键设计差异(ET vs Mosh)
|
||||
|
||||
- **协议**:Mosh 需要 Node 重写 Perl 包装器(SSH bootstrap + 抓 `MOSH CONNECT` +
|
||||
换 PTY)。**ET 不需要** —— `et` 客户端自己完成 SSH 引导 + 协议握手,我们只要
|
||||
把 `et` 当作普通 PTY 进程 `pty.spawn` 即可。所以**没有** `etHandshake.cjs`。
|
||||
- **协议**:Mosh 需要 Node 做 SSH bootstrap + 抓 `MOSH CONNECT` + 换 PTY
|
||||
(`moshHandshake` + `moshSession`)。**ET 不需要** —— `et` 客户端自己完成 SSH
|
||||
引导 + 协议握手,我们只要把 `et` 当作普通 PTY 进程 `pty.spawn` 即可。所以**没有**
|
||||
`etHandshake.cjs`。
|
||||
- **凭证注入**:Mosh 自己驱动 ssh、直接往 PTY 里敲密码;ET 内部驱动 ssh,需用
|
||||
**SSH_ASKPASS + 临时 ~/.ssh 环境**把保存的密码/密钥/跳板/算法喂给 et 内部的 ssh
|
||||
(旧实现 `prepareEtSshEnvironment` 已完整实现,直接搬运)。
|
||||
- **terminfo**:`et` 是纯传输客户端、本地不渲染终端,**无需** 捆绑 terminfo
|
||||
(Mosh 因静态 ncurses 才需要)。打包目录里只放 `et[.exe]`(+ Windows DLL)。
|
||||
- **构建系统**:Mosh 用 autotools;**ET 用 CMake + Ninja + vcpkg**
|
||||
- **纯二进制**:MoshCatty 与理想 ET 打包都是「每平台一个客户端文件」。Mosh 侧已
|
||||
无 terminfo / Cygwin DLL 袋;`et` 同样本地不渲染终端。Windows 若动态链 CRT
|
||||
才考虑可选 DLL 目录,否则只放 `et[.exe]`。
|
||||
- **构建系统**:Mosh 客户端在 **MoshCatty** 仓库用 Rust 构建并发布;Netcatty 只
|
||||
`fetch`。**ET** 用 CMake + Ninja + vcpkg
|
||||
(`cmake -DDISABLE_TELEMETRY=ON -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo`),
|
||||
产物是单个 `et`(Windows `et.exe`)。
|
||||
产物是单个 `et`(Windows `et.exe`),由 `scripts/build-et/` + `build-et-binaries.yml` 发布。
|
||||
|
||||
## 命名约定(镜像 Mosh)
|
||||
## 命名约定(镜像 Mosh / MoshCatty)
|
||||
|
||||
| Mosh | ET |
|
||||
| Mosh (MoshCatty) | ET |
|
||||
|------|----|
|
||||
| `resources/mosh/<plat-arch>/mosh-client[.exe]` | `resources/et/<plat-arch>/et[.exe]` |
|
||||
| 打包后 `<Resources>/mosh/mosh-client` | 打包后 `<Resources>/et/et` |
|
||||
| `scripts/build-mosh/` | `scripts/build-et/` |
|
||||
| 上游构建:`binaricat/MoshCatty` CI releases | `scripts/build-et/` + `build-et-binaries.yml` |
|
||||
| `scripts/fetch-mosh-binaries.cjs` | `scripts/fetch-et-binaries.cjs` |
|
||||
| `scripts/resolve-mosh-bin-release.cjs` | `scripts/resolve-et-bin-release.cjs` |
|
||||
| `scripts/mosh-extra-resources.cjs` | `scripts/et-extra-resources.cjs` |
|
||||
| env `MOSH_BIN_RELEASE` / 仓库 `Netcatty-mosh-bin` / tag `mosh-bin-*` | env `ET_BIN_RELEASE` / 仓库 `Netcatty-et-bin` / tag `et-bin-*` |
|
||||
| env `MOSH_BIN_RELEASE` / 仓库 `MoshCatty` / tag `moshcatty-*` | env `ET_BIN_RELEASE` / 仓库 `Netcatty-et-bin` / tag `et-bin-*` |
|
||||
| `npm run fetch:mosh[:dev]` | `npm run fetch:et[:dev]` |
|
||||
| `bundledMoshClient()` / `resolveBareMoshClient()` | `bundledEtClient()` / `resolveBareEtClient()` |
|
||||
| `.github/workflows/build-mosh-binaries.yml` | `.github/workflows/build-et-binaries.yml` |
|
||||
|
||||
---
|
||||
|
||||
@@ -52,7 +57,7 @@
|
||||
`/resources/et/*/et-win32-*-dlls/`。保留 `resources/et/README.md`。
|
||||
- [x] **1.3** `scripts/build-et/build-linux.sh` —— manylinux2014 + vcpkg 静态三元组
|
||||
构建 `et`(x64/arm64),产物 `et-linux-<arch>.tar.gz`(+.sha256),内含单个 `et`。
|
||||
校验非系统动态库(同 mosh 的 ldd 白名单)。
|
||||
校验非系统动态库(ldd 白名单)。
|
||||
- [x] **1.4** `scripts/build-et/build-macos.sh` —— arm64 + x86_64 分别构建后 `lipo`
|
||||
成 universal,`MACOSX_DEPLOYMENT_TARGET=11.0`,产物 `et-darwin-universal.tar.gz`。
|
||||
- [x] **1.5** `scripts/build-et/build-windows.ps1`(或 `.sh`)—— MSVC + vcpkg
|
||||
@@ -60,13 +65,12 @@
|
||||
则随附 DLL 目录 `et-win32-x64-dlls/`,否则纯静态无 DLL)。
|
||||
- [x] **1.6** `scripts/et-extra-resources.cjs` —— 镜像 `mosh-extra-resources.cjs`:
|
||||
按平台/arch 仅当 `resources/et/<plat-arch>/et[.exe]` 存在时才产出 extraResources
|
||||
指令(`to: "et/"`);Windows 额外处理可选 DLL 目录。**去掉 terminfo 分支**。
|
||||
指令(`to: "et/"`);Windows 额外处理可选 DLL 目录。纯客户端文件为主。
|
||||
- [x] **1.7** `scripts/resolve-et-bin-release.cjs` —— 镜像 `resolve-mosh-bin-release.cjs`:
|
||||
`TAG_RE=/^et-bin-.../`,默认仓库 `Netcatty-et-bin`,env `ET_BIN_RELEASE` 优先。
|
||||
- [x] **1.8** `scripts/fetch-et-binaries.cjs` —— 镜像 `fetch-mosh-binaries.cjs`:
|
||||
`TARGETS` 四项(linux-x64/arm64、darwin-universal、win32-x64),全部 tar.gz;
|
||||
SHA256SUMS 校验;解包到 `resources/et/<plat-arch>/`。**Windows 用自建产物**
|
||||
(ET 官方有 Windows 构建,无需 FluentTerminal 那种 fallback)。去掉 terminfo 校验。
|
||||
SHA256SUMS 校验;解包到 `resources/et/<plat-arch>/`。**Windows 用自建产物**。
|
||||
- [x] **1.9** 单元测试:`scripts/fetch-et-binaries.test.cjs`、
|
||||
`scripts/resolve-et-bin-release.test.cjs`、`scripts/et-extra-resources.test.cjs`
|
||||
(镜像对应 mosh 测试,改名/改路径)。
|
||||
@@ -77,13 +81,12 @@
|
||||
`scripts/*.test.cjs`(确认即可)。
|
||||
- [x] **1.11** `electron-builder.config.cjs`:引入 `etExtraResources`,在 darwin/win32/
|
||||
linux 三处把 `etExtraResources(plat)` 合并进 `extraResources`(与 mosh 数组拼接)。
|
||||
- [x] **1.12** `.github/workflows/build-et-binaries.yml` —— 镜像
|
||||
`build-mosh-binaries.yml`:四个构建 job + 一个 `release` job(dispatch 且
|
||||
`release_tag` 非空时发布到 `Netcatty-et-bin`,附 `SHA256SUMS`)。`paths` 过滤
|
||||
指向 `scripts/build-et/**`、`scripts/fetch-et-binaries.cjs`、`scripts/et-extra-resources.cjs`。
|
||||
env 用 `ET_REF`(默认 ET release tag,如 `et-v6.2.x`)。
|
||||
- [x] **1.12** `.github/workflows/build-et-binaries.yml` —— 四个构建 job + 一个
|
||||
`release` job(dispatch 且 `release_tag` 非空时发布到 `Netcatty-et-bin`,附
|
||||
`SHA256SUMS`)。`paths` 过滤指向 `scripts/build-et/**`、`scripts/fetch-et-binaries.cjs`、
|
||||
`scripts/et-extra-resources.cjs`。env 用 `ET_REF`(默认 ET release tag,如 `et-v6.2.x`)。
|
||||
> 注:实际二进制由用户手动 `workflow_dispatch` 触发产出;本地/CI 未设
|
||||
> `ET_BIN_RELEASE` 时 fetch 步骤安静跳过(同 mosh)。
|
||||
> `ET_BIN_RELEASE` 时 fetch 步骤安静跳过(同 mosh 的 `MOSH_BIN_RELEASE`)。
|
||||
|
||||
## Phase 2 — 运行时定位捆绑客户端
|
||||
|
||||
@@ -100,7 +103,7 @@
|
||||
`cleanupSessionExternalAuthArtifacts`、`execOnEtSession`、`startEtSession`。
|
||||
**改动点**:`etCmd` 由 `findExecutable('et')` 改为 `resolveBareEtClient()`
|
||||
(取捆绑二进制);找不到时抛错(同 mosh:提示跑 `npm run fetch:et:dev`)。
|
||||
Windows 若有 DLL 目录,复用 `prependEnvPath` 思路把 DLL 目录加进 PATH。
|
||||
Windows 若有动态链接 DLL 目录,可把该目录加进 PATH(MoshCatty 路径已无此需求)。
|
||||
- [x] **3.2** `terminalBridge.cjs` 接线 `createEtSessionApi(ctx)`(镜像 moshSessionApi
|
||||
的 ctx),传入 `bundledEtClient`、`tempDirBridge`、`execFile/execFileSync` 等;
|
||||
解构出 `startEtSession`、`execOnEtSession`、`cleanupStaleEtTempDirs`、
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Native Cross-Platform Mosh Client
|
||||
|
||||
Status: **shipped via [MoshCatty](https://github.com/binaricat/MoshCatty)**
|
||||
Related: [#2025](https://github.com/binaricat/Netcatty/issues/2025), [#2072](https://github.com/binaricat/Netcatty/issues/2072)
|
||||
|
||||
## Canonical repository
|
||||
|
||||
**https://github.com/binaricat/MoshCatty**
|
||||
|
||||
Netcatty only **consumes** `moshcatty-*` release binaries into `resources/mosh/`
|
||||
via `scripts/fetch-mosh-binaries.cjs` / `scripts/resolve-mosh-bin-release.cjs`
|
||||
(default `MOSH_BIN_REPO=MoshCatty`).
|
||||
|
||||
There is **no** in-tree Rust source, no Cygwin packaging path, and no
|
||||
FluentTerminal / `mosh-bin-*` fallback.
|
||||
|
||||
## Integration contract
|
||||
|
||||
```text
|
||||
MOSH_KEY=<key> mosh-client <host> <port>
|
||||
```
|
||||
|
||||
Netcatty owns SSH bootstrap (`moshHandshake` + PTY), then swaps to the
|
||||
bundled MoshCatty binary under `node-pty`.
|
||||
|
||||
| Concern | Owner |
|
||||
|---------|--------|
|
||||
| SSH auth / `MOSH CONNECT` parse | Netcatty Electron |
|
||||
| UDP Mosh data plane | MoshCatty binary |
|
||||
| Packaging / fetch / electron-builder | Netcatty scripts → MoshCatty releases |
|
||||
|
||||
## Why
|
||||
|
||||
Windows Cygwin `mosh-client` + partial runtime + ConPTY sandwich was
|
||||
architecturally broken. MoshCatty is a pure Rust, wire-compatible client with
|
||||
one code path on Linux / macOS / Windows (static CRT on Windows).
|
||||
|
||||
## Linux compatibility floors
|
||||
|
||||
MoshCatty Linux release binaries must target the **same glibc floors as
|
||||
Netcatty package jobs** (not bare `ubuntu-latest`):
|
||||
|
||||
| Target | Netcatty package image | Max GLIBC |
|
||||
|--------|------------------------|-----------|
|
||||
| `linux-x64` | `almalinux:8` | 2.28 |
|
||||
| `linux-arm64` | `debian:bullseye` | 2.31 |
|
||||
|
||||
Enforced upstream from `moshcatty-0.1.2` via MoshCatty release CI
|
||||
(`scripts/assert-max-glibc.sh`). Do not pin packaging to pre-0.1.2 Linux
|
||||
assets (they require GLIBC 2.34).
|
||||
|
||||
## Decision log
|
||||
|
||||
- **2026-07-10:** Feasibility accepted; client extracted to `binaricat/MoshCatty`.
|
||||
- **2026-07-10:** Netcatty defaults packaging to MoshCatty releases.
|
||||
- **2026-07-10:** Removed legacy Cygwin build pipeline, FluentTerminal fallback,
|
||||
`mosh-bin-*` tags, dll/terminfo runtime helpers. Pure MoshCatty only
|
||||
(`moshcatty-0.1.1`: ConPTY Ctrl+C + static MSVC CRT).
|
||||
- **2026-07-10:** Require `moshcatty-0.1.2+` for Linux glibc floors matching
|
||||
Netcatty (x64 ≤ 2.28, arm64 ≤ 2.31).
|
||||
@@ -6,11 +6,8 @@ const path = require("node:path");
|
||||
const { StringDecoder } = require("node:string_decoder");
|
||||
|
||||
const {
|
||||
addBundledMoshDllPath,
|
||||
addBundledMoshRuntimeEnv,
|
||||
addBundledMoshTerminfoEnv,
|
||||
resolveBareMoshClient,
|
||||
toCygwinPath,
|
||||
} = require("./terminalBridge.cjs");
|
||||
const { createMoshSessionApi } = require("./terminalBridge/moshSession.cjs");
|
||||
|
||||
@@ -96,149 +93,13 @@ test("mosh runtime does not fall back to system mosh or mosh-client", () => {
|
||||
assert.equal(source.includes("brew install mosh"), false);
|
||||
});
|
||||
|
||||
test("Windows dev mosh-client prepends the bundled DLL directory", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client.exe");
|
||||
const dllDir = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client-win32-x64-dlls");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(dllDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dllDir, "cygwin1.dll"), "dll");
|
||||
|
||||
const env = { Path: "C:\\Windows\\System32" };
|
||||
addBundledMoshDllPath(env, client, { platform: "win32", arch: "x64" });
|
||||
|
||||
assert.equal(env.Path.split(";")[0], dllDir);
|
||||
});
|
||||
|
||||
test("Windows dev mosh-client updates the PATH key used by child process env", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client.exe");
|
||||
const dllDir = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client-win32-x64-dlls");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(dllDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dllDir, "cygwin1.dll"), "dll");
|
||||
|
||||
const env = {
|
||||
Path: "C:\\Windows\\System32",
|
||||
PATH: "C:\\Tools",
|
||||
};
|
||||
addBundledMoshDllPath(env, client, { platform: "win32", arch: "x64" });
|
||||
|
||||
assert.equal(env.PATH.split(";")[0], dllDir);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(env, "Path"), false);
|
||||
});
|
||||
|
||||
test("Linux mosh-client prefers a sibling bundled terminfo dir", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "linux-x64", "mosh-client");
|
||||
const terminfo = path.join(tmp, "resources", "mosh", "linux-x64", "terminfo");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(path.join(terminfo, "x"), { recursive: true });
|
||||
fs.writeFileSync(path.join(terminfo, "x", "xterm-256color"), "terminfo");
|
||||
|
||||
const env = {};
|
||||
addBundledMoshTerminfoEnv(env, client, { platform: "linux" });
|
||||
|
||||
assert.equal(env.TERMINFO, terminfo);
|
||||
const dirs = env.TERMINFO_DIRS.split(":");
|
||||
assert.equal(dirs[0], terminfo);
|
||||
assert.ok(dirs.includes("/usr/share/terminfo"));
|
||||
});
|
||||
|
||||
test("Linux mosh-client falls back to distro paths when no bundle present", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "linux-x64", "mosh-client");
|
||||
writeExecutable(client);
|
||||
|
||||
const env = {};
|
||||
addBundledMoshTerminfoEnv(env, client, { platform: "linux" });
|
||||
|
||||
test("MoshCatty runtime env is a no-op (no DLL bag / terminfo)", () => {
|
||||
const env = { Path: "C:\\Windows\\System32", TERM: "xterm-256color" };
|
||||
const out = addBundledMoshRuntimeEnv(env, "C:\\app\\mosh-client.exe", { platform: "win32" });
|
||||
assert.equal(out, env);
|
||||
assert.equal(env.TERMINFO, undefined);
|
||||
const dirs = env.TERMINFO_DIRS.split(":");
|
||||
assert.ok(dirs.includes("/etc/terminfo"));
|
||||
assert.ok(dirs.includes("/lib/terminfo"));
|
||||
assert.ok(dirs.includes("/usr/share/terminfo"));
|
||||
});
|
||||
|
||||
test("Linux mosh-client merges caller-supplied TERMINFO_DIRS between bundle and system defaults", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "linux-x64", "mosh-client");
|
||||
const terminfo = path.join(tmp, "resources", "mosh", "linux-x64", "terminfo");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(path.join(terminfo, "x"), { recursive: true });
|
||||
fs.writeFileSync(path.join(terminfo, "x", "xterm-256color"), "terminfo");
|
||||
|
||||
const env = { TERMINFO_DIRS: "/home/user/.terminfo" };
|
||||
addBundledMoshTerminfoEnv(env, client, { platform: "linux" });
|
||||
|
||||
const dirs = env.TERMINFO_DIRS.split(":");
|
||||
assert.equal(dirs[0], terminfo);
|
||||
assert.equal(dirs[1], "/home/user/.terminfo");
|
||||
assert.ok(dirs.includes("/usr/share/terminfo"));
|
||||
});
|
||||
|
||||
test("Darwin mosh-client uses macOS-aware terminfo search paths", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "darwin-universal", "mosh-client");
|
||||
writeExecutable(client);
|
||||
|
||||
const env = {};
|
||||
addBundledMoshTerminfoEnv(env, client, { platform: "darwin" });
|
||||
|
||||
const dirs = env.TERMINFO_DIRS.split(":");
|
||||
assert.ok(dirs.includes("/usr/share/terminfo"));
|
||||
assert.ok(dirs.includes("/opt/homebrew/share/terminfo"));
|
||||
});
|
||||
|
||||
test("toCygwinPath converts Windows drive paths for Cygwin ncurses", () => {
|
||||
assert.equal(
|
||||
toCygwinPath("C:\\Program Files\\Netcatty\\resources\\mosh\\terminfo"),
|
||||
"/cygdrive/c/Program Files/Netcatty/resources/mosh/terminfo",
|
||||
);
|
||||
assert.equal(
|
||||
toCygwinPath("D:/Netcatty/resources/mosh/terminfo"),
|
||||
"/cygdrive/d/Netcatty/resources/mosh/terminfo",
|
||||
);
|
||||
assert.equal(toCygwinPath("/already/posix"), "/already/posix");
|
||||
});
|
||||
|
||||
test("Windows mosh-client points ncurses at bundled terminfo via Cygwin path", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client.exe");
|
||||
const terminfo = path.join(tmp, "resources", "mosh", "win32-x64", "terminfo");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(path.join(terminfo, "x"), { recursive: true });
|
||||
fs.writeFileSync(path.join(terminfo, "x", "xterm-256color"), "terminfo");
|
||||
|
||||
const env = {};
|
||||
addBundledMoshTerminfoEnv(env, client, { platform: "win32" });
|
||||
|
||||
// On macOS/Linux hosts the temp path is already POSIX, so toCygwinPath is a
|
||||
// no-op. On Windows hosts it becomes /cygdrive/<drive>/.... Either way the
|
||||
// value must not contain a drive-letter colon that would split TERMINFO_DIRS.
|
||||
assert.equal(env.TERMINFO, toCygwinPath(terminfo));
|
||||
assert.equal(env.TERMINFO_DIRS, env.TERMINFO);
|
||||
assert.ok(!/[A-Za-z]:/.test(env.TERMINFO), "Cygwin TERMINFO must not contain a Windows drive letter");
|
||||
});
|
||||
|
||||
test("Windows mosh runtime env includes DLL path and Cygwin terminfo", () => {
|
||||
const tmp = makeTmp();
|
||||
const client = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client.exe");
|
||||
const dllDir = path.join(tmp, "resources", "mosh", "win32-x64", "mosh-client-win32-x64-dlls");
|
||||
const terminfo = path.join(tmp, "resources", "mosh", "win32-x64", "terminfo");
|
||||
writeExecutable(client);
|
||||
fs.mkdirSync(dllDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dllDir, "cygwin1.dll"), "dll");
|
||||
fs.mkdirSync(path.join(terminfo, "78"), { recursive: true });
|
||||
fs.writeFileSync(path.join(terminfo, "78", "xterm-256color"), "terminfo");
|
||||
|
||||
const env = { Path: "C:\\Windows\\System32" };
|
||||
addBundledMoshRuntimeEnv(env, client, { platform: "win32", arch: "x64" });
|
||||
|
||||
assert.equal(env.Path.split(";")[0], dllDir);
|
||||
assert.equal(env.TERMINFO, toCygwinPath(terminfo));
|
||||
assert.equal(env.TERMINFO_DIRS, env.TERMINFO);
|
||||
assert.ok(!/[A-Za-z]:/.test(env.TERMINFO));
|
||||
assert.equal(env.TERMINFO_DIRS, undefined);
|
||||
assert.equal(env.Path, "C:\\Windows\\System32");
|
||||
});
|
||||
|
||||
test("mosh UTF-8 decoder preserves fragmented Chinese output", () => {
|
||||
@@ -273,3 +134,11 @@ test("removed Mosh client detection APIs are not exposed to the renderer", () =>
|
||||
assert.equal(source.includes("netcatty:mosh:pickClient"), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("Cygwin / terminfo helpers are gone from the mosh session module", () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, "terminalBridge", "moshSession.cjs"), "utf8");
|
||||
assert.equal(source.includes("toCygwinPath"), false);
|
||||
assert.equal(source.includes("findBundledMoshDllDir"), false);
|
||||
assert.equal(source.includes("findBundledMoshTerminfoDir"), false);
|
||||
assert.equal(source.includes("cygwin1"), false);
|
||||
});
|
||||
@@ -580,10 +580,7 @@ const moshSessionApi = createMoshSessionApi({
|
||||
});
|
||||
const {
|
||||
resolveBareMoshClient,
|
||||
addBundledMoshDllPath,
|
||||
addBundledMoshTerminfoEnv,
|
||||
addBundledMoshRuntimeEnv,
|
||||
toCygwinPath,
|
||||
createMoshUtf8Decoder,
|
||||
buildMoshSshAuthArgs,
|
||||
cleanupMoshAuthTempFiles,
|
||||
@@ -1444,7 +1441,7 @@ const { getDefaultShell, validatePath } = pathValidationApi;
|
||||
/**
|
||||
* Locate the mosh-client binary bundled by electron-builder via
|
||||
* `extraResources` (see electron-builder.config.cjs and
|
||||
* .github/workflows/build-mosh-binaries.yml).
|
||||
* binaricat/MoshCatty releases).
|
||||
*
|
||||
* Returns an absolute path when the binary is on disk, otherwise null.
|
||||
* In dev / non-packaged runs the path is computed against the project
|
||||
@@ -1586,10 +1583,7 @@ module.exports = {
|
||||
startMoshSession,
|
||||
bundledMoshClient,
|
||||
resolveBareMoshClient,
|
||||
addBundledMoshDllPath,
|
||||
addBundledMoshTerminfoEnv,
|
||||
addBundledMoshRuntimeEnv,
|
||||
toCygwinPath,
|
||||
createMoshUtf8Decoder,
|
||||
startEtSession,
|
||||
execOnEtSession,
|
||||
|
||||
@@ -25,158 +25,10 @@ function createMoshSessionApi(ctx) {
|
||||
function resolveBareMoshClient(_options, opts = {}) {
|
||||
return bundledMoshClient(opts);
|
||||
}
|
||||
|
||||
function getEnvPathKey(env) {
|
||||
const pathKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
|
||||
if (pathKeys.length === 0) return "PATH";
|
||||
return pathKeys.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))[0];
|
||||
}
|
||||
|
||||
function getEnvPathDelimiter(opts = {}) {
|
||||
return (opts.platform || process.platform) === "win32" ? ";" : path.delimiter;
|
||||
}
|
||||
|
||||
function normalizeEnvPathPart(part, opts = {}) {
|
||||
const pathApi = (opts.platform || process.platform) === "win32" ? path.win32 : path;
|
||||
return pathApi.normalize(part).toLowerCase();
|
||||
}
|
||||
|
||||
function prependEnvPath(env, dir, opts = {}) {
|
||||
if (!dir) return env;
|
||||
const pathKey = getEnvPathKey(env);
|
||||
const duplicatePathKeys = Object.keys(env)
|
||||
.filter((key) => key.toLowerCase() === "path" && key !== pathKey);
|
||||
for (const key of duplicatePathKeys) {
|
||||
delete env[key];
|
||||
}
|
||||
const current = env[pathKey] || "";
|
||||
const delimiter = getEnvPathDelimiter(opts);
|
||||
const parts = String(current).split(delimiter).filter(Boolean);
|
||||
const normalizedDir = normalizeEnvPathPart(dir, opts);
|
||||
if (!parts.some((part) => normalizeEnvPathPart(part, opts) === normalizedDir)) {
|
||||
env[pathKey] = current ? `${dir}${delimiter}${current}` : dir;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function findBundledMoshDllDir(bareClient, opts = {}) {
|
||||
const platform = opts.platform || process.platform;
|
||||
if (platform !== "win32" || !bareClient) return null;
|
||||
|
||||
const clientDir = path.dirname(bareClient);
|
||||
const arch = opts.arch || process.arch;
|
||||
const preferred = path.join(clientDir, `mosh-client-win32-${arch}-dlls`);
|
||||
if (fs.existsSync(preferred) && fs.statSync(preferred).isDirectory()) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
try {
|
||||
const match = fs.readdirSync(clientDir)
|
||||
.map((name) => path.join(clientDir, name))
|
||||
.find((candidate) => {
|
||||
const name = path.basename(candidate);
|
||||
return /^mosh-client-win32-.+-dlls$/.test(name)
|
||||
&& fs.existsSync(candidate)
|
||||
&& fs.statSync(candidate).isDirectory();
|
||||
});
|
||||
return match || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function addBundledMoshDllPath(env, bareClient, opts = {}) {
|
||||
const dllDir = findBundledMoshDllDir(bareClient, opts);
|
||||
return dllDir ? prependEnvPath(env, dllDir, opts) : env;
|
||||
}
|
||||
|
||||
function findBundledMoshTerminfoDir(bareClient, _opts = {}) {
|
||||
if (!bareClient) return null;
|
||||
const terminfoDir = path.join(path.dirname(bareClient), "terminfo");
|
||||
const hasXterm256 =
|
||||
fs.existsSync(path.join(terminfoDir, "x", "xterm-256color")) ||
|
||||
fs.existsSync(path.join(terminfoDir, "78", "xterm-256color"));
|
||||
return hasXterm256 ? terminfoDir : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Windows path into the POSIX form Cygwin ncurses expects for
|
||||
* TERMINFO / TERMINFO_DIRS. A raw `C:\...\terminfo` value is unusable:
|
||||
* ncurses splits TERMINFO_DIRS on `:`, so the drive letter becomes a
|
||||
* one-character bogus directory and the real path is never searched.
|
||||
* Issue #2025.
|
||||
*/
|
||||
function toCygwinPath(winPath) {
|
||||
if (typeof winPath !== "string" || !winPath) return winPath;
|
||||
const normalized = winPath.replace(/\\/g, "/");
|
||||
const drive = normalized.match(/^([A-Za-z]):(\/.*)?$/);
|
||||
if (drive) {
|
||||
const rest = drive[2] || "/";
|
||||
return `/cygdrive/${drive[1].toLowerCase()}${rest}`;
|
||||
}
|
||||
if (normalized.startsWith("/")) return normalized;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Standard locations where distros / package managers install the compiled
|
||||
// terminfo database. Used as a fallback only — the bundled directory ships
|
||||
// with the mosh release and is preferred. See issue #890 for context.
|
||||
const LINUX_SYSTEM_TERMINFO_DIRS = [
|
||||
"/etc/terminfo",
|
||||
"/lib/terminfo",
|
||||
"/usr/share/terminfo",
|
||||
"/usr/lib/terminfo",
|
||||
];
|
||||
|
||||
const DARWIN_SYSTEM_TERMINFO_DIRS = [
|
||||
"/usr/share/terminfo",
|
||||
"/opt/homebrew/share/terminfo",
|
||||
"/usr/local/share/terminfo",
|
||||
"/opt/local/share/terminfo",
|
||||
];
|
||||
|
||||
function addBundledMoshTerminfoEnv(env, bareClient, opts = {}) {
|
||||
const platform = opts.platform || process.platform;
|
||||
const terminfoDir = findBundledMoshTerminfoDir(bareClient, opts);
|
||||
|
||||
if (platform === "win32") {
|
||||
if (!terminfoDir) return env;
|
||||
// Cygwin mosh-client reads these as POSIX paths. Keep the Windows
|
||||
// form only when the caller already supplied a cygdrive path.
|
||||
const cygTerminfo = toCygwinPath(terminfoDir);
|
||||
env.TERMINFO = cygTerminfo;
|
||||
env.TERMINFO_DIRS = cygTerminfo;
|
||||
return env;
|
||||
}
|
||||
|
||||
// POSIX. The bundled terminfo is the source of truth — our static
|
||||
// ncurses' compiled-in default points at a build-time temp dir that no
|
||||
// longer exists on the user's machine. Fall back to standard distro
|
||||
// paths when the bundle is absent (e.g. running against an older mosh
|
||||
// binary release that pre-dates the bundle). A caller-supplied
|
||||
// TERMINFO_DIRS is preserved between the bundle and the system defaults.
|
||||
const existing = (typeof env.TERMINFO_DIRS === "string" && env.TERMINFO_DIRS.length > 0)
|
||||
? env.TERMINFO_DIRS.split(":").filter(Boolean)
|
||||
: [];
|
||||
const systemDirs = platform === "darwin" ? DARWIN_SYSTEM_TERMINFO_DIRS : LINUX_SYSTEM_TERMINFO_DIRS;
|
||||
const dirs = [];
|
||||
if (terminfoDir) dirs.push(terminfoDir);
|
||||
for (const dir of existing) {
|
||||
if (!dirs.includes(dir)) dirs.push(dir);
|
||||
}
|
||||
for (const dir of systemDirs) {
|
||||
if (!dirs.includes(dir)) dirs.push(dir);
|
||||
}
|
||||
if (terminfoDir) {
|
||||
env.TERMINFO = terminfoDir;
|
||||
}
|
||||
env.TERMINFO_DIRS = dirs.join(":");
|
||||
return env;
|
||||
}
|
||||
|
||||
function addBundledMoshRuntimeEnv(env, bareClient, opts = {}) {
|
||||
addBundledMoshDllPath(env, bareClient, opts);
|
||||
addBundledMoshTerminfoEnv(env, bareClient, opts);
|
||||
// MoshCatty is a pure single binary (no Cygwin DLL bag, no terminfo).
|
||||
// Runtime env only needs MOSH_KEY / TERM / LANG from the handshake path.
|
||||
function addBundledMoshRuntimeEnv(env, _bareClient, _opts = {}) {
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -790,10 +642,7 @@ function createMoshSessionApi(ctx) {
|
||||
|
||||
return {
|
||||
resolveBareMoshClient,
|
||||
addBundledMoshDllPath,
|
||||
addBundledMoshTerminfoEnv,
|
||||
addBundledMoshRuntimeEnv,
|
||||
toCygwinPath,
|
||||
createMoshUtf8Decoder,
|
||||
buildMoshSshAuthArgs,
|
||||
cleanupMoshAuthTempFiles,
|
||||
|
||||
@@ -6,7 +6,7 @@ with the Netcatty installer. Netcatty launches this bundled `et` directly
|
||||
own SSH bootstrap and EternalTerminal protocol handshake against the remote
|
||||
`etserver` / `etterminal`.
|
||||
|
||||
Unlike `mosh-client`, `et` is a pure network-transport client and does not
|
||||
Like MoshCatty `mosh-client`, `et` is a pure network-transport client and does not
|
||||
render a terminal locally, so there is **no terminfo bundle** here — only the
|
||||
single `et` (`et.exe` on Windows) binary.
|
||||
|
||||
|
||||
+44
-96
@@ -1,109 +1,57 @@
|
||||
# Bundled `mosh-client`
|
||||
# Bundled `mosh-client` (MoshCatty)
|
||||
|
||||
This directory holds the network-protocol-only `mosh-client` binary
|
||||
bundled with the Netcatty installer. Netcatty drives the `ssh` +
|
||||
`mosh-server` bootstrap itself and then launches this bundled client
|
||||
directly (see `electron/bridges/moshHandshake.cjs` and
|
||||
`electron/bridges/terminalBridge.cjs`).
|
||||
This directory holds the pure Rust `mosh-client` from
|
||||
[binaricat/MoshCatty](https://github.com/binaricat/MoshCatty).
|
||||
|
||||
## How binaries land here
|
||||
Netcatty runs SSH + `mosh-server` bootstrap itself, then launches this binary
|
||||
(see `electron/bridges/moshHandshake.cjs` and `terminalBridge/moshSession.cjs`).
|
||||
|
||||
1. `.github/workflows/build-mosh-binaries.yml` builds or fetches
|
||||
`mosh-client` on relevant pushes/PRs, or on a manual
|
||||
`workflow_dispatch`. It uses `scripts/build-mosh/build-linux.sh` and
|
||||
`scripts/build-mosh/build-macos.sh` for Linux/macOS, and
|
||||
`scripts/build-mosh/fetch-windows.sh` for the pinned Windows bundle:
|
||||
## Layout
|
||||
|
||||
| target | provenance |
|
||||
|-------------------|-----------------------------------------------------------------|
|
||||
| `linux-x64` | upstream source, manylinux2014, static third-party deps + glibc |
|
||||
| `linux-arm64` | upstream source, manylinux2014, static third-party deps + glibc |
|
||||
| `darwin-universal`| upstream source, lipo arm64 + x86_64, macOS system dylibs only |
|
||||
| `win32-x64` | Netcatty-pinned runtime bundle + FluentTerminal fallback |
|
||||
| `win32-arm64` | (not built — Cygwin arm64 port not yet stable) |
|
||||
| Target | Release asset | Local path |
|
||||
|--------|---------------|------------|
|
||||
| Linux x64 | `mosh-client-linux-x64.tar.gz` | `linux-x64/mosh-client` |
|
||||
| Linux arm64 | `mosh-client-linux-arm64.tar.gz` | `linux-arm64/mosh-client` |
|
||||
| macOS universal | `mosh-client-darwin-universal.tar.gz` | `darwin-universal/mosh-client` |
|
||||
| Windows x64 | `mosh-client-win32-x64.tar.gz` | `win32-x64/mosh-client.exe` |
|
||||
|
||||
The upstream Cygwin Windows build is not rebuilt in this workflow by
|
||||
default. Windows releases use a SHA256-pinned bundle that was built
|
||||
by Netcatty and verified for packaging.
|
||||
Each tarball contains **only** the client binary (no Cygwin DLLs, no terminfo).
|
||||
Windows builds static-link the MSVC CRT (`moshcatty-0.1.1+`).
|
||||
|
||||
2. When manually dispatched with `release_tag`, that workflow publishes
|
||||
the binaries to the dedicated `binaricat/Netcatty-mosh-bin`
|
||||
repository. The release gets a tag like `mosh-bin-1.4.0-1`, with
|
||||
`SHA256SUMS` attached.
|
||||
Release tags: `moshcatty-*` (prefer `moshcatty-0.1.2+`) from
|
||||
`binaricat/MoshCatty`, with `SHA256SUMS`.
|
||||
|
||||
3. Release packaging runs `scripts/resolve-mosh-bin-release.cjs` before
|
||||
`npm run fetch:mosh`. It uses an explicit workflow input first, then
|
||||
the `MOSH_BIN_RELEASE` repository variable, then the latest
|
||||
non-draft `mosh-bin-*` GitHub Release from the dedicated binary
|
||||
repository. The fetch step pulls the binaries into
|
||||
`resources/mosh/<platform-arch>/`. For local packaging, set
|
||||
`MOSH_BIN_RELEASE` yourself before running the same fetch command.
|
||||
Override `MOSH_BIN_OWNER` / `MOSH_BIN_REPO` only when testing a
|
||||
different binary repository. `electron-builder.config.cjs` then
|
||||
copies the matching binary into `Resources/mosh/mosh-client[.exe]`.
|
||||
### Linux glibc floors
|
||||
|
||||
Local dev uses the same binary path: `npm run dev` runs
|
||||
`npm run fetch:mosh:dev` first, which downloads the host platform's
|
||||
bundled `mosh-client` into this gitignored directory. Netcatty does
|
||||
not fall back to a system-installed `mosh` or `mosh-client`; if the
|
||||
bundled binary is missing, Mosh startup fails loudly instead of using
|
||||
whatever happens to be installed on the developer machine.
|
||||
Linux assets must start on the same distros Netcatty packages for. From
|
||||
`moshcatty-0.1.2`, MoshCatty builds Linux clients on:
|
||||
|
||||
Official Windows package builds currently ship x64 only for bundled
|
||||
Mosh coverage. Windows arm64 packaging should be added only after we
|
||||
have a tested arm64 client bundle.
|
||||
| Target | Build image | Max required GLIBC |
|
||||
|--------|-------------|--------------------|
|
||||
| `linux-x64` | AlmaLinux 8 | 2.28 |
|
||||
| `linux-arm64` | Debian bullseye | 2.31 |
|
||||
|
||||
The directory is otherwise empty (binaries are gitignored).
|
||||
Do **not** pin packaging to `moshcatty-0.1.0` / `0.1.1` Linux binaries: those
|
||||
were built on Ubuntu runners and require GLIBC 2.34.
|
||||
|
||||
## Fetch
|
||||
|
||||
```sh
|
||||
# Optional pin (prefer 0.1.2+ for Linux glibc floors)
|
||||
export MOSH_BIN_RELEASE=moshcatty-0.1.2
|
||||
npm run fetch:mosh
|
||||
|
||||
# Dev: host platform; resolves latest moshcatty-* if unset
|
||||
npm run fetch:mosh:dev
|
||||
```
|
||||
|
||||
Env: `MOSH_BIN_OWNER` / `MOSH_BIN_REPO` (default `binaricat` / `MoshCatty`),
|
||||
`MOSH_BIN_BASE_URL` for mirrors.
|
||||
|
||||
`electron-builder` packages `Resources/mosh/mosh-client[.exe]` only.
|
||||
|
||||
## Licenses
|
||||
|
||||
- Mosh itself is licensed under **GPL-3.0**
|
||||
(https://github.com/mobile-shell/mosh).
|
||||
- Netcatty is **GPL-3.0**, so redistribution as part of the installer
|
||||
is permitted.
|
||||
- The default Windows x64 artifact is a SHA256-pinned Netcatty
|
||||
`mosh-client-win32-x64.tar.gz` bundle from
|
||||
`binaricat/Netcatty-mosh-bin` release `mosh-bin-1.4.0-2`. It includes
|
||||
`mosh-client.exe`, required Cygwin runtime DLLs, and the bundled
|
||||
`xterm-256color` terminfo entry. The FluentTerminal standalone
|
||||
`mosh-client.exe` from https://github.com/felixse/FluentTerminal @
|
||||
commit `bad0f85` remains published as a pinned fallback.
|
||||
- Bundled/static deps (OpenSSL Apache-2.0, protobuf BSD-3-Clause,
|
||||
ncurses MIT) are compatible with GPL-3.0.
|
||||
|
||||
## Reproducible build
|
||||
|
||||
To reproduce the binaries locally:
|
||||
|
||||
```sh
|
||||
docker run --rm -v $PWD:/workspace -w /workspace \
|
||||
-e MOSH_REF=mosh-1.4.0 -e ARCH=x64 -e OUT_DIR=/workspace/out \
|
||||
quay.io/pypa/manylinux2014_x86_64 \
|
||||
bash scripts/build-mosh/build-linux.sh
|
||||
```
|
||||
|
||||
For macOS the build needs an Xcode toolchain; see
|
||||
`scripts/build-mosh/build-macos.sh`.
|
||||
|
||||
## Phase 2/3 — done in this PR
|
||||
|
||||
- `electron/bridges/moshHandshake.cjs` reimplements the upstream Mosh
|
||||
Perl wrapper in Node: parser + sniffer + command builders as pure
|
||||
functions.
|
||||
- `terminalBridge.startMoshSession` runs the SSH bootstrap in a
|
||||
node-pty so password / 2FA / known-hosts prompts render naturally
|
||||
in the user's terminal, then swaps `session.proc` from the ssh PTY
|
||||
to a freshly-spawned `mosh-client` PTY when `MOSH CONNECT` is
|
||||
detected. Keystrokes that arrive after the swap go to mosh-client
|
||||
because `writeToSession` reads `session.proc` lazily.
|
||||
- Mosh startup requires Netcatty's bundled `mosh-client` and a usable
|
||||
`ssh` client for the remote bootstrap. System-installed `mosh` /
|
||||
`mosh-client` binaries are intentionally ignored.
|
||||
- Windows x64 currently ships the pinned Netcatty runtime bundle. The
|
||||
old standalone client remains only as a release fallback.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- Add Windows arm64 only after a tested arm64 client bundle is
|
||||
available.
|
||||
- Make `MOSH_REF` track upstream release tags automatically.
|
||||
- MoshCatty client: **GPL-3.0-or-later**
|
||||
- Upstream Mosh protocol reference: **GPL-3.0**
|
||||
- Netcatty is **GPL-3.0**
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a portable mosh-client binary inside manylinux2014.
|
||||
#
|
||||
# Inputs (env):
|
||||
# MOSH_REF — git ref of mobile-shell/mosh to build (e.g. mosh-1.4.0)
|
||||
# ARCH — x64 | arm64 (for output naming only; container is already that arch)
|
||||
# OUT_DIR — directory to write mosh-client-linux-<arch>.tar.gz + sha256
|
||||
#
|
||||
# Output:
|
||||
# $OUT_DIR/mosh-client-linux-<arch>.tar.gz (binary + terminfo bundle)
|
||||
# $OUT_DIR/mosh-client-linux-<arch>.tar.gz.sha256
|
||||
#
|
||||
# The bundle ships a private terminfo database next to the binary because
|
||||
# our statically-linked ncurses has its compiled-in TERMINFO path pointing
|
||||
# at the build-time prefix (a temp dir). Without bundling, mosh-client on
|
||||
# distros lacking /usr/share/terminfo (or stripped containers) fails with
|
||||
# "Terminfo database could not be found." See issue #890.
|
||||
#
|
||||
# Strategy: build OpenSSL, protobuf, ncurses as static archives in a
|
||||
# scratch prefix, then build mosh against those and link libstdc++/libgcc
|
||||
# statically. The resulting binary still depends on standard Linux system
|
||||
# libraries such as glibc/libz/libutil from the manylinux2014 baseline
|
||||
# (compatible with virtually every distro released since 2014, including
|
||||
# Debian 9+, Ubuntu 18.04+, CentOS 7+).
|
||||
set -euo pipefail
|
||||
|
||||
: "${MOSH_REF:?missing MOSH_REF}"
|
||||
: "${ARCH:?missing ARCH}"
|
||||
: "${OUT_DIR:?missing OUT_DIR}"
|
||||
|
||||
validate_mosh_ref() {
|
||||
if [[ ! "$MOSH_REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] \
|
||||
|| [[ "$MOSH_REF" == *..* ]] \
|
||||
|| [[ "$MOSH_REF" == *@\{* ]] \
|
||||
|| [[ "$MOSH_REF" == */ ]] \
|
||||
|| [[ "$MOSH_REF" == *.lock ]]; then
|
||||
echo "ERROR: invalid MOSH_REF: $MOSH_REF" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
validate_mosh_ref
|
||||
|
||||
OPENSSL_VER=3.0.13
|
||||
PROTOBUF_VER=21.12
|
||||
NCURSES_VER=6.4
|
||||
|
||||
curl_retry() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
curl -fsSL --retry 8 --retry-delay 5 --retry-max-time 600 "$url" -o "$dest"
|
||||
}
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
PREFIX="$WORK/prefix"
|
||||
mkdir -p "$PREFIX/lib" "$PREFIX/include" "$OUT_DIR"
|
||||
|
||||
yum install -y -q autoconf automake libtool perl perl-IPC-Cmd make gcc gcc-c++ pkgconfig zlib-devel
|
||||
|
||||
cd "$WORK"
|
||||
|
||||
# OpenSSL static
|
||||
curl_retry "https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz" openssl.tgz
|
||||
tar xzf openssl.tgz
|
||||
( cd "openssl-$OPENSSL_VER"
|
||||
./config no-shared no-tests --prefix="$PREFIX" --openssldir="$PREFIX/ssl"
|
||||
make -j"$(nproc)"
|
||||
make install_sw )
|
||||
|
||||
# protobuf static (3.x stays compatible with mosh's generated proto code)
|
||||
curl_retry "https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VER/protobuf-cpp-3.$PROTOBUF_VER.tar.gz" protobuf.tgz
|
||||
tar xzf protobuf.tgz
|
||||
( cd "protobuf-3.$PROTOBUF_VER"
|
||||
./configure --prefix="$PREFIX" --enable-static --disable-shared --with-pic
|
||||
make -j"$(nproc)"
|
||||
make install )
|
||||
|
||||
# ncurses static
|
||||
curl_retry "https://invisible-island.net/archives/ncurses/ncurses-$NCURSES_VER.tar.gz" ncurses.tgz
|
||||
tar xzf ncurses.tgz
|
||||
( cd "ncurses-$NCURSES_VER"
|
||||
CFLAGS="-fPIC -O2" CXXFLAGS="-fPIC -O2" \
|
||||
./configure --prefix="$PREFIX" --without-shared --without-debug --without-cxx-shared --without-tests --disable-pc-files --enable-widec
|
||||
make -j"$(nproc)"
|
||||
make install )
|
||||
|
||||
# Mosh. Fetch the requested ref explicitly so branch names, tags, and commit
|
||||
# SHAs all work from workflow_dispatch.
|
||||
git init mosh
|
||||
git -C mosh remote add origin https://github.com/mobile-shell/mosh.git
|
||||
git -C mosh fetch --depth 1 origin "$MOSH_REF"
|
||||
git -C mosh checkout --detach FETCH_HEAD
|
||||
( cd mosh
|
||||
export PATH="$PREFIX/bin:$PATH"
|
||||
./autogen.sh
|
||||
PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig:$PREFIX/lib64/pkgconfig" \
|
||||
./configure --enable-completion=no --disable-server \
|
||||
CPPFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw" \
|
||||
CXXFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw -O2" \
|
||||
CFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw -O2" \
|
||||
LDFLAGS="-L$PREFIX/lib -L$PREFIX/lib64 -static-libstdc++ -static-libgcc" \
|
||||
LIBS="-ldl -lpthread"
|
||||
make -j"$(nproc)" )
|
||||
|
||||
BUNDLE_DIR="$WORK/linux-$ARCH-bundle"
|
||||
mkdir -p "$BUNDLE_DIR"
|
||||
OUT_BIN="$BUNDLE_DIR/mosh-client"
|
||||
cp mosh/src/frontend/mosh-client "$OUT_BIN"
|
||||
strip "$OUT_BIN"
|
||||
|
||||
echo "--- file ---"
|
||||
file "$OUT_BIN"
|
||||
echo "--- ldd ---"
|
||||
ldd "$OUT_BIN" || true
|
||||
echo "--- size ---"
|
||||
ls -lh "$OUT_BIN"
|
||||
|
||||
# Sanity check: must not link any non-system shared libraries. Allow only
|
||||
# the glibc runtime family and the ELF loader.
|
||||
ldd "$OUT_BIN" > "$WORK/ldd.txt" || true
|
||||
awk '
|
||||
/=>/ { print $1; next }
|
||||
/^[[:space:]]*\/.*ld-linux/ { print $1; next }
|
||||
' "$WORK/ldd.txt" > "$WORK/deps.txt"
|
||||
if grep -Ev '^(linux-vdso\.so\.1|lib(c|m|pthread|rt|dl|resolv|util|z)\.so\.[0-9]+|/lib.*/ld-linux.*\.so\.[0-9]+|ld-linux.*\.so\.[0-9]+)$' "$WORK/deps.txt"; then
|
||||
echo "ERROR: mosh-client links a non-system shared library; static linking failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bundle the terminfo entries our statically-linked ncurses needs. The
|
||||
# ncurses `make install` above populated $PREFIX/share/terminfo/ with the
|
||||
# full upstream terminfo.src. Ship a curated subset so users hit a
|
||||
# working entry regardless of TERM.
|
||||
TERMINFO_SRC="$PREFIX/share/terminfo"
|
||||
TERMINFO_OUT="$BUNDLE_DIR/terminfo"
|
||||
mkdir -p "$TERMINFO_OUT"
|
||||
copy_terminfo_entry() {
|
||||
local name="$1"
|
||||
for src in "$TERMINFO_SRC"/?/"$name" "$TERMINFO_SRC"/??/"$name"; do
|
||||
[ -f "$src" ] || continue
|
||||
local rel
|
||||
rel=$(basename "$(dirname "$src")")
|
||||
mkdir -p "$TERMINFO_OUT/$rel"
|
||||
cp "$src" "$TERMINFO_OUT/$rel/$name"
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
for entry in xterm-256color xterm xterm-color vt100 vt220 ansi screen screen-256color tmux tmux-256color dumb linux; do
|
||||
copy_terminfo_entry "$entry" || echo "WARN: terminfo entry $entry not found in $TERMINFO_SRC" >&2
|
||||
done
|
||||
if [ ! -f "$TERMINFO_OUT/x/xterm-256color" ] && [ ! -f "$TERMINFO_OUT/78/xterm-256color" ]; then
|
||||
echo "ERROR: failed to bundle xterm-256color terminfo for mosh-client (linux-$ARCH)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- bundled terminfo ---"
|
||||
find "$TERMINFO_OUT" -type f -print
|
||||
|
||||
BUNDLE_TGZ="$OUT_DIR/mosh-client-linux-$ARCH.tar.gz"
|
||||
( cd "$BUNDLE_DIR" && tar -czf "$BUNDLE_TGZ" "mosh-client" "terminfo" )
|
||||
|
||||
( cd "$OUT_DIR" && sha256sum "mosh-client-linux-$ARCH.tar.gz" > "mosh-client-linux-$ARCH.tar.gz.sha256" )
|
||||
cat "$OUT_DIR/mosh-client-linux-$ARCH.tar.gz.sha256"
|
||||
@@ -1,214 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a universal2 (arm64 + x86_64) mosh-client for macOS.
|
||||
#
|
||||
# Inputs (env):
|
||||
# MOSH_REF — git ref of mobile-shell/mosh
|
||||
# OUT_DIR — destination directory
|
||||
# MACOSX_DEPLOYMENT_TARGET — minimum macOS version (default 11.0)
|
||||
#
|
||||
# Output:
|
||||
# $OUT_DIR/mosh-client-darwin-universal.tar.gz (binary + terminfo bundle)
|
||||
# $OUT_DIR/mosh-client-darwin-universal.tar.gz.sha256
|
||||
#
|
||||
# The bundle ships a private terminfo database next to the binary because
|
||||
# our statically-linked ncurses has its compiled-in TERMINFO path pointing
|
||||
# at the build-time prefix (a temp dir). Without bundling, mosh-client
|
||||
# fails with "Terminfo database could not be found." See issue #890.
|
||||
#
|
||||
# Strategy: build OpenSSL/protobuf/ncurses for arm64 and x86_64
|
||||
# (cross-compile via Apple clang's -arch flag), link mosh-client per arch,
|
||||
# then lipo the two single-arch binaries into one universal binary. The
|
||||
# final binary is allowed to depend only on macOS system dylibs.
|
||||
set -euo pipefail
|
||||
|
||||
: "${MOSH_REF:?missing MOSH_REF}"
|
||||
: "${OUT_DIR:?missing OUT_DIR}"
|
||||
|
||||
validate_mosh_ref() {
|
||||
if [[ ! "$MOSH_REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] \
|
||||
|| [[ "$MOSH_REF" == *..* ]] \
|
||||
|| [[ "$MOSH_REF" == *@\{* ]] \
|
||||
|| [[ "$MOSH_REF" == */ ]] \
|
||||
|| [[ "$MOSH_REF" == *.lock ]]; then
|
||||
echo "ERROR: invalid MOSH_REF: $MOSH_REF" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
validate_mosh_ref
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"
|
||||
|
||||
OPENSSL_VER=3.0.13
|
||||
PROTOBUF_VER=21.12
|
||||
NCURSES_VER=6.4
|
||||
|
||||
curl_retry() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
curl -fsSL --retry 8 --retry-delay 5 --retry-max-time 600 "$url" -o "$dest"
|
||||
}
|
||||
|
||||
# Install build tools when they are not already present on the runner.
|
||||
brew list autoconf >/dev/null 2>&1 || brew install autoconf
|
||||
brew list automake >/dev/null 2>&1 || brew install automake
|
||||
brew list pkg-config >/dev/null 2>&1 || brew install pkg-config
|
||||
brew list libtool >/dev/null 2>&1 || brew install libtool
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
mkdir -p "$OUT_DIR"
|
||||
NATIVE_PROTOC_DIR=""
|
||||
|
||||
# Pre-fetch sources once.
|
||||
cd "$WORK"
|
||||
curl_retry "https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz" openssl.tgz
|
||||
curl_retry "https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VER/protobuf-cpp-3.$PROTOBUF_VER.tar.gz" protobuf.tgz
|
||||
curl_retry "https://invisible-island.net/archives/ncurses/ncurses-$NCURSES_VER.tar.gz" ncurses.tgz
|
||||
git init mosh-src
|
||||
git -C mosh-src remote add origin https://github.com/mobile-shell/mosh.git
|
||||
git -C mosh-src fetch --depth 1 origin "$MOSH_REF"
|
||||
git -C mosh-src checkout --detach FETCH_HEAD
|
||||
|
||||
build_arch() {
|
||||
local ARCH="$1"
|
||||
local TRIPLE
|
||||
case "$ARCH" in
|
||||
arm64) TRIPLE=aarch64-apple-darwin ;;
|
||||
x86_64) TRIPLE=x86_64-apple-darwin ;;
|
||||
*) echo "unknown arch: $ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
local PREFIX="$WORK/prefix-$ARCH"
|
||||
mkdir -p "$PREFIX"
|
||||
|
||||
local CFLAGS_COMMON="-arch $ARCH -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET -O2"
|
||||
local LDFLAGS_COMMON="-arch $ARCH -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
|
||||
|
||||
# OpenSSL
|
||||
rm -rf "openssl-$OPENSSL_VER"
|
||||
tar xf openssl.tgz
|
||||
( cd "openssl-$OPENSSL_VER"
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
./Configure darwin64-arm64-cc no-shared no-tests --prefix="$PREFIX" --openssldir="$PREFIX/ssl" -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET
|
||||
else
|
||||
./Configure darwin64-x86_64-cc no-shared no-tests --prefix="$PREFIX" --openssldir="$PREFIX/ssl" -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET
|
||||
fi
|
||||
make -j"$(sysctl -n hw.ncpu)"
|
||||
make install_sw )
|
||||
|
||||
# protobuf
|
||||
rm -rf "protobuf-3.$PROTOBUF_VER"
|
||||
tar xf protobuf.tgz
|
||||
( cd "protobuf-3.$PROTOBUF_VER"
|
||||
./configure --prefix="$PREFIX" --enable-static --disable-shared --with-pic --host="$TRIPLE" \
|
||||
CXX="clang++" CC="clang" \
|
||||
CFLAGS="$CFLAGS_COMMON" CXXFLAGS="$CFLAGS_COMMON" LDFLAGS="$LDFLAGS_COMMON"
|
||||
# protoc must run on the host (not the cross-target) — but here host arch is one of the two,
|
||||
# so this works directly when ARCH matches the runner. For the *other* arch we reuse the
|
||||
# protoc compiled in the first pass via PATH.
|
||||
make -j"$(sysctl -n hw.ncpu)" || make -j1
|
||||
make install )
|
||||
if [ "$ARCH" = "$NATIVE_ARCH" ]; then
|
||||
NATIVE_PROTOC_DIR="$PREFIX/bin"
|
||||
fi
|
||||
|
||||
# ncurses
|
||||
rm -rf "ncurses-$NCURSES_VER"
|
||||
tar xf ncurses.tgz
|
||||
( cd "ncurses-$NCURSES_VER"
|
||||
./configure --prefix="$PREFIX" --without-shared --without-debug --without-cxx-shared --without-tests --disable-pc-files --enable-widec --host="$TRIPLE" \
|
||||
CC="clang" CXX="clang++" \
|
||||
CFLAGS="$CFLAGS_COMMON" CXXFLAGS="$CFLAGS_COMMON" LDFLAGS="$LDFLAGS_COMMON"
|
||||
make -j"$(sysctl -n hw.ncpu)"
|
||||
make -C include install
|
||||
make -C ncurses install
|
||||
# Compile + install the terminfo database for the native arch only.
|
||||
# `tic` runs on the build host, so a cross-target build can't compile
|
||||
# terminfo entries — but the .src database is arch-independent, so a
|
||||
# single native-arch install populates $PREFIX/share/terminfo/ with
|
||||
# the data we bundle below.
|
||||
if [ "$ARCH" = "$NATIVE_ARCH" ]; then
|
||||
make -C progs install
|
||||
make -C misc install
|
||||
fi )
|
||||
|
||||
# mosh per-arch build
|
||||
( cd mosh-src
|
||||
make distclean >/dev/null 2>&1 || true
|
||||
export PATH="${NATIVE_PROTOC_DIR:-$PREFIX/bin}:$PATH"
|
||||
./autogen.sh
|
||||
PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" \
|
||||
./configure --enable-completion=no --disable-server --host="$TRIPLE" \
|
||||
CXX="clang++" CC="clang" \
|
||||
CPPFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw" \
|
||||
CXXFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw $CFLAGS_COMMON" \
|
||||
CFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw $CFLAGS_COMMON" \
|
||||
LDFLAGS="-L$PREFIX/lib $LDFLAGS_COMMON"
|
||||
make -j"$(sysctl -n hw.ncpu)"
|
||||
cp src/frontend/mosh-client "$WORK/mosh-client-$ARCH" )
|
||||
}
|
||||
|
||||
# Build host arch first so the first protobuf pass can use a native protoc.
|
||||
NATIVE_ARCH=$(uname -m)
|
||||
if [ "$NATIVE_ARCH" = "arm64" ]; then
|
||||
build_arch arm64
|
||||
build_arch x86_64
|
||||
else
|
||||
build_arch x86_64
|
||||
build_arch arm64
|
||||
fi
|
||||
|
||||
BUNDLE_DIR="$WORK/darwin-universal-bundle"
|
||||
mkdir -p "$BUNDLE_DIR"
|
||||
OUT_BIN="$BUNDLE_DIR/mosh-client"
|
||||
lipo -create "$WORK/mosh-client-arm64" "$WORK/mosh-client-x86_64" -output "$OUT_BIN"
|
||||
strip -x "$OUT_BIN" || true
|
||||
|
||||
echo "--- file ---"
|
||||
file "$OUT_BIN"
|
||||
echo "--- otool -L ---"
|
||||
otool -L "$OUT_BIN"
|
||||
echo "--- lipo -info ---"
|
||||
lipo -info "$OUT_BIN"
|
||||
echo "--- size ---"
|
||||
ls -lh "$OUT_BIN"
|
||||
|
||||
# Sanity check: must not depend on non-system dylibs.
|
||||
if otool -L "$OUT_BIN" | tail -n +2 | awk '{print $1}' | grep -Ev "^(/usr/lib/|/System/)"; then
|
||||
echo "ERROR: mosh-client links a non-system dylib; static linking failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bundle the terminfo entries our statically-linked ncurses needs. Pull
|
||||
# from the native-arch prefix where `make -C misc install` ran tic above.
|
||||
TERMINFO_SRC="$WORK/prefix-$NATIVE_ARCH/share/terminfo"
|
||||
TERMINFO_OUT="$BUNDLE_DIR/terminfo"
|
||||
mkdir -p "$TERMINFO_OUT"
|
||||
copy_terminfo_entry() {
|
||||
local name="$1"
|
||||
for src in "$TERMINFO_SRC"/?/"$name" "$TERMINFO_SRC"/??/"$name"; do
|
||||
[ -f "$src" ] || continue
|
||||
local rel
|
||||
rel=$(basename "$(dirname "$src")")
|
||||
mkdir -p "$TERMINFO_OUT/$rel"
|
||||
cp "$src" "$TERMINFO_OUT/$rel/$name"
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
for entry in xterm-256color xterm xterm-color vt100 vt220 ansi screen screen-256color tmux tmux-256color dumb; do
|
||||
copy_terminfo_entry "$entry" || echo "WARN: terminfo entry $entry not found in $TERMINFO_SRC" >&2
|
||||
done
|
||||
if [ ! -f "$TERMINFO_OUT/x/xterm-256color" ] && [ ! -f "$TERMINFO_OUT/78/xterm-256color" ]; then
|
||||
echo "ERROR: failed to bundle xterm-256color terminfo for mosh-client (darwin-universal)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- bundled terminfo ---"
|
||||
find "$TERMINFO_OUT" -type f -print
|
||||
|
||||
BUNDLE_TGZ="$OUT_DIR/mosh-client-darwin-universal.tar.gz"
|
||||
( cd "$BUNDLE_DIR" && tar -czf "$BUNDLE_TGZ" "mosh-client" "terminfo" )
|
||||
|
||||
( cd "$OUT_DIR" && shasum -a 256 "mosh-client-darwin-universal.tar.gz" > "mosh-client-darwin-universal.tar.gz.sha256" )
|
||||
cat "$OUT_DIR/mosh-client-darwin-universal.tar.gz.sha256"
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Source: pin the last known-good Windows mosh bundle built by Netcatty's
|
||||
# Cygwin workflow. The bundle carries mosh-client.exe, runtime DLLs, and the
|
||||
# xterm-256color terminfo entry needed by packaged Windows builds.
|
||||
#
|
||||
# Keep the old FluentTerminal standalone exe as a release fallback. It is
|
||||
# PE32+ x86-64 with no cygwin1.dll dependency. FluentTerminal is GPL-3.0, same
|
||||
# license as Netcatty, and the binary itself is GPL-3.0 from upstream
|
||||
# mobile-shell/mosh.
|
||||
#
|
||||
# Inputs (env): OUT_DIR
|
||||
# Output: $OUT_DIR/mosh-client-win32-x64.tar.gz (+ .sha256)
|
||||
# $OUT_DIR/mosh-client-win32-x64.exe (+ .sha256 fallback)
|
||||
set -euo pipefail
|
||||
|
||||
: "${OUT_DIR:?missing OUT_DIR}"
|
||||
|
||||
WINDOWS_BUNDLE_URL="${WINDOWS_BUNDLE_URL:-https://github.com/binaricat/Netcatty-mosh-bin/releases/download/mosh-bin-1.4.0-2/mosh-client-win32-x64.tar.gz}"
|
||||
WINDOWS_BUNDLE_SHA256="${WINDOWS_BUNDLE_SHA256:-3d4c4ae9fc8026dc8f4972856b9dedfb5e67fc623f2c23133c83b24c08bc1b2f}"
|
||||
|
||||
# Fallback pin: github.com/felixse/FluentTerminal commit bad0f85,
|
||||
# Dependencies/MoshExecutables/x64/mosh-client.exe.
|
||||
LEGACY_SOURCE_URL="${LEGACY_SOURCE_URL:-https://raw.githubusercontent.com/felixse/FluentTerminal/bad0f85/Dependencies/MoshExecutables/x64/mosh-client.exe}"
|
||||
LEGACY_EXPECTED_SHA256="${LEGACY_EXPECTED_SHA256:-5a8d84ff205c6a0711e53b961f909484a892f42648807e52d46d4fa93c05e286}"
|
||||
|
||||
check_sha256() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
local label="$3"
|
||||
local actual
|
||||
actual=$(sha256sum "$file" | awk '{print $1}')
|
||||
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "ERROR: SHA256 mismatch for $label" >&2
|
||||
echo " expected: $expected" >&2
|
||||
echo " actual: $actual" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$actual"
|
||||
}
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
BUNDLE_OUT="$OUT_DIR/mosh-client-win32-x64.tar.gz"
|
||||
LEGACY_OUT="$OUT_DIR/mosh-client-win32-x64.exe"
|
||||
|
||||
curl -fsSL "$WINDOWS_BUNDLE_URL" -o "$BUNDLE_OUT"
|
||||
BUNDLE_ACTUAL=$(check_sha256 "$BUNDLE_OUT" "$WINDOWS_BUNDLE_SHA256" "mosh-client-win32-x64.tar.gz")
|
||||
|
||||
echo "Fetched mosh-client-win32-x64.tar.gz (sha256=$BUNDLE_ACTUAL)."
|
||||
ls -lh "$BUNDLE_OUT"
|
||||
echo "$BUNDLE_ACTUAL mosh-client-win32-x64.tar.gz" > "$BUNDLE_OUT.sha256"
|
||||
cat "$BUNDLE_OUT.sha256"
|
||||
|
||||
curl -fsSL "$LEGACY_SOURCE_URL" -o "$LEGACY_OUT"
|
||||
LEGACY_ACTUAL=$(check_sha256 "$LEGACY_OUT" "$LEGACY_EXPECTED_SHA256" "mosh-client.exe")
|
||||
|
||||
echo "Fetched fallback mosh-client.exe (sha256=$LEGACY_ACTUAL)."
|
||||
ls -lh "$LEGACY_OUT"
|
||||
echo "$LEGACY_ACTUAL mosh-client-win32-x64.exe" > "$LEGACY_OUT.sha256"
|
||||
cat "$LEGACY_OUT.sha256"
|
||||
+78
-177
@@ -1,33 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
//
|
||||
// Download platform-specific mosh-client binaries built by the
|
||||
// `build-mosh-binaries` GitHub Actions workflow into resources/mosh/, so
|
||||
// electron-builder can bundle them via `extraResources`. Designed to be
|
||||
// idempotent and safe to skip in dev / CI matrix legs that don't ship
|
||||
// mosh (e.g. when MOSH_BIN_RELEASE is unset).
|
||||
// Download platform-specific mosh-client binaries from binaricat/MoshCatty
|
||||
// releases into resources/mosh/, so electron-builder can bundle them via
|
||||
// extraResources.
|
||||
//
|
||||
// Layout (MoshCatty only — pure single binary per platform, no Cygwin DLLs,
|
||||
// no terminfo bag):
|
||||
// mosh-client-linux-x64.tar.gz -> resources/mosh/linux-x64/mosh-client
|
||||
// mosh-client-linux-arm64.tar.gz -> resources/mosh/linux-arm64/mosh-client
|
||||
// mosh-client-darwin-universal.tar.gz -> resources/mosh/darwin-universal/mosh-client
|
||||
// mosh-client-win32-x64.tar.gz -> resources/mosh/win32-x64/mosh-client.exe
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/fetch-mosh-binaries.cjs # all platforms
|
||||
// node scripts/fetch-mosh-binaries.cjs
|
||||
// node scripts/fetch-mosh-binaries.cjs --platform=darwin --arch=universal
|
||||
// node scripts/fetch-mosh-binaries.cjs --host --resolve-release
|
||||
//
|
||||
// Env knobs:
|
||||
// MOSH_BIN_RELEASE — release tag in ${MOSH_BIN_OWNER}/${MOSH_BIN_REPO}.
|
||||
// Skip the whole step if unset (printed as a notice
|
||||
// so the build doesn't silently miss the bundling).
|
||||
// MOSH_BIN_OWNER — defaults to the GITHUB_REPOSITORY owner, or 'binaricat'
|
||||
// MOSH_BIN_REPO — default 'Netcatty-mosh-bin' (a dedicated binary
|
||||
// repository so the client repo stays source-only).
|
||||
// MOSH_BIN_BASE_URL — full override (e.g. for staging / local mirror).
|
||||
// MOSH_BIN_RES_DIR — override output dir for tests.
|
||||
// MOSH_BIN_ALLOW_UNVERIFIED=true — explicit local escape hatch for mirrors
|
||||
// without SHA256SUMS. Never use for release builds.
|
||||
// MOSH_BIN_FORCE_WINDOWS_CYGWIN=true — legacy debug knob kept for older
|
||||
// automation. Windows now prefers the released bundle
|
||||
// with its runtime helpers when SHA256SUMS lists it.
|
||||
// MOSH_BIN_WINDOWS_LEGACY_URL / MOSH_BIN_WINDOWS_LEGACY_SHA256 — test/mirror
|
||||
// overrides for that pinned Windows fallback.
|
||||
// Env:
|
||||
// MOSH_BIN_RELEASE — required for fetch unless --resolve-release
|
||||
// MOSH_BIN_OWNER / MOSH_BIN_REPO — default binaricat / MoshCatty
|
||||
// MOSH_BIN_BASE_URL — full release download base override
|
||||
// MOSH_BIN_RES_DIR — output dir override (tests)
|
||||
// MOSH_BIN_ALLOW_UNVERIFIED — accept missing SHA256SUMS (local mirrors only)
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
@@ -36,90 +31,33 @@ const https = require("node:https");
|
||||
const os = require("node:os");
|
||||
const crypto = require("node:crypto");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const { main: resolveMoshBinRelease } = require("./resolve-mosh-bin-release.cjs");
|
||||
const {
|
||||
main: resolveMoshBinRelease,
|
||||
validateReleaseTag,
|
||||
} = require("./resolve-mosh-bin-release.cjs");
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const DEFAULT_RES_DIR = path.join(ROOT, "resources", "mosh");
|
||||
const WINDOWS_LEGACY_FLUENT_MOSH_CLIENT = {
|
||||
id: "windows-fluentterminal-standalone",
|
||||
file: "mosh-client-win32-x64.exe",
|
||||
local: "win32-x64/mosh-client.exe",
|
||||
url: "https://raw.githubusercontent.com/felixse/FluentTerminal/bad0f85/Dependencies/MoshExecutables/x64/mosh-client.exe",
|
||||
sha256: "5a8d84ff205c6a0711e53b961f909484a892f42648807e52d46d4fa93c05e286",
|
||||
};
|
||||
|
||||
// (file basename in the release -> relative subpath under resources/mosh/)
|
||||
// Using flat names in the release for SHA256SUMS readability, then
|
||||
// fanning out into platform-arch subdirs locally.
|
||||
//
|
||||
// Linux/macOS/Windows bundle targets are tar.gz archives containing the
|
||||
// binary plus the runtime helpers each platform needs.
|
||||
// Bundling terminfo lets bundled Posix mosh-client builds work on
|
||||
// minimal hosts that don't have a
|
||||
// system ncurses-base — see issue #890.
|
||||
//
|
||||
// `legacy` describes the pre-bundle artifact name some published mosh
|
||||
// binary releases still ship (Linux/Darwin used flat files before the
|
||||
// bundle layout). When SHA256SUMS lists only the legacy name we fall
|
||||
// back to it so existing releases keep working until a new tag is
|
||||
// republished with the bundle layout.
|
||||
const TARGETS = [
|
||||
{
|
||||
platform: "linux", arch: "x64",
|
||||
file: "mosh-client-linux-x64.tar.gz", localDir: "linux-x64", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-linux-x64", local: "linux-x64/mosh-client" },
|
||||
file: "mosh-client-linux-x64.tar.gz", localDir: "linux-x64", binary: "mosh-client",
|
||||
},
|
||||
{
|
||||
platform: "linux", arch: "arm64",
|
||||
file: "mosh-client-linux-arm64.tar.gz", localDir: "linux-arm64", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-linux-arm64", local: "linux-arm64/mosh-client" },
|
||||
file: "mosh-client-linux-arm64.tar.gz", localDir: "linux-arm64", binary: "mosh-client",
|
||||
},
|
||||
{
|
||||
platform: "darwin", arch: "universal",
|
||||
file: "mosh-client-darwin-universal.tar.gz", localDir: "darwin-universal", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-darwin-universal", local: "darwin-universal/mosh-client" },
|
||||
file: "mosh-client-darwin-universal.tar.gz", localDir: "darwin-universal", binary: "mosh-client",
|
||||
},
|
||||
{
|
||||
platform: "win32", arch: "x64",
|
||||
file: "mosh-client-win32-x64.tar.gz", localDir: "win32-x64", extract: "tar.gz",
|
||||
legacy: WINDOWS_LEGACY_FLUENT_MOSH_CLIENT,
|
||||
file: "mosh-client-win32-x64.tar.gz", localDir: "win32-x64", binary: "mosh-client.exe",
|
||||
},
|
||||
];
|
||||
|
||||
function applyReleaseAssetOverrides(asset, opts = {}) {
|
||||
if (asset.id !== WINDOWS_LEGACY_FLUENT_MOSH_CLIENT.id) return asset;
|
||||
return {
|
||||
...asset,
|
||||
url: opts.windowsLegacyUrl || asset.url,
|
||||
sha256: opts.windowsLegacySha256 || asset.sha256,
|
||||
};
|
||||
}
|
||||
|
||||
function selectReleaseAsset(target, sums, opts = {}) {
|
||||
const primary = { file: target.file, extract: target.extract, local: target.local, localDir: target.localDir };
|
||||
if (!target.legacy) return primary;
|
||||
if (target.preferLegacy && !opts.forceWindowsCygwin) {
|
||||
const legacy = applyReleaseAssetOverrides(target.legacy, opts);
|
||||
if (sums.get(target.legacy.file) === legacy.sha256) {
|
||||
return { file: target.legacy.file, local: target.legacy.local, sha256: legacy.sha256 };
|
||||
}
|
||||
return legacy;
|
||||
}
|
||||
// SHA256SUMS unavailable (allowUnverified mirror) — keep the primary
|
||||
// and let download / extraction errors surface naturally.
|
||||
if (sums.size === 0) return primary;
|
||||
if (sums.has(target.file)) return primary;
|
||||
if (sums.has(target.legacy.file)) {
|
||||
const expected = sums.get(target.legacy.file);
|
||||
const fallback = applyReleaseAssetOverrides(target.legacy, opts);
|
||||
if (fallback.id && fallback.sha256 && expected !== fallback.sha256) {
|
||||
return fallback;
|
||||
}
|
||||
return { file: target.legacy.file, local: target.legacy.local, sha256: expected };
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
function log(msg) { console.log(`[fetch-mosh-binaries] ${msg}`); }
|
||||
function warn(msg) { console.warn(`[fetch-mosh-binaries] WARN ${msg}`); }
|
||||
|
||||
@@ -215,10 +153,11 @@ function chmodExecutable(filePath) {
|
||||
}
|
||||
|
||||
function parseMoshBinRepository(env) {
|
||||
const githubOwner = (env.GITHUB_REPOSITORY || "").split("/")[0];
|
||||
// Canonical default binaricat/MoshCatty — never inherit fork owner from
|
||||
// GITHUB_REPOSITORY (same policy as resolve-mosh-bin-release).
|
||||
return {
|
||||
owner: env.MOSH_BIN_OWNER || githubOwner || "binaricat",
|
||||
repo: env.MOSH_BIN_REPO || "Netcatty-mosh-bin",
|
||||
owner: env.MOSH_BIN_OWNER || "binaricat",
|
||||
repo: env.MOSH_BIN_REPO || "MoshCatty",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,53 +191,46 @@ function assertExtractedTreeSafe(root) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertBundledTerminfo(extractDir, target) {
|
||||
const terminfoDir = path.join(extractDir, "terminfo");
|
||||
const terminfoEntry = [
|
||||
path.join(terminfoDir, "x", "xterm-256color"),
|
||||
path.join(terminfoDir, "78", "xterm-256color"),
|
||||
].find((entry) => fs.existsSync(entry));
|
||||
if (terminfoEntry && !fs.lstatSync(terminfoEntry).isFile()) {
|
||||
throw new Error(`${target.file} contained invalid terminfo for xterm-256color`);
|
||||
/** Keep only the pure MoshCatty client binary under extractDir. */
|
||||
function normalizeMoshCattyBundle(extractDir, target) {
|
||||
const wanted = target.binary;
|
||||
const candidates = [
|
||||
path.join(extractDir, wanted),
|
||||
path.join(extractDir, `mosh-client-${target.platform}-${target.arch}${wanted.endsWith(".exe") ? ".exe" : ""}`),
|
||||
path.join(extractDir, wanted.endsWith(".exe") ? "mosh-client.exe" : "mosh-client"),
|
||||
];
|
||||
let found = candidates.find((p) => fs.existsSync(p) && fs.lstatSync(p).isFile());
|
||||
if (!found) {
|
||||
// Search one level deep for a correctly named binary
|
||||
for (const name of fs.readdirSync(extractDir)) {
|
||||
const child = path.join(extractDir, name);
|
||||
if (!fs.statSync(child).isDirectory()) continue;
|
||||
const nested = path.join(child, wanted);
|
||||
if (fs.existsSync(nested) && fs.lstatSync(nested).isFile()) {
|
||||
found = nested;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!terminfoEntry) {
|
||||
warn(`${target.file} did not contain terminfo for xterm-256color; ${target.platform}-${target.arch} mosh packaging will fall back to host system terminfo (issue #890).`);
|
||||
if (!found) {
|
||||
throw new Error(`${target.file} did not contain ${wanted}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWindowsBundle(extractDir, target) {
|
||||
const genericExe = path.join(extractDir, "mosh-client.exe");
|
||||
const legacyExe = path.join(extractDir, `mosh-client-${target.platform}-${target.arch}.exe`);
|
||||
if (!fs.existsSync(genericExe) && fs.existsSync(legacyExe)) {
|
||||
fs.renameSync(legacyExe, genericExe);
|
||||
}
|
||||
if (!fs.existsSync(genericExe) || !fs.lstatSync(genericExe).isFile()) {
|
||||
throw new Error(`${target.file} did not contain mosh-client.exe`);
|
||||
}
|
||||
const dllDir = path.join(extractDir, `mosh-client-${target.platform}-${target.arch}-dlls`);
|
||||
if (!fs.existsSync(dllDir) || !fs.statSync(dllDir).isDirectory()) {
|
||||
throw new Error(`${target.file} did not contain ${path.basename(dllDir)}/`);
|
||||
}
|
||||
assertBundledTerminfo(extractDir, target);
|
||||
chmodExecutable(genericExe);
|
||||
}
|
||||
// Stage a clean tree with only the client binary (drop any accidental extras).
|
||||
const cleanDir = path.join(extractDir, ".moshcatty-clean");
|
||||
fs.mkdirSync(cleanDir, { recursive: true });
|
||||
const destBinary = path.join(cleanDir, wanted);
|
||||
fs.copyFileSync(found, destBinary);
|
||||
chmodExecutable(destBinary);
|
||||
|
||||
function normalizePosixBundle(extractDir, target) {
|
||||
const binary = path.join(extractDir, "mosh-client");
|
||||
const legacyBinary = path.join(extractDir, `mosh-client-${target.platform}-${target.arch}`);
|
||||
if (!fs.existsSync(binary) && fs.existsSync(legacyBinary)) {
|
||||
fs.renameSync(legacyBinary, binary);
|
||||
for (const name of fs.readdirSync(extractDir)) {
|
||||
if (name === ".moshcatty-clean") continue;
|
||||
fs.rmSync(path.join(extractDir, name), { recursive: true, force: true });
|
||||
}
|
||||
if (!fs.existsSync(binary) || !fs.lstatSync(binary).isFile()) {
|
||||
throw new Error(`${target.file} did not contain mosh-client`);
|
||||
for (const name of fs.readdirSync(cleanDir)) {
|
||||
fs.renameSync(path.join(cleanDir, name), path.join(extractDir, name));
|
||||
}
|
||||
assertBundledTerminfo(extractDir, target);
|
||||
chmodExecutable(binary);
|
||||
}
|
||||
|
||||
function normalizeBundle(extractDir, target) {
|
||||
if (target.platform === "win32") return normalizeWindowsBundle(extractDir, target);
|
||||
return normalizePosixBundle(extractDir, target);
|
||||
fs.rmSync(cleanDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function replaceDir(srcDir, destDir) {
|
||||
@@ -328,7 +260,7 @@ function unpackTarGz(buf, target, { resDir }) {
|
||||
stdio: "inherit",
|
||||
});
|
||||
assertExtractedTreeSafe(extractDir);
|
||||
normalizeBundle(extractDir, target);
|
||||
normalizeMoshCattyBundle(extractDir, target);
|
||||
replaceDir(extractDir, destDir);
|
||||
} finally {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
@@ -336,55 +268,30 @@ function unpackTarGz(buf, target, { resDir }) {
|
||||
return destDir;
|
||||
}
|
||||
|
||||
function writeFlatAsset(buf, target, asset, { resDir }) {
|
||||
const dest = path.join(resDir, asset.local);
|
||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mosh-flat-"));
|
||||
const tmpDest = path.join(tmpRoot, path.basename(dest));
|
||||
try {
|
||||
fs.writeFileSync(tmpDest, buf);
|
||||
if (target.platform !== "win32") fs.chmodSync(tmpDest, 0o755);
|
||||
replaceDir(tmpRoot, path.dirname(dest));
|
||||
} catch (err) {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
throw err;
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
async function fetchOne(target, sums, opts) {
|
||||
const { baseUrl, resDir, allowUnverified = false } = opts;
|
||||
const asset = selectReleaseAsset(target, sums, opts);
|
||||
if (asset.file !== target.file) {
|
||||
log(`using legacy asset ${asset.file} for ${target.platform}-${target.arch}`);
|
||||
}
|
||||
const url = asset.url || `${baseUrl}/${asset.file}`;
|
||||
const url = `${baseUrl}/${target.file}`;
|
||||
let buf;
|
||||
try {
|
||||
buf = await follow(url);
|
||||
} catch (err) {
|
||||
throw new Error(`download failed for ${asset.file}: ${err.message}`);
|
||||
throw new Error(`download failed for ${target.file}: ${err.message}`);
|
||||
}
|
||||
|
||||
const expected = asset.sha256 || sums.get(asset.file);
|
||||
const expected = sums.get(target.file);
|
||||
const actual = crypto.createHash("sha256").update(buf).digest("hex");
|
||||
if (expected && expected !== actual) {
|
||||
throw new Error(`SHA256 mismatch for ${asset.file}: expected ${expected}, got ${actual}`);
|
||||
throw new Error(`SHA256 mismatch for ${target.file}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
if (!expected) {
|
||||
if (!allowUnverified) {
|
||||
throw new Error(`no SHA256 entry for ${asset.file}`);
|
||||
throw new Error(`no SHA256 entry for ${target.file}`);
|
||||
}
|
||||
warn(`no SHA256 entry for ${asset.file} - accepting actual ${actual}`);
|
||||
warn(`no SHA256 entry for ${target.file} - accepting actual ${actual}`);
|
||||
}
|
||||
|
||||
if (asset.extract === "tar.gz") {
|
||||
const destDir = unpackTarGz(buf, target, { resDir });
|
||||
log(`unpacked ${asset.file} into ${path.relative(ROOT, destDir)}/ (sha256=${actual})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const dest = writeFlatAsset(buf, target, asset, { resDir });
|
||||
log(`wrote ${path.relative(ROOT, dest)} (${buf.length} bytes, sha256=${actual})`);
|
||||
const destDir = unpackTarGz(buf, target, { resDir });
|
||||
log(`unpacked ${target.file} into ${path.relative(ROOT, destDir)}/ (sha256=${actual})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -406,16 +313,18 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
||||
release = await resolveMoshBinRelease(env);
|
||||
}
|
||||
if (!release) {
|
||||
log("MOSH_BIN_RELEASE is unset - skipping. Set it (e.g. mosh-bin-1.4.0-1) to bundle mosh-client into the package.");
|
||||
log("MOSH_BIN_RELEASE is unset - skipping. Set it (e.g. moshcatty-0.1.2) to bundle mosh-client into the package.");
|
||||
return 0;
|
||||
}
|
||||
// Reject pre-0.1.2 pins (Linux GLIBC 2.34) even when MOSH_BIN_RELEASE is set
|
||||
// without going through --resolve-release.
|
||||
release = validateReleaseTag(release);
|
||||
|
||||
const { owner, repo } = parseMoshBinRepository(env);
|
||||
const baseUrl = env.MOSH_BIN_BASE_URL ||
|
||||
`https://github.com/${owner}/${repo}/releases/download/${encodeURIComponent(release)}`;
|
||||
const resDir = path.resolve(env.MOSH_BIN_RES_DIR || DEFAULT_RES_DIR);
|
||||
const allowUnverified = env.MOSH_BIN_ALLOW_UNVERIFIED === "true";
|
||||
const forceWindowsCygwin = env.MOSH_BIN_FORCE_WINDOWS_CYGWIN === "true";
|
||||
const platformFilter = hostTarget?.platform || platformArg;
|
||||
const archFilter = hostTarget?.arch || archArg;
|
||||
|
||||
@@ -427,14 +336,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
||||
if (platformFilter && target.platform !== platformFilter) continue;
|
||||
if (archFilter && target.arch !== archFilter) continue;
|
||||
total += 1;
|
||||
if (await fetchOne(target, sums, {
|
||||
baseUrl,
|
||||
resDir,
|
||||
allowUnverified,
|
||||
forceWindowsCygwin,
|
||||
windowsLegacyUrl: env.MOSH_BIN_WINDOWS_LEGACY_URL,
|
||||
windowsLegacySha256: env.MOSH_BIN_WINDOWS_LEGACY_SHA256,
|
||||
})) ok += 1;
|
||||
if (await fetchOne(target, sums, { baseUrl, resDir, allowUnverified })) ok += 1;
|
||||
}
|
||||
log(`done - ${ok}/${total} binaries written`);
|
||||
if (ok < total) throw new Error(`only wrote ${ok}/${total} requested binaries`);
|
||||
@@ -455,10 +357,9 @@ module.exports = {
|
||||
resolveHostTarget,
|
||||
resolveTarArchiveInvocation,
|
||||
parseSums,
|
||||
selectReleaseAsset,
|
||||
validateTarEntries,
|
||||
assertExtractedTreeSafe,
|
||||
normalizeMoshCattyBundle,
|
||||
unpackTarGz,
|
||||
writeFlatAsset,
|
||||
main,
|
||||
};
|
||||
@@ -15,7 +15,7 @@ const {
|
||||
replaceDir,
|
||||
resolveHostTarget,
|
||||
resolveTarArchiveInvocation,
|
||||
selectReleaseAsset,
|
||||
TARGETS,
|
||||
} = require("./fetch-mosh-binaries.cjs");
|
||||
|
||||
function makeTmp(t) {
|
||||
@@ -40,35 +40,48 @@ function makeTarGz(t, entries) {
|
||||
return fs.readFileSync(tarPath);
|
||||
}
|
||||
|
||||
test("fetch-mosh-binaries defaults to the dedicated mosh binary repository", () => {
|
||||
assert.deepEqual(parseMoshBinRepository({}), { owner: "binaricat", repo: "Netcatty-mosh-bin" });
|
||||
async function serveAssets(t, assets) {
|
||||
const server = http.createServer((req, res) => {
|
||||
const name = decodeURIComponent(req.url.split("/").pop());
|
||||
if (!Object.prototype.hasOwnProperty.call(assets, name)) {
|
||||
res.writeHead(404);
|
||||
res.end("missing");
|
||||
return;
|
||||
}
|
||||
res.writeHead(200);
|
||||
res.end(assets[name]);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
t.after(() => server.close());
|
||||
return `http://127.0.0.1:${server.address().port}`;
|
||||
}
|
||||
|
||||
test("fetch-mosh-binaries defaults to the MoshCatty binary repository", () => {
|
||||
assert.deepEqual(parseMoshBinRepository({}), { owner: "binaricat", repo: "MoshCatty" });
|
||||
// Fork CI must not inherit GITHUB_REPOSITORY owner for MoshCatty downloads.
|
||||
assert.deepEqual(parseMoshBinRepository({ GITHUB_REPOSITORY: "owner/project" }), {
|
||||
owner: "owner",
|
||||
repo: "Netcatty-mosh-bin",
|
||||
owner: "binaricat",
|
||||
repo: "MoshCatty",
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseMoshBinRepository({ GITHUB_REPOSITORY: "owner/project", MOSH_BIN_OWNER: "bin", MOSH_BIN_REPO: "binaries" }),
|
||||
{ owner: "bin", repo: "binaries" },
|
||||
parseMoshBinRepository({ MOSH_BIN_OWNER: "other", MOSH_BIN_REPO: "fork-mosh" }),
|
||||
{ owner: "other", repo: "fork-mosh" },
|
||||
);
|
||||
});
|
||||
|
||||
test("TARGETS are pure MoshCatty tarball assets only", () => {
|
||||
for (const t of TARGETS) {
|
||||
assert.match(t.file, /^mosh-client-.+\.tar\.gz$/);
|
||||
assert.ok(t.binary === "mosh-client" || t.binary === "mosh-client.exe");
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(t, "legacy"), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveHostTarget maps the local platform to the bundled target", () => {
|
||||
assert.deepEqual(resolveHostTarget({ platform: "darwin", arch: "arm64" }), {
|
||||
platform: "darwin",
|
||||
arch: "universal",
|
||||
});
|
||||
assert.deepEqual(resolveHostTarget({ platform: "darwin", arch: "x64" }), {
|
||||
platform: "darwin",
|
||||
arch: "universal",
|
||||
});
|
||||
assert.deepEqual(resolveHostTarget({ platform: "linux", arch: "x64" }), {
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
});
|
||||
assert.deepEqual(resolveHostTarget({ platform: "linux", arch: "arm64" }), {
|
||||
platform: "linux",
|
||||
arch: "arm64",
|
||||
});
|
||||
assert.deepEqual(resolveHostTarget({ platform: "win32", arch: "x64" }), {
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
@@ -117,9 +130,7 @@ test("replaceDir falls back to copy when rename crosses devices", (t) => {
|
||||
|
||||
test("fetch-mosh-binaries host mode skips unsupported local targets", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const baseUrl = await serveAssets(t, {
|
||||
SHA256SUMS: "",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, { SHA256SUMS: "" });
|
||||
|
||||
const { stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
@@ -127,7 +138,7 @@ test("fetch-mosh-binaries host mode skips unsupported local targets", async (t)
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
@@ -140,49 +151,39 @@ test("fetch-mosh-binaries host mode skips unsupported local targets", async (t)
|
||||
assert.equal(fs.existsSync(resDir), false);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries host mode skips unsupported targets before resolving release", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
[script, "--host", "--resolve-release", "--platform=win32", "--arch=arm64"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "",
|
||||
MOSH_BIN_RELEASES_JSON: "[]",
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(stderr, /No bundled mosh-client target for win32-arm64/);
|
||||
assert.doesNotMatch(stdout, /MOSH_BIN_RELEASE is unset/);
|
||||
assert.equal(fs.existsSync(resDir), false);
|
||||
});
|
||||
|
||||
async function serveAssets(t, assets) {
|
||||
const server = http.createServer((req, res) => {
|
||||
const name = decodeURIComponent(req.url.split("/").pop());
|
||||
if (!Object.prototype.hasOwnProperty.call(assets, name)) {
|
||||
res.writeHead(404);
|
||||
res.end("missing");
|
||||
return;
|
||||
}
|
||||
res.writeHead(200);
|
||||
res.end(assets[name]);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
t.after(() => server.close());
|
||||
return `http://127.0.0.1:${server.address().port}`;
|
||||
}
|
||||
|
||||
test("fetch-mosh-binaries normalizes the Windows tarball to mosh-client.exe", async (t) => {
|
||||
test("fetch-mosh-binaries unpacks pure Windows MoshCatty tarball", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client-win32-x64.exe": "exe",
|
||||
"mosh-client.exe": "pure-moshcatty-exe",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-win32-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(resDir, "win32-x64", "mosh-client.exe"), "utf8"),
|
||||
"pure-moshcatty-exe",
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls")), false);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "terminfo")), false);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries strips accidental dll/terminfo from Windows tarball", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client.exe": "exe",
|
||||
"mosh-client-win32-x64-dlls/cygwin1.dll": "dll",
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
});
|
||||
@@ -194,216 +195,7 @@ test("fetch-mosh-binaries normalizes the Windows tarball to mosh-client.exe", as
|
||||
await execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
MOSH_BIN_FORCE_WINDOWS_CYGWIN: "true",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "mosh-client.exe")), true);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls", "cygwin1.dll")), true);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "terminfo", "x", "xterm-256color")), true);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries accepts legacy Windows bundles without terminfo", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client.exe": "exe",
|
||||
"mosh-client-win32-x64-dlls/cygwin1.dll": "dll",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-win32-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
const { stderr } = await execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
MOSH_BIN_FORCE_WINDOWS_CYGWIN: "true",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.match(stderr, /did not contain terminfo for xterm-256color/);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "mosh-client.exe")), true);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries rejects invalid Windows terminfo entries", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const srcDir = makeTmp(t);
|
||||
fs.writeFileSync(path.join(srcDir, "mosh-client.exe"), "exe");
|
||||
fs.mkdirSync(path.join(srcDir, "mosh-client-win32-x64-dlls"), { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "mosh-client-win32-x64-dlls", "cygwin1.dll"), "dll");
|
||||
fs.mkdirSync(path.join(srcDir, "terminfo", "x", "xterm-256color"), { recursive: true });
|
||||
const tarPath = path.join(makeTmp(t), "invalid-terminfo.tar.gz");
|
||||
execFileSync("tar", ["-czf", tarPath, "-C", srcDir, "mosh-client.exe", "mosh-client-win32-x64-dlls", "terminfo"], { stdio: "pipe" });
|
||||
const tar = fs.readFileSync(tarPath);
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-win32-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
MOSH_BIN_FORCE_WINDOWS_CYGWIN: "true",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
}),
|
||||
/invalid terminfo for xterm-256color/,
|
||||
);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries fails when SHA256SUMS lacks the requested asset", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client.exe": "exe",
|
||||
"mosh-client-win32-x64-dlls/cygwin1.dll": "dll",
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(Buffer.from("other"))} other-file\n`,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
MOSH_BIN_FORCE_WINDOWS_CYGWIN: "true",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("selectReleaseAsset prefers the bundled tarball when listed in SHA256SUMS", () => {
|
||||
const target = {
|
||||
platform: "linux", arch: "x64",
|
||||
file: "mosh-client-linux-x64.tar.gz", localDir: "linux-x64", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-linux-x64", local: "linux-x64/mosh-client" },
|
||||
};
|
||||
const sums = new Map([
|
||||
["mosh-client-linux-x64.tar.gz", "abc"],
|
||||
["mosh-client-linux-x64", "def"],
|
||||
]);
|
||||
assert.equal(selectReleaseAsset(target, sums).file, "mosh-client-linux-x64.tar.gz");
|
||||
});
|
||||
|
||||
test("selectReleaseAsset falls back to the legacy flat asset when only it is published", () => {
|
||||
const target = {
|
||||
platform: "linux", arch: "x64",
|
||||
file: "mosh-client-linux-x64.tar.gz", localDir: "linux-x64", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-linux-x64", local: "linux-x64/mosh-client" },
|
||||
};
|
||||
const sums = new Map([["mosh-client-linux-x64", "def"]]);
|
||||
const asset = selectReleaseAsset(target, sums);
|
||||
assert.equal(asset.file, "mosh-client-linux-x64");
|
||||
assert.equal(asset.local, "linux-x64/mosh-client");
|
||||
assert.equal(asset.extract, undefined);
|
||||
});
|
||||
|
||||
test("selectReleaseAsset stays on the primary when SHA256SUMS is empty (unverified mirror)", () => {
|
||||
const target = {
|
||||
platform: "linux", arch: "x64",
|
||||
file: "mosh-client-linux-x64.tar.gz", localDir: "linux-x64", extract: "tar.gz",
|
||||
legacy: { file: "mosh-client-linux-x64", local: "linux-x64/mosh-client" },
|
||||
};
|
||||
assert.equal(selectReleaseAsset(target, new Map()).file, "mosh-client-linux-x64.tar.gz");
|
||||
});
|
||||
|
||||
test("selectReleaseAsset prefers the released Windows bundle by default", () => {
|
||||
const target = {
|
||||
platform: "win32", arch: "x64",
|
||||
file: "mosh-client-win32-x64.tar.gz", localDir: "win32-x64", extract: "tar.gz",
|
||||
legacy: {
|
||||
id: "windows-fluentterminal-standalone",
|
||||
file: "mosh-client-win32-x64.exe",
|
||||
local: "win32-x64/mosh-client.exe",
|
||||
url: "https://example.test/mosh-client.exe",
|
||||
sha256: "abc",
|
||||
},
|
||||
};
|
||||
const sums = new Map([["mosh-client-win32-x64.tar.gz", "def"]]);
|
||||
|
||||
assert.equal(selectReleaseAsset(target, sums).file, "mosh-client-win32-x64.tar.gz");
|
||||
assert.equal(selectReleaseAsset(target, sums, { forceWindowsCygwin: true }).file, "mosh-client-win32-x64.tar.gz");
|
||||
});
|
||||
|
||||
test("selectReleaseAsset falls back to the Windows standalone asset when the bundle is absent", () => {
|
||||
const target = {
|
||||
platform: "win32", arch: "x64",
|
||||
file: "mosh-client-win32-x64.tar.gz", localDir: "win32-x64", extract: "tar.gz",
|
||||
legacy: {
|
||||
id: "windows-fluentterminal-standalone",
|
||||
file: "mosh-client-win32-x64.exe",
|
||||
local: "win32-x64/mosh-client.exe",
|
||||
url: "https://example.test/mosh-client.exe",
|
||||
sha256: "abc",
|
||||
},
|
||||
};
|
||||
const asset = selectReleaseAsset(target, new Map([["mosh-client-win32-x64.exe", "abc"]]));
|
||||
|
||||
assert.equal(asset.file, "mosh-client-win32-x64.exe");
|
||||
assert.equal(asset.local, "win32-x64/mosh-client.exe");
|
||||
assert.equal(asset.url, undefined);
|
||||
assert.equal(asset.sha256, "abc");
|
||||
});
|
||||
|
||||
test("selectReleaseAsset ignores a released Windows asset when its checksum is not the pinned standalone", () => {
|
||||
const target = {
|
||||
platform: "win32", arch: "x64",
|
||||
file: "mosh-client-win32-x64.tar.gz", localDir: "win32-x64", extract: "tar.gz",
|
||||
legacy: {
|
||||
id: "windows-fluentterminal-standalone",
|
||||
file: "mosh-client-win32-x64.exe",
|
||||
local: "win32-x64/mosh-client.exe",
|
||||
url: "https://example.test/mosh-client.exe",
|
||||
sha256: "abc",
|
||||
},
|
||||
};
|
||||
const asset = selectReleaseAsset(target, new Map([["mosh-client-win32-x64.exe", "def"]]));
|
||||
|
||||
assert.equal(asset.file, "mosh-client-win32-x64.exe");
|
||||
assert.equal(asset.local, "win32-x64/mosh-client.exe");
|
||||
assert.equal(asset.url, "https://example.test/mosh-client.exe");
|
||||
assert.equal(asset.sha256, "abc");
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries downloads the released Windows bundle by default", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client.exe": "exe",
|
||||
"mosh-client-win32-x64-dlls/cygwin1.dll": "dll",
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
});
|
||||
fs.mkdirSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls"), { recursive: true });
|
||||
fs.writeFileSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls", "cygwin1.dll"), "stale");
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-win32-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
@@ -412,24 +204,24 @@ test("fetch-mosh-binaries downloads the released Windows bundle by default", asy
|
||||
});
|
||||
|
||||
assert.equal(fs.readFileSync(path.join(resDir, "win32-x64", "mosh-client.exe"), "utf8"), "exe");
|
||||
assert.equal(fs.readFileSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls", "cygwin1.dll"), "utf8"), "dll");
|
||||
assert.equal(fs.readFileSync(path.join(resDir, "win32-x64", "terminfo", "x", "xterm-256color"), "utf8"), "terminfo");
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "mosh-client-win32-x64-dlls")), false);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "win32-x64", "terminfo")), false);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries falls back to the legacy flat asset for older releases", async (t) => {
|
||||
test("fetch-mosh-binaries unpacks pure Linux MoshCatty tarball", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const flat = Buffer.from("legacy-binary");
|
||||
fs.mkdirSync(path.join(resDir, "linux-x64", "terminfo", "78"), { recursive: true });
|
||||
fs.writeFileSync(path.join(resDir, "linux-x64", "terminfo", "78", "xterm-256color"), "stale");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client": "linux-client",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-linux-x64": flat,
|
||||
SHA256SUMS: `${sha256(flat)} mosh-client-linux-x64\n`,
|
||||
"mosh-client-linux-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-linux-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [script, "--platform=linux", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
@@ -437,92 +229,14 @@ test("fetch-mosh-binaries falls back to the legacy flat asset for older releases
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(resDir, "linux-x64", "mosh-client")), true);
|
||||
assert.equal(fs.readFileSync(path.join(resDir, "linux-x64", "mosh-client"), "utf8"), "legacy-binary");
|
||||
assert.equal(fs.readFileSync(path.join(resDir, "linux-x64", "mosh-client"), "utf8"), "linux-client");
|
||||
assert.equal(fs.existsSync(path.join(resDir, "linux-x64", "terminfo")), false);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries unpacks the Linux tarball with bundled terminfo", async (t) => {
|
||||
test("fetch-mosh-binaries rejects tarball without mosh-client", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client": "binary",
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-linux-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-linux-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [script, "--platform=linux", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(resDir, "linux-x64", "mosh-client")), true);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "linux-x64", "terminfo", "x", "xterm-256color")), true);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries warns when the Linux tarball lacks terminfo", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client": "binary",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-linux-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-linux-x64.tar.gz\n`,
|
||||
});
|
||||
|
||||
const { stderr } = await execFileAsync(process.execPath, [script, "--platform=linux", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.match(stderr, /did not contain terminfo for xterm-256color/);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "linux-x64", "mosh-client")), true);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries unpacks the Darwin tarball with bundled terminfo", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"mosh-client": "binary",
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-darwin-universal.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(tar)} mosh-client-darwin-universal.tar.gz\n`,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [script, "--platform=darwin", "--arch=universal"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
assert.equal(fs.existsSync(path.join(resDir, "darwin-universal", "mosh-client")), true);
|
||||
assert.equal(fs.existsSync(path.join(resDir, "darwin-universal", "terminfo", "x", "xterm-256color")), true);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries rejects a Linux tarball without mosh-client", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, {
|
||||
"terminfo/x/xterm-256color": "terminfo",
|
||||
"README.txt": "no binary here",
|
||||
});
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-linux-x64.tar.gz": tar,
|
||||
@@ -533,7 +247,7 @@ test("fetch-mosh-binaries rejects a Linux tarball without mosh-client", async (t
|
||||
execFileAsync(process.execPath, [script, "--platform=linux", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
@@ -544,14 +258,36 @@ test("fetch-mosh-binaries rejects a Linux tarball without mosh-client", async (t
|
||||
);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries rejects symlinks inside Windows tarballs", { skip: process.platform === "win32" }, async (t) => {
|
||||
test("fetch-mosh-binaries fails when SHA256SUMS lacks the asset", async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const tar = makeTarGz(t, { "mosh-client.exe": "exe" });
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
SHA256SUMS: `${sha256(Buffer.from("other"))} other-file\n`,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
}),
|
||||
/no SHA256 entry/,
|
||||
);
|
||||
});
|
||||
|
||||
test("fetch-mosh-binaries rejects symlinks inside tarballs", { skip: process.platform === "win32" }, async (t) => {
|
||||
const resDir = path.join(makeTmp(t), "resources", "mosh");
|
||||
const srcDir = makeTmp(t);
|
||||
fs.writeFileSync(path.join(srcDir, "outside.exe"), "outside");
|
||||
fs.symlinkSync(path.join(srcDir, "outside.exe"), path.join(srcDir, "mosh-client.exe"));
|
||||
fs.mkdirSync(path.join(srcDir, "mosh-client-win32-x64-dlls"));
|
||||
fs.writeFileSync(path.join(srcDir, "mosh-client-win32-x64-dlls", "cygwin1.dll"), "dll");
|
||||
fs.writeFileSync(path.join(srcDir, "mosh-client.exe"), "exe");
|
||||
fs.symlinkSync("mosh-client.exe", path.join(srcDir, "link.exe"));
|
||||
const tarPath = path.join(makeTmp(t), "symlink.tar.gz");
|
||||
execFileSync("tar", ["-czf", tarPath, "-C", srcDir, "mosh-client.exe", "mosh-client-win32-x64-dlls"], { stdio: "pipe" });
|
||||
execFileSync("tar", ["-czf", tarPath, "-C", srcDir, "mosh-client.exe", "link.exe"], { stdio: "pipe" });
|
||||
const tar = fs.readFileSync(tarPath);
|
||||
const baseUrl = await serveAssets(t, {
|
||||
"mosh-client-win32-x64.tar.gz": tar,
|
||||
@@ -562,14 +298,13 @@ test("fetch-mosh-binaries rejects symlinks inside Windows tarballs", { skip: pro
|
||||
execFileAsync(process.execPath, [script, "--platform=win32", "--arch=x64"], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSH_BIN_RELEASE: "test",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
MOSH_BIN_BASE_URL: baseUrl,
|
||||
MOSH_BIN_RES_DIR: path.join(makeTmp(t), "resources", "mosh"),
|
||||
MOSH_BIN_FORCE_WINDOWS_CYGWIN: "true",
|
||||
MOSH_BIN_RES_DIR: resDir,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "pipe",
|
||||
}),
|
||||
/symbolic link|did not contain mosh-client\.exe/,
|
||||
/symbolic link/,
|
||||
);
|
||||
});
|
||||
@@ -7,8 +7,6 @@ const buildWorkflow = fs.readFileSync(
|
||||
path.join(__dirname, "..", ".github", "workflows", "build.yml"),
|
||||
"utf8",
|
||||
);
|
||||
const moshLinuxScript = fs.readFileSync(path.join(__dirname, "build-mosh", "build-linux.sh"), "utf8");
|
||||
const moshMacScript = fs.readFileSync(path.join(__dirname, "build-mosh", "build-macos.sh"), "utf8");
|
||||
const etLinuxScript = fs.readFileSync(path.join(__dirname, "build-et", "build-linux.sh"), "utf8");
|
||||
const etMacScript = fs.readFileSync(path.join(__dirname, "build-et", "build-macos.sh"), "utf8");
|
||||
|
||||
@@ -134,17 +132,6 @@ test("build workflow builds Linux x64 native modules in a glibc 2.28 container",
|
||||
);
|
||||
});
|
||||
|
||||
test("mosh binary build scripts retry source downloads before failing CI", () => {
|
||||
for (const [name, script] of [
|
||||
["linux", moshLinuxScript],
|
||||
["macos", moshMacScript],
|
||||
]) {
|
||||
assert.match(script, /curl_retry\(\)/, `${name} mosh build must use retrying downloads`);
|
||||
assert.match(script, /--retry 8/, `${name} mosh build must retry transient HTTP failures`);
|
||||
assert.match(script, /--retry-max-time 600/, `${name} mosh build must bound retry time`);
|
||||
}
|
||||
});
|
||||
|
||||
test("et binary build scripts retry dependency configure and pin ninja", () => {
|
||||
for (const [name, script] of [
|
||||
["linux", etLinuxScript],
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
// Compute the platform-specific `extraResources` entry for bundling
|
||||
// mosh-client. Lives under scripts/ (eslint-ignored) so it can use
|
||||
// Node CommonJS globals freely; consumed from electron-builder.config.cjs.
|
||||
//
|
||||
// Binaries are produced by .github/workflows/build-mosh-binaries.yml and
|
||||
// downloaded into resources/mosh/<platform-arch>/ by
|
||||
// scripts/fetch-mosh-binaries.cjs (gated on MOSH_BIN_RELEASE).
|
||||
//
|
||||
// We only emit the directive when the binary is actually on disk so that
|
||||
// `npm run pack` keeps working without bundled mosh — for example, when
|
||||
// the developer skipped the fetch step or the relevant arch hasn't been
|
||||
// built yet.
|
||||
// Platform-specific electron-builder extraResources for the MoshCatty client.
|
||||
// Binaries are downloaded from binaricat/MoshCatty into resources/mosh/ by
|
||||
// scripts/fetch-mosh-binaries.cjs. Pure single-binary layout only.
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -21,10 +13,6 @@ function hasFile(file) {
|
||||
return fs.existsSync(file) && fs.statSync(file).isFile();
|
||||
}
|
||||
|
||||
function hasDir(dir) {
|
||||
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
||||
}
|
||||
|
||||
function moshExtraResources(platform) {
|
||||
const moshRoot = path.resolve(process.cwd(), "resources", "mosh");
|
||||
if (!fs.existsSync(moshRoot)) return [];
|
||||
@@ -32,52 +20,27 @@ function moshExtraResources(platform) {
|
||||
if (platform === "darwin") {
|
||||
const file = path.join(moshRoot, "darwin-universal", "mosh-client");
|
||||
if (!hasFile(file)) return [];
|
||||
const resources = [
|
||||
return [
|
||||
{ from: "resources/mosh/darwin-universal/", to: "mosh/", filter: ["mosh-client"] },
|
||||
];
|
||||
const terminfoDir = path.join(moshRoot, "darwin-universal", "terminfo");
|
||||
if (hasDir(terminfoDir)) {
|
||||
resources.push({ from: "resources/mosh/darwin-universal/terminfo/", to: "mosh/terminfo/", filter: ["**/*"] });
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
if (platform === "linux") {
|
||||
const arch = requestedArch();
|
||||
const file = path.join(moshRoot, `linux-${arch}`, "mosh-client");
|
||||
if (!hasFile(file)) return [];
|
||||
const resources = [
|
||||
return [
|
||||
{ from: `resources/mosh/linux-${arch}/`, to: "mosh/", filter: ["mosh-client"] },
|
||||
];
|
||||
const terminfoDir = path.join(moshRoot, `linux-${arch}`, "terminfo");
|
||||
if (hasDir(terminfoDir)) {
|
||||
resources.push({ from: `resources/mosh/linux-${arch}/terminfo/`, to: "mosh/terminfo/", filter: ["**/*"] });
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
if (platform === "win32") {
|
||||
// Windows normally ships the pinned runtime bundle. Keep DLL/terminfo
|
||||
// packaging optional so the standalone fallback remains packageable.
|
||||
const arch = requestedArch();
|
||||
const exe = path.join(moshRoot, `win32-${arch}`, "mosh-client.exe");
|
||||
const dllDir = path.join(moshRoot, `win32-${arch}`, `mosh-client-win32-${arch}-dlls`);
|
||||
if (!hasFile(exe)) return [];
|
||||
const resources = [
|
||||
return [
|
||||
{ from: `resources/mosh/win32-${arch}/`, to: "mosh/", filter: ["mosh-client.exe"] },
|
||||
];
|
||||
if (hasDir(dllDir)) {
|
||||
resources.push({
|
||||
from: `resources/mosh/win32-${arch}/mosh-client-win32-${arch}-dlls/`,
|
||||
to: `mosh/mosh-client-win32-${arch}-dlls/`,
|
||||
filter: ["**/*"],
|
||||
});
|
||||
}
|
||||
const terminfoDir = path.join(moshRoot, `win32-${arch}`, "terminfo");
|
||||
if (hasDir(terminfoDir)) {
|
||||
resources.push({ from: `resources/mosh/win32-${arch}/terminfo/`, to: "mosh/terminfo/", filter: ["**/*"] });
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -32,10 +32,11 @@ function writeFile(filePath) {
|
||||
fs.writeFileSync(filePath, "x");
|
||||
}
|
||||
|
||||
test("moshExtraResources returns concrete Linux arch paths (legacy bundle without terminfo)", (t) => {
|
||||
test("moshExtraResources packages pure Linux client only", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "x64");
|
||||
writeFile(path.join(root, "resources", "mosh", "linux-x64", "mosh-client"));
|
||||
writeFile(path.join(root, "resources", "mosh", "linux-x64", "terminfo", "x", "xterm-256color"));
|
||||
|
||||
const got = moshExtraResources("linux");
|
||||
assert.deepEqual(got, [
|
||||
@@ -43,20 +44,7 @@ test("moshExtraResources returns concrete Linux arch paths (legacy bundle withou
|
||||
]);
|
||||
});
|
||||
|
||||
test("moshExtraResources packages bundled terminfo on Linux when present", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "arm64");
|
||||
writeFile(path.join(root, "resources", "mosh", "linux-arm64", "mosh-client"));
|
||||
writeFile(path.join(root, "resources", "mosh", "linux-arm64", "terminfo", "x", "xterm-256color"));
|
||||
|
||||
const got = moshExtraResources("linux");
|
||||
assert.deepEqual(got, [
|
||||
{ from: "resources/mosh/linux-arm64/", to: "mosh/", filter: ["mosh-client"] },
|
||||
{ from: "resources/mosh/linux-arm64/terminfo/", to: "mosh/terminfo/", filter: ["**/*"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("moshExtraResources packages bundled terminfo on Darwin when present", (t) => {
|
||||
test("moshExtraResources packages pure Darwin client only", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "x64");
|
||||
writeFile(path.join(root, "resources", "mosh", "darwin-universal", "mosh-client"));
|
||||
@@ -65,11 +53,10 @@ test("moshExtraResources packages bundled terminfo on Darwin when present", (t)
|
||||
const got = moshExtraResources("darwin");
|
||||
assert.deepEqual(got, [
|
||||
{ from: "resources/mosh/darwin-universal/", to: "mosh/", filter: ["mosh-client"] },
|
||||
{ from: "resources/mosh/darwin-universal/terminfo/", to: "mosh/terminfo/", filter: ["**/*"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("moshExtraResources returns concrete Windows arch paths only when that arch exists", (t) => {
|
||||
test("moshExtraResources packages pure Windows client only (ignores dlls/terminfo)", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "x64");
|
||||
writeFile(path.join(root, "resources", "mosh", "win32-x64", "mosh-client.exe"));
|
||||
@@ -79,42 +66,8 @@ test("moshExtraResources returns concrete Windows arch paths only when that arch
|
||||
const got = moshExtraResources("win32");
|
||||
assert.deepEqual(got, [
|
||||
{ from: "resources/mosh/win32-x64/", to: "mosh/", filter: ["mosh-client.exe"] },
|
||||
{
|
||||
from: "resources/mosh/win32-x64/mosh-client-win32-x64-dlls/",
|
||||
to: "mosh/mosh-client-win32-x64-dlls/",
|
||||
filter: ["**/*"],
|
||||
},
|
||||
{ from: "resources/mosh/win32-x64/terminfo/", to: "mosh/terminfo/", filter: ["**/*"] },
|
||||
]);
|
||||
|
||||
process.env.npm_config_arch = "arm64";
|
||||
assert.deepEqual(moshExtraResources("win32"), []);
|
||||
});
|
||||
|
||||
test("moshExtraResources keeps legacy Windows bundles packageable", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "x64");
|
||||
writeFile(path.join(root, "resources", "mosh", "win32-x64", "mosh-client.exe"));
|
||||
writeFile(path.join(root, "resources", "mosh", "win32-x64", "mosh-client-win32-x64-dlls", "cygwin1.dll"));
|
||||
|
||||
const got = moshExtraResources("win32");
|
||||
assert.deepEqual(got, [
|
||||
{ from: "resources/mosh/win32-x64/", to: "mosh/", filter: ["mosh-client.exe"] },
|
||||
{
|
||||
from: "resources/mosh/win32-x64/mosh-client-win32-x64-dlls/",
|
||||
to: "mosh/mosh-client-win32-x64-dlls/",
|
||||
filter: ["**/*"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("moshExtraResources packages standalone Windows mosh-client.exe", (t) => {
|
||||
const root = makeTmp(t);
|
||||
withCwdAndArch(t, root, "x64");
|
||||
writeFile(path.join(root, "resources", "mosh", "win32-x64", "mosh-client.exe"));
|
||||
|
||||
const got = moshExtraResources("win32");
|
||||
assert.deepEqual(got, [
|
||||
{ from: "resources/mosh/win32-x64/", to: "mosh/", filter: ["mosh-client.exe"] },
|
||||
]);
|
||||
});
|
||||
@@ -1,38 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
//
|
||||
// Resolve the mosh-client binary release used by build-packages.
|
||||
// Resolve the MoshCatty mosh-client binary release used by packaging / dev.
|
||||
//
|
||||
// Priority:
|
||||
// 1. MOSH_BIN_RELEASE from workflow input / repository variable.
|
||||
// 2. Latest non-draft, non-prerelease GitHub Release whose tag is
|
||||
// mosh-bin-* in MOSH_BIN_OWNER/MOSH_BIN_REPO. By default this is a
|
||||
// dedicated sibling binary repository named Netcatty-mosh-bin.
|
||||
// moshcatty-* in MOSH_BIN_OWNER/MOSH_BIN_REPO (default binaricat/MoshCatty).
|
||||
//
|
||||
// In GitHub Actions, the resolved tag is written back to $GITHUB_ENV so
|
||||
// later steps can run scripts/fetch-mosh-binaries.cjs without duplicating
|
||||
// release discovery logic.
|
||||
// In GitHub Actions, the resolved tag is written to $GITHUB_ENV.
|
||||
|
||||
const fs = require("node:fs");
|
||||
const https = require("node:https");
|
||||
|
||||
const TAG_RE = /^mosh-bin-[A-Za-z0-9._-]+$/;
|
||||
// MoshCatty pure-Rust releases only.
|
||||
// Minimum 0.1.2: earlier Linux builds linked GLIBC 2.34 (above Netcatty floors).
|
||||
// Allow semver prerelease (-rc1) and build metadata (+meta); no path separators.
|
||||
const TAG_RE = /^moshcatty-[A-Za-z0-9._+-]+$/;
|
||||
const MIN_VERSION = { major: 0, minor: 1, patch: 2 };
|
||||
const MIN_TAG = `moshcatty-${MIN_VERSION.major}.${MIN_VERSION.minor}.${MIN_VERSION.patch}`;
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[resolve-mosh-bin-release] ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse moshcatty-X.Y.Z with optional prerelease (-rc1) and build (+meta).
|
||||
* Returns null if not semver-ish.
|
||||
*/
|
||||
function parseMoshCattyVersion(tag) {
|
||||
const match = String(tag || "").trim().match(
|
||||
/^moshcatty-(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
|
||||
);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
// Present when tag is e.g. moshcatty-0.1.2-rc1 (semver: prerelease < final).
|
||||
prerelease: match[4] || null,
|
||||
};
|
||||
}
|
||||
|
||||
function compareCoreVersion(a, b) {
|
||||
if (a.major !== b.major) return a.major - b.major;
|
||||
if (a.minor !== b.minor) return a.minor - b.minor;
|
||||
return a.patch - b.patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when tag is usable for packaging (core version ≥ min, with semver
|
||||
* prerelease rules: X.Y.Z-rcN is below final X.Y.Z).
|
||||
*/
|
||||
function isAtLeastMinRelease(tag) {
|
||||
const version = parseMoshCattyVersion(tag);
|
||||
if (!version) return false;
|
||||
const core = compareCoreVersion(version, MIN_VERSION);
|
||||
if (core > 0) return true;
|
||||
if (core < 0) return false;
|
||||
// Equal to the floor: final only (no prerelease suffix).
|
||||
return !version.prerelease;
|
||||
}
|
||||
|
||||
function validateReleaseTag(tag) {
|
||||
const value = String(tag || "").trim();
|
||||
if (!TAG_RE.test(value)) {
|
||||
throw new Error(`invalid mosh binary release tag: ${tag}`);
|
||||
if (!TAG_RE.test(value) || !parseMoshCattyVersion(value)) {
|
||||
throw new Error(`invalid mosh binary release tag: ${tag} (expected moshcatty-X.Y.Z[(-pre)|(+build)])`);
|
||||
}
|
||||
if (!isAtLeastMinRelease(value)) {
|
||||
throw new Error(
|
||||
`mosh binary release ${value} is below minimum ${MIN_TAG} `
|
||||
+ "(Linux glibc floors: x64 ≤ 2.28, arm64 ≤ 2.31; 0.1.0/0.1.1 require GLIBC 2.34; "
|
||||
+ "prereleases of the floor e.g. 0.1.2-rc1 are not accepted)",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseRepository(env) {
|
||||
const owner = env.MOSH_BIN_OWNER || (env.GITHUB_REPOSITORY || "").split("/")[0] || "binaricat";
|
||||
const repo = env.MOSH_BIN_REPO || "Netcatty-mosh-bin";
|
||||
// Canonical default is always binaricat/MoshCatty. Do not derive owner from
|
||||
// GITHUB_REPOSITORY — fork packaging would otherwise look for
|
||||
// <fork-owner>/MoshCatty and fail. Override only via MOSH_BIN_OWNER/REPO.
|
||||
const owner = env.MOSH_BIN_OWNER || "binaricat";
|
||||
const repo = env.MOSH_BIN_REPO || "MoshCatty";
|
||||
return { owner, repo };
|
||||
}
|
||||
|
||||
@@ -46,8 +96,10 @@ function pickLatestMoshBinRelease(releases) {
|
||||
return releases
|
||||
.map((release, index) => ({ release, index }))
|
||||
.filter(({ release }) => {
|
||||
const tag = String(release?.tag_name || "");
|
||||
return release
|
||||
&& TAG_RE.test(String(release.tag_name || ""))
|
||||
&& TAG_RE.test(tag)
|
||||
&& isAtLeastMinRelease(tag)
|
||||
&& release.draft !== true
|
||||
&& release.prerelease !== true;
|
||||
})
|
||||
@@ -122,7 +174,7 @@ async function loadReleases(env, request = requestJsonWithHeaders) {
|
||||
const { owner, repo } = parseRepository(env);
|
||||
const apiBase = (env.GITHUB_API_URL || "https://api.github.com").replace(/\/+$/, "");
|
||||
let url = `${apiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases?per_page=100`;
|
||||
log(`looking up latest mosh-bin-* release in ${owner}/${repo}`);
|
||||
log(`looking up latest moshcatty-* release in ${owner}/${repo}`);
|
||||
const releases = [];
|
||||
const seen = new Set();
|
||||
while (url) {
|
||||
@@ -159,7 +211,8 @@ async function main(env = process.env) {
|
||||
const release = pickLatestMoshBinRelease(releases);
|
||||
if (!release) {
|
||||
throw new Error(
|
||||
"could not find a non-draft mosh-bin-* release in the mosh binary repository. Publish build-mosh-binaries artifacts with release_tag (for example mosh-bin-1.4.0-1) before packaging.",
|
||||
`could not find a non-draft ${MIN_TAG}+ release in binaricat/MoshCatty. `
|
||||
+ `Publish a MoshCatty GitHub Release (e.g. ${MIN_TAG}) before packaging.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +233,10 @@ module.exports = {
|
||||
loadReleases,
|
||||
parseNextLink,
|
||||
validateReleaseTag,
|
||||
parseMoshCattyVersion,
|
||||
isAtLeastMinRelease,
|
||||
parseRepository,
|
||||
pickLatestMoshBinRelease,
|
||||
MIN_TAG,
|
||||
main,
|
||||
};
|
||||
@@ -11,6 +11,8 @@ const {
|
||||
parseNextLink,
|
||||
pickLatestMoshBinRelease,
|
||||
validateReleaseTag,
|
||||
isAtLeastMinRelease,
|
||||
MIN_TAG,
|
||||
} = require("./resolve-mosh-bin-release.cjs");
|
||||
|
||||
function makeTmp(t) {
|
||||
@@ -19,17 +21,37 @@ function makeTmp(t) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
test("validateReleaseTag accepts only mosh binary release tags", () => {
|
||||
assert.equal(validateReleaseTag("mosh-bin-1.4.0-1"), "mosh-bin-1.4.0-1");
|
||||
test("validateReleaseTag accepts only moshcatty-* tags at min version", () => {
|
||||
assert.equal(validateReleaseTag("moshcatty-0.1.2"), "moshcatty-0.1.2");
|
||||
assert.equal(validateReleaseTag("moshcatty-0.2.0"), "moshcatty-0.2.0");
|
||||
assert.equal(validateReleaseTag("moshcatty-0.1.3-rc1"), "moshcatty-0.1.3-rc1");
|
||||
assert.equal(validateReleaseTag("moshcatty-0.1.2+build.1"), "moshcatty-0.1.2+build.1");
|
||||
assert.throws(() => validateReleaseTag("mosh-bin-1.4.0-1"), /invalid mosh binary release tag/);
|
||||
assert.throws(() => validateReleaseTag("v1.2.3"), /invalid mosh binary release tag/);
|
||||
assert.throws(() => validateReleaseTag("mosh-bin-../bad"), /invalid mosh binary release tag/);
|
||||
assert.throws(() => validateReleaseTag("moshcatty-../bad"), /invalid mosh binary release tag/);
|
||||
assert.throws(() => validateReleaseTag("moshcatty-not-a-version"), /invalid mosh binary release tag/);
|
||||
assert.throws(() => validateReleaseTag("moshcatty-0.1.0"), /below minimum/);
|
||||
assert.throws(() => validateReleaseTag("moshcatty-0.1.1"), /below minimum/);
|
||||
assert.throws(() => validateReleaseTag("moshcatty-0.1.2-rc1"), /below minimum/);
|
||||
});
|
||||
|
||||
test("parseRepository falls back to the dedicated mosh binary repository", () => {
|
||||
assert.deepEqual(parseRepository({}), { owner: "binaricat", repo: "Netcatty-mosh-bin" });
|
||||
test("isAtLeastMinRelease enforces moshcatty-0.1.2 floor with semver prerelease rules", () => {
|
||||
assert.equal(MIN_TAG, "moshcatty-0.1.2");
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-0.1.1"), false);
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-0.1.2"), true);
|
||||
// Prerelease of the floor sorts below the final floor release.
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-0.1.2-rc1"), false);
|
||||
// Above the floor, prereleases are fine.
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-0.1.3-rc1"), true);
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-0.1.2+build.1"), true);
|
||||
assert.equal(isAtLeastMinRelease("moshcatty-not-a-version"), false);
|
||||
});
|
||||
|
||||
test("parseRepository defaults to binaricat/MoshCatty (ignores GITHUB_REPOSITORY fork owner)", () => {
|
||||
assert.deepEqual(parseRepository({}), { owner: "binaricat", repo: "MoshCatty" });
|
||||
assert.deepEqual(parseRepository({ GITHUB_REPOSITORY: "owner/project" }), {
|
||||
owner: "owner",
|
||||
repo: "Netcatty-mosh-bin",
|
||||
owner: "binaricat",
|
||||
repo: "MoshCatty",
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseRepository({ GITHUB_REPOSITORY: "owner/project", MOSH_BIN_OWNER: "bin", MOSH_BIN_REPO: "binaries" }),
|
||||
@@ -37,16 +59,17 @@ test("parseRepository falls back to the dedicated mosh binary repository", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("pickLatestMoshBinRelease ignores non-packaging releases", () => {
|
||||
test("pickLatestMoshBinRelease ignores non-moshcatty and pre-0.1.2 tags", () => {
|
||||
const got = pickLatestMoshBinRelease([
|
||||
{ tag_name: "v1.0.0", published_at: "2026-03-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-3", draft: true, published_at: "2026-04-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-4", prerelease: true, published_at: "2026-04-02T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-1", published_at: "2026-02-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-2", published_at: "2026-03-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-2", published_at: "2026-06-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.2", draft: true, published_at: "2026-07-11T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.0", published_at: "2026-05-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.1", published_at: "2026-07-10T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.2", published_at: "2026-07-10T12:00:00Z" },
|
||||
]);
|
||||
|
||||
assert.equal(got, "mosh-bin-1.4.0-2");
|
||||
assert.equal(got, "moshcatty-0.1.2");
|
||||
});
|
||||
|
||||
test("parseNextLink reads the next GitHub pagination URL", () => {
|
||||
@@ -69,7 +92,7 @@ test("loadReleases follows GitHub pagination until the last page", async () => {
|
||||
requested.push(url);
|
||||
if (url.includes("page=2")) {
|
||||
return {
|
||||
json: [{ tag_name: "mosh-bin-1.4.0-1", published_at: "2026-01-01T00:00:00Z" }],
|
||||
json: [{ tag_name: "moshcatty-0.1.2", published_at: "2026-01-01T00:00:00Z" }],
|
||||
headers: {},
|
||||
};
|
||||
}
|
||||
@@ -81,7 +104,7 @@ test("loadReleases follows GitHub pagination until the last page", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
assert.deepEqual(got.map((release) => release.tag_name), ["v1.0.0", "mosh-bin-1.4.0-1"]);
|
||||
assert.deepEqual(got.map((release) => release.tag_name), ["v1.0.0", "moshcatty-0.1.2"]);
|
||||
assert.equal(requested.length, 2);
|
||||
});
|
||||
|
||||
@@ -99,34 +122,45 @@ test("main keeps an explicit MOSH_BIN_RELEASE and exports it", async (t) => {
|
||||
const githubEnv = path.join(makeTmp(t), "github-env");
|
||||
|
||||
const got = await main({
|
||||
MOSH_BIN_RELEASE: "mosh-bin-1.4.0-1",
|
||||
MOSH_BIN_RELEASE: "moshcatty-0.1.2",
|
||||
GITHUB_ENV: githubEnv,
|
||||
});
|
||||
|
||||
assert.equal(got, "mosh-bin-1.4.0-1");
|
||||
assert.equal(fs.readFileSync(githubEnv, "utf8"), "MOSH_BIN_RELEASE=mosh-bin-1.4.0-1\n");
|
||||
assert.equal(got, "moshcatty-0.1.2");
|
||||
assert.equal(fs.readFileSync(githubEnv, "utf8"), "MOSH_BIN_RELEASE=moshcatty-0.1.2\n");
|
||||
});
|
||||
|
||||
test("main resolves the latest release from the release list and exports it", async (t) => {
|
||||
test("main rejects explicit pre-0.1.2 MOSH_BIN_RELEASE", async () => {
|
||||
await assert.rejects(
|
||||
main({ MOSH_BIN_RELEASE: "moshcatty-0.1.1" }),
|
||||
/below minimum/,
|
||||
);
|
||||
});
|
||||
|
||||
test("main resolves the latest moshcatty release from the list and exports it", async (t) => {
|
||||
const githubEnv = path.join(makeTmp(t), "github-env");
|
||||
const got = await main({
|
||||
GITHUB_ENV: githubEnv,
|
||||
MOSH_BIN_RELEASES_JSON: JSON.stringify([
|
||||
{ tag_name: "mosh-bin-1.4.0-1", published_at: "2026-01-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-2", published_at: "2026-02-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.0", published_at: "2026-01-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.1", published_at: "2026-07-10T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.2", published_at: "2026-07-10T12:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-2", published_at: "2026-08-01T00:00:00Z" },
|
||||
]),
|
||||
});
|
||||
|
||||
assert.equal(got, "mosh-bin-1.4.0-2");
|
||||
assert.equal(fs.readFileSync(githubEnv, "utf8"), "MOSH_BIN_RELEASE=mosh-bin-1.4.0-2\n");
|
||||
assert.equal(got, "moshcatty-0.1.2");
|
||||
assert.equal(fs.readFileSync(githubEnv, "utf8"), "MOSH_BIN_RELEASE=moshcatty-0.1.2\n");
|
||||
});
|
||||
|
||||
test("main fails when no usable release exists", async () => {
|
||||
test("main fails when no usable moshcatty release exists", async () => {
|
||||
await assert.rejects(
|
||||
main({
|
||||
MOSH_BIN_RELEASES_JSON: JSON.stringify([
|
||||
{ tag_name: "v1.0.0", published_at: "2026-01-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-1", draft: true, published_at: "2026-02-01T00:00:00Z" },
|
||||
{ tag_name: "mosh-bin-1.4.0-1", published_at: "2026-02-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.1", published_at: "2026-02-01T00:00:00Z" },
|
||||
{ tag_name: "moshcatty-0.1.2", draft: true, published_at: "2026-02-01T00:00:00Z" },
|
||||
]),
|
||||
}),
|
||||
/could not find/,
|
||||
|
||||
Reference in new issue
Block a user