Skip to content

Commit 23b46ff

Browse files
author
Yogesh Prajapati
committed
feat(build): add macOS + Linux builds via 3-OS CI matrix
Previously Windows-only. Now electron-builder produces native artifacts for all three platforms on every tagged release: Windows : .exe (NSIS installer) — windows-latest macOS : .dmg + .zip (x64 + arm64) — macos-latest, unsigned Linux : .AppImage + .deb (x64) — ubuntu-latest Release workflow restructured as a fail-fast: false matrix so a platform-specific failure doesn't block the others. Each runner builds + publishes its own artifacts via electron-builder --publish always, which also uploads the per-OS auto-update manifests (latest.yml, latest-mac.yml, latest-linux.yml) the updater needs. The release job then finalises the GitHub release with the auto-generated changelog across all three artifact sets. Renderer fixes for cross-platform compatibility: - encryptionProfilePath: use forward slash (works on every OS) instead of the hardcoded Windows backslash - legacy-whiteboard regex now matches both [\/] - terminal preference default switched from hardcoded 'powershell.exe' to a defaultShellForPlatform() that picks zsh on macOS, bash on Linux - F5 Run File uses `&&` on macOS/Linux (bash/zsh) and `;` on Windows (PowerShell) so the shell chain operator doesn't trip a parse error Caveats documented in CLAUDE.md: - macOS builds are unsigned; first launch requires right-click → Open. - Icons still use the .ico for all platforms (mac/linux get an auto- converted lower-quality version); future work to add .icns + 512.png. - Linux AppImage needs libfuse2 (CI installs it; end-users on Ubuntu 24.04+ may need libfuse2t64).
1 parent f739f73 commit 23b46ff

4 files changed

Lines changed: 158 additions & 37 deletions

File tree

.github/workflows/release.yml

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,39 @@ permissions:
1111

