Skip to content

Basic Tutorial

Paul Baudron edited this page Mar 6, 2023 · 9 revisions

Basic Tutorial

This is a tutorial that will show you the basics of using Sharpmake. You will create a Sharpmake script that generates a project and a solution for a very simple C++ project.

Building Sharpmake

The first step is to obtain Sharpmake if you have not already. You can get the most recent version of Sharpmake on Ubisoft's GitHub repository at https://github.com/ubisoft/Sharpmake. Clone the repository somewhere on your machine.

Open Sharpmake.sln in Visual Studio and compile Sharpmake.Application.

All the required binaries can be found in the Sharpmake.Application\bin\(Debug|Release)\net6.0 folder. Take all the binaries and copy them in an empty folder.

The Sample Code

In that same folder, create the file main.cpp and paste the C++ code below into the file.

This is the code we are going to create a project for.

// main.cpp
#include <iostream>
using namespace std;

int main(int argc, char** argv)
{
    cout << "Hello, Sharpmake!" << endl;
}

The Project Class

Next, create the Sharpmake script. Name it main.sharpmake.cs file (this is just an ordinary C# code file).

Constructor

Let's start by writing the class and its constructor. C++ project classes must inherit the Project class.

(If you were generating a C# project instead, you would inherit CSharpProject instead).

Each project in Sharpmake is represented by a C# class which is decorated with the [Generate] attribute.

using System.IO; // For Path.Combine
using Sharpmake; // Contains the entire Sharpmake object library.

// Represents the project that will be generated by Sharpmake and that contains
// the sample C++ code.
[Generate]
public class BasicsProject : Project
{
    public BasicsProject()
    {
        // The name of the project in Visual Studio. The default is the name of
        // the class, but you usually want to override that.
        Name = "Basics";

        // The directory that contains the source code we want to build is the
        // same as this one. This string essentially means "the directory of
        // the script you're reading right now."
        SourceRootPath = @"[project.SharpmakeCsPath]";

        // Specify the targets for which we want to generate a configuration
        // for. Instead of creating multiple targets manually here, we can
        // use the binary OR operator to define multiple targets at once.
        // Sharpmake will generate all combinations possible and generate a
        // target for it.
        //
        // The code below is the same as creating 4 separate targets having
        // those flag combinations:
        //    * Platform.win32, DevEnv.vs2015, Optimization.Debug
        //    * Platform.win32, DevEnv.vs2015, Optimization.Release
        //    * Platform.win64, DevEnv.vs2015, Optimization.Debug
        //    * Platform.win64, DevEnv.vs2015, Optimization.Release
        AddTargets(new Target(
            // we want a target that builds for both 32 and 64-bit Windows.
            Platform.win32 | Platform.win64,

            // we only care about Visual Studio 2015. (Edit as needed.)
            DevEnv.vs2015,

            // of course, we want a debug and a release configuration.
            Optimization.Debug | Optimization.Release));
    }
}

In a project's constructor, you generally do 3 things:

  1. The first thing to do is to assign a name to your project. If you don't do that, the name of the class will be used, which is probably not what you want.

  2. The second thing is to tell the project where is the code. For this we use the SharpmakeCsPath property, which is simply the path of the sharpmake script. While we could simply assign it using this.SharpmakeCsPath, here we use a special feature of Sharpmake called resolvable strings.

  3. The third and most important thing to do is to specify the targets for which this project can build for. Here we declare only one. It will create a Visual Studio 2015 solution with a 32-bit and a 64-bit configuration, each having a Debug and Release configuration.

Resolvable Strings

Most string fields in Sharpmake classes can contain reference properties and fields between square brackets. This is similar to string interpolation which was added to C# 6, but the replacements are done during the actual generation process. For example, "[project.Name]" will be replaced by the name of the project.

Resolvable strings may use the following objects: conf, target, project and solution.

  • project: The project being generated.
  • solution: The solution being generated.
  • conf: The configuration of the project or the solution being generated.
  • target: The target being generated.

Configuration

Even with a constructor, you usually need to tweak the configuration for each generated target. We do so by writing a method with the [Configure] attribute and that takes a Project.Configuration and a Target as it's arguments. Here we just redirect the configuration for each target so that it outputs all generated files in a generated sub directory.

Of course, since this is C# you have access to all the methods you are used to, such as Path.Combine

// Sets the properties of each configuration (conf) according to the target.
//
// This method is called once for every target specified by AddTargets. Since
// we only want vs2015 targets and we want 32- and 64-bit targets, each having
// a debug and a release version, we have 1 x 2 x 2 targets to configure, so it
// will be called 4 times.
//
// If we had instead specified vs2012 | vs2015 | vs2017 it would have been
// called 12 times. (3 x 2 x 2)
[Configure]
public void ConfigureAll(Project.Configuration conf, Target target)
{
    // Specify where the generated project will be. Here we generate the
    // vcxproj in a /generated directory.
    conf.ProjectPath = Path.Combine("[project.SharpmakeCsPath]", "generated");
}

About Fragments and Targets

The target's enumeration parameters that you combine with the | operator are called fragments and are what compose a target. When Sharpmake notices that a target combines fragments like this, it will separate the fragments and generate an actual Target for all possible arrangements, which will be passed to configure methods.

You can easily create your own Target and fragments if those provided are not good enough, but this is not in the scope of this tutorial.

This completes the project definition class.

The Solution Class

Just like we had to create a class that represents a project, we also have to create a class that represents the solution that will contain the project. The process is very similar, except that we need to add the project to the solution when we configure it.

Solution definition classes must inherit Solution instead of Project.

// Represents the solution that will be generated and that will contain the
// project with the sample code.
[Generate]
public class BasicsSolution : Solution
{
    public BasicsSolution()
    {
        // The name of the solution.
        Name = "Basics";

        // As with the project, define which target this solution builds for.
        // It's usually the same thing.
        AddTargets(new Target(
            Platform.win32 | Platform.win64,
            DevEnv.vs2015,
            Optimization.Debug | Optimization.Release));
    }

    // Configure for all 4 generated targets. Note that the type of the
    // configuration object is of type Solution.Configuration this time.
    // (Instead of Project.Configuration.)
    [Configure]
    public void ConfigureAll(Solution.Configuration conf, Target target)
    {
        // Puts the generated solution in the /generated folder too.
        conf.SolutionPath = @"[solution.SharpmakeCsPath]\generated";

        // Adds the project described by BasicsProject into the solution.
        // Note that this is done in the configuration, so you can generate
        // solutions that contain different projects based on their target.
        //
        // You could, for example, exclude a project that only supports 64-bit
        // from the 32-bit targets.
        conf.AddProject<BasicsProject>(target);
    }
}

The only noteworthy part is the conf.AddProject<BasicsProject>(target) call, which is used to fill the solution with projects at configuration time.

Main Function

Finally, we also need a static main function, which is generally called SharpmakeMain, is decorated with the [Sharpmake.Main] attribute and takes a Sharpmake.Arguments as it's only argument. We put it in its own static class, but it can actually be anywhere as long as it's public and static (and its container is too).

public static class Main
{
    [Sharpmake.Main]
    public static void SharpmakeMain(Sharpmake.Arguments arguments)
    {
        // Tells Sharpmake to generate the solution described by
        // BasicsSolution.
        arguments.Generate<BasicsSolution>();
    }
}

We are done with main.sharpmake.cs.

Save it.

Your directory should look like this:

  • main.cpp
  • main.sharpmake.cs
  • Sharpmake.Application.exe
  • *** // a bunch of dll dependencies and subfolders sharpmake depends on

Running Sharpmake

Time to run Sharpmake! Open a terminal and type this command.

Sharpmake.Application.exe /sources('main.sharpmake.cs')

You should now have a generated folder that contains a .sln and a .vcxproj.

Open it and make sure it builds. (It should!)