Sake Documentation¶
Sake is a C# language enabled make system and is used to build the projects that comprise the ASP.NET 5 stack. Sake uses a custom build of the Spark view engine, and additional insight into working with Sake can be gained from reviewing Spark.
Note
I pronounce “Sake” as rhyming with “make”. This seems to make the most sense, whether you consider “Sake” to be a blend of “CS make”, or “Spark make”, and it avoids confusion when discussing it along with psake (PowerShell make), which is pronounced as the Japanese rice wine.
Note
Sake was created by Louis DeJardin. I was not a contributor to the Sake project, and this documentation is based on trial and error, review of the Sake source code and Spark documentation, and looking at Sake’s use in the ASP.NET 5 projects.
See also
Source code for the samples is available on github.
Getting Started¶
To get started with Sake, create the following two files:
The build.cmd
file checks for and downloads NuGet if needed, installs the Sake NuGet package if needed, and finally executes Sake specifying makefile.shade
as the build file.
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -f makefile.shade
makefile.shade
is a Spark view engine template file that specifies a default build target and writes Hello world!
to the console.
#default
@{
Log.Info("Hello world!");
}
Note
Andrew Stanton-Nurse has a Sublime 3 package that adds colorization for .shade
files: Sublime-Sake
Note
The Spark view engine supports template files using off-side rule formatting where indentation denotes structure, as in Python, Jade, and Haml. These files have a .shade
file extension to differentate them from .spark
template files, which use opening and closing tags for structure.
Run the build:
>build.cmd
Attempting to gather dependencies information for package 'Sake.0.2.2' with respect to project 'packages', targeting 'Any,Version=v0.0'
Attempting to resolve dependencies for package 'Sake.0.2.2' with DependencyBehavior 'Lowest'
Resolving actions to install package 'Sake.0.2.2'
Resolved actions to install package 'Sake.0.2.2'
Adding package 'Sake.0.2.2' to folder 'packages'
Added package 'Sake.0.2.2' to folder 'packages'
Successfully installed 'Sake 0.2.2' to packages
info: Hello world!
The build file will restore the Sake nuget package and write out the log message.
Congratulations! You’ve created your first Sake build.
Working with Sake¶
Element Tags and C#¶
.shade
files use an offside-rule format, like Python, Jade, or Haml, which means that indentation determines structure. .shade
templates can contain a mix of element tags and C# code. The concept of element tags in a .shade
file makes sense when you consider that .shade
files are templates processed by the Spark view engine into a dynamically generated class. Originally, these classes would have been used to generate a response in a web site (think Razor and .cshtml
files); Sake repurposes Spark to process .shade
template files into classes that run a build.
Working with Element Tags¶
Sake comes with a standard set of element tags, like exec
and log
, and you can also create your own Custom Element Tags. A .shade
file will also use element tags from Spark, like use
and macro
. See the Spark elements reference for more information.
Note
I have not tried to use all of the Spark elements in a Sake build file, and some may not make sense to use in a build file, or may not work as described in the Spark documentation.
The following .shade
file illustrates the basics of working with element tags and can be run using the build.cmd
file from Getting Started. Sake requires at least one target, so we define one here named #default
. Targets are explained in detail later in the documentation.
Interesting things to note:
- Strings are delimited with single or double quotes.
- Element tags that are not indented run before targets.
-// example of a single-line comment
-/*
example of a
multi-line comment
*/
log warn="This executes first."
#default
log info='Hello world'
log warn="This also executes before the target."
Running the file above produces the following output:
warn: This executes first.
warn: This also executes before the target.
info: Hello world
Working with C#¶
C# code can be used as a code block, delimited with @{
and }
:
@{
var message = "Hello world!";
Log.Info(message);
}
C# can also appear in element tags, delimited with ${
and }
:
log info="The current date and time is ${DateTime.Now.ToString()}."
Note
Version 0.2.2 of Sake targets .NET 4.0, which corresponds to C# 4.
String Delimiters¶
As with element tags, strings in C# code can be delimited using either single or double quotes. This raises the interesting problem of working with char variables in C#. For example, the following code will generate an exception in a .shade
file:
var tokens = "a,b,c".Split(',');
The ','
argument is treated as a string, and an exception will be thrown because Split
expects a char. To work around this, cast to char:
var tokens = "a,b,c".Split((char)',');
Namespaces¶
The use
element is analogous to the using
directive in C#. In the example below, Console
and Directory
do not need to be fully qualified because the System
and System.IO
namespaces are specified by the use
elements:
use namespace="System"
use namespace="System.IO"
#default
@{
Console.WriteLine(Directory.GetCurrentDirectory());
}
The following .shade
file shows the basics of working with C# in Sake, and also how you can work with both C# and tags in the same build file.
use namespace="System"
#default
@{
var now = DateTime.Now;
Console.WriteLine("Hello world using C#!");
}
log info="Hello world using tags! It is ${now.ToString()}"
This produces the following output:
>build.cmd
Hello world using C#!
info: Hello world using tags! It is 11/14/2015 12:24:29 PM
Targets¶
Sake build steps are organized into targets. Targets are defined in a .shade
file as an element starting with a #
and can be set up to be dependent on each other.
An example .shade
file illustrating the topics presented in this page appears at the end of the page. Be sure to review the build.cmd
file as it changes slightly to pass a target to Sake.
Default Target¶
The first target in a .shade
file is the default target. If Sake is executed without specifying a target, the default target is executed.
Dependencies¶
To indicate that a target target-b
depends on target-a
to run before it, add .target-a
to the declaration of target-b
:
#target-b .target-a
For example:
#target-a
log info='target a'
#target-b .target-a
log info='target b'
#target-c .target-b
log info='target c'
When run specifying target-c
, the following output is produced:
>build.cmd target-c
info: target a
info: target b
info: target c
Dependencies can also be specified from the predecessor by using the target
attribute:
#target-1 target="target-2"
log info='target 1'
#target-2 target="target-3"
log info='target 2'
#target-3
log info='target 3'
Running target-3
executes target-1
and target-2
as expected:
>build.cmd target-3
info: target 1
info: target 2
info: target 3
Multiple Dependencies¶
To specify multiple dependencies, list them in order in the definition of the target:
#target-x
log info='target x'
#target-y
log info='target y'
#target-z .target-y .target-x
log info='target z'
Note that .target-y
appears before .target-x
in the dependency list, and when target-z
is run, target-y
is run before target-x
:
>build.cmd target-z
info: target y
info: target x
info: target z
Example¶
build.cmd
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -f makefile.shade %*
makefile.shade
use namespace="System"
#default
log info='default'
#target-c .target-b
log info='target c'
#target-a
log info='target a'
#target-b .target-a
log info='target b'
#target-3
log info='target 3'
#target-1 target="target-2"
log info='target 1'
#target-2 target="target-3"
log info='target 2'
#target-x
log info='target x'
#target-y
log info='target y'
#target-z .target-y .target-x
log info='target z'
Extending Sake¶
.shade
files containing functions, classes, or custom element tags can be imported into the primary build file. These files can be placed in a directory, which is then provided to Sake using the I
argument. For example, the following command would run Sake with import files in a directory named imports
:
Sake.exe -I imports -f makefile.shade %*
Custom Functions¶
Import files can contain C# functions and classes in a functions code block:
use namespace="System"
use namespace="System.Collections.Generic"
functions
@{
private List<CustomItem> _items = new List<CustomItem>();
public void AddCustomItem(string name)
{
_items.Add(new CustomItem { Name = name });
}
public void PrintCustomItems()
{
foreach(var item in _items)
{
Console.WriteLine(item.Name);
}
}
public class CustomItem
{
public string Name { get; set; }
}
}
These functions can be included in another .shade
file using the import
element:
use import="CustomFunctions"
#default
@{
AddCustomItem('foo');
AddCustomItem('bar');
AddCustomItem('baz');
PrintCustomItems();
}
Running this produces the following output:
>build.cmd
foo
bar
baz
Custom Element Tags¶
Import files can also be used to create custom element tags. To create a custom element, name the file with a leading underscore; the remainder of the file name will then be the element name. Within the file, default values for attributes can be specified, and any attribute values not provided with default values must be provided when the element is used.
The following simple example defines a default value of "Hello"
for the greeting
attribute. A value will be required for the name
attribute when the element is used.
default greeting='Hello'
@{
Log.Info(greeting + " " + name);
}
If the sample above is saved as _echo.shade
, it can be used in a target like so:
#echotag
echo name="Bob"
Running the echotag
target produces the following output:
>build.cmd echotag
info: Hello Bob
To use a custom element in C# code, you can define a macro
:
macro name='Echo' name='string' greeting='string'
echo
The macro can then be called as you would a C# function:
#echomacro
@{
Echo("Jack", "Good morning");
}
Examples¶
The following files include the code samples in this page. The build.cmd
file calls Sake specifying an import folder:
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -I imports -f makefile.shade %*
Save the makefile.shade
file in the same folder as the build.cmd
file:
use import="CustomFunctions"
#default
@{
AddCustomItem('foo');
AddCustomItem('bar');
AddCustomItem('baz');
PrintCustomItems();
}
#echotag
echo name="Bob"
#echomacro
@{
Echo("Jack", "Good morning");
}
macro name='Echo' name='string' greeting='string'
echo
Create an imports
folder within the folder containing the build.cmd
file and create the following files in it.
CustomFunctions.shade
:
use namespace="System"
use namespace="System.Collections.Generic"
functions
@{
private List<CustomItem> _items = new List<CustomItem>();
public void AddCustomItem(string name)
{
_items.Add(new CustomItem { Name = name });
}
public void PrintCustomItems()
{
foreach(var item in _items)
{
Console.WriteLine(item.Name);
}
}
public class CustomItem
{
public string Name { get; set; }
}
}
_echo.shade
:
default greeting='Hello'
@{
Log.Info(greeting + " " + name);
}
Examples¶
Basic Sake examples will be included here. For comprehensive, real-world examples of Sake build files, see the ASP.NET 5 projects, particularly the Universe project.
Console Example¶
This example shows custom functions used to write to the Console in different colors.
build.cmd
:
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -I imports -f makefile.shade %*
Console.shade
saved to the imports
directory:
use namespace="System"
use namespace="System.IO"
functions
@{
void WriteLine(string text, string colorText)
{
ConsoleColor color;
if (Enum.TryParse<ConsoleColor>(colorText, true, out color))
{
WriteLine(text, color);
return;
}
WriteLine(text);
}
void WriteLine(string text = null, ConsoleColor? color = null)
{
if (text != null && color != null)
{
Console.ForegroundColor = color.Value;
}
Console.WriteLine(text);
if (text != null && color != null)
{
Console.ResetColor();
}
}
void Write(string text, string colorText)
{
ConsoleColor color;
if (Enum.TryParse<ConsoleColor>(colorText, true, out color))
{
Write(text, color);
return;
}
Write(text);
}
void Write(string text = null, ConsoleColor? color = null)
{
if (text != null && color != null)
{
Console.ForegroundColor = color.Value;
}
Console.Write(text);
if (text != null && color != null)
{
Console.ResetColor();
}
}
}
makefile.shade
:
use namespace="System.Linq"
use import="Console"
#default
@{
WriteLine();
WriteLine(" Colors in ConsoleColor", "yellow");
WriteLine(" ======================", "cyan");
foreach(var color in Enum.GetValues(typeof(ConsoleColor)).Cast<ConsoleColor>())
{
Write(" ");
WriteLine(color.ToString(), color);
}
WriteLine();
WriteLine(" ======================", "cyan");
WriteLine();
}
Output:

