From b9098ece5c9ab9da35e8ffaee6ea1de553f6583c Mon Sep 17 00:00:00 2001 From: yonglin Date: Thu, 17 Sep 2026 16:16:44 +0800 Subject: [PATCH] =?UTF-8?q?windows=20release=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron-builder.yml | 28 ++++++------ package.json | 1 + scripts/ipc-smoke.mjs | 8 ++++ scripts/verify-package.mjs | 74 +++++++++++++------------------- src/main/session/LocalBackend.ts | 29 +++++++++++-- tests/localBackend.test.ts | 22 ++++++++++ 6 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 tests/localBackend.test.ts diff --git a/electron-builder.yml b/electron-builder.yml index 2b6e50b..5aa0b44 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -10,19 +10,10 @@ files: - out/**/* - package.json - # Ship only this platform's native prebuilds. - # - # better-sqlite3 alone carries eight, and node-pty another eleven — for other - # operating systems, other architectures, and musl. None of them can be - # loaded here and together they are about 10MB of the package. - # better-sqlite3 ships these as flat FILES (`win32-arm64.node`), not as - # directories, so the pattern must not require a trailing `/**`. - - '!node_modules/better-sqlite3/prebuilds/win32-*' - - '!node_modules/better-sqlite3/prebuilds/darwin-*' - - '!node_modules/better-sqlite3/prebuilds/linuxmusl-*' - - '!node_modules/better-sqlite3/prebuilds/linux-arm64*' - - '!node_modules/node-pty/prebuilds/win32-*/**' - - '!node_modules/node-pty/prebuilds/darwin-*/**' + # Keep every native prebuild. electron-builder applies this file on Linux and + # Windows, and removing another platform's binary here also removes the one a + # cross-platform build needs. The small size saving is not worth a package + # that builds successfully and then fails on its target machine. - '!node_modules/ssh2/lib/protocol/crypto/build/Release/obj/**' # Native modules cannot be loaded from inside an asar archive. @@ -45,6 +36,17 @@ asarUnpack: # risk replacing working binaries with differently-built ones, for no gain. npmRebuild: false +win: + target: + - nsis + icon: build/icon.png + +nsis: + oneClick: false + allowToChangeInstallationDirectory: true + createDesktopShortcut: true + createStartMenuShortcut: true + linux: target: - AppImage diff --git a/package.json b/package.json index 83be615..2a9f114 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "e2e:ssh": "node scripts/ssh-e2e.mjs", "rebuild:native": "electron-builder install-app-deps", "dist": "npm run build && electron-builder", + "dist:win": "npm run build && electron-builder --win nsis", "verify:package": "node scripts/verify-package.mjs" }, "dependencies": { diff --git a/scripts/ipc-smoke.mjs b/scripts/ipc-smoke.mjs index 3cd6e3d..75a027a 100644 --- a/scripts/ipc-smoke.mjs +++ b/scripts/ipc-smoke.mjs @@ -146,6 +146,14 @@ const script = `(async () => { report.validationRejected = true } + // --- local PTY ------------------------------------------------------- + // This catches platform-specific shell selection and packaged node-pty + // failures, neither of which the repository tests can exercise. + const local = await window.nexusterm.session.openLocal() + if (!local.ok) throw new Error(local.message) + report.localShell = true + await window.nexusterm.session.close(local.sessionId) + // --- clean up -------------------------------------------------------- tree = await api.list() for (const node of tree) { diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index f85162d..4e26b3e 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -1,6 +1,6 @@ // Checks that a built package is actually runnable, rather than merely built. // -// node scripts/verify-package.mjs [path/to/NexusTerm-*.AppImage] +// node scripts/verify-package.mjs [path/to/AppImage-or-unpacked-directory] // // Native modules cannot be loaded from inside an asar archive, so they have to // be unpacked beside it — and when that goes wrong the app still builds, still @@ -14,35 +14,30 @@ import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' -const EXPECTED_NATIVE = [ - 'node_modules/better-sqlite3/prebuilds/linux-x64.node', - 'node_modules/node-pty/build/Release/pty.node', -] - -/** - * Other-platform *binaries*, not everything with a platform in its name. - * - * better-sqlite3 also ships `lib/darwin-arm64.js` and friends — small loader - * shims that are part of the library on every platform. Flagging those would - * make this check cry wolf, and a check that cries wolf gets ignored. - */ -const FORBIDDEN = [ - /win32-.*\.node$/, - /win32-.*\.dll$/, - /darwin-.*\.node$/, - /darwin-.*spawn-helper$/, - /linuxmusl-.*\.node$/, - /linux-arm64.*\.node$/, -] +const arch = process.arch +const EXPECTED_NATIVE = + process.platform === 'win32' + ? [ + `node_modules/better-sqlite3/prebuilds/win32-${arch}.node`, + `node_modules/node-pty/prebuilds/win32-${arch}/pty.node`, + `node_modules/node-pty/prebuilds/win32-${arch}/conpty.node`, + `node_modules/node-pty/prebuilds/win32-${arch}/winpty-agent.exe`, + ] + : [ + `node_modules/better-sqlite3/prebuilds/linux-${arch}.node`, + 'node_modules/node-pty/build/Release/pty.node', + ] function fail(message) { console.error(`\n ✗ ${message}`) process.exitCode = 1 } -function findDefaultAppImage() { +function findDefaultPackage() { const dir = resolve('release') if (!existsSync(dir)) return null + const unpacked = join(dir, 'win-unpacked') + if (process.platform === 'win32' && existsSync(unpacked)) return unpacked const name = readdirSync(dir).find((f) => f.endsWith('.AppImage')) return name ? join(dir, name) : null } @@ -56,13 +51,13 @@ function walk(dir, out = []) { return out } -const appImage = process.argv[2] ? resolve(process.argv[2]) : findDefaultAppImage() -if (!appImage || !existsSync(appImage)) { - fail('no AppImage found — build one first with `npx electron-builder --linux`') +const packagePath = process.argv[2] ? resolve(process.argv[2]) : findDefaultPackage() +if (!packagePath || !existsSync(packagePath)) { + fail('no unpacked package found — build one first with `npm run dist` or `npm run dist:win`') process.exit(1) } -console.log(` inspecting ${appImage}`) +console.log(` inspecting ${packagePath}`) const workdir = mkdtempSync(join(tmpdir(), 'nexusterm-verify-')) @@ -71,12 +66,12 @@ try { // form matters because `electron-builder --dir` is what you iterate against — // it skips the squashfs and compression steps, which dominate a full build. let resources - if (statSync(appImage).isDirectory()) { - resources = join(appImage, 'resources') + if (statSync(packagePath).isDirectory()) { + resources = join(packagePath, 'resources') } else { // `--appimage-extract` writes ./squashfs-root relative to the working // directory, so this has to run from inside the scratch dir. - execFileSync(appImage, ['--appimage-extract'], { + execFileSync(packagePath, ['--appimage-extract'], { cwd: workdir, stdio: 'ignore', timeout: 180_000, @@ -92,7 +87,7 @@ try { const files = walk(unpacked) for (const expected of EXPECTED_NATIVE) { - const found = files.some((f) => f.endsWith(expected)) + const found = files.some((f) => f.replaceAll('\\', '/').endsWith(expected)) if (found) console.log(` ✓ ${expected}`) else fail(`${expected} is not unpacked — this module will fail to load at runtime`) } @@ -102,22 +97,11 @@ try { // a JS implementation silently — so nothing would ever report it. const sshCrypto = files.filter((f) => f.endsWith('sshcrypto.node')) if (sshCrypto.length > 0) console.log(` ✓ ssh2 sshcrypto.node (${sshCrypto.length})`) - else fail('ssh2 sshcrypto.node is missing') + else if (process.platform === 'win32') { + console.log(' ! ssh2 sshcrypto.node is absent; ssh2 will use its JavaScript fallback') + } else fail('ssh2 sshcrypto.node is missing') - const strays = files.filter((f) => FORBIDDEN.some((p) => p.test(f))) - if (strays.length === 0) { - console.log(' ✓ no other-platform binaries') - } else { - // A warning rather than a failure: these are dead weight, not a defect. - console.log(` ! ${strays.length} other-platform file(s) shipped:`) - for (const stray of strays.slice(0, 5)) { - console.log(` ${stray.slice(unpacked.length + 1)}`) - } - } - - const size = Number( - execFileSync('du', ['-sb', unpacked], { encoding: 'utf8' }).split('\t')[0], - ) + const size = files.reduce((total, file) => total + statSync(file).size, 0) console.log(` · unpacked: ${(size / 1024 / 1024).toFixed(1)} MB`) const asar = join(resources, 'app.asar') diff --git a/src/main/session/LocalBackend.ts b/src/main/session/LocalBackend.ts index 96fb938..403ef0b 100644 --- a/src/main/session/LocalBackend.ts +++ b/src/main/session/LocalBackend.ts @@ -2,8 +2,28 @@ import { spawn, type IPty } from 'node-pty' import { homedir } from 'node:os' import type { SessionBackend } from './backend' -/** Fallback when the environment does not say which shell the user prefers. */ -const FALLBACK_SHELL = '/bin/bash' +interface LocalShellCommand { + shell: string + args: string[] +} + +/** Pick a shell invocation that exists on the current operating system. */ +export function resolveLocalShell( + platform: NodeJS.Platform = process.platform, + env: NodeJS.ProcessEnv = process.env, +): LocalShellCommand { + if (platform === 'win32') { + return { + shell: env.ComSpec ?? env.COMSPEC ?? 'cmd.exe', + args: [], + } + } + + return { + shell: env.SHELL ?? '/bin/bash', + args: ['-l'], + } +} export interface LocalBackendOptions { shell?: string @@ -28,9 +48,10 @@ export class LocalBackend implements SessionBackend { private closed = false constructor(options: LocalBackendOptions = {}) { - const shell = options.shell ?? process.env.SHELL ?? FALLBACK_SHELL + const defaults = resolveLocalShell() + const shell = options.shell ?? defaults.shell - this.pty = spawn(shell, options.args ?? ['-l'], { + this.pty = spawn(shell, options.args ?? defaults.args, { name: 'xterm-256color', cols: options.cols ?? 80, rows: options.rows ?? 24, diff --git a/tests/localBackend.test.ts b/tests/localBackend.test.ts new file mode 100644 index 0000000..6f72092 --- /dev/null +++ b/tests/localBackend.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { resolveLocalShell } from '../src/main/session/LocalBackend' + +describe('local shell selection', () => { + it('uses ComSpec without Unix login arguments on Windows', () => { + expect(resolveLocalShell('win32', { ComSpec: 'C:\\Windows\\System32\\cmd.exe' })).toEqual({ + shell: 'C:\\Windows\\System32\\cmd.exe', + args: [], + }) + }) + + it('falls back to cmd.exe when ComSpec is unavailable', () => { + expect(resolveLocalShell('win32', {})).toEqual({ shell: 'cmd.exe', args: [] }) + }) + + it('keeps the login-shell behaviour on Unix platforms', () => { + expect(resolveLocalShell('linux', { SHELL: '/bin/zsh' })).toEqual({ + shell: '/bin/zsh', + args: ['-l'], + }) + }) +})