Publishing .NET apps as a single file is a popular feature-request. Ideally, we want a single-file solution that:
- Is compatible with all .NET Core applications
- Bundles IL, R2R, native code and custom (data) files
- Doesn't require installation or cleanup steps
- Runs directly from the bundle, without extracting components to disk
- Reduces publish-size
- Improves startup cost
- Works cohesively with debuggers, profilers, Watson dump etc.
This document explores a few options to realize the single-file publish feature, with different feature-set vs development cost trade-offs. The document can also be considered a development staging plan, where each stage spills fewer items onto temporary files, while paying an incremental development cost.
The first stage is to develop a pack and extract tool. While this stage is technically simplistic, the interface and tooling will match the final solution, giving partner teams and potential customers a chance to prepare for adopting the technology.
This stage implements:
- A packaging tool (bundler) that embeds the application and all of its dependencies (essentially the contents of the publish directory) into a host executable.
- This version will not support compression of the bundled assemblies.
- When the executable runs, it will extract all of those files into a temporary directory, and then run as though the app was published to that temporary directory.
The mechanism will support:
- Reuse: Extract on first-run, reuse on subsequent runs (if apps choose to do so).
- Upgrade: Each version of the app extracts to a unique location, supporting side-by-side use of multiple versions.
- Uninstall: Users can identify and delete extracted files when the app is no longer needed.
- Access control: Processes running with elevated access can extract to admin-only-writable locations.
Best suited for:
- Environments requiring maximal compatibility -- need to embed different kinds of files (IL, native, etc) into one, without losing functionality such as debuggability.
Limitations:
- Unsuitable for environments that require that the app does not perform disk-writes at startup
Advantages:
- Low cost of development
- Bundler tool can be used as-is for most further stages
- Provides ability to develop test infrastructure and prototypes
This stage improves on Stage 1 in that
- IL files bundled into the single-file will load and execute directly from the executable.
- Native libraries will still need to
- Remain in the publish directory unmerged, or
- Extracted to the disk like the previous self-extractor stage
- Debugging support is unaffected.
Best suited for:
- Framework dependent purely managed apps that are not ready-to-run compiled.
Limitations:
- Unsuitable for environments that
- Have native dependencies (ex: published
--self-contained
, or depend on custom native libraries), and - Cannot tolerate native libraries to remain unbundled or extracted to disk
- Have native dependencies (ex: published
- The app may need to be aware that its dependencies will be embedded into the single-file, when using certain LoadLibrary APIs.
In this stage
- IL and Ready-to-run files bundled into the single-file will load and execute directly from the executable.
- Native library support is the same as the previous stage.
- Debugging support is unaffected.
Best suited for:
- Framework dependent managed apps that may be ready-to-run compiled.
Limitations:
- Unsuitable for environments that have native dependencies that must be bundled and cannot be extracted.
In this stage
- .NET native libraries are statically linked to the single-file host executable.
- Handling of custom native libraries is the same as the previous stage.
- A large portion of the work involved in this stage is to make debuggers and tools compatible with the statically linked runtime.
Best suited for:
- Self-contained managed apps
Limitations:
- Environments that have dependencies on custom native libries that must be bundled and cannot be extracted.
- Watson dumps may not be supported.
This stage improves on the previous one by providing the ability to statically link custom native code along into the host executable. This involves:
- Publishing the runtime as a library.
- Provide tools and guidance to statically link user's native-code with the runtime library to obtain a custom host executable
- Tooling to embed managed dependencies into this custom executable.
Debugging support is the same as the previous stage.
For native library dependencies, there are two possible scenarios:
- On systems where it's not possible to load them from memory, they'll have to be spilled out to disk, and loaded as usual with the standard method of loading shared libraries.
- On Linux systems, it's possible to use
memfd_create()
to create an ephemeral file that's backed by an anonymous memory mapping. If one writes the bundled shared library into that file and pass the ephemeral path (/proc/self/fd/${FILE_DESCRIPTOR}
) todlopen()
, the file library should open as usual. (Seals may be added withfnctl()
for good measure.)
Best suited for:
- Self-contained managed apps with custom native dependencies (that can be linked).
Limitations:
- Watson dumps may not be supported.