Build a .NET Library from Modules with Mono al2

al2 takes the .netmodule files a compiler leaves behind and links them into a real .NET library with proper assembly metadata attached. The examples use the installed Mono Assembly Linker, al2, from Mono package version 6.8.0.105+dfsg-3.6ubuntu2.

Allow about fifteen minutes. You need a shell, the mono-devel package, a module produced by a compatible compiler, and enough free space for a second copy of the assembly. This guide only creates files in a working directory: nothing gets installed, signed or handed to a service.

1. Confirm the installed linker

Check the executable and package version first. Ordinary, read-only commands, no elevated privileges needed:

$ command -v al2
/usr/bin/al2
$ al2 --version
Mono Assembly Linker (al.exe) version 6.8.0.105
$ dpkg-query -W -f='${Package} ${Version}\n' mono-devel
mono-devel 6.8.0.105+dfsg-3.6ubuntu2

The compressed manual treats al and al2 as the same tool, and says to reach for al2 when targeting 2.0 assemblies. The installed program's own help is the best local check for the exact option spelling, so read it before adapting anyone's script:

$ al2 -help | sed -n '1,18p'
Mono Assembly Linker (al.exe) version 6.8.0.105
Usage: al [options] [sources]
Options: ('/out' must be specified)

Checkpoint: you should have a working al2 command and a package version. If the command is missing, stop here and install or repair Mono through your normal package-management process rather than copying a binary from another host.

2. Prepare a module input

al2 links modules, manifest files and resources; it does not compile C# source code. For a self-contained test, create a small module with the Mono C# compiler, run as your normal user in an empty working directory:

$ mkdir -p "$HOME/al2-demo"
$ cd "$HOME/al2-demo"
$ printf '%s\n' 'public class Demo { }' > Demo.cs
$ mcs -target:module -out:Demo.netmodule Demo.cs
$ file Demo.netmodule
Demo.netmodule: MS .NetModule

That shell redirection creates or truncates Demo.cs, so never point it at a source file you actually need. Already have modules? Skip the compiler command and check each input with file instead:

$ file module-one.netmodule module-two.netmodule

A module is an input, not the final library. Keep the originals until you have inspected the linked output. No elevated privileges are needed here either, unless your chosen directory is not writable, and then the fix is a directory you own, not sudo.

3. Link the module into a library

Use -out to name the manifest assembly and -target:lib to request a library. Keep the output path different from every input path:

$ al2 -nologo \
    -target:lib \
    -out:Demo.dll \
    -description:'Small al2 demonstration' \
    Demo.netmodule
$ file Demo.dll
Demo.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly

-nologo only suppresses the startup banner. The options that matter are the target, the output name and the input module. The installed help displays a slash in its examples, but this Unix command accepts the hyphen form shown above; if a future Mono build ever rejects that spelling, follow whatever syntax its own help prints.

The output is a managed library with an assembly manifest attached. A successful exit status only means the linker finished, not that the library has the API or metadata your application actually expects. Check the file type and inspect the assembly with a tool such as monodis when it is available:

$ monodis --assembly Demo.dll | sed -n '1,12p'
Assembly Table
Name:          Demo
Hash Algoritm: 0x00008004

4. Add more module inputs

List additional module files after the options and they all land in the same output manifest:

$ al2 -nologo -target:lib -out:Combined.dll \
    module-one.netmodule module-two.netmodule
$ test -s Combined.dll && echo 'linked library is non-empty'
linked library is non-empty

Only use modules built for compatible runtime and assembly versions. A successful link is not a compatibility guarantee: duplicate type definitions, unresolved references or an unsuitable target can all still blow up when another tool loads the result. Keep input order stable in build scripts so a review can see exactly what changed.

Warning: never overwrite a known-good library while experimenting. The linker writes straight to the path given at -out, so pick a new name such as Combined.test.dll, inspect it, then promote it through your normal build process. Created an unwanted test file by accident? Remove only that file, after checking the path. The original module inputs are not an undo mechanism for an overwritten output.

5. Set the assembly identity deliberately

The linker can drop descriptive values straight into assembly metadata. Use the documented aliases whenever a build needs them:

$ al2 -nologo -target:lib -out:Widget.dll \
    -title:'Widget library' \
    -company:'Example Company' \
    -product:'Widget' \
    -version:2.4.0.0 \
    Widget.netmodule

Quote any value containing spaces. The version option takes a version string and can use * to auto-generate the remaining numbers, per the manual, but skip that form in reproducible release builds unless your build policy explicitly allows generated version components.

Security boundary: assembly signing is a separate, security-sensitive decision. -keyfile reads a strong-name key pair and -keyname uses a key container. Never put a private key path into a shared example, commit a key to source control, or experiment with production signing keys. Delay signing is not full signing, and the manual is clear that a full key pair is required unless delay signing is selected.

6. Diagnose failures without guessing

Omit the output name and the installed program rejects the whole invocation outright:

$ al2 -nologo -target:lib Demo.netmodule
ALINK: error A1017: No target filename was specified

Add -out:NAME.dll; never assume the linker will derive a safe name from the first input on its own. If an input cannot be opened, check its path and permissions without touching them:

$ test -r Demo.netmodule && echo readable
readable
$ ls -l Demo.netmodule

If a build fails after adding metadata, strip it back to the smallest successful command, then add one option at a time. That separates a bad input module from an unsupported metadata value. Options marked unimplemented in the local manual, including -baseaddress and -bugreport, are not working features just because they appear in the option list.

Done means