Skip to content

Dependency-free, multi-OS self-extracting archives - #393

Open
Hawkynt wants to merge 11 commits into
feat/retire-wpf-frontendfrom
feat/aot-multi-os-sfx
Open

Hawkynt wants to merge 11 commits into
feat/retire-wpf-frontendfrom
feat/aot-multi-os-sfx

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Self-extracting archives run with nothing installed, and one file can target several operating systems.

No runtime needed. Both stubs were framework-dependent, so an SFX sent to a machine without .NET failed. They are NativeAOT now. Costura had to go: it resolves assemblies through Assembly.Load(byte[]), which AOT cannot do.

Two tiers. win-x64 sizes:

stub bytes
tar, carved 980,992
zip, carved 1,504,256
7z, carved 1,559,040
RAR, carved 2,142,208
universal, all 237 archive formats 11,581,952
previous stub, which needed .NET 7,166,323

Carved stubs cover eight common formats; every other format falls back to the universal stub. The rule that makes carving work: a stub must not reference Compression.Lib, whose generated RegisterFormats() constructs every descriptor and so roots all of them. The universal stub gets an archive-only registration from the same generator, scoped to its own references. Routed through Compression.Lib it would be 33 MB.

Multi-OS in one file. --sfx-target win-x64,linux-x64,osx-arm64 writes a file that is both a PE and a POSIX shell script. The bootstrap lives in the DOS stub and no PE header moves. The shell's first line must end at 0x40 exactly: 0x3F is the high byte of e_lfanew. On POSIX, run it as sh file.exe, not ./file.exe, because MZ takes the bytes a shebang would need.

Third-party tools can still open it. The payload stays contiguous and unmodified at the end, so unzip file.exe readme.txt and 7z e file.exe readme.txt extract a named entry without running our stub. Tar is the exception: no signature at offset zero and no end-anchored directory.

CI. AOT cannot cross-compile, so the stub step becomes a 108-leg matrix (12 universal + 96 carved) on windows-latest, windows-11-arm, ubuntu-latest, ubuntu-24.04-arm, macos-13 and macos-latest.

Also fixed: --sfx with --sfx-ui silently built the GUI stub; an unknown --sfx-target only failed later as a missing stub; SFX failure exited 0; four divergent RID lists are now one; SupportedTargets no longer lists win-x86 or musl, which AOT cannot build.

Stacked on #389, because the RID-list change touches the ported create-options dialog.

Verified:

  • One polyglot file ran natively on Windows and via sh in WSL; both extractions byte-identical.
  • All eight carved formats built and extracted, including compound tar, which needs a mini-registry inside the stub.
  • unzip and 7-Zip pulled named entries from single-target and multi-OS containers.
  • 47 SFX tests green.

Not verified locally: the linux and macOS AOT legs. This machine only builds win-x64, so this PR's CI run is their first real test.

…uild for Linux too

+ SfxSlim, a reduced library flavour for the stubs: they extract, so they carry no audio support
- 2.1 MB from every GUI self-extracting archive and 2.4 MB from every console one
! SfxSlim builds into bin/slim; sharing bin with the full build silently replaces its assemblies
…, so every assembly attribute was declared twice
+ SfxTrailer as the single definition of the twelve bytes, read by the stubs and written by the builder

A carved stub cannot reference Compression.Lib: the generated RegisterFormats() there constructs
every descriptor in the closure, so one reference roots the whole format library and nothing trims.
…ger needs .NET installed

+ two stub tiers: carved (one format, 1.5 MB) and universal (all 237 archive formats, 11.6 MB)
- Costura and PublishSingleFile from the stub, which AOT cannot use at all

The universal tier gets its registration from the source generator scoped to its own reference
closure, so it covers every archive format without listing any by hand. It deliberately does not
reference Compression.Lib: routing through it adds the filesystem and audio descriptors, which no
payload can ever be, and takes the binary from 11.6 MB to 33 MB.
…e one

+ the macOS NativeForms backend, so GUI self-extractors cover win, linux and osx

Carved GUI stub is 1.7 MB against 7.5 MB, and needs no runtime installed.
…POSIX shell script

+ SfxBuilder.CreateUniversal, bundling a stub per operating system into a single archive
* stub resolution prefers a stub carved for the payload's format and falls back to the universal one

The DOS stub is dead weight Windows never reads, so the shell bootstrap lives there and no PE
header moves. Line one reads "MZ=1 #", which is an assignment plus a comment to a shell and the
required magic to the loader; the comment has to run past e_lfanew, whose high byte sits at 0x3F,
so the newline goes at 0x40 exactly. Verified on both legs of the same file.
… straight out of our SFX

Verified against the real tools, on both the single-target and the multi-OS container. This is what
pins the layout: the payload has to stay one contiguous run of unmodified archive bytes at the end,
so nothing may reframe it and the per-OS stubs must sit ahead of it.

! tar payloads are not discoverable behind a prepended stub - no signature at offset zero and no
! end-anchored directory. Recorded by a test rather than left to be found later.
…runs on all of them

# an unknown --sfx-target was only reported as a missing-stub error much later
# --sfx together with --sfx-ui silently produced the GUI stub
# SFX creation failing still exited 0, so a script could not tell the artefact was missing
* SfxBuilder.SupportedTargets is now the only RID list; the UI kept a fourth, already-diverged copy
# publish-sfx-stubs.ps1 still skipped the GUI stub off Windows and looked for a net10.0-windows path
…ompile, so each RID builds on its own runner

* the stub step consumes artifacts instead of publishing in-runner, since staging no longer crosses one host
* SupportedTargets lists only runtimes a stub is actually built for

108 legs: 12 universal plus 96 carved. win-x86 and the musl RIDs are gone from the advertised set
because NativeAOT has no x86 target and no musl stub is built; offering them turned "unsupported"
into "file not found".
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.

1 participant