mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(legal): bundle small dependencies, add notices, credits, and GPL source offers
Bundle (v4.7.0): - MarkItDown frozen to a single ~75MB per-platform binary (PyInstaller via npm run bundle:markitdown; ML extras excluded) — built and verified locally: HTML/XLSX/PDF conversions pass through the bundled binary, and the app resolves bin/linux/markitdown first at runtime - Packaging copies bundled markitdown alongside Pandoc for win/mac/linux; FFmpeg/sharp/KaTeX/fonts were already bundled Legal artifacts: - THIRD-PARTY-NOTICES.md: complete license inventory of everything distributed (binaries, npm runtime deps, fonts, embedded Python packages) - SOURCES.md: GPL §3(b) written source offers for Pandoc 3.9.0.2, ffmpeg-static's GPL build, and the PyInstaller bootloader; LGPL relinking note for libvips - third-party-licenses/: canonical GPL-2.0, LGPL-2.1, MPL-2.0, Apache-2.0, OFL-1.1, PSF-Python texts - Help > Third-Party Notices & Licenses: in-app viewer for both documents - README: 'Bundled Dependencies, Legal Notices & Credits' section Hardening: - download-tools.js now SHA-256 pins every artifact, verifies after download AND against the cache on every run, and hard-fails on mismatch (closes security finding D6) Large tools intentionally not bundled (documented): LibreOffice, MiKTeX/TeX Live, ImageMagick, PlantUML+JRE, Calibre. 637/637 tests green; lint clean; clean boot; bundled binary verified.
This commit is contained in:
+68
@@ -1561,6 +1561,12 @@ function createMenu() {
|
||||
label: 'Dependencies & Requirements',
|
||||
click: () => showDependenciesDialog(),
|
||||
},
|
||||
{
|
||||
// Legal: bundled-component licenses + GPL source offer (in-app copy
|
||||
// of THIRD-PARTY-NOTICES.md and SOURCES.md)
|
||||
label: 'Third-Party Notices & Licenses',
|
||||
click: () => showThirdPartyNoticesWindow(),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
@@ -1685,6 +1691,68 @@ function showAboutDialog() {
|
||||
}
|
||||
|
||||
// Show Dependencies Dialog
|
||||
/**
|
||||
* In-app legal window: renders THIRD-PARTY-NOTICES.md and SOURCES.md (both
|
||||
* ship inside the app — dev: repo root, packaged: asar root via build.files).
|
||||
* The markdown is shown verbatim in a <pre> with light styling rather than
|
||||
* rendered, so license texts stay exactly as written.
|
||||
*/
|
||||
function showThirdPartyNoticesWindow() {
|
||||
const noticesWindow = new BrowserWindow({
|
||||
width: 820,
|
||||
height: 640,
|
||||
parent: mainWindow,
|
||||
title: 'Third-Party Notices & Licenses',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
noticesWindow.setMenuBarVisibility(false);
|
||||
|
||||
// Both files are read defensively so a packaging slip degrades gracefully
|
||||
const readDoc = (file) => {
|
||||
try {
|
||||
return fs.readFileSync(path.join(__dirname, '..', file), 'utf-8');
|
||||
} catch {
|
||||
return `(Could not read ${file} in this installation — see the source repository.)`;
|
||||
}
|
||||
};
|
||||
const esc = (s) =>
|
||||
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
const html =
|
||||
'<!doctype html><html><head><meta charset="utf-8"><title>Third-Party Notices</title><style>' +
|
||||
'body{margin:0;font:13px/1.6 system-ui,sans-serif;background:#fafafa;color:#222}' +
|
||||
'header{position:sticky;top:0;background:#fff;border-bottom:1px solid #e5e7eb;padding:10px 20px;display:flex;gap:16px;align-items:center;z-index:1}' +
|
||||
'header h1{font-size:15px;margin:0;flex:1}' +
|
||||
'header button{padding:6px 14px;cursor:pointer;border:1px solid #d1d5db;border-radius:4px;background:#fff}' +
|
||||
'header button.active{background:#4a90d9;color:#fff;border-color:#4a90d9}' +
|
||||
'pre{white-space:pre-wrap;word-break:break-word;padding:20px 24px;margin:0;font:12px/1.65 ui-monospace,Menlo,Consolas,monospace}' +
|
||||
'body.dark pre{background:#1f2937;color:#e5e7eb}body.dark{background:#111}' +
|
||||
'</style></head><body>' +
|
||||
'<header><h1>MarkdownConverter — Third-Party Notices</h1>' +
|
||||
'<button id="tab-notices" class="active">Notices</button>' +
|
||||
'<button id="tab-sources">Source Offers</button></header>' +
|
||||
`<pre id="content"></pre>` +
|
||||
'<script>' +
|
||||
'const notices=' +
|
||||
JSON.stringify(esc(readDoc('THIRD-PARTY-NOTICES.md'))) +
|
||||
';' +
|
||||
'const sources=' +
|
||||
JSON.stringify(esc(readDoc('SOURCES.md'))) +
|
||||
';' +
|
||||
'const c=document.getElementById("content");' +
|
||||
'c.textContent=notices;' +
|
||||
'document.getElementById("tab-notices").onclick=e=>{c.textContent=notices;swap(e)};' +
|
||||
'document.getElementById("tab-sources").onclick=e=>{c.textContent=sources;swap(e)};' +
|
||||
'function swap(e){document.querySelectorAll("header button").forEach(b=>b.classList.remove("active"));e.target.classList.add("active")}' +
|
||||
'</script></body></html>';
|
||||
|
||||
noticesWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
|
||||
}
|
||||
|
||||
function showDependenciesDialog() {
|
||||
const depsWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
|
||||
+43
-10
@@ -23,15 +23,47 @@ const CONVERT_TIMEOUT_MS = 120000;
|
||||
const MAX_OUTPUT_BUFFER = 20 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Candidate command templates probed in order. `argsPrefix` is prepended to
|
||||
* the user path when invoking (e.g. ['-m', 'markitdown'] for module-style
|
||||
* invocation through a python launcher).
|
||||
* 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).
|
||||
*/
|
||||
const COMMAND_CANDIDATES = [
|
||||
{ command: 'markitdown', argsPrefix: [] },
|
||||
{ command: process.platform === 'win32' ? 'python' : 'python3', argsPrefix: ['-m', 'markitdown'] },
|
||||
{ command: 'python3', argsPrefix: ['-m', 'markitdown'] },
|
||||
];
|
||||
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 {
|
||||
const electron = require('electron');
|
||||
if (electron.app?.isPackaged) {
|
||||
const packaged = pathUtil.join(process.resourcesPath, 'bin', exe);
|
||||
if (fs.existsSync(packaged)) return packaged;
|
||||
}
|
||||
} catch {
|
||||
/* not running under Electron (tests) — fall through to dev layout */
|
||||
}
|
||||
}
|
||||
const dev = pathUtil.join(__dirname, '..', '..', 'bin', process.platform, exe);
|
||||
return fs.existsSync(dev) ? dev : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidate command templates probed in order (bundled binary first). The
|
||||
* bundled candidate is verified with the same --version probe as the rest.
|
||||
*/
|
||||
function commandCandidates() {
|
||||
const bundled = getBundledMarkItDownPath();
|
||||
const candidates = [];
|
||||
if (bundled) candidates.push({ command: bundled, argsPrefix: [], bundled: true });
|
||||
candidates.push({ command: 'markitdown', argsPrefix: [] });
|
||||
candidates.push({
|
||||
command: process.platform === 'win32' ? 'python' : 'python3',
|
||||
argsPrefix: ['-m', 'markitdown'],
|
||||
});
|
||||
candidates.push({ command: 'python3', argsPrefix: ['-m', 'markitdown'] });
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** Run one probe: `--version` exits 0 when the tool is importable. */
|
||||
function probeCandidate(runner, candidate) {
|
||||
@@ -61,7 +93,7 @@ function probeCandidate(runner, candidate) {
|
||||
* @returns {Promise<{command: string, argsPrefix: string[], version: string|null}|null>}
|
||||
*/
|
||||
async function resolveMarkItDown(runner) {
|
||||
for (const candidate of COMMAND_CANDIDATES) {
|
||||
for (const candidate of commandCandidates()) {
|
||||
const resolved = await probeCandidate(runner, candidate);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
@@ -153,5 +185,6 @@ async function convertToMarkdown(inputPath, options = {}) {
|
||||
module.exports = {
|
||||
resolveMarkItDown,
|
||||
convertToMarkdown,
|
||||
COMMAND_CANDIDATES,
|
||||
commandCandidates,
|
||||
getBundledMarkItDownPath,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user