windows release支持
This commit is contained in:
+15
-13
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+27
-43
@@ -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,25 +14,18 @@ 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',
|
||||
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`,
|
||||
]
|
||||
|
||||
/**
|
||||
* 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$/,
|
||||
: [
|
||||
`node_modules/better-sqlite3/prebuilds/linux-${arch}.node`,
|
||||
'node_modules/node-pty/build/Release/pty.node',
|
||||
]
|
||||
|
||||
function fail(message) {
|
||||
@@ -40,9 +33,11 @@ function fail(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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user