Bundle a Mono Application into a Native Executable with mkbundle
mkbundle turns a compiled Mono assembly into a single native executable, no C launcher required. You will build one, run it, and check exactly what got included. This guide uses the installed mkbundle from Mono 6.8.0.105, supplied by Ubuntu's mono-devel package. Allow about fifteen minutes. You need a working .exe assembly and enough disk space for a second copy of the runtime and assemblies.
The route
Jump straight to the step you need, or tick off Done means at the end.
The examples use simple mode, the easier of the tool's two paths and the one that does not require compiling a C launcher yourself. The resulting file is native, but it still contains the CIL assembly and needs the appropriate runtime support for how it was built.
1. Check the installed tool
Confirm the package and executable first. These are ordinary, read-only commands and need no sudo:
$ command -v mkbundle
/usr/bin/mkbundle
$ dpkg-query -W -f='${Package} ${Version}\n' mono-devel
mono-devel 6.8.0.105+dfsg-3.6ubuntu2
$ mkbundle --help | sed -n '1,45p'
Usage is: mkbundle [options] assembly1 [assembly2 ...]
mkbundle --version is not a version query on this installation: it treats --version as an assembly name and fails. Use the package query above for an auditable version instead.
Checkpoint
Do not continue until command -v points to the Mono installation you intend to use. A different mkbundle on PATH can have different defaults.
2. Keep the input assembly and output separate
Choose a new output path. -o names the generated executable, and the final arguments name one or more assemblies to embed. Do not point -o at the input file or at a useful existing program.
$ APP=/path/to/hello.exe
$ OUT=/path/to/build/hello-bundle
$ test -r "$APP" && echo "assembly is readable"
assembly is readable
$ test ! -e "$OUT" && echo "output path is unused"
output path is unused
Replace both placeholders with real paths. The first check only confirms readability; the second refuses to proceed if the output already exists, which stops an accidental overwrite while you are testing.
3. Build in simple mode
Use --simple explicitly. On this installed build, simple mode already includes referenced assemblies by default, according to the local help, but the manpage describes --deps as the switch for dependency inclusion. So state the intended behaviour explicitly rather than relying on the local default:
$ mkbundle --simple --deps -o "$OUT" "$APP"
Using runtime: /usr/bin/mono
Assembly: /path/to/hello.exe
Assembly: /usr/lib/mono/4.5/mscorlib.dll
Generated /path/to/build/hello-bundle
The exact assembly list and runtime path vary. What matters is the final Generated line and a zero exit status. --deps can make the output larger, but it reduces the chance of leaving out a managed dependency; if you deliberately want only the named assemblies, use --nodeps and test the result thoroughly.
Warning
"bundled" does not mean every native dependency is inside the file. --library [LIB,]PATH can embed a dynamic library, and the manpage says multiple libraries should be supplied in dependency order. Native libraries bring their own licensing, ABI and loading behaviour that need a separate deployment check.
4. Handle configuration files deliberately
Simple mode tries to include configuration files for its selected runtime. On this machine a plain build can fail if the expected files are not under the SDK prefix:
$ mkbundle --simple --deps -o "$OUT" "$APP"
ERROR: Could not locate the file machine.config file ... use --machine-config FILE or --no-machine-config
The path in the error is installation-specific. Do not copy a configuration file from an unrelated Mono installation just to silence it. If your application uses System.Configuration, supply the correct file explicitly:
$ mkbundle --simple --deps \
--machine-config /usr/lib/mono/4.5/machine.config \
--config /etc/mono/config \
-o "$OUT" "$APP"
Check that each file is the one intended for the runtime and application before rebuilding. If the program does not need these files, opt out explicitly instead:
$ mkbundle --simple --deps --no-machine-config --no-config \
-o "$OUT" "$APP"
Generated /path/to/build/hello-bundle
These options change what the generated program can discover at runtime. They are not cosmetic cleanup switches. Keep the original assembly and any configuration files until verification passes, so recovery is just rebuilding with the previous option set.
5. Verify the generated executable
Inspect the output before handing it to another system:
$ test -x "$OUT" && echo "bundle is executable"
bundle is executable
$ file "$OUT"
/path/to/build/hello-bundle: ELF 64-bit LSB pie executable, ...
$ "$OUT"
hello world
$ printf 'exit status: %s\n' "$?"
exit status: 0
The format line depends on host architecture; a normal Linux build should identify an ELF executable. The program's own output must match what you expect, and its exit status must be zero. A successful mkbundle run only proves generation completed, not that configuration, native libraries or runtime options actually work.
Test from a clean directory or staging area too, to catch accidental reliance on the original assembly, an adjacent config file or the current working directory. If you need to inspect dependencies, use a read-only tool such as ldd "$OUT", remembering that ldd output is not a complete test of runtime loading.
6. Choose cross-compilation only when you have a target runtime
--cross TARGET is not a generic CPU switch. The target must be an installed directory under ~/.mono/targets/, normally obtained from Mono's target tooling. List what is already present without downloading anything:
$ mkbundle --local-targets
Available targets:
default - Current System Mono
For remote choices, --list-targets queries the configured Mono distribution server, and --fetch-target TARGET downloads a runtime into your home directory. Treat that as a deliberate supply-chain and disk-space decision: check the target name, source and checksum policy your organisation uses before fetching one, and do not use sudo for these per-user targets.
Once a target is installed, build with its exact name:
$ mkbundle --simple --cross TARGET_NAME --deps \
-o /path/to/build/hello-target "$APP"
$ file /path/to/build/hello-target
Test a cross-built executable on the target platform. A successful local build proves nothing about another architecture or system library set.
7. Know when the older custom mode applies
The older form, mkbundle -o hello hello.exe, creates a C stub and compiles it with the host C compiler. The manpage describes it as host-only, needing a working compiler. It is useful when you need to link additional native libraries or control the generated C, but it is a different workflow from simple mode.
Use -c if you want the stub rather than a compiled executable, and -oo for its helper object file. --nomain is for embedding the bundle in another native program. Do not switch to this mode just because simple mode reported a missing config file: fix the configuration choice first, or use the explicit no-config options where appropriate.
Done means
- Tool checked. The package and
mkbundlepath were confirmed on the build host. - Paths separated. The output path was distinct from the input and did not overwrite anything existing.
- Dependencies decided. Simple mode built the assembly with an explicit dependency choice.
- Configuration handled. Machine and Mono configuration files were supplied or deliberately disabled after checking application needs.
- Result verified.
file, a real execution test and the exit status all passed. - Cross-target tested. Any cross-target runtime was identified and tested on its actual target platform.