# Phase 0 — Infrastructure > Goal of Phase 0: **make the entire stack compile and run on Linux, macOS, and > Windows.** No editing, no rendering features. Success = the engine library > builds clean on all three platforms and the smoke test passes. Phase 1 does > not begin until Gate G0 and G0b are reached. ## Phase 0 task board | # | Task | Owner | This session | |---|------|-------|--------------| | 1 | CMake root + vcpkg + CI/CD pipeline | Dev 1 | **Done — scaffolded** | | 2 | PDFium build (depot_tools + GN + Ninja) | Dev 1 | Build scripts staged; not yet run | | 3 | Skia build integration | Dev 2 | Not started | | 4 | FreeType + HarfBuzz vcpkg integration | Dev 3 | In manifest; wrappers not started | | 5 | FastAPI service scaffolding | Dev 1 | Placeholder dir only | | 6 | React + TypeScript frontend scaffolding | Dev 2 | Placeholder dir only | | 7 | WASM hello-world build (Emscripten) | Dev 1 | Toolchain hook + preset stubbed | | 8 | Frozen interface contracts (Gate G0b) | All | Placeholder header; **not designed** | This session delivered **Task 1 in full** plus the repository structure for everything else. Scope was deliberately limited to the build pipeline — see "What is intentionally not done" below. ## What was built this session ``` Code/ ├── CMakeLists.txt root build; refuses to configure without a pinned baseline ├── CMakePresets.json debug/release/asan per platform + a wasm stub ├── vcpkg.json dependency manifest (freetype, harfbuzz, spdlog, gtest) ├── .clang-format .clang-tidy style + naming rules from blueprint §16.1 ├── .gitignore .gitattributes .editorconfig ├── cmake/ │ ├── pdfium.cmake turns the PDFium install tree into pdfium::pdfium │ ├── CompilerWarnings.cmake high warning levels per compiler │ ├── Sanitizers.cmake ASan/UBSan wiring │ └── toolchains/wasm.cmake Emscripten hook (stub) ├── engine/ │ ├── CMakeLists.txt │ ├── include/pdfengine/ public headers (version, umbrella, pdf_document placeholder) │ ├── src/core/ engine_info.cpp — version/build introspection │ ├── src/parser/ pdfium_loader — the ONLY FPDF_-allowed dir (Rule R2) │ └── tests/ gtest smoke test backing Gate G0 ├── third_party/pdfium/ from-source build scripts + pinned-ref file + args.gn ├── scripts/ │ ├── bootstrap.{sh,ps1} installs vcpkg, pins the dependency baseline │ └── check_pdfium_boundary.{sh,ps1} Rule R2 enforcement ├── .github/workflows/ci.yml Linux/macOS/Windows build matrix + lint jobs ├── bindings/ gateway/ frontend/ wasm/ corpus/ placeholder dirs with READMEs └── docs/phase0.md this file ``` ## How to build (developer onboarding) ### Prerequisites | Tool | Version | Notes | |------|---------|-------| | CMake | >= 3.25 | presets v6 | | Ninja | any recent | the only generator used | | C++ compiler | MSVC 19.36+ / GCC 13+ / Clang 16+ | needs C++23 | | vcpkg | — | `scripts/bootstrap` installs it if `VCPKG_ROOT` is unset | | Git | any recent | | | clang-format | 22.1.5 | not natively packaged on Windows — `pip install clang-format==22.1.5` (CI is pinned to this exact version) | > **Windows:** either Visual Studio 2022/2026 (with the *"Desktop development > with C++"* workload) or **Build Tools 2026** (no IDE — installer product > `Microsoft.VisualStudio.Product.BuildTools`) is supported. `cl.exe` is not on > `PATH` by default — run builds from a *Developer PowerShell* or import > `VC\Auxiliary\Build\vcvars64.bat` first. Build Tools is not a default > `vswhere` product, so detection scripts need `vswhere -products *` (the > PDFium build script already does this). CI uses `ilammy/msvc-dev-cmd`. > > `VCPKG_ROOT` set via `setx` (or the bootstrap script's persistent install) > does **not** propagate into already-open shells — set `$env:VCPKG_ROOT` > explicitly in that shell, or open a new terminal. PowerShell 5.1 is fine; > `pwsh` (7+) is not required by anything in this repo. ### Steps ```sh # 1. One-time setup — checks tools, installs vcpkg, pins the dependency baseline. pwsh scripts/bootstrap.ps1 # Windows ./scripts/bootstrap.sh # Linux / macOS # 2. Configure + build + test. cmake --preset windows-debug # linux-debug | macos-debug cmake --build --preset windows-debug ctest --preset windows-debug ``` The first configure compiles the vcpkg dependencies (freetype, harfbuzz, spdlog, gtest) — slow once, cached after. > **Verifying everything at once:** use `scripts/test_phase0.{ps1,sh}` to run > the R2 boundary check, engine, gateway, and WASM smoke test as a single > command and get one pass/fail summary. See > ["Verifying your setup"](#verifying-your-setup--scriptstest_phase0ps1sh) below. ### Building with PDFium PDFium is built separately from source (Task 2): ```sh # Pin the revision first — edit third_party/pdfium/pdfium.pinned (see its README). pwsh third_party/pdfium/build_pdfium.ps1 # or .sh ``` Until then the engine builds with PDFium code paths `#ifdef`-ed out, which is the correct Phase 0 default — it keeps the pipeline green while Task 2 runs. On Linux/macOS, enabling PDFium is a single flag added to a debug build: ```sh cmake --preset linux-debug -DPDFENGINE_WITH_PDFIUM=ON ``` On Windows there is more to it — see the next section. ### Windows + PDFium PDFium's static-lib GN build forces the static CRT (`/MT`, `is_debug=false`) and offers no knob for "static lib + dynamic CRT". The engine, vcpkg deps, and PDFium must therefore all use the same static CRT, or the link dies with `LNK2038: 'RuntimeLibrary' mismatch`. The repo is wired for this ("Option A, all static CRT, release-flavored"): - The hidden `windows-base` preset in [CMakePresets.json](../CMakePresets.json) sets `VCPKG_TARGET_TRIPLET=x64-windows-static` (vcpkg deps as static lib + static CRT — first configure rebuilds them, ~7 min one-time) and `CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$:Debug>`. - PDFium's own [args.gn](../third_party/pdfium/args.gn) keeps `is_debug=false` (i.e. `/MT`). - The PDFium-linked engine build must be **RelWithDebInfo, not Debug** — a Debug engine is `/MTd` and still mismatches PDFium's `/MT`. The plain `windows-debug -DPDFENGINE_WITH_PDFIUM=ON` recipe will not link; use the `win-local-pdfium` user preset below. #### `win-local-pdfium` user preset `CMakeUserPresets.json` is git-ignored (build dirs are per-developer and live outside OneDrive). Drop this preset in at `Code/CMakeUserPresets.json`, with your own username in `binaryDir`: ```json { "version": 6, "configurePresets": [ { "name": "win-local-pdfium", "inherits": "windows-release", "binaryDir": "C:/Users//pdfeng-build/win-local-pdfium", "cacheVariables": { "PDFENGINE_WITH_PDFIUM": "ON" } } ], "buildPresets": [ { "name": "win-local-pdfium", "configurePreset": "win-local-pdfium" } ], "testPresets": [ { "name": "win-local-pdfium", "inherits": "common", "configurePreset": "win-local-pdfium" } ] } ``` Then, from a shell with `vcvars64.bat` imported: ```powershell cmake --preset win-local-pdfium cmake --build --preset win-local-pdfium ctest --preset win-local-pdfium ``` Success looks like `pdfengine_smoke.exe` linking cleanly and logging `pdfium=on`. The build dir is intentionally outside OneDrive and on a space-free path — see the OneDrive section below for why. #### Gotcha: depot_tools shadows `ninja` The PDFium build adds `depot_tools` to `PATH` and the depot_tools `ninja` / `ninja.bat` are not real Ninja — they fail with: ``` Running ninja --version failed with unknown error ... CMAKE_CXX_COMPILER not set, after EnableLanguage ``` If `depot_tools` ended up on your persistent `PATH`, CMake will pick its broken `ninja` for the engine build. Two fixes: - **Short-term:** point CMake at the real Ninja explicitly, e.g. `cmake --preset win-local-pdfium -D CMAKE_MAKE_PROGRAM=C:/path/to/real/ninja.exe`. This caches, so only the first configure needs the flag. - **Long-term (recommended):** keep `depot_tools` **off** the persistent `PATH`. `third_party/pdfium/build_pdfium.ps1` already prepends it per-run, so the PDFium build still works. ## Verifying your setup — `scripts/test_phase0.{ps1,sh}` After bootstrap, this aggregator runs every Phase 0 check in sequence and prints one pass/fail summary. It is the local mirror of [.github/workflows/ci.yml](../.github/workflows/ci.yml) — new teammates should use it as the "did I set everything up right?" one-liner. Pieces it runs, in order: 1. **Rule R2 boundary** — `scripts/check_pdfium_boundary.*` 2. **Engine** — `cmake --preset … --build --preset … ctest --preset …` 3. **Gateway** — `pip install -e .[dev]` + ruff (lint + format check) + pytest 4. **WASM hello-world** — `cmake --preset wasm` + `node wasm/hello.test.mjs` A piece **skips** (not fails) when its toolchain is absent — no Emscripten on `PATH` skips WASM, no `python` skips the gateway, etc. Each piece is independent: one failure does not abort later pieces. Exit code is non-zero only if a piece genuinely **failed**, so you can pipe it into CI. ### Windows `vcvars64.bat` must be active in the shell (or use *Developer PowerShell for VS*) and `VCPKG_ROOT` must be set: ```powershell ./scripts/test_phase0.ps1 -Preset win-local # debug, no PDFium - fastest ./scripts/test_phase0.ps1 -Preset win-local-pdfium # release + PDFium - full ``` Both presets live in your local `CMakeUserPresets.json` (template in the "Windows + PDFium" section above), with `binaryDir` pointed outside OneDrive on a space-free path. Other useful flags: `-BinaryDir ` (override the preset's binaryDir), `-SkipEngine` / `-SkipGateway` / `-SkipWasm` (skip a piece explicitly). ### Linux / macOS ```sh ./scripts/test_phase0.sh # auto-picks linux-debug or macos-debug ./scripts/test_phase0.sh linux-asan # different preset SKIP_WASM=1 ./scripts/test_phase0.sh # if Emscripten not installed PHASE0_BINARY_DIR=/tmp/build ./scripts/test_phase0.sh # override binaryDir ``` Compatible with bash 3.2 (macOS default), so no `brew install bash` needed. ### Gotcha: stale build dir after a triplet switch If a build dir was previously configured against the dynamic (`x64-windows`) vcpkg triplet and you re-run against the static (`x64-windows-static`) one — which is what the `windows-base` preset now pins for the static-CRT story — vcpkg correctly purges the old libs, but ninja's build graph still references them. You will see: ``` ninja: error: 'vcpkg_installed/x64-windows/debug/lib/harfbuzz.lib' ... missing ``` Fix by regenerating from scratch: ```powershell cmake --preset win-local --fresh # or whichever preset ./scripts/test_phase0.ps1 -Preset win-local ``` `--fresh` is the cleanest option (CMake ≥ 3.24); it preserves the build dir but invalidates the cache so all targets are re-resolved. Alternatively, delete the build dir and reconfigure. ## Dependency pinning The blueprint rule is *"pin all dependency versions on Day 1, never track rolling HEAD."* Two mechanisms: - **vcpkg deps** — `scripts/bootstrap` runs `vcpkg x-update-baseline --add-initial-baseline`, which writes a `builtin-baseline` commit into `vcpkg.json`. That pins the entire dependency registry to one commit. The root `CMakeLists.txt` **refuses to configure** until this is present. → **The first commit to the repo must include the bootstrapped `vcpkg.json`**, otherwise CI fails at the configure step (by design). - **PDFium** — `third_party/pdfium/pdfium.pinned` holds an exact commit SHA. The build script refuses to run while it is the placeholder. Rebases are a deliberate, scheduled (quarterly) action. ## Engineering conventions - **Naming** (`.clang-tidy`): `CamelCase` types, `camelBack` functions, `snake_case` file names. - **Errors**: `std::expected` internally; `int error_code` across the C ABI. - **Branches**: `main`, `develop`, `feature/*`, `release/*`. - **Rule R2**: only `engine/src/parser/` may use raw `FPDF_*` APIs — enforced by `scripts/check_pdfium_boundary.*` locally and in CI. ## What is intentionally NOT done this session - **PDFium is not actually built** — scripts are staged; the revision needs to be pinned and the (long) build run as the second half of Task 2. - **No interface contracts** — `engine/include/pdfengine/pdf_document.hpp` is a placeholder. `PdfDocument` / `PdfPage` are designed and frozen at **Gate G0b** in an all-devs session; nothing proceeds until it is signed off. - **Skia / FreeType / HarfBuzz wrappers** — FreeType + HarfBuzz are in the vcpkg manifest and link-tested, but the actual wrappers are Dev 3's Phase 0/1 work. Skia is Dev 2's task. - **FastAPI / React / WASM** — placeholder directories only. WASM has a toolchain hook and preset stub so the integration point exists (Rule R5: WASM never blocks shipping). ## Gates ahead | Gate | Criterion | Unblocks | |------|-----------|----------| | **G0** | All platforms build clean; PDFium + Skia + FreeType + HarfBuzz compile | Phase 1 | | **G0b** | `PdfDocument` / `PdfPage` contracts locked by all 3 devs | Coding begins | The CI `build` matrix is the automated half of G0. The smoke test (`engine/tests/smoke_test.cpp`) is what it runs. ## OneDrive warning This checkout lives under `OneDrive\Work\Maskan\PDF Editor\Code`. The path is both OneDrive-synced **and** contains a space (`PDF Editor`). Both bite C++ builds: 1. **Sync churn** — build output is thousands of `.obj`/`.o` files. `out/` is git-ignored, but OneDrive still tries to upload it. 2. **File locks** — OneDrive can hold a handle on a file mid-sync, causing intermittent "permission denied" errors during compile or link. 3. **Spaces in build paths break tooling** — vcpkg/meson (harfbuzz) fail with `LNK1181` when `vcpkg_installed` is under the spaced path, and `depot_tools` / GN / Ninja `.bat` wrappers cannot handle a space in their own path at all. **On Windows, building inside the repo path is not viable** — put the build dir outside OneDrive on a space-free path. The `win-local-pdfium` preset above already does this (`C:/Users//pdfeng-build/...`); do the same for any non-PDFium preset by overriding `binaryDir`: ```powershell cmake --preset windows-release -B C:/Users//pdfeng-build/windows-release ``` The PDFium build is even stricter: `third_party/pdfium/build_pdfium.ps1` takes a `PDFIUM_BUILD_ROOT` env var and hard-errors if it contains a space. Use e.g. `C:\Users\\pdfium-build`. On Linux/macOS the OneDrive path is still a sync nuisance but the toolchain itself is fine. Either point the build dir outside OneDrive, or exclude `out/` and `vcpkg/` from sync, or pause sync while building. Long term, the repository should live outside OneDrive on a real Git remote.