patches/ applied in the order patches/series lists them. Every input is pinned in build/, and every step is a script in scripts/. A full build takes hours, so compile the files a patch touches first with scripts/checkfile.sh (Patches).
Targets
An arm64 Linux machine cannot build either Linux target, because the container needs an x86_64 host. A
linux-arm64 build cannot run on the machine that built it, so scripts/smoke-binary.sh checks the ELF machine type of that binary instead of running it.
What the host needs
- Every host. Python 3, Git, and free space in the workspace: 68 GB for
linux-x64,linux-arm64andmacos-arm64, 46 GB forwindows-x64.scripts/bootstrap.shstops below that and warns below 100 GB. After the checkout,scripts/fetch-sources.shstops if less than 20 GB is left for build output. - Linux. Docker with a running daemon,
tarthat writes zstd, andsudowithapt, becausefetch-sources.shruns Chromium’sbuild/install-build-deps.shon the host. - macOS. Xcode or the Command Line Tools with the macOS 26.5 SDK (build 25F70), and
tarthat writes zstd. The build uses the pinned SDK even when a newer Xcode is active, because the Xcode 27.0 SDK’slibSystem.tbddeclares a target the bundled linker cannot parse.scripts/checkfile.shandscripts/verify-release-inputs.shneed Bash 4 or later first onPATH. The/bin/bashthat macOS ships is 3.2. - Windows. Git for Windows (the scripts run in its bash), 7-Zip, Visual Studio 2022 with the C++ x64 toolset and ATL, the Windows 11 SDK 10.0.26100.7705 with Debugging Tools for Windows, and the Visual C++ Redistributable (x64).
scripts/provision-windows-toolchain.shinstalls the Visual Studio components and the SDK parts that are missing.
scripts/verify-host-tooling.sh lists everything a target needs that the host lacks, in one run:
Pins
scripts/lib.sh exports DEPOT_TOOLS_UPDATE=0, so depot_tools stays at its pinned commit. Do not run gclient sync yourself. scripts/fetch-sources.sh syncs to the pinned tag. build/MANIFEST.lock is output, not input. scripts/build.sh writes the inputs and output hashes of the last build into it.
Build step by step
The checkout lives in.workspace/ in the repository. Set APOSTATE_WORKSPACE to put it elsewhere. Set the target first. bootstrap.sh reads it for the disk check, and fetch-sources.sh writes it into .gclient, so a linux-arm64 checkout also fetches the arm64 dependencies. Then run the scripts in order:
scripts/provision-windows-toolchain.sh before bootstrap.sh.
The browser is written to .workspace/src/out/<target>/: chrome on Linux, Chromium.app on macOS, chrome.exe on Windows. build.sh does not apply patches. After you change a patch, run apply-patches.sh again before you build.
package-artifact.sh writes three things:
- The archive. Its name must be one the release policy lists:
apostate-152.0.7977.83-<target>.tar.zstfor the Linux targets,apostate-152.0.7977.83-<target>.zipfor macOS and Windows. Any other name is refused. <archive>.manifest.jsonbeside it, with the archive’s SHA-256 and the fields a release manifest carries (Releases).- The staging directory the archive was made from, named like the archive without its extension.
LICENSE, build/MANIFEST.lock and resources/profiles/. On macOS it packages the signed bundle when sign-macos.sh produced one.
Settings
A build with
GOOGLE_API_KEY lists Google’s network speech voices and speaks through them. The key is compiled into the binary, and anyone who downloads the binary can extract it. On the Linux targets, configure.sh runs in the container, which does not receive these variables, so a Linux build has no keys.
scripts/apply-patches.sh --check reports whether the series is applied without changing anything. It exits 0 when the series is applied or the tree is pristine, and 3 when a real run would reset and reapply. A normal run does nothing when the applied series is current. Otherwise it runs git checkout -- . in src and in every sub-repository the series edits, removes the files the series creates, and applies the whole series again. That discards every uncommitted edit to a tracked file in those repositories.
The Linux container
On Linux,configure.sh, build.sh, checkfile.sh, checkseries.sh, run-chromium-hooks.sh and verify-reproducible.sh re-run themselves inside the container, so you call them the same way on every host. scripts/in-linux-build-container.sh builds a linux/amd64 image from build/linux/Dockerfile and Chromium’s own dependency installer at the pinned tag, then runs one repository script in it with only the repository and workspace mounted. The image tag is a hash of its inputs, so a change to any input builds a new image.
--prepare builds the image and runs nothing. It needs a fetched checkout, because the dependency installer comes from it. --user UID:GID runs the script as another user. Each run writes a receipt with the image ID and a hash of the installed package list to $APOSTATE_WORKSPACE/build-container/current.json.
prepare-linux-sysroot.sh runs on the host, not in the container, because it downloads. It installs the Debian bullseye sysroot the checkout’s build/linux/sysroot_scripts/sysroots.json pins for the target, and records a hash of the extracted tree in $APOSTATE_WORKSPACE/sysroot-receipts/<target>.json. A later run fails if the tree no longer matches that hash.
Sign on macOS
scripts/sign-macos.sh macos-arm64 signs Chromium.app with a Developer ID certificate, notarizes and staples it, and writes the result to $APOSTATE_WORKSPACE/signed/macos-arm64/. It leaves out/ as built, so the hashes in build/MANIFEST.lock still describe what ninja produced. package-artifact.sh packages the signed copy when there is one.
The script reads six variables:
With none of them set, the script prints
sign-macos: no signing identity configured; the bundle stays unsigned and exits 0. With only some set, it fails and names the missing ones. Signing needs Python 3.11 or later. APOSTATE_NOTARY_TIMEOUT sets how long it waits for Apple’s notary service (default 30m). Linux and Windows archives are not signed.
Reproducibility
out/<target>, running apply-patches.sh, configure.sh and build.sh each time, and compares the output hashes the two builds wrote to build/MANIFEST.lock. --against-manifest builds once and compares with the hashes already in build/MANIFEST.lock. The script prints reproducible: both builds produced identical outputs or NOT REPRODUCIBLE with the differing lines, and exits 1 on a difference.
The hashed outputs per target are listed in scripts/build.sh: Chromium.app and chrome_crashpad_handler on macOS, chrome, chrome_crashpad_handler, libEGL.so and libGLESv2.so on Linux, and chrome.exe, chrome.dll, chrome_elf.dll, libEGL.dll and libGLESv2.dll on Windows. Google API keys change the binary, so give both builds the same key variables or none.
Besides the hashes, build/MANIFEST.lock records the Chromium commit, the depot_tools revision, the build container image, digests of patches/series, of the patch contents and of args.gn, and whether the build was fresh or incremental. On Windows it also records the Visual Studio, MSVC toolset and Windows SDK versions the build resolved. Each build appends one line to $APOSTATE_WORKSPACE/.apostate-build-lineage.jsonl.
CI workflows
CI builds start from an empty workspace and restore no build cache, because the checkout alone is larger than the GitHub Actions cache limit. The build workflows run on WarpBuild runners, and
scripts/resolve-build-targets.sh maps each target to a runner label. In a fork without those runners, a build job waits in the queue until GitHub cancels it. The repository variable APOSTATE_BUILD_TARGETS, a comma-separated list such as macos-arm64,linux-x64, limits the nightly and release builds to those targets.