Psake build automation for .NET projects using dotnet.exe

Powershell based psake build automation for .NET projects using dotnet.exe

Posted by Alfus Jaganathan on Wednesday, September 22, 2021


To learn more about the background and why psake, please refer to the article Psake build automation for .NET projects using msbuild.exe


Let’s have a detail look at, how to implement psake for automating build of a .NET project using dotnet.exe

Below are the three script files required

  1. build.cmd for windows or for linux/ macOS - this is the entry point script used to be called with some arguments
  2. configure-build.ps1 - this script is called first, which configures the dependencies. e.g. install psake, install vssetup, etc.
  3. default.ps1 - psake based script file which contains all the build tasks to be invoked

Let’s build these files one by one with minimal implementation.

Build default.ps1

Note: Here we use dotnet.exe to perform underlying tasks like clean, restore, build, etc. We can also use msbuild.exe and its corresponding arguments instead of dotnet.exe. Unlike dotnet.exe, msbuild.exe, can be used only in windows. You can find those details in this article Psake build automation for .NET projects using msbuild.exe

1. Build Reusable Functions - these are some of the basic powershell functions we need to build, so that use them later in the process.

(i) Function that returns the location of dotnet.exe

function global:Get-Dotnet()
	return (Get-Command dotnet).Path

(ii) Function that can delete a directory, whether empty or not, we use here in cleanup tasks.

function global:Delete-Directory($directory_name)
  rd $directory_name -recurse -force  -ErrorAction SilentlyContinue | out-null

2. Setting Up Properties/Variables - these are the variables required by the tasks (restore, build, test, etc.). We can keep the solution name and project name same for simplicity. Another point to note here is, $solution_name will be injected via command line parameter/argument.

$script:project_config = "Debug" # script level variable to store configuration, that we will be used when building/publishing the project

properties {
  $base_dir = resolve-path . # resolves current working directory
  $publish_dir = "$base_dir\_publish" # the directory where the artifacts are published
  $solution_file = "$base_dir\$solution_name.sln" # solution file path
  $project_dir = "$base_dir\src\$solution_name" # primary project file path
  $project_file = "$project_dir\$solution_name.csproj" # primary project file path
  $release_id = "win-x64" # rid that we will be used when publishing the project

  $dotnet_exe = Get-Dotnet

3. Create Tasks - Create basic tasks like clean, restore, build, publish, etc.

(i) Clean the solution using dotnet clean command, in addition to directories like bin, obj, etc.

task Clean 
  if (Test-Path $publish_dir) {
      delete_directory $publish_dir
  Get-ChildItem .\ -include obj,bin -Recurse | foreach ($_) { remove-item $_.fullname -Force -Recurse}
  exec { & $dotnet_exe clean -c $project_config $solution_file }

(ii) Restores all solution dependencies using dotnet restore command

task Restore 
  exec { & $dotnet_exe restore $solution_file }

(iii) Compile the solution using dotnet build command. Additionally, we can use dependency on tasks as below so that it makes sure that dependendency task is executed before the current task

task Compile -depends Restore 
  exec { & $dotnet_exe build -c $project_config $solution_file }

(iv) Execute tests on project that name matches pattern *.Tests, using dotnet test

task RunTests
  exec { & $dotnet_exe test -c $project_config "$project_dir.Tests" -- xunit.parallelizeTestCollections=true }

(v) Publish the primary project using dotnet publish command

task Publish
  exec { & $dotnet_exe publish -c $project_config $project_file -o $publish_dir -r $release_id}

(vi) Set project configuration as needed (Debug or Release)

task SetDebugBuild {
    $script:project_config = "Debug"

task SetReleaseBuild {
    $script:project_config = "Release"

4. Create Task Groups - Group these tasks as another single task, so that we can execute them together using a single task.

task DevBuild -depends SetDebugBuild, Restore, Clean, Compile, RunTests
task DevPublish -depends DevBuild, Publish
task CiBuild -depends SetReleaseBuild, Restore, Clean, Compile, RunTests
task CiPublish -depends CiBuild, Publish

5. Create Aliases - Create alises or short names for the grouped build tasks (nice to have)

task default -depends DevBuild #default used if none of the targets/tasks are mentioned with build.cmd
task cb -depends CiBuild
task cp -depends CiPublish
task dp -depends DevPublish

Build configure-build.ps1

This is the script file used to configure dependencies. In this case, we need powershell module psake to be installed to perform the build operations. Here is how the script file looks like.

$has_psake = Get-Module -ListAvailable | Select-String -Pattern "Psake" -Quiet
if(-Not($has_psake)) {
    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
    Install-Module Psake -Scope CurrentUser

Build build.cmd or

Note: Use cmd for windows platform and bash for linux/ macOS, to run these build scripts.

build.cmd or is the entry point for executing build. Based on the sample we built above, we can execute the build from a command line using build.cmd or ./ which uses the default task.

To run a specific task, we can execute by passing a task or alias as a command argument. e.g. build.cmd DevPublish, build.cmd dp, build.cmd cp, ./ DevPublish, ./ dp, ./ cp, etc.

Here is how build.cmd file looks like.

@echo off
powershell.exe -NoProfile -ExecutionPolicy bypass -Command "& {.\configure-build.ps1 }"
powershell.exe -NoProfile -ExecutionPolicy bypass -Command "& {invoke-psake .\default.ps1 %1 -parameters @{"solution_name"="'<solution file name>'"}; exit !($psake.build_success) }"

Here is how file looks like.

pwsh -NoProfile -ExecutionPolicy bypass -Command "& {.\configure-build.ps1 }"
pwsh -NoProfile -ExecutionPolicy bypass -Command "& {Invoke-psake .\default.ps1 $1 -parameters @{'solution_name'='<solution file name>'}; exit !('$psake.build_success') }"


  1. <solution file name> has to replaced by the actual solution name.
  2. Make sure to convert file an executable using chmod +x ./

Hope you find this article, an useful one!

Love helping the community!

comments powered by Disqus