1212
jobs:
1313
# ───────────────────────────────────────────────────────────────────────────
14-
# JOB 1 — BUILD
15-
# Compiles the installer and the electron-updater manifest. Uploads them as
16-
# workflow artifacts so the release job (and humans, if needed) can fetch
17-
# them. We use `--publish always` so electron-builder also uploads the
18-
# auto-updater files (latest.yml + *.blockmap) straight to the GitHub
19-
# release — these MUST sit next to the .exe or auto-update can't find them.
14+
# JOB 1 — BUILD (matrix across Windows + macOS + Linux)
15+
# Each runner compiles its own native artifacts and publishes them to the
16+
# GitHub release. electron-updater needs the per-platform manifest files
17+
# (latest.yml, latest-mac.yml, latest-linux.yml) AND the artifacts to sit
18+
# on the same release — `--publish always` handles both in one step.
2019
# ───────────────────────────────────────────────────────────────────────────
2120
build:
22-
name: Build Windows Installer
23-
runs-on: windows-latest
21+
name: Build (${{ matrix.os }})
22+
strategy:
23+
fail-fast: false # one platform's failure shouldn't block the others
24+
matrix:
25+
include:
26+
- os: windows-latest
27+
target_flag: --win nsis
28+
artifact_glob: |
29+
dist/*.exe
30+
dist/latest.yml
31+
dist/*.blockmap
32+
- os: macos-latest
33+
target_flag: --mac
34+
artifact_glob: |
35+
dist/*.dmg
36+
dist/*.zip
37+
dist/latest-mac.yml
38+
dist/*.blockmap
39+
- os: ubuntu-latest
40+
target_flag: --linux
41+
artifact_glob: |
42+
dist/*.AppImage
43+
dist/*.deb
44+
dist/latest-linux.yml
45+
dist/*.blockmap
46+
runs-on: ${{ matrix.os }}
2447
outputs:
2548
version: ${{ steps.version.outputs.VERSION }}
2649

@@ -34,22 +57,28 @@ jobs:
3457
node-version: '20'
3558
cache: 'npm'
3659

60+
# Linux-only: AppImage builds need libfuse2 on the GH runner image.
61+
- name: Install libfuse2 (Linux only)
62+
if: matrix.os == 'ubuntu-latest'
63+
run: sudo apt-get update && sudo apt-get install -y libfuse2
64+
3765
- name: Install dependencies
3866
run: npm ci
3967

40-
- name: Build & publish installer (uploads .exe + latest.yml + .blockmap to GitHub release)
41-
run: npm run build:wb && npx electron-builder --win nsis --publish always
68+
- name: Build & publish installer (electron-builder publishes to GitHub release)
69+
run: npm run build:wb && npx electron-builder ${{ matrix.target_flag }} --publish always
4270
env:
4371
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72+
# Skip Mac code-signing in CI — we ship unsigned builds for now.
73+
# Users on macOS will need to right-click → Open the first time.
74+
CSC_IDENTITY_AUTO_DISCOVERY: false
4475

45-
- name: Upload installer artifacts (for the release job)
76+
- name: Upload platform artifacts (for the release job)
4677
uses: actions/upload-artifact@v4
4778
with:
48-
name: installer
49-
path: |
50-
dist/*.exe
51-
dist/latest.yml
52-
dist/*.blockmap
79+
name: dist-${{ matrix.os }}
80+
path: ${{ matrix.artifact_glob }}
81+
if-no-files-found: warn
5382

5483
- name: Get version from tag
5584
id: version
@@ -58,22 +87,37 @@ jobs:
5887

5988
# ───────────────────────────────────────────────────────────────────────────
6089
# JOB 2 — RELEASE
61-
# Marks the GitHub release as Latest with auto-generated notes (changelog
62-
# built from commits/PRs since the previous tag). softprops/action-gh-release
63-
# upserts the release — if electron-builder already created one for this
64-
# tag, this step just refines its title/notes and adds the same files.
90+
# Marks the GitHub release as Latest with auto-generated notes. By this
91+
# point all three platforms have already uploaded their artifacts via
92+
# electron-builder --publish always; this step just finalises the title
93+
# and changelog and ensures it's flipped to Latest.
6594
# ───────────────────────────────────────────────────────────────────────────
6695
release:
6796
name: Publish GitHub Release
6897
needs: build
6998
runs-on: ubuntu-latest
7099

71100
steps:
72-
- name: Download installer artifacts from build job
101+
- name: Download Windows artifacts
102+
uses: actions/download-artifact@v4
103+
with:
104+
name: dist-windows-latest
105+
path: dist
106+
continue-on-error: true
107+
108+
- name: Download macOS artifacts
109+
uses: actions/download-artifact@v4
110+
with:
111+
name: dist-macos-latest
112+
path: dist
113+
continue-on-error: true
114+
115+
- name: Download Linux artifacts
73116
uses: actions/download-artifact@v4
74117
with:
75-
name: installer
118+
name: dist-ubuntu-latest
76119
path: dist
120+
continue-on-error: true
77121

78122
- name: Create / update GitHub Release
79123
uses: softprops/action-gh-release@v2
@@ -86,7 +130,13 @@ jobs:
86130
generate_release_notes: true
87131
files: |
88132
dist/*.exe
133+
dist/*.dmg
134+
dist/*.zip
135+
dist/*.AppImage
136+
dist/*.deb
89137
dist/latest.yml
138+
dist/latest-mac.yml
139+
dist/latest-linux.yml
90140
dist/*.blockmap
91141
env:
92142
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CLAUDE.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Located at `D:\NewNotepad`. GitHub: https://github.com/YogeshPraj/Note-
2525
| Bundler (iframe only) | esbuild 0.24 |
2626
| Security | `contextIsolation: true`, `nodeIntegration: false` |
2727
| Node bridge | `src/preload.js``window.electronAPI` |
28-
| Platform | Windows (primary), Node.js |
28+
| Platform | Windows, macOS, Linux (cross-platform builds via electron-builder matrix CI) |
29+
| Auto-update | electron-updater + GitHub provider (latest.yml / latest-mac.yml / latest-linux.yml on every release) |
2930

3031
---
3132

@@ -264,15 +265,35 @@ npm start # or double-click launch.bat
264265
## How to Build
265266

266267
```bash
267-
npm run build # electron-builder (needs electron-builder in devDeps)
268+
# Build for the current OS (auto-detects)
269+
npm run build
270+
271+
# Or target a specific platform explicitly
272+
npm run build:win # Windows: NSIS installer (.exe)
273+
npm run build:msi # Windows: MSI installer
274+
npm run build:mac # macOS: .dmg + .zip (x64 + arm64)
275+
npm run build:linux # Linux: .AppImage + .deb (x64)
268276
```
269277

278+
**Cross-platform note:** electron-builder must be run on the target OS for native binaries (node-pty, native deps) to compile correctly. You can't build Mac artifacts on a Windows machine and vice-versa. The GitHub Actions workflow (`.github/workflows/release.yml`) handles this automatically via a 3-OS matrix on every tag push — each runner builds + publishes its own platform's artifacts to the same GitHub release.
279+
280+
**Mac caveat:** builds are currently unsigned (no Apple Developer cert configured in CI). First-launch on macOS requires right-click → Open to bypass Gatekeeper.
281+
282+
**Linux caveat:** AppImage builds need `libfuse2` on the build machine (CI installs it automatically). End-users on modern Ubuntu (24.04+) may need `sudo apt install libfuse2t64` to launch the AppImage.
283+
270284
---
271285

272286
## Known Issues / Future Work
273287

274-
- [ ] No diff view / git integration in editor yet
275-
- [ ] No LSP server connections (only Monaco's built-in JS/TS IntelliSense)
288+
- [ ] macOS code-signing (currently shipping unsigned; Gatekeeper warning on first launch)
289+
- [ ] Linux icon assets (currently using `.ico` for all platforms; mac/linux fall back to a lower-quality conversion)
290+
- [ ] No Snap or Flatpak Linux targets yet (AppImage + .deb only)
291+
292+
### Recently resolved (Session 8 — cross-platform builds)
293+
- ~~Windows-only builds~~ → electron-builder now produces `.exe` (Windows), `.dmg`/`.zip` (macOS x64+arm64), `.AppImage`/`.deb` (Linux x64). CI is a 3-OS matrix; tag push triggers parallel builds on `windows-latest`, `macos-latest`, `ubuntu-latest`. All artifacts plus `latest*.yml` updater manifests land on the same GitHub release.
294+
- ~~Hardcoded `\\` separators in renderer paths~~ → switched to `/` and `[\\/]` regex (works on every OS).
295+
- ~~`powershell.exe` hardcoded as terminal default~~`defaultShellForPlatform()` picks `powershell.exe` on Windows, `/bin/zsh` on macOS, `/bin/bash` on Linux.
296+
- ~~F5 Run File used PowerShell-specific `;` operator~~ → uses `&&` on macOS/Linux so bash/zsh parse cleanly.
276297

277298
### Recently resolved (Session 6)
278299
- ~~`node-pty` not installed~~`node-pty ^1.1.0` is in `dependencies`, ConPTY binary built, `main.js` uses it for true PTY with `proc.resize()` wired up.

package.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
"build:wb": "node build-whiteboard.js",
1010
"build:wb:watch": "node build-whiteboard.js --watch",
1111
"postinstall": "node build-whiteboard.js",
12-
"build": "npm run build:wb && electron-builder --win",
12+
"build": "npm run build:wb && electron-builder",
1313
"build:win": "npm run build:wb && electron-builder --win nsis --publish never",
1414
"build:msi": "npm run build:wb && electron-builder --win msi --publish never",
15+
"build:mac": "npm run build:wb && electron-builder --mac --publish never",
16+
"build:linux": "npm run build:wb && electron-builder --linux --publish never",
1517
"build:dir": "npm run build:wb && electron-builder --dir"
1618
},
1719
"build": {
@@ -339,6 +341,36 @@
339341
"icon": "src/assets/icon.ico",
340342
"artifactName": "${productName}-Setup-${version}.${ext}"
341343
},
344+
"mac": {
345+
"target": [
346+
{ "target": "dmg", "arch": ["x64", "arm64"] },
347+
{ "target": "zip", "arch": ["x64", "arm64"] }
348+
],
349+
"icon": "src/assets/icon.ico",
350+
"category": "public.app-category.developer-tools",
351+
"artifactName": "${productName}-${version}-${arch}.${ext}",
352+
"hardenedRuntime": false,
353+
"gatekeeperAssess": false,
354+
"identity": null
355+
},
356+
"dmg": {
357+
"title": "${productName} ${version}",
358+
"icon": "src/assets/icon.ico",
359+
"writeUpdateInfo": true,
360+
"sign": false
361+
},
362+
"linux": {
363+
"target": [
364+
{ "target": "AppImage", "arch": ["x64"] },
365+
{ "target": "deb", "arch": ["x64"] }
366+
],
367+
"icon": "src/assets/icon.ico",
368+
"category": "Development",
369+
"artifactName": "${productName}-${version}-${arch}.${ext}",
370+
"synopsis": "Developer-focused text editor with IntelliSense, integrated terminal, and live preview",
371+
"description": "Note++ is an Electron + Monaco editor aimed at developers — a modern alternative to Notepad++ with IntelliSense, integrated PowerShell/bash terminal, live HTML/Markdown/Mermaid preview, multi-tab editing, cloud session sync, encryption, integrated git, and a built-in whiteboard powered by Excalidraw.",
372+
"maintainer": "Yogesh Prajapati"
373+
},
342374
"nsis": {
343375
"oneClick": false,
344376
"allowToChangeInstallationDirectory": true,

src/renderer.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,11 +1134,13 @@ async function closeTab(id) {
11341134
if (r.response === 2) return;
11351135
if (r.response === 0) {
11361136
// Legacy auto-backed whiteboards (pre-v1.5.x sessions) live under
1137-
// %AppData%\notepp\Whiteboards\ — for those, force Save As so the
1138-
// user picks a real location rather than silently writing back to
1139-
// the hidden AppData copy.
1137+
// the per-user app data folder (Windows: %AppData%\notepp\Whiteboards,
1138+
// macOS: ~/Library/Application Support/notepp/Whiteboards,
1139+
// Linux: ~/.config/notepp/Whiteboards). For those, force Save As so
1140+
// the user picks a real location rather than silently writing back
1141+
// to the hidden app-data copy.
11401142
const isLegacyAutoBacking =
1141-
tab.filePath && /\\Whiteboards\\whiteboard-\d+\.json$/i.test(tab.filePath);
1143+
tab.filePath && /[\\/]Whiteboards[\\/]whiteboard-\d+\.json$/i.test(tab.filePath);
11421144
const ok = await saveTabFile(tab, /* forceAs */ isLegacyAutoBacking);
11431145
if (!ok) return;
11441146
}
@@ -1983,9 +1985,12 @@ async function runCurrentFile() {
19831985
// it a beat before typing into the PTY on first launch.
19841986
await new Promise(r => setTimeout(r, 80));
19851987

1986-
// PowerShell: cd to the file's folder, then run. `;` chains regardless of
1987-
// exit code so we always see the result. CR (\r) submits the line.
1988-
const line = `cd "${cwd}"; ${cmd}\r`;
1988+
// Sniff the host OS so we use the right shell-chain operator. PowerShell
1989+
// (Windows default) accepts both `;` and `&&`; bash/zsh (macOS + Linux)
1990+
// require `&&`. Sending the wrong one wedges the prompt with a parse error.
1991+
const isWin = /windows/i.test(navigator.userAgent || '');
1992+
const chain = isWin ? ';' : '&&';
1993+
const line = `cd "${cwd}" ${chain} ${cmd}\r`;
19891994
await window.electronAPI.terminalInput(terminalId, line);
19901995
}
19911996

