@@ -73,16 +73,19 @@ jobs:
7373 target : x86_64-unknown-linux-gnu
7474 bundles : deb,appimage
7575 compress : true
76+ pdfium_asset : pdfium-linux-x64
7677 - os : ubuntu-24.04-arm
7778 label : linux-aarch64
7879 target : aarch64-unknown-linux-gnu
7980 bundles : deb
8081 compress : true
82+ pdfium_asset : pdfium-linux-arm64
8183 - os : windows-latest
8284 label : windows-x86_64
8385 target : x86_64-pc-windows-msvc
8486 bundles : msi,nsis
8587 compress : true
88+ pdfium_asset : pdfium-win-x64
8689 # macOS x86_64 (Intel artifacts): CROSS-COMPILED on the Apple
8790 # Silicon runner instead of the Intel `macos-13` runner. Intel
8891 # macOS hosted runners are scarce / being phased down and jobs can
@@ -95,14 +98,40 @@ jobs:
9598 bundles : app,dmg
9699 compress : false
97100 rosetta : true
101+ pdfium_asset : pdfium-mac-x64
98102 - os : macos-14
99103 label : macos-aarch64
100104 target : aarch64-apple-darwin
101105 bundles : app,dmg
102106 compress : false
107+ pdfium_asset : pdfium-mac-arm64
103108 steps :
104109 - uses : actions/checkout@v4
105110
111+ # Download the PDFium shared library matching the TARGET arch (not the
112+ # runner — macOS x86_64 is cross-built on the arm64 runner). Staged into
113+ # the desktop's Tauri resources and exported for the CLI/MCP build + smoke.
114+ - name : Download + stage PDFium (fast PDF backend)
115+ shell : bash
116+ run : |
117+ set -euo pipefail
118+ url="https://github.com/bblanchon/pdfium-binaries/releases/latest/download/${{ matrix.pdfium_asset }}.tgz"
119+ curl -fsSL -o pdfium.tgz "$url"
120+ mkdir -p pdfium_extract && tar xzf pdfium.tgz -C pdfium_extract
121+ case "$RUNNER_OS" in
122+ Windows) src="pdfium_extract/bin/pdfium.dll"; libname="pdfium.dll" ;;
123+ macOS) src="pdfium_extract/lib/libpdfium.dylib"; libname="libpdfium.dylib" ;;
124+ *) src="pdfium_extract/lib/libpdfium.so"; libname="libpdfium.so" ;;
125+ esac
126+ test -f "$src" || { echo "::error::PDFium lib not found at $src"; exit 1; }
127+ # Bundle into the desktop app (Tauri resource glob pdfium/*).
128+ mkdir -p app/desktop/src-tauri/pdfium
129+ cp "$src" "app/desktop/src-tauri/pdfium/$libname"
130+ # Export for later steps (CLI/MCP build, smoke, packaging).
131+ echo "PDFIUM_LIB=$PWD/$src" >> "$GITHUB_ENV"
132+ echo "PDFIUM_LIBNAME=$libname" >> "$GITHUB_ENV"
133+ echo "staged PDFium ($libname) for ${{ matrix.label }}"
134+
106135 - name : Install Tauri system dependencies (Linux only)
107136 if : runner.os == 'Linux'
108137 run : |
@@ -140,9 +169,9 @@ jobs:
140169 run : cargo test --locked
141170
142171 # ---- build standalone CLI + MCP (release, native target) ------------
143- - name : Build CLI + MCP (release)
172+ - name : Build CLI + MCP (release, with PDFium )
144173 working-directory : app
145- run : cargo build --release --locked --target ${{ matrix.target }} -p markitdown-cli -p markitdown-mcp
174+ run : cargo build --release --locked --features pdfium -- target ${{ matrix.target }} -p markitdown-cli -p markitdown-mcp
146175
147176 # ---- compress the standalone binaries (UPX) -------------------------
148177 # The release profile already does LTO + strip + opt-level="s". UPX
@@ -182,7 +211,9 @@ jobs:
182211 # empty or errors, and asserts a non-format binary is still rejected.
183212 - name : Smoke + format-regression test on the released binary
184213 shell : bash
185- run : BIN="app/target/${{ matrix.target }}/release" bash .github/scripts/smoke.sh "${{ matrix.label }}"
214+ # MARKITDOWN_PDFIUM_LIB makes the smoke exercise the bundled PDFium
215+ # backend (and proves it loads on this OS/arch).
216+ run : MARKITDOWN_PDFIUM_LIB="$PDFIUM_LIB" BIN="app/target/${{ matrix.target }}/release" bash .github/scripts/smoke.sh "${{ matrix.label }}"
186217
187218 # ---- install frontend deps + build the Tauri desktop installers -----
188219 - name : Install frontend dependencies
@@ -205,12 +236,15 @@ jobs:
205236 mkdir -p out staging
206237 cp "app/target/$TRIPLE/release/markitdown$EXE" staging/
207238 cp "app/target/$TRIPLE/release/markitdown-mcp$EXE" staging/
239+ # Bundle the PDFium library next to the binaries so the fast PDF
240+ # backend is auto-discovered (next-to-exe). Keep them together.
241+ cp "$PDFIUM_LIB" "staging/$PDFIUM_LIBNAME"
208242 (
209243 cd staging
210244 if [[ "$RUNNER_OS" == "Windows" ]]; then
211- 7z a "../out/markitdown-cli-mcp-$LABEL.zip" "markitdown$EXE" "markitdown-mcp$EXE"
245+ 7z a "../out/markitdown-cli-mcp-$LABEL.zip" "markitdown$EXE" "markitdown-mcp$EXE" "$PDFIUM_LIBNAME"
212246 else
213- tar czf "../out/markitdown-cli-mcp-$LABEL.tar.gz" markitdown markitdown-mcp
247+ tar czf "../out/markitdown-cli-mcp-$LABEL.tar.gz" markitdown markitdown-mcp "$PDFIUM_LIBNAME"
214248 fi
215249 )
216250 # Desktop installers (only the types that exist for this platform).
@@ -254,14 +288,19 @@ jobs:
254288 if-no-files-found : error
255289
256290 # OPTIONAL Python fallback engine (OCR, transcription, Azure, plugins),
257- # packaged so users can enable `--engine auto` fallback without building it
258- # themselves. Best-effort: the whole job is `continue-on-error` so a flaky
259- # PyInstaller build NEVER blocks the lightweight Rust release. PyInstaller
260- # cannot cross-compile, so this covers the 4 natively-buildable targets;
261- # Intel macOS users build locally with python-engine/build_binary.sh (an
262- # Apple-Silicon Python binary can't run on Intel).
291+ # packaged for EVERY OS/arch so `--engine auto`/`python` works without users
292+ # building it. Covers Linux x86_64/aarch64, Windows x86_64, and macOS Apple
293+ # Silicon + Intel. PyInstaller cannot cross-compile, so each arch builds on
294+ # its own native runner (Intel macOS uses the scarce `macos-13` image).
295+ #
296+ # Robustness: this job `needs: release` and runs AFTER the main release is
297+ # already published, then APPENDS each binary to that release. Combined with
298+ # `continue-on-error`, a flaky or slow runner (e.g. a queued macos-13) can
299+ # NEVER block or delay the Rust/desktop release — the Python asset simply
300+ # shows up late, or not at all for that one arch.
263301 python-engine :
264302 name : python fallback (${{ matrix.label }})
303+ needs : release
265304 continue-on-error : true
266305 strategy :
267306 fail-fast : false
@@ -271,11 +310,16 @@ jobs:
271310 - { os: ubuntu-24.04-arm, label: linux-aarch64 }
272311 - { os: windows-latest, label: windows-x86_64 }
273312 - { os: macos-14, label: macos-aarch64 }
313+ - { os: macos-13, label: macos-x86_64 } # Intel; runner may be scarce
274314 runs-on : ${{ matrix.os }}
275315 steps :
276316 - uses : actions/checkout@v4
277317 - uses : actions/setup-python@v5
278318 with :
319+ # 3.12: a supported version with prebuilt wheels for markitdown[all]'s
320+ # native deps (magika/onnxruntime, lxml, numpy). MUST stay within the
321+ # 3.10–3.13 range enforced by app/python-engine/build_binary.sh, and
322+ # matches that script's preferred default so local and CI builds agree.
279323 python-version : " 3.12"
280324
281325 - name : Build the PyInstaller fallback binary
@@ -288,36 +332,44 @@ jobs:
288332 bash app/python-engine/build_binary.sh
289333 fi
290334
291- - name : Smoke test + stage the fallback binary
335+ - name : Smoke test + archive the fallback engine (onedir)
292336 shell : bash
293337 run : |
294338 set -euo pipefail
295339 EXE=""; [[ "$RUNNER_OS" == "Windows" ]] && EXE=".exe"
296- PY="app/python-engine/dist/markitdown-py$EXE"
297- test -f "$PY" || { echo "::error::fallback binary not produced"; exit 1; }
340+ # build_binary defaults to onedir: a folder at dist/markitdown-py/
341+ # whose launcher is dist/markitdown-py/markitdown-py[.exe].
342+ DIR="app/python-engine/dist/markitdown-py"
343+ PY="$DIR/markitdown-py$EXE"
344+ test -x "$PY" || { echo "::error::fallback engine not produced at $PY"; exit 1; }
298345 # It must actually convert a real document.
299346 "$PY" packages/markitdown/tests/test_files/test.docx > py_out.md
300347 grep -q AutoGen py_out.md
301348 mkdir -p out
302- cp "$PY" "out/markitdown-py-${{ matrix.label }}$EXE"
303- echo "fallback engine OK on ${{ matrix.label }} ($(du -h "$PY" | cut -f1))"
349+ # Archive the whole folder (onedir must travel together). Users extract
350+ # and point at the inner markitdown-py[.exe].
351+ if [[ "$RUNNER_OS" == "Windows" ]]; then
352+ (cd app/python-engine/dist && 7z a "$GITHUB_WORKSPACE/out/markitdown-py-${{ matrix.label }}.zip" markitdown-py >/dev/null)
353+ else
354+ tar czf "out/markitdown-py-${{ matrix.label }}.tar.gz" -C app/python-engine/dist markitdown-py
355+ fi
356+ echo "fallback engine OK on ${{ matrix.label }} ($(du -sh "$DIR" | cut -f1))"
304357
305- - name : Upload fallback binary
306- uses : actions/upload-artifact@v4
358+ - name : Append the fallback engine to the release
359+ uses : softprops/action-gh-release@v2
307360 with :
308- name : markitdown-py- ${{ matrix.label }}
309- path : out/*
310- if-no-files-found : error
361+ tag_name : ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
362+ files : out/markitdown-py-${{ matrix.label }}. *
363+ fail_on_unmatched_files : true
311364
312- # Gather every platform's artifacts and publish them as ONE GitHub Release.
313- # Runs both for `app-v*` tag pushes (normal release) AND for manual
314- # workflow_dispatch runs (publishes a prerelease named by the `tag` input),
315- # so the .dmg / installers / binaries are always visible on the Releases
316- # page — not buried under the Actions run's Artifacts. Includes the Python
317- # fallback binaries when their (best-effort) job produced any.
365+ # Gather the Rust + desktop artifacts and publish them as ONE GitHub Release.
366+ # Runs for `app-v*` tag pushes AND manual workflow_dispatch (prerelease named
367+ # by the `tag` input). Depends ONLY on `build` so it publishes promptly; the
368+ # Python fallback binaries are appended afterward by the `python-engine` job
369+ # (which `needs: release`), so a slow Python runner never delays the release.
318370 release :
319371 name : publish release
320- needs : [ build, python-engine]
372+ needs : build
321373 if : >-
322374 always() && needs.build.result == 'success' &&
323375 (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch')
@@ -377,10 +429,13 @@ jobs:
377429 - Windows: `*.msi` + `*-setup.exe`
378430
379431 **Optional Python fallback engine** (enables OCR / transcription /
380- Azure via `--engine auto` + `MARKITDOWN_PY_BIN`) — large, only needed
381- for those extras:
382- - `markitdown-py-{linux-x86_64,linux-aarch64,windows-x86_64,macos-aarch64}`
383- - Intel macOS: build locally with `app/python-engine/build_binary.sh`.
432+ Azure, and is much faster on very large PDFs) — large, only needed
433+ for those extras; appended shortly after the release is published:
434+ - `markitdown-py-<platform>.{tar.gz,zip}` for linux-x86_64,
435+ linux-aarch64, windows-x86_64, macos-aarch64, macos-x86_64.
436+ - It's a *folder* build (fast startup). Extract it, keep the folder
437+ intact, and point the desktop **Settings → Python engine** field
438+ (or `MARKITDOWN_PY_BIN`) at the inner `markitdown-py` executable.
384439
385440 - `SHA256SUMS.txt` — checksums for every asset.
386441
0 commit comments