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
35 changes: 14 additions & 21 deletions .github/workflows/desktop-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,23 @@ jobs:
- name: Stage release assets
working-directory: desktop-app
run: |
VERSION="${GITHUB_REF_NAME}"
STAGING="release-assets"
mkdir -p "$STAGING"

# Windows executable from the portable Neutralino bundle
WIN_EXE="dist/markdown-viewer/markdown-viewer-win_x64.exe"
if [ -f "$WIN_EXE" ]; then
cp "$WIN_EXE" "$STAGING/markdown-viewer-win_x64.exe"
fi

# Portable bundle directory from neu build --release
(cd dist && zip -r "../$STAGING/markdown-viewer-${VERSION}-portable.zip" markdown-viewer)

# 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
# Publish every platform as a separate self-contained download.
EXPECTED_BINARIES=(
markdown-viewer-linux_arm64
markdown-viewer-linux_armhf
markdown-viewer-linux_x64
markdown-viewer-mac_arm64
markdown-viewer-mac_universal
markdown-viewer-mac_x64
markdown-viewer-win_x64.exe
)
for filename in "${EXPECTED_BINARIES[@]}"; do
test -f "dist/markdown-viewer/$filename"
cp "dist/markdown-viewer/$filename" "$STAGING/$filename"
done

# SHA256 checksums
cd "$STAGING"
Expand Down
164 changes: 164 additions & 0 deletions desktop-app/build-standalone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env node

/**
* Build each Neutralino target as a self-contained executable.
*
* Embedding every target in one neu invocation can exhaust Node.js's heap when
* the application has many offline resources. Each target is therefore built
* separately, then copied into one flat dist/markdown-viewer directory.
*/

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 DIST_DIR = path.join(APP_DIR, "dist");
const FINAL_DIR = path.join(DIST_DIR, "markdown-viewer");
const TEMP_BUILD_ROOT = path.join(DIST_DIR, ".standalone-build");
const NEU_CLI = "@neutralinojs/neu@11.7.0";

const TARGETS = [
"linux_arm64",
"linux_armhf",
"linux_x64",
"mac_arm64",
"mac_universal",
"mac_x64",
"win_x64.exe",
].map((platform) => ({
sourceName: `neutralino-${platform}`,
outputName: `markdown-viewer-${platform}`,
buildKey: platform.replace(/\.exe$/, ""),
}));

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

const tmpDir = path.join(APP_DIR, ".tmp");
fs.mkdirSync(tmpDir, { recursive: true });
const configFile = path.join(
tmpDir,
`neutralino.${buildKey}.${process.pid}.config.json`,
);
fs.writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
return configFile;
}

function hideOtherBinaries(selectedSourceName, holdingDir) {
const hidden = [];
fs.mkdirSync(holdingDir, { 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 === selectedSourceName) continue;

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

return hidden;
}

function restoreBinaries(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 buildTarget(target) {
const sourcePath = path.join(BIN_DIR, target.sourceName);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Missing ${target.sourceName}. Run npm run setup first.`);
}

const targetDist = path.join(TEMP_BUILD_ROOT, target.buildKey);
const holdingDir = path.join(
BIN_DIR,
`.non-target-disabled-${target.buildKey}-${process.pid}`,
);
const configFile = createConfigOverride(targetDist, target.buildKey);
const hidden = hideOtherBinaries(target.sourceName, holdingDir);

try {
console.log(`\nBuilding standalone ${target.outputName}...`);
const result = spawnSync(
"npx",
[
"-y",
NEU_CLI,
"build",
"--embed-resources",
"--clean",
"--config-file",
configFile,
],
{
cwd: APP_DIR,
stdio: "inherit",
env: process.env,
shell: process.platform === "win32",
},
);

if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${target.outputName} build exited with code ${result.status}`);
}

const builtFile = path.join(
targetDist,
"markdown-viewer",
target.outputName,
);
if (!fs.existsSync(builtFile)) {
throw new Error(`Build succeeded but ${target.outputName} was not generated.`);
}

const finalFile = path.join(FINAL_DIR, target.outputName);
fs.copyFileSync(builtFile, finalFile);
fs.chmodSync(finalFile, 0o755);
} finally {
restoreBinaries(hidden);
fs.rmSync(holdingDir, { recursive: true, force: true });
fs.rmSync(configFile, { force: true });
}
}

function main() {
fs.rmSync(DIST_DIR, { recursive: true, force: true });
fs.mkdirSync(FINAL_DIR, { recursive: true });

try {
for (const target of TARGETS) buildTarget(target);
} finally {
fs.rmSync(TEMP_BUILD_ROOT, { recursive: true, force: true });
}

const outputs = fs.readdirSync(FINAL_DIR).sort();
if (outputs.length !== TARGETS.length) {
throw new Error(`Expected ${TARGETS.length} binaries, generated ${outputs.length}.`);
}

console.log(`\nGenerated ${outputs.length} standalone binaries in ${FINAL_DIR}:`);
for (const output of outputs) console.log(` ${output}`);
}

try {
main();
} catch (error) {
console.error(`\nStandalone build failed: ${error.message}`);
process.exit(1);
}
2 changes: 1 addition & 1 deletion desktop-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"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 --release --clean && node -e \"const fs = require('fs'); if (fs.existsSync('dist/markdown-viewer-release.zip')) fs.unlinkSync('dist/markdown-viewer-release.zip');\""
"build": "node build-standalone.js"
},
"dependencies": {}
}
6 changes: 5 additions & 1 deletion wiki/Desktop-App.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ npm run dev
npm run build
```

The current package script runs Neutralino release build with `--clean` and removes `dist/markdown-viewer-release.zip` if it exists. Generated files appear under `desktop-app/dist/`.
The build generates seven platform-specific executables under
`desktop-app/dist/markdown-viewer/`. Each executable is built separately with
`--embed-resources`, so every download is self-contained and does not require a
neighboring `resources.neu` file. Release publishing uploads the seven binaries
individually rather than creating a portable application ZIP.

## Runtime Configuration

Expand Down
6 changes: 5 additions & 1 deletion wiki/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ npm run setup
npm run dev
```

Build release resources and binaries with:
Build the seven self-contained platform binaries with:

```bash
npm run build
```

The results are written to `desktop-app/dist/markdown-viewer/`. Each Windows,
Linux, and macOS binary embeds the application resources, so users can download
only the file matching their operating system and architecture.

What setup does:

- Downloads Neutralino binaries.
Expand Down
Loading