Skip to content

Shrink the FwHeadless image and run ChorusMerge on .NET 10 - #2611

Merged
hahn-kev merged 8 commits into
developfrom
claude/dotnet-rollforward-runtime-d4df7e
Sep 4, 2026
Merged

Shrink the FwHeadless image and run ChorusMerge on .NET 10#2611
hahn-kev merged 8 commits into
developfrom
claude/dotnet-rollforward-runtime-d4df7e

Conversation

@hahn-kev-bot

@hahn-kev-bot hahn-kev-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

we can remove .net 8 from fw-headless by making chorus merge roll forward major. I also worked on trimming down the docker image from 990MB to 570MB!


🤖 AI summary

Shrinks the FwHeadless Docker image from 990MB → 570MB (~42%) and drops the bundled second .NET runtime, plus trims the build context from ~30MB → ~6.5MB. Each change is its own commit and every image build is gated by the in-image smoke tests.

  • Roll ChorusMerge onto .NET 10 — ChorusMerge ships only a net8.0 build; the chorusmerge wrapper now sets DOTNET_ROLL_FORWARD=Major so it runs on the shared framework already in the base image, removing the bundled .NET 8 runtime.
  • Smoke tests → throwaway stage — the FixFwData/chorusmerge smoke tests moved out of the shipped final stage (into a smoketest stage gated by a marker COPY), so their fixtures and churn no longer ship.
  • Stop double-shipping MercurialExcludeAssets="native" on SIL.Chorus.Mercurial drops the runtimes/.../Mercurial copy that duplicated the Content-staged Mercurial/ the app actually uses (~108MB, incl. the never-used Windows tree).
  • RID-specific publish (-r linux-x64 --no-self-contained) — prunes runtimes/ native assets for every other RID (win, osx, android, ios, …) while staying framework-dependent.
  • Prune unused-Python Mercurial modules — Mercurial bundles compiled extensions (zstd et al.) for Python 3.8–3.13; only the image's python3 ABI is kept, pruned during the copy so the space is reclaimed. The hg smoke test was upgraded from hg version to hg debuginstall (verifies the compression modules load).
  • Trim Docker context**/*.Tests excluded from the backend/ context: no image builds a test project, and via COPY . . test-snapshot edits previously invalidated the image cache.

Follow-up (not in this PR): the Mercurial double-ship and the six-Python bundling both originate in SIL.Chorus.Mercurial packaging and are worth an upstream issue at sillsdev/Mercurial4Chorus; ExcludeAssets="native" + the module prune are the consumer-side workarounds.

Test plan

  • docker build of backend/FwHeadless/Dockerfile succeeds; the three build-time smoke tests gate it and pass: hg debuginstall (Mercurial + compression modules load), chorusmerge (real 3-way FieldWorks merge preserves both edits), FixFwData (publish co-located correctly).
  • Shipped image contains only the .NET 10 runtime; native libs (libe_sqlite3.so, libMono.Unix.so) and the unix System.Private.ServiceModel.dll fallback land at the app root; only the live-python3 Mercurial modules remain.
  • FwHeadless boots through DI + config binding (fails only on absent runtime config — no assembly/native load errors).
  • CI builds and publishes the image; GHA integration tests exercise a real Send/Receive against it.

hahn-kev and others added 7 commits September 2, 2026 09:51
…runtime

ChorusMerge ships only a net8.0 build, so its runtimeconfig requests
Microsoft.NETCore.App 8.0.0. Previously the FwHeadless image copied the
whole .NET 8 shared framework (and fxr host) from a runtime:8.0 stage to
satisfy that.

Instead, set DOTNET_ROLL_FORWARD=Major in the chorusmerge wrapper so it
rolls forward onto the .NET 10 shared framework already present in the
aspnet:10.0 base image. Scoped to the wrapper so FwHeadless itself is
unaffected. Drops the dotnet8 build stage and its two COPY lines.

Verified: the image builds without the net8 runtime and the in-image
chorusmerge smoke test performs a real 3-way FieldWorks merge on .NET 10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The FixFwData and chorusmerge smoke tests previously ran in the final
(shipped) stage, leaving build-only artifacts in the runtime image: the
1.47MB test-template-repo.zip fixture (COPYd then deleted in a later
layer, so never reclaimed) plus the FixFwData smoke RUN's temp churn.

Split the runtime contents into a `runtime` stage, run both smoke tests
in a `smoketest` stage derived from it, and produce `final` from
`runtime` with a 0-byte marker COPYd from `smoketest`. The marker forces
buildkit to build (and therefore pass) the smoke tests before the image
is produced, so they still gate the build — the shipped image just no
longer carries their fixtures or churn. ~10MB smaller (990MB -> 980MB).

Also moved `chmod +x FixFwData` into the runtime stage (it used to live
in the smoke RUN); FixFwData is shelled out to at runtime by liblcm, so
its exec bit must survive in the shipped image. python3 stays: the
bundled Linux hg is a python3 script and needs it for Send/Receive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bundled Linux hg is a python3 script that loads native modules (zstd,
etc.); a missing python3 or broken native asset would break every
Send/Receive but wouldn't surface until runtime. Run `hg version` in the
smoketest stage and assert its banner, so a broken Mercurial fails the
build instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SIL.Chorus.Mercurial build targets (CopyFiles) stage the
runtimes/<rid>/native/Mercurial tree into the project's Mercurial/
folder, which ships to the image via the existing Content Include and is
the copy the app actually uses (mercurial.ini fixup, hg exec bit, and
Chorus's hg lookup all target /app/Mercurial). Publish was ALSO copying
the same tree under runtimes/, so every image carried Mercurial twice.

ExcludeAssets="native" on the package reference drops the runtimes/
native copy from publish while leaving the build targets (build assets)
importing, so CopyFiles still stages Mercurial/. Removes ~108MB of
duplicate linux-x64 Mercurial plus the never-used win Mercurial native
tree: image 990MB -> 757MB. Smoke tests (hg version, chorusmerge,
FixFwData) still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A portable (no-RID) publish copies runtimes/ native assets for every RID
(win, osx, android, ios, riscv64, s390x, ppc64le, ...), none of which a
linux-x64 container can load. Publish RID-specific but framework-dependent
(-r linux-x64 --no-self-contained, still relying on the base image's
shared framework) so only linux-x64 natives (and unix managed fallbacks
like System.Private.ServiceModel) ship, flattened into the app root.

This was previously blocked by NETSDK1152: RID publish flattens the
SIL.Chorus.Mercurial native tree into the output, colliding with the
Content-copied Mercurial/. The prior commit's ExcludeAssets="native"
removes that native copy, so the collision is gone.

Image 757MB -> 618MB (372MB / 38% below the pre-cleanup 990MB). Verified:
all three smoke tests pass, native libs (libe_sqlite3.so, libMono.Unix.so)
and the unix ServiceModel fallback land at the app root, and FwHeadless
boots through DI/config binding (fails only on absent runtime config, no
assembly/native load errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SIL.Chorus.Mercurial bundles its compiled extension modules (zstd et al.)
for Python 3.8-3.13, but the runtime image has a single python3 (Debian's
3.12) and hg only ever loads the matching cpython ABI set. The other five
versions were ~45MB of dead native code.

Copy the published app in via a bind mount and, in the same layer, delete
every Mercurial *.cpython-3*.so whose ABI tag doesn't match the live
python3 — pruning during the copy so the space is actually reclaimed
rather than hidden under a whiteout. Keyed to the running python3 so a
future base-image Python bump keeps the correct set by construction.

Upgrade the hg smoke test from `hg version` to `hg debuginstall`, which
loads the extension modules and verifies the compression engines (zstd,
zlib, ...); a mis-pruned ABI set now fails the build instead of surfacing
at Send/Receive time.

Image 618MB -> 570MB. All three smoke tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No image built from the backend/ context compiles a test project — each
Dockerfile (LexBoxApi, FwHeadless) builds a single app csproj via its
ProjectReference graph, and the only Testing dependency (the
test-template-repo.zip fixture) is already whitelisted. Yet the *.Tests
projects' snapshot data (~20MB of *.verified.txt) dominated the ~30MB
context, and because both Dockerfiles do `COPY . .`, any edit to a test
snapshot invalidated the image build cache and forced a full rebuild +
smoke tests.

Excluding **/*.Tests cuts the context to ~6.5MB and decouples the image
builds from test-data churn. FwHeadless still builds and all smoke tests
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 3b9b7e89-9597-42d1-be6c-fe2e9ce5b86e

📥 Commits

Reviewing files that changed from the base of the PR and between 373f542 and 4a0e4bb.

📒 Files selected for processing (4)
  • backend/.dockerignore
  • backend/FwHeadless/Dockerfile
  • backend/FwHeadless/FwHeadless.csproj
  • backend/FwHeadless/chorusmerge

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the 📦 Lexbox issues related to any server side code, fw-headless included label Sep 2, 2026

@myieye myieye left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is USER root needed in the smoketest stage? It inherits www-data from runtime, so dropping that line would make the three smoke tests run as the same user the app runs as and a file www-data cannot read or write would fail the build instead of production. If it was added intentionally, a comment saying so might be good.

@hahn-kev
hahn-kev merged commit 0a5d483 into develop Sep 4, 2026
16 checks passed
@hahn-kev
hahn-kev deleted the claude/dotnet-rollforward-runtime-d4df7e branch September 4, 2026 03:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📦 Lexbox issues related to any server side code, fw-headless included

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants