Skip to content

Add opt-in assembly trimming for self-contained .NET 8 publishes#1272

Draft
jimmyp wants to merge 2 commits into
mainfrom
claude/assembly-tree-trimming-tentacle-xwf9pi
Draft

Add opt-in assembly trimming for self-contained .NET 8 publishes#1272
jimmyp wants to merge 2 commits into
mainfrom
claude/assembly-tree-trimming-tentacle-xwf9pi

Conversation

@jimmyp

@jimmyp jimmyp commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Background

Self-contained Tentacle publishes carry the full .NET runtime (~102 MB on linux-x64). .NET's assembly trimming can remove the unused parts of the runtime at publish time. This PR adds an opt-in trimming mode to measure and validate that saving without changing any default build behaviour.

Trimming is enabled with -p:EnableTrimming=true at publish time and uses TrimMode=partial, which only trims assemblies that declare themselves trim-compatible (the framework). App and NuGet assemblies (Autofac, Newtonsoft.Json, NLog, KubernetesClient, the attribute-scanned Halibut services) are kept whole, so their reflection-based behaviour is unaffected.

One trimmer root descriptor (TrimmerRoots.xml) is required: Autofac materialises Lazy<ICommand, CommandMetadata> command registrations by invoking Lazy constructors through reflection, which the trimmer cannot see and would otherwise strip.

An experimental full-trim configuration (-p:TrimMode=full) with root assemblies for the reflection-driven libraries is also included but is NOT the supported path — see Results.

Results

Measured on net8.0 Release self-contained publishes (linux-x64):

Configuration Size vs baseline Status
Baseline 102 MB (265 files)
Partial trim (this PR's supported mode) 59 MB (203 files) -42% Integration-tested locally, see below
Partial trim + framework feature switches 57.7 MB -44% Measured only
Full trim + reflection roots (experimental) 49 MB -52% Passes smoke tests, hangs integration tests
Partial trim + compressed single-file 26 MB (single exe) -74% Blocked on Assembly.Location usage (IL3000)

win-x64 shows the same ratio for partial trim (103 MB → 58 MB).

Before

dotnet publish -f net8.0 -r linux-x64 -c Release --self-contained → 102 MB

After

dotnet publish -f net8.0 -r linux-x64 -c Release --self-contained -p:EnableTrimming=true → 59 MB

Test evidence (partial trim, linux-x64)

Ran Octopus.Tentacle.Tests.Integration classes CapabilitiesServiceV2Test, ScriptServiceV2IntegrationTest, FileTransferServiceTests, TentacleCommandLineTests, TentacleStartupAndShutdownTests, ScriptServiceTests, WorkspaceCleanerTests, ScriptsAreGivenRequiredEnvironmentVariables, MachineConfigurationHomeDirectoryTests with the trimmed publish substituted as the Tentacle under test: 105 passed, 12 failed — and an untrimmed control run fails the identical 12 test cases (old 5.0.4/5.0.12 downloaded tentacle binaries and service-manager tests that cannot run in the sandbox container), i.e. no trimming-attributable failures.

Also smoke-tested: version, help, create-instance, new-certificate, show-thumbprint, list-instances --format json, and an agent run with the Halibut listener bound and NLog file logging working.

How to review this PR

This is an experiment to validate trimming viability — an "in principle" review is the goal. Nothing changes for existing builds: trimming only activates when -p:EnableTrimming=true is passed at publish time, and only for the net8.0/net8.0-windows targets. The interesting review questions are whether partial-mode trimming is an acceptable risk profile for reflection-heavy code, and what test coverage should gate turning this on in the release pipeline.

Quality ✔️
Full Octopus.Tentacle.Tests.Integration suite on CI against a trimmed publish is the remaining gate before wiring this into the release pipeline; the local subset run above (105 tests, failures identical to untrimmed control) is the evidence so far.

Pre-requisites

  • I have read How we use GitHub Issues for help deciding when and where it's appropriate to make an issue.
  • I have considered informing or consulting the right people, according to the ownership map.
  • I have considered appropriate testing for my change.

🤖 Generated with Claude Code

https://claude.ai/code/session_011AL61wGJeScoyKmdhtRUPy

Publishing with -p:EnableTrimming=true enables PublishTrimmed in
partial mode, which trims only assemblies that declare themselves
trim-compatible (the framework). App and NuGet assemblies are kept
whole, so reflection-based code (Autofac registrations, Newtonsoft
serialization, NLog config, attribute-scanned Halibut services)
keeps working.

TrimmerRoots.xml preserves System.Lazy`2, which Autofac instantiates
via reflection for Lazy<ICommand, CommandMetadata> command
registrations and which the trimmer would otherwise strip.

Measured on linux-x64 self-contained Release publish:
- baseline: 102 MB (265 files)
- partial trim: 59 MB (203 files), -42%
- full trim: 32 MB, but breaks NLog custom layout renderers at
  startup and cannot be safely verified against Newtonsoft-based
  Halibut wire serialization, so it is not the default.

win-x64 shows the same ratio (103 MB -> 58 MB).

Smoke-tested the partial-trimmed linux-x64 output: version, help,
create-instance, new-certificate, show-thumbprint, list-instances,
and a 10s agent run with the Halibut listener bound on 10933.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AL61wGJeScoyKmdhtRUPy
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Full trim mode (-p:EnableTrimming=true -p:TrimMode=full) member-trims
app and NuGet assemblies as well as the framework. Root the
reflection-driven assemblies (Tentacle projects, Halibut,
Newtonsoft.Json, NLog, Autofac, KubernetesClient) so they are kept
whole.

Measured on linux-x64: 49 MB vs 59 MB for partial mode. With these
roots the full-trimmed Tentacle passes command and agent smoke tests
(create-instance, new-certificate, agent with Halibut listener) but
still hangs the integration test suite, so full mode remains
experimental; partial mode is the supported configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AL61wGJeScoyKmdhtRUPy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants