static void

MsBuild

MSDN

Command Line/.bat/cmd file

%systemroot%\Microsoft.Net\Framework\v4.0.30319\MSBuild.exe build.xml /t:Publish /fl & pause

For C#6 (string interpolation, null conditionals etc) or C#7 (pattern matching, out variables and return tuples) you need the Roslyn, with alternative MSBuild locations:

VS2017 puts msbuild in a variable location, but from VS2017 update 2 you can use "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" to find it again. (For older VS2017, get the vswhere binary)

Below is from the example

@echo off
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -latest -products * -requires Microsoft.Component.MSBuild -property installationPath`) do (
set InstallDir=%%i
)

if exist "%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" (
"%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" build.xml /t:PublishAll /fl & pause
) else (
echo "Must install VS2017 update 2 or greater"
)

Build script

This is a command line build script, which could be used locally or on a build server. It does a "publish" (as in Visual Studio "Publish..." with a file target, for XCopy deployment).

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Publish">
 
  <UsingTask TaskName="TransformXml"
            AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>
 
  <PropertyGroup>
    <!-- properties that are used in this build file - referenced as $(PropertyName) -->
    <VersionAssembly>1.0.1.0</VersionAssembly>
    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
    <!-- Solution folder relative to this script -->
    <SolutionRoot>.\</SolutionRoot>
    <!-- Everything is put in a "publish" folder -->
    <OutputDir>$(MSBuildProjectDirectory)\publish</OutputDir>
    <ConsoleOutputPath>$(OutputDir)\$(Configuration)\console\</ConsoleOutputPath>
    <WebsiteOutputPath>$(OutputDir)\$(Configuration)\website\</WebsiteOutputPath>
  </PropertyGroup>
 
  <!-- targets -->
 
  <Target Name="BuildConsole">
    <Message Text="Building console" />
    <RemoveDir Directories="$(ConsoleOutputPath)"/>
    <MSBuild Projects="$(SolutionRoot)\MarsConsole\MarsConsole.csproj"
      Properties="Configuration=Release;OutputPath=$(ConsoleOutputPath);VersionAssembly=$(VersionAssembly)"/>
  </Target>
 
  <Target Name="BuildWebsite">
    <Message Text="Building website" />
    <RemoveDir Directories="$(WebsiteOutputPath)"/>
    <!--rebuild-->
    <MSBuild Projects="$(SolutionRoot)\Mars\Mars.csproj"
        Properties="Configuration=Release;VersionAssembly=$(VersionAssembly)" />
    <!-- do a copy -->
    <MSBuild Projects="$(SolutionRoot)\Mars\Mars.csproj"
        Targets="ResolveReferences;_CopyWebApplication"
        Properties="Configuration=Release;
        WebProjectOutputDir=$(WebsiteOutputPath);
        OutDir=$(WebsiteOutputPath)\bin\;
                      VersionAssembly=$(VersionAssembly)" />
  </Target>
 
  <Target Name="Transform">
    <!-- Transform the website -->
    <TransformXml Source="$(SolutionRoot)\Mars\Web.config"
                  Transform="$(SolutionRoot)\Mars\Web.$(Configuration).config"
                  Destination="$(WebsiteOutputPath)\Web.config"
                  StackTrace="$(StackTraceEnabled)" />
    <!-- Transform the console (MUST have transform.config eg use SLOW CHEETAH -->
    <TransformXml Source="$(SolutionRoot)\MarsConsole\app.config"
              Transform="$(SolutionRoot)\MarsConsole\app.$(Configuration).config"
              Destination="$(ConsoleOutputPath)\MarsConsole.exe.config"
              StackTrace="$(StackTraceEnabled)" />
  </Target>
  <Target Name="InitDir">
    <!-- empty all directories -->
    <RemoveDir Directories="$(WebsiteOutputPath)\" ContinueOnError="true" />
    <RemoveDir Directories="$(ConsoleOutputPath)\" ContinueOnError="true" />
  </Target>
 
  <Target Name="CleanConfigs">
    <Message Text="Cleaning out configs" />
    <!-- website artefacts-->
    <Delete Files="$(WebsiteOutputPath)\packages.config" />
    <Delete Files="$(WebsiteOutputPath)\web.Debug.config" />
    <Delete Files="$(WebsiteOutputPath)\web.Release.config" />
    <Delete Files="$(WebsiteOutputPath)\web.ACC.config" />
    <Delete Files="$(WebsiteOutputPath)\web.PRD.config" />
 
    <!-- Delete certain directories -->
    <RemoveDir Directories="$(WebsiteOutputPath)\App_Readme\" ContinueOnError="true" />
    <!-- CodeAnalysis etc-->
    <ItemGroup>
      <FilesToDelete Include="$(ConsoleOutputPath)\*.CodeAnalysisLog.xml" />
      <FilesToDelete Include="$(ConsoleOutputPath)\*.lastcodeanalysissucceeded" />
    </ItemGroup>
    <Delete Files="@(FilesToDelete)" />
    <!--any zips-->
    <Delete Files="$(OutputDir)\publish.zip" />
  </Target>
 
  <!-- Default target - does all the other targets in order -->
  <Target Name="Publish"
          DependsOnTargets="InitDir;
          BuildConsole;
          BuildWebsite;
          Transform;
          CleanConfigs">
    <Message Text="Built All" />
  </Target>
 
</Project>

Version Numbers

The above script passes a property for version number. The following is based on this lionhack blogpost.

The .csproj file needs the following at the bottom (under Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"). Make sure the get the correct path to BuildCommon.targets.

<Import Project="..\..\BuildCommon.targets" />

BuildCommon.targets looks like this. If you link to a "CommonAssemblyInfo.cs" from all your projects, change the file name below. We are also setting AssemblyInformationalVersion (to a string, not a version number).

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!--
    http://www.lionhack.com/2014/02/13/msbuild-override-assembly-version/
-->
  <PropertyGroup>
  <Target Name="BeforeCompile" DependsOnTargets="CommonBuildDefineModifiedAssemblyVersion">
  </Target>
 
  <!--
    Creates modified version of AssemblyInfo.cs, replaces [AssemblyVersion] attribute with the one specifying actual build version (from MSBuild properties), and includes that file instead of the original AssemblyInfo.cs in the compilation.
 
    Works with both, .cs and .vb version of the AssemblyInfo file, meaning it supports C# and VB.Net projects simultaneously.
-->
  <Target Name="CommonBuildDefineModifiedAssemblyVersion" Condition="'$(VersionAssembly)' != ''">
    <PropertyGroup>
      <CurrentDate>$([System.DateTime]::Now.ToString(yyyyMMdd HH:mm:ss))</CurrentDate>
    </PropertyGroup>
    <!-- Find AssemblyInfo.cs or AssemblyInfo.vb in the "Compile" Items. Remove it from "Compile" Items because we will use a modified version instead. -->
    <ItemGroup>
      <OriginalAssemblyInfo Include="@(Compile)" Condition="%(Filename) == 'AssemblyInfo' And (%(Extension) == '.vb' Or %(Extension) == '.cs')" />
      <Compile Remove="**/AssemblyInfo.vb" />
      <Compile Remove="**/AssemblyInfo.cs" />
    </ItemGroup>
    <!-- Copy the original AssemblyInfo.cs/.vb to obj\ folder, i.e. $(IntermediateOutputPath). The copied filepath is saved into @(ModifiedAssemblyInfo) Item. -->
    <Copy SourceFiles="@(OriginalAssemblyInfo)"
          DestinationFiles="@(OriginalAssemblyInfo->'$(IntermediateOutputPath)%(Identity)')">
      <Output TaskParameter="DestinationFiles" ItemName="ModifiedAssemblyInfo"/>
    </Copy>
    <!-- Replace the version bit (in AssemblyVersion and AssemblyFileVersion attributes) using regular expression. Use the defined property: $(VersionAssembly). -->
    <Message Text="Setting AssemblyVersion to $(VersionAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                Regex="Version\(&quot;(\d+)\.(\d+)(\.(\d+)\.(\d+)|\.*)&quot;\)"
                ReplacementText="Version(&quot;$(VersionAssembly)&quot;)"
                />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                Regex="AssemblyInformationalVersion\(&quot;Development build&quot;\)"
                ReplacementText="AssemblyInformationalVersion(&quot;$(Configuration) build on $(COMPUTERNAME) $(CurrentDate)&quot;)"
                />
    <!-- Include the modified AssemblyInfo.cs/.vb file in "Compile" items (instead of the original). -->
    <ItemGroup>
      <Compile Include="@(ModifiedAssemblyInfo)" />
    </ItemGroup>
  </Target>
 
  <UsingTask TaskName="RegexUpdateFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll">
    <ParameterGroup>
      <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Regex ParameterType="System.String" Required="true" />
      <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Using Namespace="Microsoft.Build.Framework" />
      <Using Namespace="Microsoft.Build.Utilities" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            try {
                var rx = new System.Text.RegularExpressions.Regex(this.Regex);
                for (int i = 0; i < Files.Length; ++i)
                {
                    var path = Files[i].GetMetadata("FullPath");
                    if (!File.Exists(path)) continue;
 
                    var txt = File.ReadAllText(path);
                    txt = rx.Replace(txt, this.ReplacementText);
                    File.WriteAllText(path, txt);
                }
                return true;
            }
            catch (Exception ex) {
                Log.LogErrorFromException(ex);
                return false;
            }
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>

Copy files

Copying files recursively needs some obscure syntax. This is copying everything from the Release build into Acc (not shown is a target to rerun the config transforms for Acc, almost identical to above).

<Target Name="CopyAll">
  <Message Text="Copying all from $(OutputDir)\Release\ to $(OutputDir)\Acc\" />
  <ItemGroup>
    <CopyItems Include="$(OutputDir)\Release\**\*.*" />
  </ItemGroup>
  <Copy
      ContinueOnError="false"
      SourceFiles="@(CopyItems)"
      DestinationFiles="@(CopyItems->'$(OutputDir)\Acc\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>