Compile a Small C# Program with Mono's gmcs Interface

gmcs is the old name for Mono's C# compiler, and it stopped shipping as a separate binary years ago. The local gmcs(1) page still documents it, but this shows you what actually runs in its place, then compiles a small C# program, runs it, and produces a debug build without touching system configuration. On this machine, package mono-mcs is version 6.8.0.105+dfsg-3.6ubuntu2, mcs is installed, and gmcs is not a separate executable; the installed mcs accepts the same relevant options.

Allow about fifteen minutes. You need a shell, the mono-mcs package and a writable working directory. The examples use /tmp/gmcs-example, so they do not need sudo. Keep source files and output outside a system directory while learning the command.

1. Confirm the compiler name and version

Check what is actually installed before copying an example into a build script:

$ command -v mcs
/usr/bin/mcs
$ command -v gmcs
$ 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

An empty result for command -v gmcs is expected here. The manpage's gmcs label is still useful reference material, but scripts should call the binary that exists. The current Mono source also keeps a gmcs wrapper which invokes the mcs compiler, so using mcs directly avoids depending on an obsolete wrapper name.

Checkpoint: Continue with mcs when it prints a version. If neither command exists, install the distribution's Mono compiler package through your normal package-management process before continuing.

2. Create a source file in a scratch directory

Make a directory and write one complete source file. The compiler expects C# source files to use the .cs extension:

$ mkdir -p /tmp/gmcs-example
$ cd /tmp/gmcs-example
$ cat > Hello.cs <<'CS'
using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello from Mono");
    }
}
CS

This here-document is ordinary shell input. It creates no privileged files and can be repeated safely. If you are adapting the example, keep the Main method: an executable target needs an entry point.

3. Build and run the executable

Compile the source with the default target, then name the output explicitly so it is easy to find:

$ mcs -target:exe -out:hello.exe Hello.cs
$ mono hello.exe
Hello from Mono

A successful compiler invocation normally prints nothing and returns status zero. The output is a managed executable containing CIL, not a native Linux ELF binary, so run it with mono. Verify both the file and its exit status:

$ file hello.exe
hello.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
$ printf 'program status: %s\n' "$?"
program status: 0

The wording from file can vary by version. The useful result is that the file exists and identifies as a managed Mono or .NET assembly. Do not mistake a silent compiler for a failed build; check $? immediately after the compiler when a script needs to enforce failure:

$ mcs -target:exe -out:hello.exe Hello.cs
$ test "$?" -eq 0 && echo 'compile succeeded'
compile succeeded

4. Select the language and framework profile deliberately

This compiler supports language-version selection with -langversion. The installed help lists versions through C# 6 and Experimental; the manpage also documents Default, ISO levels and versions 3 through 6. Choose a limit when a project must reject newer syntax, rather than assuming that a compiler upgrade will preserve that policy:

$ mcs -langversion:6 -sdk:4 -target:exe -out:hello-csharp6.exe Hello.cs
$ mono hello-csharp6.exe
Hello from Mono

-sdk:4 selects the .NET 4 base class library set on this installation. The manpage records 4 as the default for its gmcs profile, while this installed compiler also reports 4.5 as its default SDK option. These are compiler-reference choices, not a promise that every API from a newer runtime exists on the machine running the output. If compatibility matters, test on the oldest target runtime you support.

5. Add references and produce a library

The default references are limited. To use an assembly outside those defaults, pass it with -r or -reference. A library target has no required Main method:

$ cat > Greeting.cs <<'CS'
using System;

public static class Greeting
{
    public static string Text(string name)
    {
        return "Hello, " + name;
    }
}
CS
$ mcs -target:library -out:Greeting.dll Greeting.cs
$ file Greeting.dll
Greeting.dll: PE32 executable (DLL) Intel 80386 Mono/.Net assembly, for MS Windows

To reference a local assembly from another build, use its path:

$ cat > UseGreeting.cs <<'CS'
using System;

class UseGreeting
{
    static void Main()
    {
        Console.WriteLine(Greeting.Text("reader"));
    }
}
CS
$ mcs -r:Greeting.dll -out:use-greeting.exe UseGreeting.cs
$ mono use-greeting.exe
Hello, reader

The compiler searches for a named assembly in its normal locations, the current directory and paths supplied with -lib. When a reference fails, first check the spelling and path. Installing assemblies into the Global Assembly Cache is a separate administrative action and is not needed for this local build.

6. Make a debug build without overwriting the release output

Use -debug and a distinct output name when you need debugging information. Mono stores the symbol data beside the assembly in an .mdb file:

$ mcs -debug -optimize- -out:hello-debug.exe Hello.cs
$ ls -l hello-debug.exe hello-debug.exe.mdb
-rwxr-xr-x 1 user user ... hello-debug.exe
-rw-r--r-- 1 user user ... hello-debug.exe.mdb

File ownership, permissions and the exact size vary. The important check is that both names exist. The manpage recommends leaving optimisation off for the best debugging experience. Run the debugger with its own documented debug option when you need source locations; the compiler's -debug flag only creates the information.

7. Treat output replacement and unsafe code as explicit choices

-out can replace an existing file. Preserve a known-good executable or compile to a temporary name before moving it into place:

$ mcs -out:hello.exe.new Hello.cs
$ mono hello.exe.new
Hello from Mono
$ mv hello.exe.new hello.exe

The final mv changes state. Do it only after testing the new output. If compilation fails, remove the incomplete hello.exe.new and the old executable remains available. Do not use -unsafe casually: it permits unsafe C# code and should be paired with a code review and a deliberate runtime-risk decision. Neither option needs elevated privileges in the scratch directory.

Done means