windows release支持

This commit is contained in:
yonglin
2026-09-17 16:16:44 +08:00
parent 36d60bab1e
commit b9098ece5c
6 changed files with 100 additions and 62 deletions
+15 -13
View File
@@ -10,19 +10,10 @@ files:
- out/**/* - out/**/*
- package.json - package.json
# Ship only this platform's native prebuilds. # 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
# better-sqlite3 alone carries eight, and node-pty another eleven — for other # cross-platform build needs. The small size saving is not worth a package
# operating systems, other architectures, and musl. None of them can be # that builds successfully and then fails on its target machine.
# 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-*/**'
- '!node_modules/ssh2/lib/protocol/crypto/build/Release/obj/**' - '!node_modules/ssh2/lib/protocol/crypto/build/Release/obj/**'
# Native modules cannot be loaded from inside an asar archive. # 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. # risk replacing working binaries with differently-built ones, for no gain.
npmRebuild: false npmRebuild: false
win:
target:
- nsis
icon: build/icon.png
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
linux: linux:
target: target:
- AppImage - AppImage
+1
View File
@@ -27,6 +27,7 @@
"e2e:ssh": "node scripts/ssh-e2e.mjs", "e2e:ssh": "node scripts/ssh-e2e.mjs",
"rebuild:native": "electron-builder install-app-deps", "rebuild:native": "electron-builder install-app-deps",
"dist": "npm run build && electron-builder", "dist": "npm run build && electron-builder",
"dist:win": "npm run build && electron-builder --win nsis",
"verify:package": "node scripts/verify-package.mjs" "verify:package": "node scripts/verify-package.mjs"
}, },
"dependencies": { "dependencies": {
+8
View File
@@ -146,6 +146,14 @@ const script = `(async () => {
report.validationRejected = true 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 -------------------------------------------------------- // --- clean up --------------------------------------------------------
tree = await api.list() tree = await api.list()
for (const node of tree) { for (const node of tree) {
+28 -44
View File
@@ -1,6 +1,6 @@
// Checks that a built package is actually runnable, rather than merely built. // 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 // 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 // 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 { tmpdir } from 'node:os'
import { join, resolve } from 'node:path' import { join, resolve } from 'node:path'
const EXPECTED_NATIVE = [ const arch = process.arch
'node_modules/better-sqlite3/prebuilds/linux-x64.node', 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', '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$/,
]
function fail(message) { function fail(message) {
console.error(`\n ✗ ${message}`) console.error(`\n ✗ ${message}`)
process.exitCode = 1 process.exitCode = 1
} }
function findDefaultAppImage() { function findDefaultPackage() {
const dir = resolve('release') const dir = resolve('release')
if (!existsSync(dir)) return null 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')) const name = readdirSync(dir).find((f) => f.endsWith('.AppImage'))
return name ? join(dir, name) : null return name ? join(dir, name) : null
} }
@@ -56,13 +51,13 @@ function walk(dir, out = []) {
return out return out
} }
const appImage = process.argv[2] ? resolve(process.argv[2]) : findDefaultAppImage() const packagePath = process.argv[2] ? resolve(process.argv[2]) : findDefaultPackage()
if (!appImage || !existsSync(appImage)) { if (!packagePath || !existsSync(packagePath)) {
fail('no AppImage found — build one first with `npx electron-builder --linux`') fail('no unpacked package found — build one first with `npm run dist` or `npm run dist:win`')
process.exit(1) process.exit(1)
} }
console.log(` inspecting ${appImage}`) console.log(` inspecting ${packagePath}`)
const workdir = mkdtempSync(join(tmpdir(), 'nexusterm-verify-')) const workdir = mkdtempSync(join(tmpdir(), 'nexusterm-verify-'))
@@ -71,12 +66,12 @@ try {
// form matters because `electron-builder --dir` is what you iterate against — // form matters because `electron-builder --dir` is what you iterate against —
// it skips the squashfs and compression steps, which dominate a full build. // it skips the squashfs and compression steps, which dominate a full build.
let resources let resources
if (statSync(appImage).isDirectory()) { if (statSync(packagePath).isDirectory()) {
resources = join(appImage, 'resources') resources = join(packagePath, 'resources')
} else { } else {
// `--appimage-extract` writes ./squashfs-root relative to the working // `--appimage-extract` writes ./squashfs-root relative to the working
// directory, so this has to run from inside the scratch dir. // directory, so this has to run from inside the scratch dir.
execFileSync(appImage, ['--appimage-extract'], { execFileSync(packagePath, ['--appimage-extract'], {
cwd: workdir, cwd: workdir,
stdio: 'ignore', stdio: 'ignore',
timeout: 180_000, timeout: 180_000,
@@ -92,7 +87,7 @@ try {
const files = walk(unpacked) const files = walk(unpacked)
for (const expected of EXPECTED_NATIVE) { 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}`) if (found) console.log(` ✓ ${expected}`)
else fail(`${expected} is not unpacked — this module will fail to load at runtime`) 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. // a JS implementation silently — so nothing would ever report it.
const sshCrypto = files.filter((f) => f.endsWith('sshcrypto.node')) const sshCrypto = files.filter((f) => f.endsWith('sshcrypto.node'))
if (sshCrypto.length > 0) console.log(` ✓ ssh2 sshcrypto.node (${sshCrypto.length})`) 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))) const size = files.reduce((total, file) => total + statSync(file).size, 0)
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],
)
console.log(` · unpacked: ${(size / 1024 / 1024).toFixed(1)} MB`) console.log(` · unpacked: ${(size / 1024 / 1024).toFixed(1)} MB`)
const asar = join(resources, 'app.asar') const asar = join(resources, 'app.asar')
+25 -4
View File
@@ -2,8 +2,28 @@ import { spawn, type IPty } from 'node-pty'
import { homedir } from 'node:os' import { homedir } from 'node:os'
import type { SessionBackend } from './backend' import type { SessionBackend } from './backend'
/** Fallback when the environment does not say which shell the user prefers. */ interface LocalShellCommand {
const FALLBACK_SHELL = '/bin/bash' 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 { export interface LocalBackendOptions {
shell?: string shell?: string
@@ -28,9 +48,10 @@ export class LocalBackend implements SessionBackend {
private closed = false private closed = false
constructor(options: LocalBackendOptions = {}) { 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', name: 'xterm-256color',
cols: options.cols ?? 80, cols: options.cols ?? 80,
rows: options.rows ?? 24, rows: options.rows ?? 24,
+22
View File
@@ -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'],
})
})
})