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:
2026-09-05 22:30:54 +05:30
parent 58bd19ecd1
commit 1e24b52f3e
18 changed files with 2415 additions and 19 deletions
+43 -10
View File
@@ -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,
};