Keep a Chromium fork alive across new upstream releases. Stores your changes as per-file patches and replays them onto each new Chromium version with rollback safety.
Not battle-tested. These scripts work on the fork I maintain. They have not been run through a formal test suite or audited against every checkout shape. Run
--dry-runfirst. Keep yourPatches/directory under git so a bad day is recoverable.
- A working
chromium/srccheckout (managed by depot_tools). - Python 3.8+ (depot_tools already pulled this in).
- git.
That's it. Stdlib only. No pip install. Linux, macOS, and Windows.
Windows users: prefix the scripts with python (e.g. python tools\export.py). Same behaviour.
The tooling expects Patches/ to sit next to your src/. Each Chromium version you maintain becomes a subdirectory under Patches/:
chromium/
├── src/ ← your Chromium checkout
└── Patches/ ← you create this
├── tools/ ← cloned from this repo
├── 146.0.7680.177/ ← appears after first export for v146
│ ├── manifest.json
│ └── chrome/browser/foo.cc.patch
└── 147.0.7700.100/ ← next version sits beside it
├── manifest.json
└── chrome/browser/foo.cc.patch
Create it once:
cd ~/chromium # the directory that contains src/
mkdir Patches
cd Patches
git init # version your patch history (optional but recommended)
git clone https://github.com/Redrrx/chromium_fork_tooling toolsDone. Three commands.
Run the synthetic test. It builds a throwaway repo under the system temp dir, runs every scenario, cleans up. Touches nothing in your real checkout.
# Linux / macOS
./tools/test.py
# Windows
python tools\test.pyShould print PASSED 24/24. If it doesn't, something on your machine is off (usually missing git user.email / user.name, or core.autocrlf set to true). Don't proceed until it passes.
The tooling parses your branch name to know which upstream tag your changes target. The branch must be named my_local_<tag>.
Pick a Chromium release you want as your base (find live versions at chromiumdash.appspot.com/releases), then:
cd ~/chromium/src
gclient sync --with_branch_heads --with_tags
git fetch --tags
git checkout -B my_local_146.0.7680.177 146.0.7680.177
gclient sync --with_branch_heads --with_tagsCommit your changes on this branch like normal git work.
After you've committed work on my_local_<tag>:
cd ~/chromium/Patches
./tools/export.py # snapshot to Patches/<tag>/
./tools/export.py --dry-run # preview without writingPatches land in Patches/<tag>/. Other version dirs are untouched.
If you wiped src/, switched machines, or are syncing a teammate:
./tools/apply.py # uses current branch's tag automatically
./tools/apply.py --from=146.0.7680.177 # explicit version
./tools/apply.py --dry-run # preview without writingapply.py auto-detects the version from your current my_local_<tag> branch. Use --from=<tag> if you're on a different branch or want to apply a different version's snapshot.
When upstream ships a new tag:
./tools/upgrade.py 147.0.7700.100What this does:
- Reads your current branch (must be
my_local_<prev-tag>) to know which version to replay from. - Fetches the new tag.
- Branches
my_local_147.0.7700.100from it. - Runs
apply.py --from=<prev-tag>to replay your patches onto the new branch. - Rolls back cleanly if anything fails before patches start applying.
After upgrade.py succeeds:
cd ../src
gclient sync --with_branch_heads --with_tags
autoninja -C out/Default chrome
# fix any API breakage from the version bump
cd ../Patches
./tools/export.py # creates Patches/147.0.7700.100/ alongside the older versionsPatches/
├── tools/ the scripts
├── 146.0.7680.177/ one dir per Chromium tag you've patched
│ ├── manifest.json metadata: tag, sha256, file count
│ └── chrome/browser/foo.cc.patch one .patch per source file
└── 147.0.7700.100/
├── manifest.json
└── chrome/browser/foo.cc.patch
Path rule: chromium/src/<path> → Patches/<tag>/<path>.patch. Mirrored exactly inside the version dir.
To compare versions: diff -r Patches/146.0.7680.177 Patches/147.0.7700.100. To see all versions: ls Patches/.
Three failures you'll see often enough to know in advance:
Clean patches were applied. Conflicting ones weren't. Your tree is partially modified. The output tells you which files. Inspect each conflict, fix it manually, then re-export:
# fix the conflicts (see the list apply.py printed)
cd ~/chromium/src
# edit the conflicting files
# when the build works again:
cd ../Patches
./tools/export.pyTo roll back instead:
cd ~/chromium/src
git checkout HEAD -- <files apply.py listed>Auto-rollback should have returned you to the previous branch. If it didn't:
cd ~/chromium/src
git reset --hard
git checkout <prev branch>
git branch -D my_local_<new-tag>Nothing was published. Old patches are untouched. Investigate the diff manually:
cd ~/chromium/src
git diff <tag>..my_local_<tag> --stat --diff-filter=d| code | meaning |
|---|---|
| 0 | success |
| 1 | user error or recoverable failure (wrong branch, dirty tree, conflicts) |
| 2 | bad arguments |
| 3 | catastrophic apply failure; tree was auto-rolled-back |
| variable | what |
|---|---|
CHROMIUM_SRC |
path to chromium/src if not at ../src relative to Patches/ |
LOG_LEVEL |
quiet | info (default) | debug |
Every run appends a line to Patches/.log.
- Per-file patches, not a monolithic diff. Each
.patchis grep-able and reviewable on its own. Survives upstream renames with one-line surgery. - Pre-flight before any write.
apply.pyreports every conflict at once instead of partially mutating the tree. - Surgical rollback. Only files this run touched get restored. No
git checkout -- .on the whole tree. - Round-trip verification. The concatenation of per-file patches must sha256-match the combined diff or
export.pyrefuses to publish. - Locks. Each script holds a
flockonPatches/.<name>.lockso two copies can't race.