MsBuild
Command Line/.bat/cmd file
%systemroot%\Microsoft.Net\Framework\v4.0.30319\MSBuild.exe build.xml /t:Publish /fl & pause
- .net 4 version of msBuild. MSDN command line reference
- /t is target, which here is "Publish"
- /fl is file logger, by default writes to "msbuild.log". Change that by adding /flp:LogFile=myproj.log
- Add /p for properties such as configuration: /p:Configuration=Acc
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:
- v4: "%systemroot%\Microsoft.Net\Framework\v4.0.30319\MSBuild.exe"
- VS2015/C#6: "%PROGRAMFILES(X86)%\MSBuild\14.0\Bin\MsBuild.exe"
- VS2017/C#7: "%PROGRAMFILES(X86)%\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MsBuild.exe" (check VS edition) or
(if applicable) "install-package Microsoft.NET.Sdk" or "dotnet msbuild"
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)
VS2019 moves the location of msbuild (from MSBuild\15.0\Bin to MSBuild\Current\Bin) but the following works...
Below is from the example
setlocal enabledelayedexpansion
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do (
set InstallDir=%%i
)
"%InstallDir%" build.xml /t:PublishAll /fl
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).
- Configuration is "Release" but can be overridden (typically I have a ACC acceptance and PRD production, based on Release)
- See later for how version works
- This build script is for a console and a website.
- The website is rebuilt, then copied (including scripts, views etc) using the _CopyWebApplication target
_CopyWebApplication (legacy and deprecated) can be replaced with _WPPCopyWebApplication but you may need to add the property CleanWebProjectOutputDir=False if dependent dlls get zapped. - web.config and app.config are transformed (using Microsoft.Web.Publishing.Tasks)
- For app.config, you need a app.{configuration}.config, for instance using Slow Cheetah extension
- Transform configs are deleted in another target
<?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\("(\d+)\.(\d+)(\.(\d+)\.(\d+)|\.*)"\)"
ReplacementText="Version("$(VersionAssembly)")"
/>
<RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
Regex="AssemblyInformationalVersion\("Development build"\)"
ReplacementText="AssemblyInformationalVersion("$(Configuration) build on $(COMPUTERNAME) $(CurrentDate)")"
/>
<!-- 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>