diff --git a/.cargo/config.toml b/.cargo/config.toml index b86e955..5577111 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -12,6 +12,11 @@ rustflags = [ "-C", "link-arg=dynamic_lookup", ] +[target.'cfg(all(target_os = "windows", target_env = "msvc"))'] +# A static PDFium leaves its Windows API imports to the final link, including pdfium-render's +# own DLL link, which core/build.rs can't reach. Unused import libraries cost nothing. +rustflags = ["-l", "dylib=gdi32", "-l", "dylib=user32", "-l", "dylib=advapi32"] + # pdfium's renderer is not thread-safe; run all tests sequentially to avoid # heap corruption (SIGABRT) when multiple tests exercise pdfium concurrently. [env] diff --git a/.github/workflows/_php-binding-tests.yml b/.github/workflows/_php-binding-tests.yml index 4330ac0..7d248d2 100644 --- a/.github/workflows/_php-binding-tests.yml +++ b/.github/workflows/_php-binding-tests.yml @@ -50,9 +50,8 @@ jobs: uses: actions/cache@v6 with: path: core/.pdfium_cache - key: pdfium-${{ runner.os }}-${{ hashFiles('core/build.rs') }} - restore-keys: | - pdfium-${{ runner.os }}- + # Keyed on the release itself, with no fallback: an older release must never be restored. + key: pdfium-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('core/pdfium-release') }} - uses: extractions/setup-just@v4 diff --git a/.github/workflows/_php-static-check.yml b/.github/workflows/_php-static-check.yml new file mode 100644 index 0000000..5eb363a --- /dev/null +++ b/.github/workflows/_php-static-check.yml @@ -0,0 +1,202 @@ +name: PHP binding tests (static lib build) + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + php-binding-tests: + name: Test static PHP binding (${{ matrix.platform.pdfium_target }}, PHP ${{ matrix.php }} ${{ matrix.thread_safety }}) + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - { runner: ubuntu-latest, pdfium_target: linux-x64-glibc } + - { runner: ubuntu-24.04-arm, pdfium_target: linux-arm64-glibc } + - { runner: macos-latest, pdfium_target: mac-arm64 } + - { runner: macos-15-intel, pdfium_target: mac-x64 } + php: [ "8.1", "8.2", "8.3", "8.4", "8.5" ] + thread_safety: [ "zts", "nts" ] + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Download static PDFium + env: + GH_TOKEN: ${{ github.token }} + run: | + bash tools/pdfium-static/download.sh ${{ matrix.platform.pdfium_target }} "$RUNNER_TEMP/pdfium-static" + echo "PDFIUM_STATIC_LIB_PATH=$RUNNER_TEMP/pdfium-static" >> "$GITHUB_ENV" + + - uses: extractions/setup-just@v4 + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@1.99.0 + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + key: php-static-${{ matrix.platform.pdfium_target }}-php${{ matrix.php }}-${{ matrix.thread_safety }} + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + env: + phpts: ${{ matrix.thread_safety }} + + - name: Build and test (static PDFium) + run: just php test-static + + - name: Check the extension doesn't load PDFium dynamically (macOS) + if: runner.os == 'macOS' + run: | + deps="$(otool -L target/release/libbernard_ledit_php.dylib)" + echo "$deps" + if grep -qi pdfium <<< "$deps"; then + echo "::error::The extension links PDFium dynamically" + exit 1 + fi + + # The extension must load on the oldest glibc the static PDFium was built against. + - name: Check the extension doesn't load PDFium dynamically and runs on glibc 2.35 (Linux) + if: runner.os == 'Linux' + run: | + ext=target/release/libbernard_ledit_php.so + deps="$(ldd "$ext")" + echo "$deps" + if grep -qi pdfium <<< "$deps"; then + echo "::error::The extension links PDFium dynamically" + exit 1 + fi + newest_glibc="$(objdump -T "$ext" | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)*' | sed 's/GLIBC_//' | sort -Vu | tail -n 1)" + echo "Newest glibc symbol version required: $newest_glibc" + if [ "$(printf '%s\n' "$newest_glibc" 2.35 | sort -V | tail -n 1)" != 2.35 ]; then + echo "::error::The extension requires glibc $newest_glibc, newer than 2.35" + exit 1 + fi + + # Alpine runs through `docker run` rather than a job container: GitHub can't run JavaScript + # actions inside Alpine containers on arm64 runners. + php-binding-tests-musl: + name: Test static PHP binding (${{ matrix.platform.pdfium_target }}, PHP ${{ matrix.php }} ${{ matrix.thread_safety }}) + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - { runner: ubuntu-latest, pdfium_target: linux-x64-musl } + - { runner: ubuntu-24.04-arm, pdfium_target: linux-arm64-musl } + php: [ "8.1", "8.2", "8.3", "8.4", "8.5" ] + thread_safety: [ "zts", "nts" ] + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Download static PDFium + env: + GH_TOKEN: ${{ github.token }} + run: bash tools/pdfium-static/download.sh ${{ matrix.platform.pdfium_target }} .pdfium-static + + # Rust links the C runtime statically on musl by default, which a PHP extension must not do. + - name: Build and test in Alpine + env: + IMAGE: php:${{ matrix.php }}-${{ matrix.thread_safety == 'zts' && 'zts' || 'cli' }}-alpine + run: | + docker run --rm -v "$PWD:/w" -w /w \ + -e PDFIUM_STATIC_LIB_PATH=/w/.pdfium-static \ + -e RUSTFLAGS="-C target-feature=-crt-static" \ + "$IMAGE" sh -euc ' + apk add --no-cache bash git curl unzip build-base cmake nasm clang-dev linux-headers just rustup libstdc++ + rustup-init -y --default-toolchain none --profile minimal + . "$HOME/.cargo/env" + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + just php test-static + deps="$(ldd target/release/libbernard_ledit_php.so 2>/dev/null || true)" + echo "$deps" + if echo "$deps" | grep -qi pdfium; then + echo "The extension links PDFium dynamically" >&2 + exit 1 + fi + ' + + php-binding-tests-windows: + name: Test static PHP binding (${{ matrix.pdfium_target }}, PHP ${{ matrix.php }} ${{ matrix.thread_safety }}) + runs-on: windows-latest + env: + # ext-php-rs needs nightly on Windows (vectorcall ABI); this overrides rust-toolchain.toml. + RUSTUP_TOOLCHAIN: nightly + strategy: + fail-fast: false + matrix: + php: [ "8.1", "8.2", "8.3", "8.4", "8.5" ] + thread_safety: [ "zts", "nts" ] + # The extension must link with the MSVC toolset its PHP version was built with. + include: + - { php: "8.1", msvc: "14.29", pdfium_target: win-x64-v142 } + - { php: "8.2", msvc: "14.29", pdfium_target: win-x64-v142 } + - { php: "8.3", msvc: "14.29", pdfium_target: win-x64-v142 } + - { php: "8.4", msvc: "14.44", pdfium_target: win-x64-v143 } + - { php: "8.5", msvc: "14.44", pdfium_target: win-x64-v143 } + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Download static PDFium + env: + GH_TOKEN: ${{ github.token }} + run: | + bash tools/pdfium-static/download.sh ${{ matrix.pdfium_target }} "$RUNNER_TEMP/pdfium-static" + echo "PDFIUM_STATIC_LIB_PATH=$RUNNER_TEMP/pdfium-static" >> "$GITHUB_ENV" + + - uses: extractions/setup-just@v4 + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + key: php-static-${{ matrix.pdfium_target }}-php${{ matrix.php }}-${{ matrix.thread_safety }} + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + env: + phpts: ${{ matrix.thread_safety }} + + - name: Pin the MSVC toolset of the PHP build + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + toolset: ${{ matrix.msvc }} + + # Git Bash puts its GNU link.exe ahead of MSVC's on PATH, where Rust looks up the linker. + - name: Remove Git's GNU link.exe + run: rm -f "/c/Program Files/Git/usr/bin/link.exe" + + - name: Build and test (static PDFium) + run: just php test-static + + - name: Check the extension doesn't load PDFium dynamically + run: | + imports="$("/c/Program Files/LLVM/bin/llvm-readobj.exe" --coff-imports target/release/bernard_ledit_php.dll | grep -i '^ *Name:' || true)" + echo "$imports" + if grep -qi pdfium <<< "$imports"; then + echo "::error::The extension links PDFium dynamically" + exit 1 + fi diff --git a/.github/workflows/build-static-pdfium.yml b/.github/workflows/build-static-pdfium.yml new file mode 100644 index 0000000..30c0d7f --- /dev/null +++ b/.github/workflows/build-static-pdfium.yml @@ -0,0 +1,196 @@ +name: Static PDFium +on: + workflow_dispatch: + inputs: + force: + description: "Rebuild and overwrite targets that are already published" + type: boolean + default: false + push: + branches: [ add-php-ext-publish ] +permissions: + contents: write +# Runs queue instead of building and publishing the same targets side by side. +concurrency: + group: static-pdfium-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Targets live in tools/pdfium-static/targets.json. win-arm64 builds on windows-2022: the + # VS 2026 image behind windows-latest has no ARM64 libraries for the 14.44 toolset. + plan: + name: Find the targets to build + runs-on: ubuntu-latest + outputs: + release: ${{ steps.plan.outputs.release }} + targets: ${{ steps.plan.outputs.targets }} + steps: + - uses: actions/checkout@v6 + + # A target is up to date once both its library and its checksum are published. + - name: List the unpublished targets + id: plan + env: + GH_TOKEN: ${{ github.token }} + FORCE: ${{ inputs.force }} + run: | + release="pdfium-static-$(tr -d '[:space:]' < core/pdfium-release)" + release="${release//\//_}" + published="$(gh release view "$release" --json assets --jq '[.assets[].name]' 2>/dev/null || echo '[]')" + targets="$(jq -c --argjson published "$published" --arg force "$FORCE" ' + def is_published($name): $published | any(.[]; . == $name); + map(select( + "libpdfium-\(.target).\(if .os == "win" then "lib" else "a" end)" as $asset + | $force == "true" or ((is_published($asset) and is_published($asset + ".sha256")) | not) + ))' tools/pdfium-static/targets.json)" + echo "release=$release" >> "$GITHUB_OUTPUT" + echo "targets=$targets" >> "$GITHUB_OUTPUT" + echo "To build: $(jq -r 'map(.target) | join(", ")' <<< "$targets")" + + build: + name: Build ${{ matrix.target }} + needs: plan + if: needs.plan.outputs.targets != '[]' + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + # googlesource throttles parallel gclient syncs. + max-parallel: 2 + matrix: + include: ${{ fromJSON(needs.plan.outputs.targets) }} + defaults: + run: + shell: bash + env: + PDFIUM_OS: ${{ matrix.os }} + PDFIUM_CPU: ${{ matrix.cpu }} + PDFIUM_ENV: ${{ matrix.env }} + MSVC_TOOLSET: ${{ matrix.msvc }} + # Makes depot_tools use the runner's Visual Studio instead of Google's internal toolchain. + DEPOT_TOOLS_WIN_TOOLCHAIN: "0" + WORK_DIR: ${{ matrix.os == 'win' && 'C:/pdfium-static' || '/tmp/pdfium-static' }} + OUT_DIR: ${{ github.workspace }}/out + LIB_NAME: ${{ matrix.os == 'win' && 'pdfium.lib' || 'libpdfium.a' }} + ASSET: libpdfium-${{ matrix.target }}.${{ matrix.os == 'win' && 'lib' || 'a' }} + steps: + - uses: actions/checkout@v6 + + # Sources only: Ninja could reuse stale objects from a cached out/ after a toolchain change. + - name: Cache the PDFium sources + uses: actions/cache@v6 + with: + path: | + ${{ env.WORK_DIR }}/pdfium-binaries/pdfium + !${{ env.WORK_DIR }}/pdfium-binaries/pdfium/out + key: pdfium-src-${{ hashFiles('core/pdfium-release') }}-${{ matrix.target }} + + # Chromium's checkout has paths beyond Windows' default length limit. The runner's LLVM is the + # namespacer's fallback for tools that Chromium's LLVM package lacks. + - name: Prepare the Windows build + if: runner.os == 'Windows' + run: | + git config --global core.longpaths true + echo "C:/Program Files/LLVM/bin" >> "$GITHUB_PATH" + + - name: Set up Python (Windows) + if: runner.os == 'Windows' + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + # Retries the build when it fails on a network error, which googlesource returns under load. + - name: Build + run: | + max_attempts=5 + for attempt in $(seq "$max_attempts"); do + if bash tools/pdfium-static/build.sh 2>&1 | tee build.log; then + exit 0 + fi + if ! tail -n 60 build.log | grep -qE 'HTTP 5[0-9]{2}|RPC failed|Could not resolve host'; then + exit 1 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + delay=$((attempt * 120 + RANDOM % 60)) + echo "::warning::Network failure on attempt $attempt/$max_attempts, retrying in ${delay}s" + sleep "$delay" + fi + done + echo "::error::Network failures persisted after $max_attempts attempts" + exit 1 + + # The Linux build runs as root in Docker; the cache can only save files the runner user can read. + - name: Fix ownership (Linux) + if: runner.os == 'Linux' + run: sudo chown -R "$(id -u):$(id -g)" "$WORK_DIR" + + - name: Pin the target MSVC toolset (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.cpu == 'arm64' && 'amd64_arm64' || 'x64' }} + toolset: ${{ matrix.msvc }} + + # Links a throwaway DLL that pulls in the PDFium API bernard-ledit uses, with the target + # toolset's link.exe and runtime libraries. This entry-point-less link also leaves the C + # runtime's startup objects unresolved, so only symbols referenced from pdfium.lib count. + # The full path avoids Git Bash's GNU `link`. + - name: Check the library links with the target toolset (Windows) + if: runner.os == 'Windows' + env: + MACHINE: ${{ matrix.cpu == 'arm64' && 'ARM64' || 'X64' }} + run: | + link_exe="$(cygpath -u "$VCToolsInstallDir")/bin/Hostx64/$PDFIUM_CPU/link.exe" + [ -f "$link_exe" ] || { echo "::error::link.exe not found at $link_exe"; exit 1; } + MSYS_NO_PATHCONV=1 "$link_exe" /NOLOGO /DLL /NOENTRY "/MACHINE:$MACHINE" /OUT:linktest.dll \ + /INCLUDE:FPDF_InitLibraryWithConfig /INCLUDE:FPDF_LoadMemDocument64 \ + /INCLUDE:FPDF_RenderPageBitmap /INCLUDE:FPDFText_LoadPage \ + /INCLUDE:FPDFPage_GenerateContent /INCLUDE:FPDF_SaveAsCopy \ + "$(cygpath -w "$OUT_DIR/$LIB_NAME")" msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib \ + gdi32.lib user32.lib advapi32.lib > linktest.log 2>&1 || true + if grep -E '(^|[\\/])pdfium\.lib\(.*unresolved external symbol' linktest.log; then + echo "::error::pdfium.lib needs symbols that toolset $MSVC_TOOLSET doesn't provide" + exit 1 + fi + if grep -E 'fatal error LNK' linktest.log | grep -v LNK1120; then + cat linktest.log + echo "::error::The link test stopped before resolving pdfium.lib's symbols" + exit 1 + fi + echo "pdfium.lib links with toolset $MSVC_TOOLSET" + + # Matrix jobs publish in parallel, so each asset gets its own checksum file, and a job that + # loses the race to create the release uploads to the one its sibling created. + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + RELEASE: ${{ needs.plan.outputs.release }} + run: | + cp "$OUT_DIR/$LIB_NAME" "$ASSET" + if command -v sha256sum >/dev/null; then + sha256sum "$ASSET" > "$ASSET.sha256" + else + shasum -a 256 "$ASSET" > "$ASSET.sha256" + fi + if ! gh release view "$RELEASE" >/dev/null 2>&1; then + gh release create "$RELEASE" --prerelease --title "$RELEASE" \ + --notes "Static PDFium for bernard-ledit: a build input for CI and local static builds, not a product release." || + gh release view "$RELEASE" >/dev/null + fi + gh release upload "$RELEASE" "$ASSET" "$ASSET.sha256" --clobber + + - name: Upload the build log + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-log-${{ matrix.target }} + path: build.log + + # Tests the PHP extension against the published libraries, on the commit that built them. Also + # runs when every target was already published (build skipped), but not after a failed build. + test: + name: Test + needs: [ plan, build ] + if: ${{ !cancelled() && !failure() }} + permissions: + contents: read + uses: ./.github/workflows/_php-static-check.yml diff --git a/.gitignore b/.gitignore index 3fe52a8..3187ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ $RECYCLE.BIN/ # Tools .rtk -local_test \ No newline at end of file +local_test + +# Build +tools/pdfium-static/pdfium-static-namespaced/ diff --git a/Cargo.lock b/Cargo.lock index e9819e1..a21b50f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,11 +95,11 @@ dependencies = [ [[package]] name = "askama" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +checksum = "6024d73179f43f15ccd2b881bfea6fee7f3a46ec53f33b52210dea749ebebaa4" dependencies = [ - "askama_derive", + "askama_macros", "itoa", "percent-encoding", "serde", @@ -108,31 +108,42 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +checksum = "071ee5ebf2138e3ad180e0aacf6940c2cab5e6d8333741d9925c7bee2b153f39" dependencies = [ "askama_parser", "basic-toml", + "glob", "memchr", "proc-macro2", "quote", "rustc-hash", "serde", "serde_derive", - "syn 2.0.119", + "syn 3.0.9", +] + +[[package]] +name = "askama_macros" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643e1c7cbb6aec1d920332fe51a7c0d8219e273dcb8602db03f5263e4d16487b" +dependencies = [ + "askama_derive", ] [[package]] name = "askama_parser" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +checksum = "2c5ae75772275d268b03ab8bdccdd12117b6169ee23256942b34e46c9f476583" dependencies = [ - "memchr", + "rustc-hash", "serde", "serde_derive", - "winnow 0.7.15", + "unicode-ident", + "winnow", ] [[package]] @@ -156,7 +167,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror", + "thiserror 2.0.21", "v_frame", "y4m", ] @@ -209,16 +220,14 @@ dependencies = [ name = "bernard-ledit" version = "0.1.0" dependencies = [ - "cargo-husky", "flate2", "image", - "log", "mozjpeg", "pdfium-render", "serde", "tar", "test-case", - "thiserror", + "thiserror 2.0.21", "ureq", ] @@ -382,52 +391,57 @@ dependencies = [ ] [[package]] -name = "cargo-husky" -version = "1.5.0" +name = "cargo-platform" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] [[package]] name = "cargo-platform" -version = "0.1.9" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", + "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.14.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", - "cargo-platform", + "cargo-platform 0.1.9", "semver", "serde", "serde_json", + "thiserror 1.0.69", ] [[package]] name = "cargo_metadata" -version = "0.19.2" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", - "cargo-platform", + "cargo-platform 0.3.3", "semver", "serde", "serde_json", - "thiserror", + "thiserror 2.0.21", ] [[package]] name = "cc" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" dependencies = [ "find-msvc-tools", "jobserver", @@ -450,6 +464,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.45" @@ -498,9 +518,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", @@ -534,9 +554,12 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "constant_time_eq" -version = "0.4.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9" +dependencies = [ + "rustversion", +] [[package]] name = "convert_case" @@ -772,15 +795,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", -] - [[package]] name = "exr" version = "1.74.2" @@ -800,9 +814,9 @@ dependencies = [ [[package]] name = "ext-php-rs" -version = "0.16.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa5549b3473b5c9f66fdb47a888ec014083a426b197184c3dd174277d29d6ea" +checksum = "f1279870fe6585ac0ba42ed6af0c5c5477b2b7f44dc073d4a6586a5b4a9c4aa0" dependencies = [ "anyhow", "bindgen", @@ -817,7 +831,7 @@ dependencies = [ "once_cell", "parking_lot", "skeptic", - "thiserror", + "thiserror 2.0.21", "ureq", "zip", ] @@ -833,9 +847,9 @@ dependencies = [ [[package]] name = "ext-php-rs-derive" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c273fd80ca04dd6cdfe0cb6b48ec91bb7174bd3525c7b3549dc32d432d6879a" +checksum = "d31fcc898471e5ab9f4495a8772408f1219f2caa7e1dd36244598a76c0a868cf" dependencies = [ "convert_case 0.12.0", "darling", @@ -884,9 +898,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "flate2" @@ -916,9 +930,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "fs-err" -version = "2.11.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +checksum = "b5c95b673b8f6f7235229ae11c5642d81b04c2e64c1e2fb417bc0cf73ca45f29" dependencies = [ "autocfg", ] @@ -1151,9 +1165,9 @@ dependencies = [ [[package]] name = "inout" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f" dependencies = [ "hybrid-array", ] @@ -1214,7 +1228,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.21", "walkdir", "windows-link", ] @@ -1286,9 +1300,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libfuzzer-sys" @@ -1358,9 +1372,9 @@ dependencies = [ [[package]] name = "lzma-rust2" -version = "0.16.5" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" +checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb" dependencies = [ "sha2", ] @@ -1538,6 +1552,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -1824,9 +1850,9 @@ checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppmd-rust" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1" +checksum = "494b4379484fc92217cc7d9e3a532375b1d01a61889ec8333025dc3b0377af56" [[package]] name = "ppv-lite86" @@ -2057,7 +2083,7 @@ dependencies = [ "rand", "rand_chacha", "simd_helpers", - "thiserror", + "thiserror 2.0.21", "v_frame", "wasm-bindgen", ] @@ -2227,9 +2253,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -2253,9 +2279,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" dependencies = [ "windows-sys 0.61.2", ] @@ -2438,16 +2464,18 @@ checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "skeptic" -version = "0.13.7" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +checksum = "d82adb9b794e23afb98bb1c89fd84b1971dc74d07b476a3967a8c95b13c2f678" dependencies = [ "bytecount", - "cargo_metadata 0.14.2", - "error-chain", + "cargo_metadata 0.15.4", "glob", "pulldown-cmark", + "serde", + "serde_json", "tempfile", + "thiserror 1.0.69", "walkdir", ] @@ -2588,13 +2616,33 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -2644,18 +2692,18 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" dependencies = [ "pin-project-lite", ] [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" dependencies = [ "indexmap", "serde_core", @@ -2663,14 +2711,14 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] @@ -2681,7 +2729,7 @@ version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ - "winnow 1.0.4", + "winnow", ] [[package]] @@ -2692,9 +2740,9 @@ checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "typed-path" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c" [[package]] name = "typenum" @@ -2728,12 +2776,12 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "uniffi" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46eefd5468602930da46b1f49d3448c6dfc2e81295f93120f23f8174fd70267f" +checksum = "76407f5f396a2c949a069eff4a9fca9ce462410eb872bffef4c2b7b89804a77a" dependencies = [ "anyhow", - "cargo_metadata 0.19.2", + "cargo_metadata 0.23.1", "uniffi_bindgen", "uniffi_build", "uniffi_core", @@ -2743,14 +2791,14 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a0c9b375d32e1365cdb2bdd7cb495eecf6fac851ddbad077412b4ee1888514" +checksum = "3df3ced8f0eda3de99f6d50820e8628f443da8e4f447f477d1403ceba29dacbd" dependencies = [ "anyhow", "askama", "camino", - "cargo_metadata 0.19.2", + "cargo_metadata 0.23.1", "fs-err", "glob", "goblin", @@ -2769,9 +2817,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744fe15bcd3e2b1712a4573a45ce749af19cf28d69027ca5789619014955668c" +checksum = "925be5c36297fff0d416fa2f03585ace718b6e6c635466754128d00f0862d5f1" dependencies = [ "anyhow", "camino", @@ -2780,9 +2828,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec017b112701681f6fbbe5d92014b5c468eb0b177a94389de03ceec40665095" +checksum = "f7530ae8efeaaa488622865966763fe0294832779fff80508c36d69c6a874a6a" dependencies = [ "anyhow", "bytes", @@ -2792,9 +2840,9 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4641669b48fefbc5e80ff08c5004d9c7617fb91232131a6734ab6712779cb04c" +checksum = "0f4bad017164375d99450d70495014810d15a84ba9da6ed14b9628b1424223ba" dependencies = [ "anyhow", "indexmap", @@ -2805,9 +2853,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8617ee814de22caf7417bf514715ba0b3f46bd9d5a5d794413fd8282cb737" +checksum = "5b855a58da153739ce6e863f6e296edc3716a093768c4d3d957f17f5e4317c60" dependencies = [ "camino", "fs-err", @@ -2822,9 +2870,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58d5b94fc92803d21b2928bd15c6f06e57609b95caf98ea561c99cda1b6d2a25" +checksum = "af0c88664a9cb9559856f5a250283c9708923b303170e2e5c7db8f4ad65e9459" dependencies = [ "anyhow", "siphasher", @@ -2834,9 +2882,9 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "032739b3ec725576914c15899dedaf080163ced86b6934566c20ec2b20ce90ca" +checksum = "d00f5c044b4a3dca0b1226c92ddaeea626f337470e22180dd182160c87a06b0b" dependencies = [ "anyhow", "heck", @@ -2847,9 +2895,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a1d0a0252ce1af9e8ce78ba67ac0d8937fb2bedaf10cbddd43d3614d06ec6" +checksum = "55dfd9c778d6b39cb5acd541d9ff63431974035d2bad83c897b766a6ee3152ac" dependencies = [ "anyhow", "textwrap", @@ -2885,9 +2933,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" dependencies = [ "base64", "http", @@ -3035,18 +3083,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] @@ -3218,19 +3266,13 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -3281,9 +3323,9 @@ checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zip" -version = "8.6.0" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "aes", "bzip2", @@ -3296,6 +3338,7 @@ dependencies = [ "indexmap", "lzma-rust2", "memchr", + "nix", "pbkdf2", "ppmd-rust", "sha1", @@ -3332,27 +3375,27 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.3.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.1.1+zstd.1.5.7" +version = "2.1.2+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" dependencies = [ "cc", "pkg-config", diff --git a/bindings/dotnet/Cargo.toml b/bindings/dotnet/Cargo.toml index e5493a0..04439f0 100644 --- a/bindings/dotnet/Cargo.toml +++ b/bindings/dotnet/Cargo.toml @@ -18,10 +18,10 @@ crate-type = ["cdylib"] [dependencies] bernard-ledit = { path = "../../core", features = ["full"] } -uniffi = { version = "0.31.1" } +uniffi = { version = "0.32.1" } [build-dependencies] -uniffi = { version = "0.31.1", features = ["build"] } +uniffi = { version = "0.32.1", features = ["build"] } [lints] workspace = true diff --git a/bindings/php/Cargo.toml b/bindings/php/Cargo.toml index c645343..44b5e82 100644 --- a/bindings/php/Cargo.toml +++ b/bindings/php/Cargo.toml @@ -18,13 +18,16 @@ crate-type = ["cdylib"] [dependencies] bernard-ledit = { path = "../../core", features = ["full"] } -ext-php-rs = "=0.16.1" +ext-php-rs = "=0.16.3" [build-dependencies] -ext-php-rs = { version = "=0.16.1", features = ["closure"] } +ext-php-rs = { version = "=0.16.3", features = ["closure"] } [lints] workspace = true [features] -static-pdfium = ["bernard-ledit/static-pdfium"] +static-link = ["bernard-ledit/static-link"] +static-pdfium = ["static-link", "bernard-ledit/static-pdfium"] +static-pdfium-libcxx = ["static-link", "bernard-ledit/static-pdfium-libcxx"] +static-pdfium-msvc = ["static-link", "bernard-ledit/static-pdfium-msvc"] diff --git a/bindings/php/justfile b/bindings/php/justfile index 5801039..921502a 100644 --- a/bindings/php/justfile +++ b/bindings/php/justfile @@ -1,36 +1,16 @@ set shell := ["bash", "-c"] -# List the default tasks -[private] -default: - @just --list - -[private] -php-ext-dir: - #!/usr/bin/env bash - set -euo pipefail - php -r 'echo ini_get("extension_dir");' - ext_name := if os() == "macos" { "libbernard_ledit_php.dylib" } else if os() == "windows" { "bernard_ledit_php.dll" } else { "libbernard_ledit_php.so" } ext_debug := "../../target/debug/" + ext_name ext_release := "../../target/release/" + ext_name +# `php -n` skips php.ini, so the extensions the test suite needs are loaded explicitly. php_test_exts := "-d extension=dom -d extension=xml -d extension=mbstring -d extension=tokenizer -d extension=ctype -d extension=xmlwriter" -# Builds the PHP extension (debug, dynamic pdfium) - fast inner dev loop -build: - cargo build -p bernard-ledit-php +static_feature := if os() == "macos" { "static-pdfium-libcxx" } else if os() == "windows" { "static-pdfium-msvc" } else { "static-pdfium" } -# Builds the PHP extension in release mode (dynamic pdfium) -build-release: - cargo build -p bernard-ledit-php --release - -# Builds the PHP extension in release mode with pdfium statically linked. -# Requires PDFIUM_STATIC_LIB_PATH= (see php-bindings-solo-plan.md §7). -build-static: - cargo build -p bernard-ledit-php --release --features static-pdfium - -pdfium_release := replace(trim(read("../../core/pdfium-release")), "/", "_") +# Where core's build downloads the dynamic PDFium that debug builds load. +pdfium_release := replace(`tr -d '[:space:]' < ../../core/pdfium-release`, "/", "_") pdfium_target := if os() == "macos" { if arch() == "aarch64" { "mac-arm64" } else { "mac-x64" } } else if os() == "windows" { @@ -38,129 +18,132 @@ pdfium_target := if os() == "macos" { } else { if arch() == "aarch64" { "linux-arm64" } else { "linux-x64" } } +pdfium_dir := "../../core/.pdfium_cache/" + pdfium_release + "/" + pdfium_target +pdfium_lib := if os() == "macos" { "libpdfium.dylib" } else if os() == "windows" { "pdfium.dll" } else { "libpdfium.so" } -# Resolves the dynamically-linked libpdfium.so used by build/build-release, building core if missing. -# Not used by build-static, which has no sibling libpdfium.* to find. +# Lists the recipes. [private] -pdfium-path: - #!/usr/bin/env bash +default: + @just --list - set -euo pipefail - case "{{os()}}" in - macos) lib_name="libpdfium.dylib" ;; - windows) lib_name="pdfium.dll" ;; - *) lib_name="libpdfium.so" ;; - esac - dir="../../core/.pdfium_cache/{{pdfium_release}}/{{pdfium_target}}" - find_pdfium() { - [ -d "$dir" ] || return 0 - find "$dir" -name "$lib_name" 2>/dev/null | head -n 1 || true - } - PDFIUM_FILE=$(find_pdfium) - if [ -z "$PDFIUM_FILE" ]; then - just -f ../../justfile core build >&2 - PDFIUM_FILE=$(find_pdfium) - fi - if [ -z "$PDFIUM_FILE" ]; then - echo "Could not locate $lib_name for {{pdfium_release}}/{{pdfium_target}} under core/.pdfium_cache" >&2 - exit 1 - fi - realpath "$PDFIUM_FILE" +# Builds the extension in debug mode, against the dynamic PDFium. +build: + cargo build -p bernard-ledit-php -# Runs the debug build against a php script/flags with pdfium pre-wired. -# Usage: just run tests/smoke.php (for one-liners with special shell chars, use run-code instead) -run *ARGS: build - #!/usr/bin/env bash - set -euo pipefail - export PDFIUM_PATH="$(just pdfium-path)" - export EXT_DIR="$(just php-ext-dir)" - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} {{ARGS}} - -# Runs a single PHP expression (via `php -r`) against the debug build. Safely quotes CODE for you, -# so parens/quotes in the snippet don't need manual shell-escaping. -# Usage: just run-code 'var_dump(extension_loaded("bernard-ledit-php"));' -run-code CODE: build - #!/usr/bin/env bash - set -euo pipefail - export PDFIUM_PATH="$(just pdfium-path)" - export EXT_DIR="$(just php-ext-dir)" - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} -r {{quote(CODE)}} +# Builds the extension in release mode, against the dynamic PDFium. +build-release: + cargo build -p bernard-ledit-php --release + +# PDFIUM_STATIC_LIB_PATH is the directory of libpdfium.a (pdfium.lib on Windows), as saved by +# tools/pdfium-static/download.sh, or built by tools/pdfium-static/build.sh. + +# Builds the extension in release mode with PDFium linked in; needs PDFIUM_STATIC_LIB_PATH. +build-static: + cargo build -p bernard-ledit-php --release --features {{static_feature}} + +# Runs PHP with the debug build, e.g. `just php run tests/smoke.php`; see run-code for inline code. +run *ARGS: build (php-with "debug" ARGS) + +# Runs PHP code with the debug build, e.g. `just php run-code 'print_r(get_loaded_extensions());'`. +run-code CODE: build (php-with "debug" "-r " + quote(CODE)) -# Checks the module loads at all - the first thing to run when something seems broken. -# The module registers as `bernard_ledit` (see src/lib.rs). +# Checks that the debug build loads. smoke: build - #!/usr/bin/env bash - set -euo pipefail - export PDFIUM_PATH="$(just pdfium-path)" - export EXT_DIR="$(just php-ext-dir)" - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} -m | grep -i bernard.ledit + just php-with debug -m | grep -iE '^bernard[-_]ledit' -# Dumps the reflected classes/methods/functions of the compiled module - faster than reading stubs. -# Auto-detects the loaded module name since it currently differs from the final `bernard_ledit` -# (see the `smoke` note above). +# Prints the classes and functions the debug build registers. reflect: build #!/usr/bin/env bash set -euo pipefail - export PDFIUM_PATH="$(just pdfium-path)" - export EXT_DIR="$(just php-ext-dir)" - MODULE=$(php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} -m | grep -i bernard.ledit | head -n 1) - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} --re "$MODULE" - -# Runs phpunit (bindings/php test suite) against the debug build; installs composer deps on first run. -# NB: this is the binding-level suite (bindings/php/tests), not the mindee-api-php SDK suite. -test *ARGS: build - #!/usr/bin/env bash - set -euo pipefail - [ -d vendor ] || composer install - export PDFIUM_PATH="$(just pdfium-path)" - export EXT_DIR="$(just php-ext-dir)" - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_debug}} vendor/bin/phpunit {{ARGS}} - -# Runs phpunit against the static-pdfium release build with NO PDFIUM_PATH set and no libpdfium.so -# on disk - the only test that actually proves static linking works (ldd/dlopen checks lie, see -# php-bindings-solo-plan.md §7.2). Run this before trusting build-static. -test-static: build-static - #!/usr/bin/env bash - set -euo pipefail - [ -d vendor ] || composer install - unset PDFIUM_PATH - export EXT_DIR="$(just php-ext-dir)" - php -n -d extension_dir="$EXT_DIR" {{php_test_exts}} -d extension={{ext_release}} vendor/bin/phpunit + module="$(just php-with debug -m | grep -iE '^bernard[-_]ledit' | head -n 1)" + just php-with debug --re "$module" -# Regenerates the PHP stub file (bernard_ledit.stubs.php) for IDEs/phpstan from the compiled extension +# Runs the binding's PHPUnit suite against the debug build, e.g. `just php test --testdox`. +test *ARGS: composer-deps build (php-with "debug" "vendor/bin/phpunit " + ARGS) + +# Runs the PHPUnit suite against the static build with no PDFium to load: proves it's linked in. +test-static: composer-deps build-static (php-with "static" "vendor/bin/phpunit") + +# Regenerates the IDE and PHPStan stub file from the debug build. stubs: build bash tools/stubs/generate.sh "$(realpath {{ext_debug}})" "$PWD/bernard_ledit.stubs.php" -# Regenerate and check the committed IDE/PHPStan API snapshot. +# Regenerates the stub file and fails if it differs from the committed one. stubs-check: stubs git ls-files --error-unmatch bernard_ledit.stubs.php > /dev/null git diff --exit-code -- bernard_ledit.stubs.php -# Runs cargo clippy on the PHP bindings crate +# Runs PHPStan against the generated stubs, if Composer installed it. +phpstan: stubs + #!/usr/bin/env bash + set -euo pipefail + if [ -f vendor/bin/phpstan ]; then + vendor/bin/phpstan analyse + else + echo "phpstan isn't installed (composer require --dev phpstan/phpstan); skipping." >&2 + fi + +# Runs clippy on the binding crate. lint: cargo clippy -p bernard-ledit-php --all-targets --all-features -- -D warnings -# Runs cargo fmt on the PHP bindings crate +# Formats the binding crate. format: cargo fmt -p bernard-ledit-php -# Checks formatting without modifying files +# Checks the binding crate's formatting. format-check: cargo fmt -p bernard-ledit-php --check -# Runs PHPStan against the generated stubs. No-op with a warning until phpstan is composer-required. -phpstan: stubs - #!/usr/bin/env bash - set -euo pipefail - if [ -f vendor/bin/phpstan ]; then - vendor/bin/phpstan analyse - else - echo "phpstan not installed yet (composer require --dev phpstan/phpstan); skipping." >&2 - fi - -# Combined CI check: format, lint, build, test +# Runs the checks CI runs: formatting, lint, build and tests. ci: format-check lint build test -# Cleans build artifacts local to this binding (run root `just clean` for the shared cargo target) +# Removes the Composer dependencies; the root `just clean` removes the shared cargo target. clean: rm -rf vendor/ + +# Runs PHP without php.ini, with the test suite's extensions and one build of this extension: +# `debug` loads the dynamic PDFium, `static` has none to load. ARGS is pasted into the command line +# as shell code, so callers quote what needs quoting. +[private] +php-with MODE *ARGS: + #!/usr/bin/env bash + set -euo pipefail + case "{{MODE}}" in + debug) + PDFIUM_PATH="$(just pdfium-path)" + export PDFIUM_PATH + ext="{{ext_debug}}" + ;; + static) + unset PDFIUM_PATH + ext="{{ext_release}}" + ;; + *) + echo "unknown mode '{{MODE}}' (expected debug or static)" >&2 + exit 1 + ;; + esac + ext_dir="$(php -r 'echo ini_get("extension_dir");')" + php -n -d extension_dir="$ext_dir" {{php_test_exts}} -d extension="$ext" {{ARGS}} + +# Prints the path of the dynamic PDFium, downloading it through core's build if it's missing. +[private] +pdfium-path: + #!/usr/bin/env bash + set -euo pipefail + find_pdfium() { + find "{{pdfium_dir}}" -name "{{pdfium_lib}}" 2>/dev/null | head -n 1 || true + } + lib="$(find_pdfium)" + if [ -z "$lib" ]; then + just -f ../../justfile core build >&2 + lib="$(find_pdfium)" + fi + [ -n "$lib" ] || { echo "{{pdfium_lib}} not found in {{pdfium_dir}}" >&2; exit 1; } + realpath "$lib" + +# Installs the test suite's Composer dependencies on first use. +[private] +composer-deps: + [ -d vendor ] || composer install diff --git a/bindings/php/src/pdf/mod.rs b/bindings/php/src/pdf/mod.rs index a9fc851..5022d65 100644 --- a/bindings/php/src/pdf/mod.rs +++ b/bindings/php/src/pdf/mod.rs @@ -15,7 +15,7 @@ use ext_php_rs::{ use std::ffi::CStr; use std::sync::OnceLock; -#[cfg(not(feature = "static-pdfium"))] +#[cfg(not(feature = "static-link"))] use ext_php_rs::zend::ExecutorGlobals; pub const INI_PDFIUM_PATH: &CStr = c"bernard_ledit.pdfium_path"; @@ -56,12 +56,12 @@ pub(crate) fn ensure_pdfium() -> PhpResult<()> { .map_err(PhpException::from_class::) } -#[cfg(feature = "static-pdfium")] +#[cfg(feature = "static-link")] fn bind_pdfium() -> Result<(), bernard_ledit::pdf::PdfError> { bernard_ledit::pdf::initialize_static() } -#[cfg(not(feature = "static-pdfium"))] +#[cfg(not(feature = "static-link"))] fn bind_pdfium() -> Result<(), bernard_ledit::pdf::PdfError> { use bernard_ledit::pdf::{PdfError, initialize}; let ini = ExecutorGlobals::get().ini_values(); diff --git a/bindings/ruby/Cargo.toml b/bindings/ruby/Cargo.toml index 96aecd2..27fc2bd 100644 --- a/bindings/ruby/Cargo.toml +++ b/bindings/ruby/Cargo.toml @@ -18,10 +18,10 @@ crate-type = ["cdylib"] [dependencies] bernard-ledit = { path = "../../core", features = ["full"] } -uniffi = { version = "0.31.1" } +uniffi = { version = "0.32.1" } [build-dependencies] -uniffi = { version = "0.31.1", features = ["build"] } +uniffi = { version = "0.32.1", features = ["build"] } [lints] workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 13def10..2dad7fc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -24,14 +24,16 @@ default = [] pdf = [] image = [] full = ["pdf", "image"] -static-pdfium = ["pdf", "pdfium-render/static", "pdfium-render/libstdc++"] +static-link = ["pdf", "pdfium-render/static"] +static-pdfium = ["static-link", "pdfium-render/libstdc++"] # Linux (glibc, musl) +static-pdfium-libcxx = ["static-link", "pdfium-render/libc++"] # macOS +static-pdfium-msvc = ["static-link"] # Windows: MSVC links its C++ runtime itself [dependencies] serde = { version = "1.0.228", features = ["derive"] } pdfium-render = "0.9.4" thiserror = "2.0.18" image = "0.25.9" -log = "0.4.33" mozjpeg = "0.10.13" [build-dependencies] @@ -41,7 +43,6 @@ ureq = "3.3.0" [dev-dependencies] test-case = { version = "3.3" } -cargo-husky = { version = "1", default-features = false, features = ["user-hooks"] } [lints] workspace = true diff --git a/core/build.rs b/core/build.rs index 5e044c8..adf2fbb 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,120 +1,118 @@ -//! Build script for the `pdfium-render` crate. +//! Build script: fetches the `PDFium` shared library for dynamic builds. +//! Static builds (`static-pdfium*` features) link a prebuilt archive instead and skip all of this. use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; fn main() { - println!("cargo:rerun-if-changed=pdfium-release"); - let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let release_file = std::path::Path::new(&manifest).join("pdfium-release"); - println!("cargo:rerun-if-changed={}", release_file.display()); - let release = std::fs::read_to_string(&release_file) - .unwrap_or_else(|e| panic!("cannot read {}: {e}", release_file.display())) - .trim() - .to_owned(); - if env::var_os("CARGO_FEATURE_STATIC_PDFIUM").is_some() { + if is_static_build() { println!("cargo:rerun-if-env-changed=PDFIUM_STATIC_LIB_PATH"); + if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + for lib in ["gdi32", "user32", "advapi32"] { + println!("cargo:rustc-link-lib=dylib={lib}"); + } + } return; } - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - let target = env::var("TARGET").unwrap(); - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let target = env::var("TARGET").unwrap(); + let release = read_release(&manifest_dir); + let (os_arch, lib_name) = platform(&target); - let os_arch = match target.as_str() { - "x86_64-apple-darwin" => "mac-x64", - "aarch64-apple-darwin" => "mac-arm64", - "x86_64-unknown-linux-gnu" => "linux-x64", - "aarch64-unknown-linux-gnu" => "linux-arm64", - "x86_64-pc-windows-msvc" => "win-x64", - _ => panic!("Unsupported target architecture: {target}"), - }; - - let download_url = format!( - "https://github.com/bblanchon/pdfium-binaries/releases/download/{}/pdfium-{os_arch}.tgz", - release.replace('/', "%2F") - ); - - let safe_release_name = release.replace('/', "_"); let cache_dir = manifest_dir .join(".pdfium_cache") - .join(safe_release_name) + .join(release.replace('/', "_")) .join(os_arch); - - if let Err(e) = fs::create_dir_all(&cache_dir) { - panic!("Failed to create cache directory: {e}"); - } - - let lib_name = if target.contains("apple") { - "libpdfium.dylib" - } else if target.contains("linux") { - "libpdfium.so" + // Windows ships the DLL under bin/, the others under lib/. + let lib_subdir = if target.contains("windows") { + "bin" } else { - "pdfium.dll" + "lib" }; + let source_path = cache_dir.join(lib_subdir).join(lib_name); - let lib_dir = cache_dir.join("lib"); - let bin_dir = cache_dir.join("bin"); + ensure_downloaded(&release, os_arch, &cache_dir, &source_path); + println!("cargo:rustc-env=PDFIUM_PATH={}", source_path.display()); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + copy_library(&source_path, &out_dir.join(lib_name)); - let source_path = if target.contains("windows") { - bin_dir.join(lib_name) + let python_package_dir = manifest_dir.join("../bindings/python/bernard_ledit"); + if python_package_dir.is_dir() { + copy_library(&source_path, &python_package_dir.join(lib_name)); } else { - lib_dir.join(lib_name) - }; + println!( + "cargo:warning=Skipping Python PDFium bundling: {} does not exist", + python_package_dir.display() + ); + } +} - if source_path.exists() { +fn is_static_build() -> bool { + env::var_os("CARGO_FEATURE_STATIC_LINK").is_some() +} + +/// Reads the pinned pdfium-binaries tag (e.g. `chromium/8076`) from `pdfium-release`. +fn read_release(manifest_dir: &Path) -> String { + let release_file = manifest_dir.join("pdfium-release"); + println!("cargo:rerun-if-changed={}", release_file.display()); + fs::read_to_string(&release_file) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", release_file.display())) + .trim() + .to_owned() +} + +/// pdfium-binaries' platform name and the library file name for a Rust target triple. +fn platform(target: &str) -> (&'static str, &'static str) { + match target { + "x86_64-apple-darwin" => ("mac-x64", "libpdfium.dylib"), + "aarch64-apple-darwin" => ("mac-arm64", "libpdfium.dylib"), + "x86_64-unknown-linux-gnu" => ("linux-x64", "libpdfium.so"), + "aarch64-unknown-linux-gnu" => ("linux-arm64", "libpdfium.so"), + "x86_64-pc-windows-msvc" => ("win-x64", "pdfium.dll"), + _ => panic!("Unsupported target architecture: {target}"), + } +} + +/// Downloads and unpacks the release into `cache_dir` unless `library` is already there. +fn ensure_downloaded(release: &str, os_arch: &str, cache_dir: &Path, library: &Path) { + if library.exists() { println!( "cargo:warning=Using cached PDFium binary from: {}", - source_path.display() + library.display() ); - } else { - if cache_dir.exists() { - fs::remove_dir_all(&cache_dir).unwrap(); - } - fs::create_dir_all(&cache_dir).unwrap(); - - println!("cargo:warning=Downloading PDFium from: {download_url}"); - let response = ureq::get(&download_url) - .call() - .expect("Failed to download PDFium"); - let reader = response.into_body().into_reader(); - let tar = flate2::read::GzDecoder::new(reader); - let mut archive = tar::Archive::new(tar); - archive.unpack(&cache_dir).unwrap(); + return; } + if cache_dir.exists() { + fs::remove_dir_all(cache_dir).unwrap(); + } + fs::create_dir_all(cache_dir) + .unwrap_or_else(|e| panic!("Failed to create cache directory: {e}")); - let target_path = out_dir.join(lib_name); - println!("cargo:rustc-env=PDFIUM_PATH={}", source_path.display()); - let python_package_dir = manifest_dir.join("../bindings/python/bernard_ledit"); + let url = format!( + "https://github.com/bblanchon/pdfium-binaries/releases/download/{}/pdfium-{os_arch}.tgz", + release.replace('/', "%2F") + ); + println!("cargo:warning=Downloading PDFium from: {url}"); + let response = ureq::get(&url).call().expect("Failed to download PDFium"); + let tar = flate2::read::GzDecoder::new(response.into_body().into_reader()); + tar::Archive::new(tar).unpack(cache_dir).unwrap(); - if source_path.exists() { - fs::copy(&source_path, &target_path).unwrap_or_else(|e| { - panic!( - "Failed to copy PDFium library from {} to {}: {e}", - source_path.display(), - target_path.display() - ); - }); - if python_package_dir.is_dir() { - let bundled_pdfium_path = python_package_dir.join(lib_name); - fs::copy(&source_path, &bundled_pdfium_path).unwrap_or_else(|e| { - panic!( - "Failed to copy PDFium library from {} to {}: {e}", - source_path.display(), - bundled_pdfium_path.display() - ); - }); - } else { - println!( - "cargo:warning=Skipping Python PDFium bundling: {} does not exist", - python_package_dir.display() - ); - } - } else { + assert!( + library.exists(), + "PDFium extraction succeeded, but expected library was not found at: {}", + library.display() + ); +} + +/// Copies the `PDFium` library from `from` to `to`. +fn copy_library(from: &Path, to: &Path) { + fs::copy(from, to).unwrap_or_else(|e| { panic!( - "PDFium extraction succeeded, but expected library was not found at: {}", - source_path.display() - ); - } + "Failed to copy PDFium library from {} to {}: {e}", + from.display(), + to.display() + ) + }); } diff --git a/core/src/pdf/mod.rs b/core/src/pdf/mod.rs index 69087dc..689569c 100644 --- a/core/src/pdf/mod.rs +++ b/core/src/pdf/mod.rs @@ -44,7 +44,7 @@ pub fn is_initialized() -> bool { /// Binds `PDFium` from a shared library on disk. Does NOT work for `static-pdfium`. /// # Errors /// * `PdfiumError` If `PDFium` library binding fails. -#[cfg(not(feature = "static-pdfium"))] +#[cfg(not(feature = "static-link"))] pub fn initialize(library_path: &str) -> Result<(), PdfError> { if PDFIUM.get().is_some() { return Ok(()); @@ -54,7 +54,7 @@ pub fn initialize(library_path: &str) -> Result<(), PdfError> { Ok(()) } -#[cfg(not(feature = "static-pdfium"))] +#[cfg(not(feature = "static-link"))] pub(crate) fn pdfium() -> &'static Pdfium { PDFIUM.get_or_init(|| { let path = std::env::var("PDFIUM_PATH").unwrap_or_else(|_| env!("PDFIUM_PATH").to_string()); @@ -64,7 +64,7 @@ pub(crate) fn pdfium() -> &'static Pdfium { }) } -#[cfg(feature = "static-pdfium")] +#[cfg(feature = "static-link")] pub(crate) fn pdfium() -> &'static Pdfium { PDFIUM.get_or_init(|| { let bindings = Pdfium::bind_to_statically_linked_library() @@ -76,7 +76,7 @@ pub(crate) fn pdfium() -> &'static Pdfium { /// Binds the statically linked `PDFium`. Only exists with `static-pdfium`. /// # Errors /// * `PdfiumError` if binding fails. -#[cfg(feature = "static-pdfium")] +#[cfg(feature = "static-link")] pub fn initialize_static() -> Result<(), PdfError> { if PDFIUM.get().is_some() { return Ok(()); diff --git a/deny.toml b/deny.toml index 4d6081a..b29695e 100644 --- a/deny.toml +++ b/deny.toml @@ -6,6 +6,7 @@ ignore = [ # paste is unmaintained (RUSTSEC-2024-0436) but is a transitive dep via # rav1e -> ravif -> image with no available upgrade path. { id = "RUSTSEC-2024-0436", reason = "Transitive dep via rav1e/ravif/image; no upstream fix available" }, + { id = "RUSTSEC-2026-0374", reason = "basic-toml via askama_derive <- uniffi_bindgen (build-time only); no upstream fix yet" }, ] [licenses] @@ -40,6 +41,7 @@ highlight = "all" # Known exceptions for duplicate versions (populate as needed) skip = [ { name = "cargo_metadata" }, + { name = "cargo-platform" }, { name = "convert_case" }, { name = "getrandom" }, { name = "itertools" }, @@ -48,7 +50,8 @@ skip = [ { name = "nom" }, { name = "r-efi" }, { name = "syn" }, - { name = "winnow" }, + { name = "thiserror" }, + { name = "thiserror-impl" }, { name = "windows-sys" }, ] skip-tree = [] diff --git a/rust.just b/rust.just index cb4cc23..1318cad 100644 --- a/rust.just +++ b/rust.just @@ -30,14 +30,17 @@ check-deps: hack-check: cargo hack check -p bernard-ledit --feature-powerset --no-dev-deps -# Tests all feature combinations using nextest. -# `static-pdfium` is excluded: linking it needs a prebuilt libpdfium.a (PDFIUM_STATIC_LIB_PATH). +# Features that link PDFium statically: linking them needs a prebuilt static library +# (PDFIUM_STATIC_LIB_PATH), which build-static-pdfium.yml's tests provide. +static_features := "static-link,static-pdfium,static-pdfium-libcxx,static-pdfium-msvc" + +# Tests all feature combinations using nextest, except the static-linking ones. hack-test: - cargo hack nextest run -p bernard-ledit --feature-powerset --exclude-features static-pdfium + cargo hack nextest run -p bernard-ledit --feature-powerset --exclude-features {{static_features}} -# Tests the core Rust library in release mode (`static-pdfium` excluded, see hack-test) +# Tests all feature combinations in release mode, except the static-linking ones. hack-test-release: - cargo hack nextest run -p bernard-ledit --release --feature-powerset --exclude-features static-pdfium + cargo hack nextest run -p bernard-ledit --release --feature-powerset --exclude-features {{static_features}} # Test the core Rust library test: diff --git a/tools/pdfium-static/build.sh b/tools/pdfium-static/build.sh new file mode 100644 index 0000000..87d2b9e --- /dev/null +++ b/tools/pdfium-static/build.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Builds a static, JPEG-namespaced PDFium library for one target with bblanchon/pdfium-binaries: +# inside an ubuntu:22.04 container on Linux, natively on macOS and Windows. +# +# Environment (all optional unless stated): +# PDFIUM_OS target OS: linux (default), mac or win +# PDFIUM_CPU target CPU: x64 (default) or arm64 +# PDFIUM_ENV "musl" for a Linux musl build, empty otherwise +# PDFIUM_TAG pdfium-binaries tag; default: the contents of core/pdfium-release +# MSVC_TOOLSET required on Windows: MSVC toolset to compile with, e.g. 14.29 +# WORK_DIR checkout and build directory; default: /tmp/pdfium-static +# OUT_DIR output directory for the namespaced library; +# default: target/pdfium-static//-[-] +# MUSL_URL mirror of the musl cross toolchains (musl.cc is unreliable) +# SKIP_NAMESPACE set to 1 to stop after the raw PDFium build +set -euo pipefail + +readonly PDFIUM_BINARIES_URL="https://github.com/bblanchon/pdfium-binaries.git" +readonly DEFAULT_MUSL_URL="https://github.com/mindee/bernard-ledit/releases/download/musl-toolchains-gcc11.2.1-musl1.2.2-gb76f37fd" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" +namespacer="$script_dir/namespace_pdfium_jpeg.py" + +PDFIUM_OS="${PDFIUM_OS:-linux}" +PDFIUM_CPU="${PDFIUM_CPU:-x64}" +PDFIUM_ENV="${PDFIUM_ENV:-}" +PDFIUM_TAG="${PDFIUM_TAG:-$(tr -d '[:space:]' < "$repo_root/core/pdfium-release")}" +WORK_DIR="${WORK_DIR:-/tmp/pdfium-static}" +OUT_DIR="${OUT_DIR:-$repo_root/target/pdfium-static/${PDFIUM_TAG//\//_}/$PDFIUM_OS-$PDFIUM_CPU${PDFIUM_ENV:+-$PDFIUM_ENV}}" + +die() { + echo "error: $*" >&2 + exit 1 +} + +# In-place sed for both GNU sed (Linux, Git Bash) and BSD sed (macOS). +sedi() { + local file="${*: -1}" + sed -i.bak "$@" && rm -f "$file.bak" +} + +# Removes a directory, using sudo for files left behind by the root-owned build container. +remove_dir() { + rm -rf "$1" 2>/dev/null || sudo rm -rf "$1" +} + +# Checks the target and the tools before the long build, and sets PYTHON. +# Windows ships a python3 stub that opens the Microsoft Store, so it must use `python`. +validate_inputs() { + case "$PDFIUM_OS-$PDFIUM_ENV" in + linux- | linux-musl | mac- | win-) ;; + *) die "unsupported target: PDFIUM_OS='$PDFIUM_OS', PDFIUM_ENV='$PDFIUM_ENV'" ;; + esac + [ -f "$namespacer" ] || die "missing $namespacer" + if [ "$PDFIUM_OS" = linux ]; then + command -v docker >/dev/null || die "docker is required" + fi + if [ "$PDFIUM_OS" = win ]; then + [ -n "${MSVC_TOOLSET:-}" ] || die "MSVC_TOOLSET is required on Windows" + PYTHON="$(command -v python || true)" + else + PYTHON="$(command -v python3 || command -v python || true)" + fi + [ -n "$PYTHON" ] || die "python is required" +} + +# Checks out the pdfium-binaries tag matching the PDFium release: its patches target that exact +# PDFium revision. Initialises in place because a restored source cache may already contain pdfium/, +# and `checkout -f` discards our patches from a previous run. Leftovers from a failed gclient sync +# (_bad_scm) would break the next sync. +checkout_pdfium_binaries() { + mkdir -p "$binaries" + if [ ! -d "$binaries/.git" ]; then + git -C "$binaries" init --quiet + git -C "$binaries" remote add origin "$PDFIUM_BINARIES_URL" + fi + git -C "$binaries" fetch --tags --quiet origin + git -C "$binaries" checkout --quiet -f "$PDFIUM_TAG" + remove_dir "$binaries/_bad_scm" +} + +# Lowers gclient's parallel fetches (googlesource answers CI bursts with HTTP 503), and drops the +# self-test and packaging steps: the library we need is already staged by then. +patch_build_steps() { + sedi 's/gclient sync /gclient sync --jobs 4 /' "$binaries/steps/02-checkout.sh" + sedi '/09-test.sh/d; /10-pack.sh/d' "$binaries/build.sh" +} + +# Windows: appends two patches to pdfium-binaries' patch step. pdfium-binaries' checkout step +# resets these files beforehand, so the patches always apply to pristine sources. +# The first links against the DLL C runtime (/MD), like Rust and PHP, instead of Chromium's static +# runtime (/MT). The second compiles with MSVC_TOOLSET through vcvarsall.bat's -vcvars_ver, so the +# library links with the toolset of the PHP versions it targets. +patch_windows_toolchain() { + local crt_gn="$binaries/pdfium/build/config/win/BUILD.gn" + local toolchain_py="$binaries/pdfium/build/toolchain/win/setup_toolchain.py" + cat >> "$binaries/steps/03-patch.sh" <&2; exit 1; } +sed -i 's/configs = \[ ":static_crt" \]/configs = [ ":dynamic_crt" ]/' "$crt_gn" + +# bernard-ledit: compile with MSVC toolset $MSVC_TOOLSET +sed -i "/def _LoadEnvFromBat/!s/_LoadEnvFromBat(args)/_LoadEnvFromBat(args + ['-vcvars_ver=$MSVC_TOOLSET'])/" "$toolchain_py" +grep -q "vcvars_ver=$MSVC_TOOLSET" "$toolchain_py" || { echo "vcvarsall call not found in $toolchain_py" >&2; exit 1; } +EOF +} + +# Runs pdfium-binaries' build on the host (macOS, Windows). +# Flags must precede the positional arguments: build.sh parses them with getopts. +build_natively() { + (cd "$binaries" && ./build.sh -s -b "$PDFIUM_TAG" "$PDFIUM_OS" "$PDFIUM_CPU") +} + +# Runs pdfium-binaries' build inside ubuntu:22.04, whose glibc 2.35 becomes the oldest glibc the +# extension supports. Debconf is preseeded so that no package install opens a dialog, including +# those run through sudo, which drops DEBIAN_FRONTEND. PDFIUM_ENV, when set, is build.sh's optional +# third positional argument. +build_in_container() { + docker run --rm -v "$binaries:/w" -w /w \ + -e PDFIUM_TAG="$PDFIUM_TAG" -e PDFIUM_OS="$PDFIUM_OS" -e PDFIUM_CPU="$PDFIUM_CPU" \ + -e PDFIUM_ENV="$PDFIUM_ENV" -e MUSL_URL="${MUSL_URL:-$DEFAULT_MUSL_URL}" \ + ubuntu:22.04 bash -eu -c ' + export DEBIAN_FRONTEND=noninteractive + echo "debconf debconf/frontend select Noninteractive" | debconf-set-selections + apt-get update + apt-get install -y git curl python3 lsb-release sudo build-essential pkg-config file + ./build.sh -s -b "$PDFIUM_TAG" "$PDFIUM_OS" "$PDFIUM_CPU" $PDFIUM_ENV + ' +} + +# Windows: fails if the library still requests the static C runtime, i.e. the /MD patch didn't apply. +verify_windows_crt() { + local lib="$binaries/pdfium/out/obj/pdfium.lib" + [ -f "$lib" ] || die "the PDFium build produced no $lib" + if grep -Eaqi 'defaultlib:"?libcmt' "$lib"; then + die "pdfium.lib requests the static C runtime (libcmt); the /MD patch did not apply" + fi +} + +# Windows: fetches Chromium's llvm-nm, which matches the compiler that built the objects. The +# namespacer falls back to the LLVM on PATH if this download fails. +fetch_windows_llvm_tools() { + (cd "$binaries/pdfium" && "$PYTHON" tools/clang/scripts/update.py --package=objdump) || + echo "warning: could not fetch Chromium's LLVM tools; the namespacer will use the LLVM on PATH" >&2 +} + +main() { + validate_inputs + mkdir -p "$WORK_DIR" + binaries="$(cd "$WORK_DIR" && pwd)/pdfium-binaries" + + checkout_pdfium_binaries + patch_build_steps + case "$PDFIUM_OS" in + linux) + build_in_container + ;; + mac) + build_natively + ;; + win) + patch_windows_toolchain + build_natively + verify_windows_crt + fetch_windows_llvm_tools + ;; + esac + + if [ "${SKIP_NAMESPACE:-0}" = 1 ]; then + echo "Raw PDFium build left in $binaries" + return + fi + mkdir -p "$OUT_DIR" + "$PYTHON" "$namespacer" "$binaries" "$OUT_DIR" +} + +main "$@" diff --git a/tools/pdfium-static/download.sh b/tools/pdfium-static/download.sh new file mode 100644 index 0000000..f5e8a90 --- /dev/null +++ b/tools/pdfium-static/download.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Downloads the static PDFium library of one target from the repository's pdfium-static- +# GitHub release, checks it against its published SHA-256, and saves it under the name the linker +# looks for: libpdfium.a, or pdfium.lib for Windows targets. +# +# Usage: download.sh +# target a release asset target, e.g. linux-x64-glibc, mac-arm64, win-x64-v143 +# directory where to save the library; pass it to the build as PDFIUM_STATIC_LIB_PATH +# +# Run it from inside the repository, with an authenticated `gh` (GH_TOKEN in CI). +set -euo pipefail + +die() { + echo "error: $*" >&2 + exit 1 +} + +# Prints the SHA-256 of a file with whichever tool the platform has (macOS lacks sha256sum). +sha256_of() { + if command -v sha256sum >/dev/null; then + sha256sum < "$1" | cut -d' ' -f1 + else + shasum -a 256 < "$1" | cut -d' ' -f1 + fi +} + +main() { + [ $# -eq 2 ] || die "usage: $0 " + local target="$1" dir="$2" + local repo_root release asset linked_name + + repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" + release="pdfium-static-$(tr -d '[:space:]' < "$repo_root/core/pdfium-release")" + release="${release//\//_}" + case "$target" in + win-*) asset="libpdfium-$target.lib" linked_name="pdfium.lib" ;; + *) asset="libpdfium-$target.a" linked_name="libpdfium.a" ;; + esac + + mkdir -p "$dir" + gh release download "$release" --pattern "$asset" --pattern "$asset.sha256" --dir "$dir" --clobber + [ "$(sha256_of "$dir/$asset")" = "$(cut -d' ' -f1 "$dir/$asset.sha256")" ] || + die "$asset does not match its published SHA-256" + mv -f "$dir/$asset" "$dir/$linked_name" + rm -f "$dir/$asset.sha256" + echo "Saved $asset from $release as $dir/$linked_name" +} + +main "$@" diff --git a/tools/pdfium-static/namespace_pdfium_jpeg.py b/tools/pdfium-static/namespace_pdfium_jpeg.py new file mode 100644 index 0000000..61396e4 --- /dev/null +++ b/tools/pdfium-static/namespace_pdfium_jpeg.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""Prefix the libjpeg-turbo symbols bundled in a static PDFium library. + +PDFium and mozjpeg both contain libjpeg-turbo, so linking both into one binary clashes on symbols +such as jpeg_* and jsimd_*. This writes a copy of PDFium's archive in which its libjpeg-turbo +symbols are renamed to bernard_pdfium_, then checks that nothing else changed. + +ELF and Mach-O archives are rewritten with llvm-objcopy. COFF archives (Windows) are rewritten by +this script, because llvm-objcopy drops the symbol table of NASM-assembled COFF objects. +""" + +import argparse +from collections import Counter +from pathlib import Path +import shutil +import struct +import subprocess +import sys +import tempfile + +PREFIX = "bernard_pdfium_" +MARKER_SYMBOL = "jpeg_natural_order" +IS_WINDOWS = sys.platform == "win32" + +AR_MAGIC = b"!\n" +AR_HEADER_SIZE = 60 +AR_INDEX_MEMBERS = (b"/", b"//", b"/SYM64/") +COFF_MACHINES = (0x8664, 0xAA64, 0x14C) +COFF_BIGOBJ_CLASSID = bytes.fromhex("c7a1bad1eebaa94baf20faf66aa4dcb8") + + +def find_tool(name, bundled_dir): + """Return PDFium's bundled LLVM tool `name`, or the one on PATH.""" + bundled = bundled_dir / (f"{name}.exe" if IS_WINDOWS else name) + if bundled.is_file(): + return bundled + on_path = shutil.which(name) + if on_path: + return Path(on_path) + raise FileNotFoundError(f"{name} not found in {bundled_dir} or on PATH") + + +def read_symbols(nm, archives, defined_only=False): + """Count the (name, type) pairs of the external symbols in `archives`.""" + command = [str(nm), "--extern-only", "--format=posix"] + if defined_only: + command.append("--defined-only") + output = subprocess.check_output(command + [str(path) for path in archives], text=True) + counts = Counter() + for line in output.splitlines(): + if not line.strip() or line.endswith(":"): + continue + fields = line.split() + if len(fields) < 2 or len(fields[1]) != 1: + raise RuntimeError(f"Unexpected llvm-nm output: {line}") + counts[(fields[0], fields[1])] += 1 + return counts + + +def locate_inputs(build_root): + """Return the PDFium archive and libjpeg-turbo's component archives of a pdfium-binaries build. + + pdfium-binaries doesn't stage static Windows builds, so there the archive comes from the build + output. The component archives tell which symbols belong to libjpeg-turbo. + """ + if IS_WINDOWS: + source = build_root / "pdfium/out/obj/pdfium.lib" + else: + source = build_root / "staging/lib/libpdfium.a" + if not source.is_file(): + raise FileNotFoundError(f"PDFium archive not found: {source}") + jpeg_dir = build_root / "pdfium/out/obj/third_party/libjpeg_turbo" + jpeg_archives = sorted(jpeg_dir.glob("*.lib" if IS_WINDOWS else "*.a")) + if not jpeg_archives: + raise FileNotFoundError(f"No libjpeg-turbo archives in {jpeg_dir}") + return source, jpeg_archives + + +def build_symbol_map(nm, source, jpeg_archives, original): + """Map each libjpeg-turbo symbol defined in `source` to its prefixed name. + + Mach-O symbols start with an underscore, which stays in front so that the C name becomes + bernard_pdfium_ on every platform. + """ + defined = {name for name, _ in read_symbols(nm, [source], defined_only=True)} + jpeg = {name for name, _ in read_symbols(nm, jpeg_archives, defined_only=True)} + names = jpeg & defined + underscore = "_" if "_" + MARKER_SYMBOL in names else "" + if underscore + MARKER_SYMBOL not in names: + raise RuntimeError( + f"{MARKER_SYMBOL} is not defined in {source}: use the original archive " + "and the component archives of the same build" + ) + if any(name.removeprefix(underscore).startswith("FPDF") for name in names): + raise RuntimeError("The libjpeg-turbo archives unexpectedly define PDFium API symbols") + mapping = { + name: underscore + PREFIX + name.removeprefix(underscore) for name in sorted(names) + } + if set(mapping.values()) & {name for name, _ in original}: + raise RuntimeError(f"{source} already contains {PREFIX}* symbols") + return mapping + + +def coff_symbol_table(obj): + """Return (offset, symbol count, record size) of a COFF object's symbol table, or None.""" + if obj[:4] == b"\x00\x00\xff\xff": + is_bigobj = ( + len(obj) >= 56 + and struct.unpack_from("= 2 + and obj[12:28] == COFF_BIGOBJ_CLASSID + ) + if not is_bigobj: + return None + offset, count = struct.unpack_from("= 20 and struct.unpack_from("