Bundle a Mono application into a native executable with mkbundle
You will turn a compiled Mono assembly into a native executable, run it, and check what was 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, which is the current tool's easier path and does not require you to compile a C launcher yourself. The resulting file is native, but it still contains the CIL assembly and needs the appropriate runtime support for the way it was built.
1. Check the installed tool
First confirm the package and executable. These are ordinary, read-only commands and do not need 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 when you need an auditable version.
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. The -o option names the generated executable, while 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 command only checks readability. The second refuses to proceed if the output already exists, which avoids an accidental replacement while you are testing.
3. Build in simple mode
Use --simple explicitly. On this installed build, simple mode automatically includes referenced assemblies by default, as the local help reports. The manpage describes --deps as the switch for dependency inclusion, so the portable choice is to state the intended behaviour explicitly:
$ 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. The useful result is a final Generated line and a zero exit status. --deps can make the output larger, but it reduces the chance of omitting a managed dependency. If you deliberately want only the named assemblies, use --nodeps and test the result thoroughly.
Do not assume that "bundled" means every native dependency is inside the file. The --library [LIB,]PATH option can embed a dynamic library, and the manpage says multiple libraries should be supplied in dependency order. Native libraries and their licensing, ABI and loading behaviour 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 may 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 the error. 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 configuration files, opt out explicitly:
$ 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 the verification step has passed; recovery is then 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 the host architecture. On a normal Linux build it should identify an ELF executable. The program's own output must match the expected result, and its exit status must be zero. A successful mkbundle command alone proves only that generation completed; it does not prove that configuration, native libraries or runtime options work.
Test from a clean directory or staging area as well. This catches accidental reliance on the original assembly, adjacent config files or the current working directory. If you need to inspect dependencies, use read-only tools 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. --fetch-target TARGET downloads a runtime into your home directory, so treat it as a deliberate supply-chain and disk-space decision. Check the target name, source and checksum policy used by your organisation before fetching one. Do not use sudo for these per-user targets.
When 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
A cross-built executable must be tested on the target platform. Do not treat a successful local build as proof that it will run on 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 and requires 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. The --nomain option is for embedding the bundle in another native program. Do not switch to this mode merely because simple mode reported a missing config file: fix the configuration choice first, or use the explicit no-config options where appropriate.
Done means
- The package and
mkbundlepath were checked on the build host. - The output path was separate from the input and did not overwrite an existing file.
- Simple mode built the assembly with an explicit dependency decision.
- Machine and Mono configuration files were supplied or deliberately disabled after checking application needs.
file, a real execution test and the exit status all passed.- Any cross-target runtime was identified and tested on its actual target platform.