Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 81 additions & 89 deletions .github/workflows/desktop-build.yml
Original file line number Diff line number Diff line change
@@ -1,89 +1,81 @@
name: Build Desktop App Binaries

on:
push:
tags:
- "desktop-v*"

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"

# PERF-022: Cache Neutralino binaries and downloaded CDN libraries
- name: Cache Neutralino binaries
uses: actions/cache@v4
with:
path: |
desktop-app/bin
desktop-app/resources/libs
key: neutralino-${{ hashFiles('desktop-app/neutralino.config.json') }}

- name: Setup Neutralinojs binaries
working-directory: desktop-app
run: npm run setup

- name: Build all binaries (embedded + portable)
working-directory: desktop-app
run: npm run build:all

- name: Stage release assets
working-directory: desktop-app
run: |
VERSION="${GITHUB_REF_NAME#desktop-}"
STAGING="release-assets"
mkdir -p "$STAGING"

# --- Embedded single-file executables ---
for bin in dist/markdown-viewer/markdown-viewer-*; do
[ -f "$bin" ] || continue
filename=$(basename "$bin")

if [[ "$filename" == *.exe ]]; then
# Windows .exe — ship as-is
cp "$bin" "$STAGING/$filename"
else
# Linux/macOS — tar.gz for Unix convention
tar -czf "$STAGING/${filename}.tar.gz" -C "$(dirname "$bin")" "$filename"
fi
done

# --- Portable ZIP bundle (from neu build --release) ---
for zip in dist/*.zip; do
[ -f "$zip" ] || continue
cp "$zip" "$STAGING/"
done

# --- Source archives ---
cd ..
tar -czf "desktop-app/$STAGING/source.tar.gz" \
--exclude='desktop-app/dist' \
--exclude='desktop-app/bin' \
--exclude='desktop-app/node_modules' \
--exclude='desktop-app/output' \
--exclude="desktop-app/$STAGING" \
--exclude='.git' \
desktop-app/
cd desktop-app

# --- SHA256 checksums ---
cd "$STAGING"
sha256sum * > SHA256SUMS.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "Markdown Viewer Desktop ${{ github.ref_name }}"
generate_release_notes: true
files: desktop-app/release-assets/*
name: Build Desktop App Binaries

on:
push:
tags:
- "desktop-v*"

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"

# PERF-022: Cache Neutralino binaries and downloaded CDN libraries
- name: Cache Neutralino binaries
uses: actions/cache@v4
with:
path: |
desktop-app/bin
desktop-app/resources/libs
key: neutralino-${{ hashFiles('desktop-app/neutralino.config.json') }}

- name: Setup Neutralinojs binaries
working-directory: desktop-app
run: npm run setup

- name: Build desktop bundles
working-directory: desktop-app
run: npm run build:all

- name: Stage release assets
working-directory: desktop-app
run: |
VERSION="${GITHUB_REF_NAME#desktop-}"
STAGING="release-assets"
mkdir -p "$STAGING"

# Windows embedded single-file executable
WIN_EMBEDDED="dist/windows-embedded/markdown-viewer/markdown-viewer-win_x64.exe"
if [ -f "$WIN_EMBEDDED" ]; then
cp "$WIN_EMBEDDED" "$STAGING/markdown-viewer-win_x64.exe"
fi

# Portable ZIP bundle from neu build --release
for zip in dist/*.zip; do
[ -f "$zip" ] || continue
cp "$zip" "$STAGING/"
done

# Source archive
cd ..
tar -czf "desktop-app/$STAGING/source.tar.gz" \
--exclude='desktop-app/dist' \
--exclude='desktop-app/bin' \
--exclude='desktop-app/node_modules' \
--exclude='desktop-app/output' \
--exclude="desktop-app/$STAGING" \
--exclude='.git' \
desktop-app/
cd desktop-app

# SHA256 checksums
cd "$STAGING"
sha256sum * > SHA256SUMS.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "Markdown Viewer Desktop ${{ github.ref_name }}"
generate_release_notes: true
files: desktop-app/release-assets/*
31 changes: 18 additions & 13 deletions desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,33 @@ For more information, see the [Neutralinojs documentation](https://neutralino.js

### Building the app

**Default** Single-file executables with embedded resources:
**Default / Windows** - Single-file Windows executable with embedded resources:

```bash
npm run build
```

**Portable** ZIP bundle with separate `resources.neu` file:
**Portable** - ZIP bundle with separate `resources.neu` file:

```bash
npm run build:portable
```

**Both** Build embedded + portable in one step:
**Both** - Build the portable bundle and a Windows embedded EXE in one step:

```bash
npm run build:all
```

Build output is placed in `dist/`.
Build output is placed in `dist/`.

Note: `npm run build` now uses the Windows-only embedded helper and writes
`dist/markdown-viewer/markdown-viewer-win_x64.exe`. The helper temporarily hides
non-Windows Neutralino binaries so the CLI does not run out of memory while
embedding every platform before it reaches the Windows target. Use
`npm run build:portable` for the all-platform portable ZIP with `resources.neu`;
`npm run build:all` writes that ZIP plus a Windows embedded EXE at
`dist/windows-embedded/markdown-viewer/markdown-viewer-win_x64.exe`.

For more information, see the [Neutralinojs documentation](https://neutralino.js.org/docs/cli/neu-cli#neu-build).

Expand All @@ -83,15 +91,12 @@ Prebuilt binaries are automatically built and published as GitHub Releases when

Each release includes:

| Asset | Description |
| ----- | ----------- |
| `markdown-viewer-win_x64.exe` | Windows x64 executable |
| `markdown-viewer-linux_x64.tar.gz` | Linux x64 executable (tarball) |
| `markdown-viewer-linux_arm64.tar.gz` | Linux ARM64 executable (tarball) |
| `markdown-viewer-mac_*.tar.gz` | macOS executables (tarball) |
| `markdown-viewer-release.zip` | Portable bundle with `resources.neu` (all platforms) |
| `source.tar.gz` | Desktop app source archive |
| `SHA256SUMS.txt` | Checksums for all release assets |
| Asset | Description |
| ----- | ----------- |
| `markdown-viewer-win_x64.exe` | Windows x64 executable |
| `markdown-viewer-release.zip` | Portable bundle with `resources.neu` (all platforms) |
| `source.tar.gz` | Desktop app source archive |
| `SHA256SUMS.txt` | Checksums for all release assets |

## License

Expand Down
124 changes: 124 additions & 0 deletions desktop-app/build-windows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env node

/**
* Build a Windows-only embedded Neutralino executable.
*
* `neu build --embed-resources` embeds every platform binary it finds in bin/.
* With this app's offline libraries, embedding all platforms can exhaust Node's
* heap before the Windows binary is reached, leaving a stale Windows EXE in
* dist/. Temporarily hiding non-Windows binaries makes the CLI embed only the
* target users actually run on Windows.
*/

