Compile and Check a Small C# Program with mcs

mcs is Mono's command-line C# compiler, and this guide takes a small source file all the way to a running assembly with debug symbols intact. You will see the switches that matter for an everyday build: libraries, language-version limits and warnings treated as errors. Allow about fifteen minutes; you need a shell, the mono-mcs package and a working Mono runtime.

This guide describes the installed command on this machine: mono-mcs 6.8.0.105+dfsg-3.6ubuntu2, reporting Mono C# compiler version 6.8.0.105. The manual page is old, so keep the version check in your build notes when reproducing these commands on another host.

1. Check the compiler and write a source file

These are ordinary, unprivileged commands. No sudo is needed if you work in a directory you can write:

$ command -v mcs
/usr/bin/mcs
$ dpkg-query -W -f='${Package} ${Version}\n' mono-mcs
mono-mcs 6.8.0.105+dfsg-3.6ubuntu2
$ mcs --version
Mono C# compiler version 6.8.0.105

Create Hello.cs with this deliberately small program:

using System;

class Hello
{
    public static void Main()
    {
        Console.WriteLine("mcs-ok");
    }
}

The file must have a .cs extension. Compilation is not a partial-file operation: pass every source file the assembly needs on the same command line, or use a recursive source pattern explicitly.

2. Build and run an executable

Compile the source as a normal executable. -out gives the assembly a predictable name:

$ mcs -out:hello.exe Hello.cs
$ mono hello.exe
mcs-ok

With no errors, mcs stays quiet. The output is a .NET or CLI assembly containing CIL byte code, not a native Linux ELF binary: run it with the Mono runtime, and do not infer native portability from the .exe suffix.

Checkpoint: confirm that the output exists and is an assembly before you pass it to another tool:

$ file hello.exe
hello.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, 3 sections
$ test -s hello.exe && echo 'executable output is non-empty'
executable output is non-empty

3. Select the output kind

The default target is an executable. Use -target:library when the source should produce a component for another assembly instead. -out is still worth using, since it avoids relying on the source file name:

$ mcs -target:library -out:Greeter.dll Hello.cs
$ file Greeter.dll
Greeter.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, 3 sections

This example only demonstrates the compiler target: a reusable library normally exposes types for another program to reference rather than containing the application's entry point. If several classes contain Main, pick the entry point with -main:CLASS when building an executable.

Do not replace a useful output blindly. Both compilation and shell redirection can overwrite a destination. Build to a new name first, check it, then replace the old file only if that is intentional:

$ cp --preserve=all hello.exe hello.exe.bak
$ mcs -out:hello.new.exe Hello.cs
$ mono hello.new.exe
mcs-ok
$ mv hello.new.exe hello.exe

Recovery: cp and mv change files but need no elevated privileges in your own directory. To undo this example before the new output is trusted, restore the backup with mv hello.exe.bak hello.exe. That replaces the new file, so check the paths carefully.

4. Add source files and references

Pass multiple source files together when they form one assembly:

$ mcs -out:hello.exe Hello.cs OtherClass.cs

For a type supplied by another assembly, use -r:ASSEMBLY or its longer -reference spelling. Supply a path when the assembly is not in the normal search locations:

$ mcs -r:/path/to/Library.dll -out:hello.exe Hello.cs

For package-managed Mono components, -pkg:PACKAGE asks pkg-config for the libraries and directories; the manual gives the form mcs -pkg:gtk-sharp demo.cs. Only use a package name that pkg-config can resolve on this host. -pkg:dotnet is a shortcut for the available System.* libraries, not a request for every Mono library.

When a reference cannot be found, check the exact file and the relevant package before adding random search paths. -lib:PATHLIST supplies comma-separated library directories, while repeated -L PATH options also add search paths.

5. Control language features and diagnostics

The installed manual describes C# 1.0 through 6.0, with partial C# 7.0 support. The default language setting is the compiler's latest supported setting, documented for this installation as C# 6.0. Pin an older feature set when compatibility matters:

$ mcs -langversion:5 -out:legacy.exe Hello.cs
$ mono legacy.exe
mcs-ok

This switch limits language features; it does not by itself choose a different base class library. Use -sdk:2 or -sdk:4 when you need the documented base-library compatibility target (4 is the compiler's default), and check the target's available assemblies before relying on it in a portable build.

Warnings sit at level 4 by default. Treat them as errors in a build that must stay clean:

$ mcs -warn:4 -warnaserror -out:hello.exe Hello.cs

If one warning should stay non-fatal, use -warnaserror:W1 style selection, or disable a known warning with -nowarn:WARNLIST. Keep the warning number and the reason in the build configuration: suppressing everything just makes later failures harder to see.

6. Produce debugging information

Add -debug for debugging information. On this Mono compiler it lands beside the assembly in an .mdb file:

$ mcs -debug -out:debug.exe Hello.cs
$ ls -l debug.exe debug.exe.mdb
-rwxr-xr-x 1 user user 3072 Sep 25 01:17 debug.exe
-rw-r--r-- 1 user user  259 Sep 25 01:17 debug.exe.mdb

File sizes and timestamps vary; the useful check is that both files exist after a successful build. To get runtime stack traces with this debugging data, the manual says to start Mono with --debug:

$ mono --debug debug.exe
mcs-ok

Do not commit generated assemblies or debug files by accident. Check your project ignore rules and review git status before staging a build directory.

7. Diagnose a failed build

Read the first compiler error from the source location it names. If paths are too terse for a larger build, add -fullpaths so diagnostics include absolute source paths. If you just need to confirm parsing without producing an assembly, --parse is the documented benchmarking mode, but it is not a replacement for a normal build check.

For repeatable invocations, put options and source names in a response file and pass it with an @ prefix. Review the file before running it, because it is still executable build input:

$ mcs @build.rsp
$ printf 'compiler exit status: %s\n' "$?"
compiler exit status: 0

Warning: keep private signing material out of casual examples. -keyfile:FILE signs an output with a strong-name key pair, while -delaysign+ embeds only a public key for later signing. Treat key files as security-sensitive: do not copy them into a shared build directory or paste their contents into a response file.

Done means