From 7297f7c339186f8af50f4f0ed3734e4ad4b47aed Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Thu, 30 Jul 2026 09:00:55 +0800 Subject: Cross-platform support: Windows build target and CI release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normalize watcher paths to forward slashes (Windows events use backslashes; all sync maps are '/'-keyed), path.delimiter for PATH/TEXINPUTS, dirname() instead of '/'-substring splits, bridge log in os.tmpdir() - Platform-aware TeX search paths (TeX Live / MiKTeX on Windows), terminal shell default (COMSPEC/powershell), native window frame off macOS - electron-builder: NSIS target + win icon + GitHub publish provider - GitHub Actions workflow builds the Windows installer on tag push (node-pty needs MSVC — cannot cross-compile from macOS) and attaches it to the release Co-Authored-By: Claude Fable 5 --- src/main/compilationManager.ts | 28 +++++++++++++++++----------- src/main/fileSyncBridge.ts | 17 ++++++++++++----- src/main/index.ts | 29 ++++++++++++++++++++--------- 3 files changed, 49 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/main/compilationManager.ts b/src/main/compilationManager.ts index 72c0c5e..2e27476 100644 --- a/src/main/compilationManager.ts +++ b/src/main/compilationManager.ts @@ -2,7 +2,7 @@ // Licensed under AGPL-3.0 - see LICENSE file // Manages temp directory for Overleaf socket-mode compilation -import { join, basename } from 'path' +import { join, basename, dirname, delimiter } from 'path' import { writeFile, mkdir, rm } from 'fs/promises' import { existsSync } from 'fs' import { spawn } from 'child_process' @@ -42,7 +42,7 @@ export class CompilationManager { await mkdir(this.tmpDir, { recursive: true }) for (const [relPath, content] of this.docContents) { const fullPath = join(this.tmpDir, relPath) - const dir = fullPath.substring(0, fullPath.lastIndexOf('/')) + const dir = dirname(fullPath) await mkdir(dir, { recursive: true }) await writeFile(fullPath, content, 'utf-8') } @@ -53,7 +53,7 @@ export class CompilationManager { if (this.fileRefCache.has(relativePath)) return const fullPath = join(this.tmpDir, relativePath) - const dir = fullPath.substring(0, fullPath.lastIndexOf('/')) + const dir = dirname(fullPath) await mkdir(dir, { recursive: true }) return new Promise((resolve, reject) => { @@ -99,17 +99,23 @@ export class CompilationManager { ): Promise<{ success: boolean; log: string; pdfPath: string }> { await this.syncDocs() - const texPaths = [ - '/Library/TeX/texbin', - '/usr/local/texlive/2024/bin/universal-darwin', - '/usr/texbin', - '/opt/homebrew/bin' - ] - const envPath = texPaths.join(':') + ':' + (process.env.PATH || '') + const texPaths = process.platform === 'win32' + ? [ + 'C:\\texlive\\2025\\bin\\windows', + 'C:\\texlive\\2024\\bin\\windows', + join(process.env.LOCALAPPDATA || '', 'Programs', 'MiKTeX', 'miktex', 'bin', 'x64') + ] + : [ + '/Library/TeX/texbin', + '/usr/local/texlive/2024/bin/universal-darwin', + '/usr/texbin', + '/opt/homebrew/bin' + ] + const envPath = texPaths.join(delimiter) + delimiter + (process.env.PATH || '') // Use // suffix for recursive search of ALL subdirectories in the project tree. // This ensures .sty, .bst, .cls, images, etc. are always found regardless of nesting. - const texInputs = `${this.tmpDir}//:` + const texInputs = `${this.tmpDir}//${delimiter}` const texBase = basename(mainTexRelPath, '.tex') const pdfPath = join(this.tmpDir, texBase + '.pdf') diff --git a/src/main/fileSyncBridge.ts b/src/main/fileSyncBridge.ts index 3072ae6..ae22eb8 100644 --- a/src/main/fileSyncBridge.ts +++ b/src/main/fileSyncBridge.ts @@ -2,7 +2,8 @@ // Licensed under AGPL-3.0 - see LICENSE file // Bidirectional file sync bridge: temp dir ↔ Overleaf via OT (text) + REST (binary) -import { join, dirname } from 'path' +import { join, dirname, relative, sep } from 'path' +import { tmpdir } from 'os' import { existsSync } from 'fs' import { readFile, writeFile, mkdir, unlink, rename as fsRename, appendFile, readdir, rm } from 'fs/promises' import { createHash } from 'crypto' @@ -16,7 +17,7 @@ import type { OtOp } from './otTypes' import { isInsert, isDelete } from './otTypes' const dmp = new diff_match_patch() -const LOG_FILE = '/tmp/lattex-bridge.log' +const LOG_FILE = join(tmpdir(), 'lattex-bridge.log') function bridgeLog(msg: string, ...rest: unknown[]) { const line = `[${new Date().toISOString()}] ${msg}${rest.length ? ' ' + rest.map(String).join(' ') : ''}` console.log(line) @@ -225,7 +226,7 @@ export class FileSyncBridge { }, 60_000) this.watcher.on('change', (absPath: string) => { - const relPath = absPath.replace(this.tmpDir + '/', '') + const relPath = this.relFromAbs(absPath) bridgeLog(`[FileSyncBridge] chokidar change: ${relPath}`) // A real fs event means new content — give parked failures a fresh chance this.permanentFailures.delete(relPath) @@ -237,7 +238,7 @@ export class FileSyncBridge { }) this.watcher.on('add', (absPath: string) => { - const relPath = absPath.replace(this.tmpDir + '/', '') + const relPath = this.relFromAbs(absPath) bridgeLog(`[FileSyncBridge] chokidar add: ${relPath}`) this.permanentFailures.delete(relPath) if (this.pathDocMap[relPath] || this.pathFileRefMap[relPath]) { @@ -250,7 +251,7 @@ export class FileSyncBridge { }) this.watcher.on('unlink', (absPath: string) => { - const relPath = absPath.replace(this.tmpDir + '/', '') + const relPath = this.relFromAbs(absPath) bridgeLog(`[FileSyncBridge] chokidar unlink: ${relPath}`) }) @@ -1478,6 +1479,12 @@ export class FileSyncBridge { } } + /** Absolute watcher path → project-relative path with forward slashes + (Windows watcher events use backslashes; all maps are '/'-keyed) */ + private relFromAbs(absPath: string): string { + return relative(this.tmpDir, absPath).split(sep).join('/') + } + // ── Compile-artifact detection ─────────────────────────────── /** Register a file our own local compile will write (e.g. root main.pdf) */ diff --git a/src/main/index.ts b/src/main/index.ts index 4f23762..8dffc26 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,7 +2,7 @@ // Licensed under AGPL-3.0 - see LICENSE file import { app, BrowserWindow, ipcMain, dialog, shell, net } from 'electron' -import { join, basename, dirname, relative, extname } from 'path' +import { join, basename, dirname, relative, extname, delimiter } from 'path' import { copyFile, readFile, writeFile, mkdir as mkdirAsync, unlink, readdir, stat, rename as fsRename, rm, cp } from 'fs/promises' import { existsSync } from 'fs' import { spawn } from 'child_process' @@ -125,8 +125,11 @@ function createWindow(): void { height: 900, minWidth: 900, minHeight: 600, - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 15, y: 15 }, + // Frameless inset title bar is a macOS affordance; use the native + // frame elsewhere + ...(process.platform === 'darwin' + ? { titleBarStyle: 'hiddenInset' as const, trafficLightPosition: { x: 15, y: 15 } } + : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, @@ -255,19 +258,25 @@ ipcMain.handle('settings:setApiKeys', async (_e, keys: Record) = // ── LaTeX Compilation ──────────────────────────────────────────── -// Ensure TeX binaries are in PATH (Electron launched from Finder may miss them) -const texPaths = ['/Library/TeX/texbin', '/usr/local/texlive/2024/bin/universal-darwin', '/usr/texbin', '/opt/homebrew/bin'] +// Ensure TeX binaries are in PATH (GUI-launched apps may miss them) +const texPaths = process.platform === 'win32' + ? [ + 'C:\\texlive\\2025\\bin\\windows', + 'C:\\texlive\\2024\\bin\\windows', + join(process.env.LOCALAPPDATA || '', 'Programs', 'MiKTeX', 'miktex', 'bin', 'x64') + ] + : ['/Library/TeX/texbin', '/usr/local/texlive/2024/bin/universal-darwin', '/usr/texbin', '/opt/homebrew/bin'] const currentPath = process.env.PATH || '' for (const p of texPaths) { if (!currentPath.includes(p)) { - process.env.PATH = `${p}:${process.env.PATH}` + process.env.PATH = `${p}${delimiter}${process.env.PATH}` } } // SyncTeX: PDF position → source file:line (inverse search) ipcMain.handle('synctex:editFromPdf', async (_e, pdfPath: string, page: number, x: number, y: number) => { return new Promise<{ file: string; line: number } | null>((resolve) => { - const pdfDir = pdfPath.substring(0, pdfPath.lastIndexOf('/')) + const pdfDir = dirname(pdfPath) console.log(`[synctex] edit -o ${page}:${x}:${y}:${pdfPath} (cwd: ${pdfDir})`) const proc = spawn('synctex', ['edit', '-o', `${page}:${x}:${y}:${pdfPath}`], { env: process.env, @@ -418,8 +427,10 @@ ipcMain.handle('pty:spawn', async (_e, id: string, cwd: string, cmd?: string, ar ptyInstances.delete(id) } - const shellPath = cmd || process.env.SHELL || '/bin/zsh' - const shellArgs = args || ['-l'] + const shellPath = cmd || (process.platform === 'win32' + ? process.env.COMSPEC || 'powershell.exe' + : process.env.SHELL || '/bin/zsh') + const shellArgs = args || (process.platform === 'win32' ? [] : ['-l']) const ptyEnv: Record = { ...(process.env as Record), TERM: 'xterm-256color', -- cgit v1.2.3