mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
build(release): dynamic builder config + fix bundled-tool lookup in packages
- electron-builder config moves to electron-builder.config.js so markitdown bundling is conditional per platform (PyInstaller only builds for the host OS; a missing binary now logs a warning and ships without it instead of failing the build); package.json static build section removed, all npm scripts pointed at the config; third-party-licenses/ added to packaged files - FIX (pre-existing): packaged apps looked for bundled pandoc in resources/bin, but extraFiles land next to the executable (Contents/ on macOS) — packaged builds silently fell back to system pandoc since 4.5. New bundledToolDir() resolves the real location for pandoc + markitdown - download-tools.js pins the win32 pandoc.exe SHA-256 (fetched + verified) - sharp packaging test accepts sharp 0.35's versioned binding filename - release.yml: bundle-markitdown step (best-effort) on every OS and a new macOS job; release aggregates linux+windows+macos artifacts Local release artifacts built and verified (dist/): - MarkdownConverter-4.7.0.AppImage (363MB, pandoc+markitdown bundled, packaged app boots clean, tools resolve at the fixed path) - markdown-converter_4.7.0_amd64.deb (293MB) - MarkdownConverter-Setup-4.7.0.exe (223MB), portable exe (223MB), zip (305MB) — pandoc bundled; markitdown omitted (cannot cross-build), legal docs verified inside app.asar 637/637 tests green; lint clean.
This commit is contained in:
@@ -25,6 +25,13 @@ jobs:
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Bundle MarkItDown (optional, best effort)
|
||||
# Freezes markitdown + Python into bin/linux/markitdown; failure is
|
||||
# non-fatal — the build then ships without it and the app falls back
|
||||
# to a system markitdown at runtime.
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
@@ -59,6 +66,12 @@ jobs:
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Bundle MarkItDown (optional, best effort)
|
||||
# Windows runners ship python + venv; non-fatal on failure — the
|
||||
# package then omits the bundled binary by design
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
@@ -94,8 +107,44 @@ jobs:
|
||||
dist/*.zip
|
||||
retention-days: 5
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Bundle MarkItDown (optional, best effort)
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build macOS packages
|
||||
run: npm run build:mac -- --publish=never
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
retention-days: 5
|
||||
|
||||
release:
|
||||
needs: [build-linux, build-windows]
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -115,6 +164,13 @@ jobs:
|
||||
name: windows-artifacts
|
||||
path: dist
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* electron-builder configuration (replaces the static package.json "build"
|
||||
* section so it can be computed dynamically).
|
||||
*
|
||||
* Why a JS config: the bundled markitdown binary is OPTIONAL at build time —
|
||||
* PyInstaller can only build for the host OS, so e.g. a Windows package
|
||||
* built on a machine without bin/win32/markitdown.exe must simply omit it
|
||||
* (the app falls back to system-installed markitdown at runtime) instead of
|
||||
* failing on a missing extraFiles source.
|
||||
*
|
||||
* Everything else mirrors the previous static config verbatim.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/** Common extraFiles for a platform: pandoc always, markitdown when built. */
|
||||
function toolExtraFiles(platform) {
|
||||
const exe = platform === 'win32' ? '.exe' : '';
|
||||
const files = [{ from: `bin/${platform}/pandoc${exe}`, to: `bin/pandoc${exe}` }];
|
||||
const markitdown = `bin/${platform}/markitdown${exe}`;
|
||||
if (fs.existsSync(path.join(__dirname, markitdown))) {
|
||||
files.push({ from: markitdown, to: `bin/markitdown${exe}` });
|
||||
} else {
|
||||
console.warn(
|
||||
`[electron-builder.config] ${markitdown} not found — building WITHOUT the ` +
|
||||
'bundled markitdown (the app will use a system install if present). ' +
|
||||
'Run "npm run bundle:markitdown" on this platform to bundle it.'
|
||||
);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appId: 'com.concreteinfo.markdownconverter',
|
||||
productName: 'MarkdownConverter',
|
||||
copyright: 'Copyright (C) 2024-2025 ConcreteInfo',
|
||||
directories: { output: 'dist' },
|
||||
icon: 'assets/icon',
|
||||
files: [
|
||||
'src/**/*',
|
||||
'assets/**/*',
|
||||
'scripts/**/*',
|
||||
'node_modules/**/*',
|
||||
'package.json',
|
||||
'THIRD-PARTY-NOTICES.md',
|
||||
'SOURCES.md',
|
||||
'third-party-licenses/**/*',
|
||||
],
|
||||
asarUnpack: [
|
||||
'node_modules/ffmpeg-static/**',
|
||||
'node_modules/sharp/**',
|
||||
'node_modules/@img/**',
|
||||
'node_modules/@napi-rs/**',
|
||||
'assets/fonts/**',
|
||||
],
|
||||
fileAssociations: [
|
||||
{
|
||||
ext: 'md',
|
||||
name: 'Markdown Document',
|
||||
description: 'Markdown Document',
|
||||
mimeType: 'text/markdown',
|
||||
role: 'Editor',
|
||||
},
|
||||
{
|
||||
ext: 'markdown',
|
||||
name: 'Markdown Document',
|
||||
description: 'Markdown Document',
|
||||
mimeType: 'text/markdown',
|
||||
role: 'Editor',
|
||||
},
|
||||
{
|
||||
ext: 'pdf',
|
||||
name: 'PDF Document',
|
||||
description: 'PDF Document',
|
||||
mimeType: 'application/pdf',
|
||||
role: 'Editor',
|
||||
},
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.productivity',
|
||||
identity: null,
|
||||
extraFiles: toolExtraFiles('darwin'),
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{ target: 'nsis', arch: ['x64'] },
|
||||
{ target: 'portable', arch: ['x64'] },
|
||||
{ target: 'zip', arch: ['x64'] },
|
||||
],
|
||||
artifactName: '${productName}-${version}-${arch}.${ext}',
|
||||
requestedExecutionLevel: 'asInvoker',
|
||||
legalTrademarks: 'Copyright (C) 2024-2025 ConcreteInfo',
|
||||
verifyUpdateCodeSignature: false,
|
||||
signAndEditExecutable: false,
|
||||
extraFiles: toolExtraFiles('win32'),
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
displayLanguageSelector: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: 'MarkdownConverter',
|
||||
runAfterFinish: true,
|
||||
menuCategory: 'Productivity',
|
||||
license: 'LICENSE',
|
||||
warningsAsErrors: false,
|
||||
artifactName: '${productName}-Setup-${version}.${ext}',
|
||||
deleteAppDataOnUninstall: false,
|
||||
differentialPackage: true,
|
||||
},
|
||||
linux: {
|
||||
target: ['deb', 'AppImage', 'snap'],
|
||||
category: 'Utility',
|
||||
maintainer: 'ConcreteInfo <amit.wh@gmail.com>',
|
||||
extraFiles: toolExtraFiles('linux'),
|
||||
},
|
||||
deb: {
|
||||
depends: ['imagemagick', 'libreoffice-common'],
|
||||
description: 'Professional Markdown editor and universal file converter',
|
||||
maintainer: 'ConcreteInfo <amit.wh@gmail.com>',
|
||||
},
|
||||
};
|
||||
+10
-153
@@ -12,17 +12,17 @@
|
||||
"lint:fix": "eslint src tests --fix",
|
||||
"format": "prettier --write src tests",
|
||||
"format:check": "prettier --check src tests",
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx electron-builder --win",
|
||||
"build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
|
||||
"build": "electron-builder --config electron-builder.config.js",
|
||||
"build:win": "electron-builder --config electron-builder.config.js --win",
|
||||
"build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx electron-builder --config electron-builder.config.js --win",
|
||||
"build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config electron-builder.config.js --win",
|
||||
"create-cert": "powershell -ExecutionPolicy Bypass -File scripts/create-selfsigned-cert.ps1",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:linux-ci": "electron-builder --linux deb AppImage",
|
||||
"build:local": "electron-builder --linux --win",
|
||||
"dist": "electron-builder --publish=never",
|
||||
"dist:all": "electron-builder -mwl",
|
||||
"build:mac": "electron-builder --config electron-builder.config.js --mac",
|
||||
"build:linux": "electron-builder --config electron-builder.config.js --linux",
|
||||
"build:linux-ci": "electron-builder --config electron-builder.config.js --linux deb AppImage",
|
||||
"build:local": "electron-builder --config electron-builder.config.js --linux --win",
|
||||
"dist": "electron-builder --config electron-builder.config.js --publish=never",
|
||||
"dist:all": "electron-builder --config electron-builder.config.js -mwl",
|
||||
"download-tools": "node scripts/download-tools.js",
|
||||
"bundle:markitdown": "node scripts/bundle-markitdown.js",
|
||||
"generate-icons": "node scripts/generate-icons.js"
|
||||
@@ -99,148 +99,5 @@
|
||||
"lodash.pick": "npm:lodash@^4.17.21",
|
||||
"lodash-es": "^4.18.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.concreteinfo.markdownconverter",
|
||||
"productName": "MarkdownConverter",
|
||||
"copyright": "Copyright (C) 2024-2025 ConcreteInfo",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"icon": "assets/icon",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"assets/**/*",
|
||||
"scripts/**/*",
|
||||
"node_modules/**/*",
|
||||
"package.json",
|
||||
"THIRD-PARTY-NOTICES.md",
|
||||
"SOURCES.md"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/**",
|
||||
"node_modules/sharp/**",
|
||||
"node_modules/@img/**",
|
||||
"node_modules/@napi-rs/**",
|
||||
"assets/fonts/**"
|
||||
],
|
||||
"extraFiles": [],
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": "md",
|
||||
"name": "Markdown Document",
|
||||
"description": "Markdown Document",
|
||||
"mimeType": "text/markdown",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "markdown",
|
||||
"name": "Markdown Document",
|
||||
"description": "Markdown Document",
|
||||
"mimeType": "text/markdown",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "pdf",
|
||||
"name": "PDF Document",
|
||||
"description": "PDF Document",
|
||||
"mimeType": "application/pdf",
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"identity": null,
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/darwin/pandoc",
|
||||
"to": "bin/pandoc"
|
||||
},
|
||||
{
|
||||
"from": "bin/darwin/markitdown",
|
||||
"to": "bin/markitdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
"requestedExecutionLevel": "asInvoker",
|
||||
"legalTrademarks": "Copyright (C) 2024-2025 ConcreteInfo",
|
||||
"verifyUpdateCodeSignature": false,
|
||||
"signAndEditExecutable": false,
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/win32/pandoc.exe",
|
||||
"to": "bin/pandoc.exe"
|
||||
},
|
||||
{
|
||||
"from": "bin/win32/markitdown.exe",
|
||||
"to": "bin/markitdown.exe"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"displayLanguageSelector": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "MarkdownConverter",
|
||||
"runAfterFinish": true,
|
||||
"menuCategory": "Productivity",
|
||||
"license": "LICENSE",
|
||||
"warningsAsErrors": false,
|
||||
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"differentialPackage": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"deb",
|
||||
"AppImage",
|
||||
"snap"
|
||||
],
|
||||
"category": "Utility",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/linux/pandoc",
|
||||
"to": "bin/pandoc"
|
||||
},
|
||||
{
|
||||
"from": "bin/linux/markitdown",
|
||||
"to": "bin/markitdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"imagemagick",
|
||||
"libreoffice-common"
|
||||
],
|
||||
"description": "Professional Markdown editor and universal file converter",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ const PANDOC_VERSION = '3.9.0.2';
|
||||
*/
|
||||
const KNOWN_SHA256 = {
|
||||
'linux:pandoc': '7d124235998ecd3cdd9a463b1e5f6691a178b6461824c29a36170a0882f05597',
|
||||
'win32:pandoc.exe': 'e83f8354c0f507222b5684797b9c5ae766f03889785995d14aac27816ec456ba',
|
||||
// Fill these from a trusted machine after the first download of each
|
||||
// platform (the script prints the computed hash):
|
||||
// 'win32:pandoc.exe': '…',
|
||||
// 'darwin:pandoc': '…',
|
||||
'fonts:FiraCode-Regular.ttf': '3c79d234a9161c790410ebb2a80de7efb7c15f581062c130e0fa78503ccdd0da',
|
||||
'fonts:FiraCode-Bold.ttf': '975f26779fac1029c2cbdac1e9fac7e9ddeec05e064675e4aac63bffa121742f',
|
||||
|
||||
+17
-1
@@ -27,11 +27,27 @@ if (process.platform === 'win32') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory of the bundled external tools (pandoc/markitdown) in a PACKAGED
|
||||
* app. electron-builder extraFiles land next to the executable on Linux and
|
||||
* Windows, and inside Contents/ on macOS — NOT in resources/bin (which is
|
||||
* where the lookup incorrectly pointed before v4.7, making packaged builds
|
||||
* silently fall back to system-installed tools).
|
||||
*/
|
||||
function bundledToolDir() {
|
||||
if (process.platform === 'darwin') {
|
||||
// resourcesPath = …/Contents/Resources → tools live in …/Contents/bin
|
||||
return path.join(path.dirname(process.resourcesPath), 'bin');
|
||||
}
|
||||
// exePath = …/MarkdownConverter(.exe) → tools live in …/bin
|
||||
return path.join(path.dirname(process.execPath), 'bin');
|
||||
}
|
||||
|
||||
// Returns path to pandoc: bundled binary when packaged, dev bin or system fallback otherwise.
|
||||
function getPandocPath() {
|
||||
if (app.isPackaged) {
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
return path.join(process.resourcesPath, 'bin', `pandoc${ext}`);
|
||||
return path.join(bundledToolDir(), `pandoc${ext}`);
|
||||
}
|
||||
// Development: prefer locally-downloaded binary in bin/<platform>/
|
||||
const devBin = path.join(
|
||||
|
||||
+15
-8
@@ -24,25 +24,32 @@ const MAX_OUTPUT_BUFFER = 20 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Path to the bundled markitdown binary (built by scripts/bundle-markitdown.js
|
||||
* via PyInstaller), when the app ships one. Mirrors getPandocPath's layout:
|
||||
* dev: bin/<platform>/markitdown · packaged: <resourcesPath>/bin/markitdown.
|
||||
* Returns null when no bundle exists (PATH/python fallbacks apply).
|
||||
* via PyInstaller), when the app ships one. Layout matches getPandocPath:
|
||||
* dev: bin/<platform>/markitdown · packaged: <app dir>/bin/markitdown (next
|
||||
* to the executable on Linux/Windows, Contents/bin on macOS — the same
|
||||
* extraFiles destination). Returns null when absent (PATH/python fallbacks).
|
||||
*/
|
||||
function getBundledMarkItDownPath() {
|
||||
const pathUtil = require('path');
|
||||
const fs = require('fs');
|
||||
const exe = process.platform === 'win32' ? 'markitdown.exe' : 'markitdown';
|
||||
if (typeof process === 'object' && process.resourcesPath && !process.resourcesPath.includes('node_modules')) {
|
||||
// Packaged (Electron) — resourcesPath only exists in a real app runtime
|
||||
try {
|
||||
// Only meaningful under Electron; tests import this module under Node
|
||||
const electron = require('electron');
|
||||
if (electron.app?.isPackaged) {
|
||||
const packaged = pathUtil.join(process.resourcesPath, 'bin', exe);
|
||||
const base =
|
||||
process.platform === 'darwin'
|
||||
? pathUtil.dirname(electron.app.getPath('exe')) // …/Contents/MacOS
|
||||
: pathUtil.dirname(process.execPath);
|
||||
const packaged =
|
||||
process.platform === 'darwin'
|
||||
? pathUtil.join(base, '..', 'bin', exe) // …/Contents/bin
|
||||
: pathUtil.join(base, 'bin', exe);
|
||||
if (fs.existsSync(packaged)) return packaged;
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
/* not running under Electron (tests) — fall through to dev layout */
|
||||
}
|
||||
/* not running under Electron (tests) — use the dev layout below */
|
||||
}
|
||||
const dev = pathUtil.join(__dirname, '..', '..', 'bin', process.platform, exe);
|
||||
return fs.existsSync(dev) ? dev : null;
|
||||
|
||||
@@ -87,9 +87,18 @@ describe('Legal compliance artifacts', () => {
|
||||
expect(sources).toMatch(/https:\/\/ffmpeg\.org\/releases\//);
|
||||
});
|
||||
|
||||
test('packaging includes the legal documents in build.files', () => {
|
||||
test('packaging includes the legal documents in build files', () => {
|
||||
// The build config lives in electron-builder.config.js (dynamic — it
|
||||
// conditionally bundles the markitdown binary per platform)
|
||||
const configSrc = fs.readFileSync(
|
||||
path.join(rootDir, 'electron-builder.config.js'),
|
||||
'utf-8'
|
||||
);
|
||||
expect(configSrc).toContain("'THIRD-PARTY-NOTICES.md'");
|
||||
expect(configSrc).toContain("'SOURCES.md'");
|
||||
expect(configSrc).toContain('third-party-licenses');
|
||||
// And package.json must not carry a stale static build section anymore
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
|
||||
expect(pkg.build.files).toContain('THIRD-PARTY-NOTICES.md');
|
||||
expect(pkg.build.files).toContain('SOURCES.md');
|
||||
expect(pkg.build).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,8 +50,11 @@ function listFilesRecursively(dir) {
|
||||
);
|
||||
|
||||
test('sharp-linux-x64 ships its native binding', () => {
|
||||
const binding = path.join(UNPACKED_IMG_DIR, 'sharp-linux-x64', 'lib', 'sharp-linux-x64.node');
|
||||
expect(fs.existsSync(binding)).toBe(true);
|
||||
// sharp 0.35 names the binding with a version suffix
|
||||
// (sharp-linux-x64-0.35.4.node); 0.34 used a bare name — accept both
|
||||
const libDir = path.join(UNPACKED_IMG_DIR, 'sharp-linux-x64', 'lib');
|
||||
const bindings = fs.readdirSync(libDir).filter((f) => /^sharp-linux-x64.*\.node$/.test(f));
|
||||
expect(bindings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('sharp-libvips-linux-x64 ships the bundled libvips shared libraries', () => {
|
||||
|
||||
Reference in New Issue
Block a user