@@ -4671,18 +4676,29 @@ async function saveBackupPrefs(settings) {
46714676
}
46724677

46734678
// ===== Terminal Preferences =====
4679+
// Pick the sensible per-OS default shell. The renderer doesn't know its
4680+
// platform directly, so we sniff via navigator.userAgent (Electron exposes
4681+
// the host OS there). Wrong-OS guess is harmless — main.js falls back to
4682+
// its own platform-aware getShell() anyway.
4683+
function defaultShellForPlatform() {
4684+
const ua = (navigator.userAgent || '').toLowerCase();
4685+
if (ua.includes('windows')) return 'powershell.exe';
4686+
if (ua.includes('mac')) return '/bin/zsh';
4687+
return '/bin/bash';
4688+
}
4689+
46744690
async function loadTerminalPrefs() {
46754691
const s = await window.electronAPI.readSettings();
46764692
const t = s.terminal || {};
46774693
const shellEl = document.getElementById('pref-shell');
46784694
const fontSizeEl = document.getElementById('pref-term-fontsize');
4679-
if (shellEl) shellEl.value = t.shell || 'powershell.exe';
4695+
if (shellEl) shellEl.value = t.shell || defaultShellForPlatform();
46804696
if (fontSizeEl) fontSizeEl.value = t.fontSize || 13;
46814697
}
46824698

46834699
async function saveTerminalPrefs(settings) {
46844700
settings.terminal = {
4685-
shell: document.getElementById('pref-shell')?.value?.trim() || 'powershell.exe',
4701+
shell: document.getElementById('pref-shell')?.value?.trim() || defaultShellForPlatform(),
46864702
fontSize: parseInt(document.getElementById('pref-term-fontsize')?.value || '13'),
46874703
};
46884704
}
@@ -5330,7 +5346,9 @@ async function initGitIntegration() {
53305346

53315347
async function encryptionProfilePath() {
53325348
const userData = await window.electronAPI.getUserDataPath();
5333-
return userData + '\\encryption\\profile.json';
5349+
// Forward slashes work on Windows, macOS, and Linux. Node's fs APIs in
5350+
// the main process normalise both separators correctly.
5351+
return userData + '/encryption/profile.json';
53345352
}
53355353

53365354
// Read profile.json from disk into `appEnc.profile`. Returns the parsed profile,

0 commit comments

Comments
 (0)