const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");

const APP_DIR = __dirname;
const BIN_DIR = path.join(APP_DIR, "bin");
const CONFIG_FILE = path.join(APP_DIR, "neutralino.config.json");
const WIN_BINARY = "neutralino-win_x64.exe";
const NEU_CLI = "@neutralinojs/neu@11.7.0";

function getArgValue(name) {
const prefix = `${name}=`;
for (let i = 2; i < process.argv.length; i += 1) {
const arg = process.argv[i];
if (arg === name) return process.argv[i + 1] || "";
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
}
return "";
}

function createConfigOverride(distributionPath) {
if (!distributionPath) return CONFIG_FILE;

const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
config.cli = config.cli || {};
config.cli.distributionPath = distributionPath.replace(/\\/g, "/");

const tmpDir = path.join(APP_DIR, ".tmp");
fs.mkdirSync(tmpDir, { recursive: true });

const tempConfigFile = path.join(tmpDir, `neutralino.windows.${process.pid}.config.json`);
fs.writeFileSync(tempConfigFile, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
return tempConfigFile;
}

function hideNonWindowsBinaries(tempDir) {
const hidden = [];
fs.mkdirSync(tempDir, { recursive: true });

for (const entry of fs.readdirSync(BIN_DIR, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith("neutralino-")) continue;
if (entry.name === WIN_BINARY) continue;

const from = path.join(BIN_DIR, entry.name);
const to = path.join(tempDir, entry.name);
fs.renameSync(from, to);
hidden.push({ from, to });
}

return hidden;
}

function restoreHiddenBinaries(hidden) {
for (let i = hidden.length - 1; i >= 0; i -= 1) {
const item = hidden[i];
if (fs.existsSync(item.to)) {
fs.renameSync(item.to, item.from);
}
}
}

function main() {
const windowsBinaryPath = path.join(BIN_DIR, WIN_BINARY);
if (!fs.existsSync(windowsBinaryPath)) {
console.error(`Missing ${WIN_BINARY}. Run npm run setup before building.`);
process.exit(1);
}

const distributionPath = getArgValue("--dist");
const tempDir = path.join(BIN_DIR, `.nonwin-disabled-${process.pid}`);
const configFile = createConfigOverride(distributionPath);
const hidden = hideNonWindowsBinaries(tempDir);
let exitCode = 0;

try {
const npx = "npx";
const args = [
"-y",
NEU_CLI,
"build",
"--embed-resources",
"--clean",
"--config-file",
configFile,
];

const result = spawnSync(npx, args, {
cwd: APP_DIR,
stdio: "inherit",
env: process.env,
shell: process.platform === "win32",
});

if (result.error) {
console.error(result.error.message);
exitCode = 1;
} else {
exitCode = result.status || 0;
}
} finally {
restoreHiddenBinaries(hidden);
fs.rmSync(tempDir, { recursive: true, force: true });
if (configFile !== CONFIG_FILE) {
fs.rmSync(configFile, { force: true });
}
}

process.exit(exitCode);
}

main();
10 changes: 7 additions & 3 deletions desktop-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
"predev": "npm run setup",
"dev": "npx -y @neutralinojs/neu@11.7.0 run",
"prebuild": "npm run setup",
"build": "npx -y @neutralinojs/neu@11.7.0 build --embed-resources",
"build:portable": "npx -y @neutralinojs/neu@11.7.0 build --release",
"build:all": "npm run build && npm run build:portable"
"build": "node build-windows.js",
"prebuild:windows": "npm run setup",
"build:windows": "node build-windows.js",
"prebuild:portable": "npm run setup",
"build:portable": "npx -y @neutralinojs/neu@11.7.0 build --release --clean",
"prebuild:all": "npm run setup",
"build:all": "npx -y @neutralinojs/neu@11.7.0 build --release --clean && node build-windows.js --dist dist/windows-embedded"
},
"dependencies": {}
}
Loading
Loading