Help Example¶
This example shows custom functions and classes used to enumerate the targets in the build and list them in the console. Targets have a description
attribute, and this example allows for a group to be included in the description, separated from the actual target description using a |
character. This example makes use of the Console Example to output text in color; include Console.shade
in the imports
folder if you aren’t using the source code from github.
build.cmd
:
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -I imports -f makefile.shade %*
Help.shade
saved to the imports
directory:
use namespace="System"
use namespace="System.IO"
use namespace="System.Collections"
use namespace="System.Collections.Generic"
use import="Console"
functions
@{
void WriteHelp()
{
WriteHelpHeader();
var groups = GetTargetGroups();
WriteHelpGroups(groups);
WriteHelpFooter();
}
void WriteHelpHeader()
{
WriteLine();
Write("********************************", ConsoleColor.DarkGreen);
Write(" HELP ", ConsoleColor.Green);
WriteLine("********************************", ConsoleColor.DarkGreen);
WriteLine();
Write("This build script has the following build ");
Write("targets", ConsoleColor.Green);
WriteLine(" set up:");
}
TargetGroups GetTargetGroups()
{
var groups = new TargetGroups();
foreach(var kvp in Targets)
{
var target = kvp.Value;
var tokens = target.Description.Split((char)'|');
if (tokens.Length == 2)
{
groups.Add(tokens[0], target.Name, tokens[1]);
}
else
{
groups.AddUngrouped(target.Name, target.Description);
}
}
return groups;
}
void WriteHelpGroups(TargetGroups groups)
{
// write out any ungrouped targets first
foreach(var target in groups.UngroupedItems)
{
WriteLine();
Write(" ");
Write(target.Name, ConsoleColor.Green);
Write(" = ");
WriteLine(target.Description);
}
// write out groups
foreach(var group in groups)
{
WriteLine();
Write(" ");
WriteLine(group.Name, ConsoleColor.DarkGreen);
foreach(var target in group.Targets)
{
Write(" > ");
Write(target.Name, ConsoleColor.Green);
Write(" = ");
WriteLine(target.Description);
}
}
}
void WriteHelpFooter()
{
WriteLine();
WriteLine(" For a complete list of build tasks, view makefile.shade.");
WriteLine();
WriteLine("**********************************************************************", ConsoleColor.DarkGreen);
}
public class TargetItem
{
public TargetItem(string name, string description)
{
Name = name;
Description = description;
}
public string Name { get; private set; }
public string Description { get; private set; }
}
public class TargetGroup
{
private readonly List<TargetItem> _targets;
public TargetGroup(string name)
{
_targets = new List<TargetItem>();
Name = name;
}
public string Name { get; private set; }
public List<TargetItem> Targets { get { return _targets; } }
public TargetItem Add(string name, string description)
{
var item = new TargetItem(name, description);
_targets.Add(item);
return item;
}
}
public class TargetGroups : IEnumerable<TargetGroup>
{
private readonly Dictionary<string, TargetGroup> _groups;
private readonly List<TargetItem> _ungrouped;
public TargetGroups()
{
_groups = new Dictionary<string, TargetGroup>();
_ungrouped = new List<TargetItem>();
}
public List<TargetItem> UngroupedItems
{
get { return _ungrouped; }
}
public TargetGroup Add(string groupName, string itemName, string itemDescription)
{
var group = _groups.ContainsKey(groupName) ? _groups[groupName] : null;
if (group == null)
{
group = new TargetGroup(groupName);
_groups.Add(group.Name, group);
}
group.Add(itemName, itemDescription);
return group;
}
public TargetItem AddUngrouped(string itemName, string itemDescription)
{
var item = new TargetItem(itemName, itemDescription);
_ungrouped.Add(item);
return item;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public IEnumerator<TargetGroup> GetEnumerator()
{
foreach(var kvp in _groups)
{
yield return kvp.Value;
}
}
}
}
makefile.shade
:
use import="Console"
use import="Help"
#default description="Comprehensive|Performs a full clean, build and test"
#clean description="Build|Remove artifacts of a previous build"
#dnx description="Build|Check for and install DNX."
#restore description="Build|Restore packages for the project"
#build description="Build|Build the project"
#alltest description="Test|Run all tests"
#unittest description="Test|Run unit tests"
#inttest description="Test|Run integration tests"
#help description="Help|Displays a list of build commands"
@{
WriteHelp();
}
Output:

MSBuild Example¶
The _build
element that comes with Sake is written to use MSBuild 4.0. If your source code uses features of C# 5 or 6, this may not work. For example, when building an application that uses string interpolation, which was added in C# 6, the build fails indicating that $
is an unexpected character.
This example shows a custom element, based on _build
, which uses MSBuild 14.0 (VS 2015, C# 6) or MSBuild 12.0 (VS 2012/13, C# 5).
The solution that this example builds can be found along with the other files on github. Note in the example makefile.shade
, the output directory is relative the .csproj
files.
build.cmd
:
@echo off
cd %~dp0
SETLOCAL
SET NUGET_VERSION=latest
SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe
IF EXIST "%CACHED_NUGET%" goto copynuget
echo Downloading latest version of NuGet.exe...
IF NOT EXIST "%LocalAppData%\NuGet" md "%LocalAppData%\NuGet"
@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'"
:copynuget
IF EXIST .nuget\nuget.exe goto restore
md .nuget
copy "%CACHED_NUGET%" .nuget\nuget.exe > nul
:restore
IF EXIST packages\Sake goto run
.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages
:run
packages\Sake\tools\Sake.exe -I imports -f makefile.shade %*
_msbuild.shade
saved to the imports
directory:
@{/*
build
Executes msbuild to compile your project or solution
projectFile=''
Required. Path to the project or solution file to build.
configuration='Release'
Determines which configuration to use when building.
outputDir=''
Directs all compiler outputs into the target path. Note: this will be relative to the project files (not the solution file if building a solution).
extra=''
Additional commandline parameters for msbuild
*/}
default configuration='Release'
default outputDir=''
default extra=''
use namespace="System"
use namespace="System.IO"
use namespace="System.Reflection"
var buildProgram=''
@{
Assembly buildUtilities = null;
string toolsVersion = null;
try
{
buildUtilities = Assembly.Load("Microsoft.Build.Utilities.Core, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
toolsVersion = "14.0";
}
catch
{
buildUtilities = Assembly.Load("Microsoft.Build.Utilities.v12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
toolsVersion = "12.0";
}
var helper = buildUtilities.GetType("Microsoft.Build.Utilities.ToolLocationHelper");
var method = helper.GetMethod("GetPathToBuildTools", new Type[] { typeof(string) } );
var path = method.Invoke(helper, new object[] { toolsVersion }).ToString();
buildProgram = Path.Combine(path, "msbuild.exe");
}
var OutDirProperty=''
set OutDirProperty='OutDir=${outputDir}${Path.DirectorySeparatorChar};' if='!string.IsNullOrWhiteSpace(outputDir)'
exec program="${buildProgram}" commandline='${projectFile} "/p:${OutDirProperty}Configuration=${configuration}" ${extra}'
makefile.shade
:
#default .build
#clean
msbuild projectFile="src/SakeMsBuild.sln" outputDir="../../output" extra="/t:Clean"
#build .clean
msbuild projectFile="src/SakeMsBuild.sln" outputDir="../../output" extra="/t:Rebuild /m"
Output:
