WebDeploy: Change Package Output folder in Web Publishing Pipeline - msbuild

I have a Visual Studio solution with multiple webapp projects. The build should create a web package for each project. The web packages should finally end up in a folder structor like this:
$(Outputfolder)
|
+-- Web
|
+-- <name package 1>
| |
| +-- ... package files ...
|
+-- <name package 2>
| |
| +-- ... package files ...
|
+-- ...
|
In order to change the destination folder for a web package I have added a .wpp.targets file to each web app project. Here I have adjusted the DefaultPackageOutputDir property:
<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<DefaultPackageOutputDir Condition=" '$(DefaultPackageOutputDir)'=='' ">$(OutFolder)Web\Webapp1\</DefaultPackageOutputDir>
</PropertyGroup>
</Project>
This is it how I call MSBuild. I simply hand over the output folder as a property:
<MSBuild Projects="#(ItemToBuild)"
Targets="Build"
Properties="Configuration=$(Configuration);
Platform=$(Platform);
DeployOnBuild=True;
DeployTarget=Package;
OutFolder=$(OutFolder)" />
This does the trick but I'm not completely satisfied. I want to make the build more general. It bothers me, that I have to name the webapp explicitly. My idea was to use the property DefaultMSDeployDestinationApplicationName instead:
<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<DefaultPackageOutputDir Condition=" '$(DefaultPackageOutputDir)'=='' ">$(OutFolder)Web\$(DefaultMSDeployDestinationApplicationName)\</DefaultPackageOutputDir>
</PropertyGroup>
</Project>
Unfortunately the property DefaultMSDeployDestinationApplicationName seems to be empty. The package files end up in the Web folder. I guess the property DefaultMSDeployDestinationApplicationName is not yet defined at the time the .wpp.targets file is readed.
Does somebody know a better place to define the property DefaultPackageOutputDir?

I was able to change the output directory for a Web Project by specifying the following options to MsBuild:
/p:OutDir="$(build.artifactstagingdirectory)\$(BuildConfiguration)" /p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true

Related

How to ship the stylecop.json and custom.ruleset files with a NuGet package in VS2017

At the moment we are switching from VS2015 to VS2017. One of our upgrade steps is to switch from stylecop to the new Stylecop.Analyzer package. The new Stylecop is using 2 files. The stylecop.json and the Stylecop.ruleset.
The target: I want to provide the stylecop files as a custom nuget package. But I dont know how to create the needed .csproj entries.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<CodeAnalysisRuleSet>packages\My.StyleCop.1.0.0-pre15\RuleSet\My.StyleCop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
...
<ItemGroup>
<AdditionalFiles Include="packages\My.StyleCop.1.0.0-pre15\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
In the past, there was the possibility to use a install.ps1 script to do this stuff. But with NuGet 3. (or 4.) the install scripts are obsolete and will be ignored.
I already tried to use My.StyleCop.targets:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="packages\My.StyleCop.1.0.0-pre17\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
But here I have some issues, too. Since NuGet 3. (or 4.) there is no solution wide package folder and I dont know any variable or placeholder I can use here to get a absolute or relative path to my package.
You can add .props or .targets files to the build folder in your packages and they will be imported to the projects.
On the .props file, you can use the MSBuildThisFileDirectory MSBuild variable that represents the folder where that file is located.
Thanks to Paulo.
How I did it:
This is the structure of my NuGet package.
The solution is quiet easy. You need to create to files. A .props and a .targets file named like the NuGet package and place them in the build folder of your package.
In these MSBuild files you can use the $(MSBuildThisFileDirectory) variable to get the path of your NuGet package.
MSBuildThisFileDirectory = C:\Users\UserName\.nuget\packages\sig.stylecop\1.0.0-pre23\build\
My SIG.StyleCop.props file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\..\RuleSet\SIG.combiLink.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
My SIG.StyleCop.targets file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\..\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
Cause of the structure of my package i need to navigate (..) into the Config and into the RuleSet folder.
The variable $(MSBuildThisFileDirectory) already includes the backslash at the end. It is important to omit the backslash when you reference the ruleset and the stylecop.json file:
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)..\RuleSet\SIG.combiLink.ruleset</CodeAnalysisRuleSet>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)..\Config\stylecop.json">
With the double backslash I experienced two strange problems in Visual Studio 2017:
Unit tests rebuild the code each time I start them, even without any code change
The IDE shows many StyleCop errors in the Error List window and shows red marks in the scroll bar even for rules that are explicitly disabled in the rule set.

Execute .bat file at end of VS2017 Asp.Net Core publish action?

In Visual Studio 2017 when publishing an Asp.Net Core Website using the File System publish method I want to trigger the execution of a .bat file after the publish operation copied the files to the output directory.
I have learned that the settings for the publish operation are stored by Visual Studio in the project's Properties/PublishProviles directory in a .pubxml file. So in may case the file is FolderProfile.pubxml and it currently looks like this:
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>False</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PublishFramework>net461</PublishFramework>
<ProjectGuid>f58b5ca0-8221-4c97-aa6d-7fba93a3abeb</ProjectGuid>
<publishUrl>C:\inetpub\wwwGiftOasisResponsivePublished</publishUrl>
<DeleteExistingFiles>True</DeleteExistingFiles>
</PropertyGroup>
</Project>
Based on the comments in the .pubxml file and additional research, it's my understanding this file is essentially an msbuild file and ultimately msbuild is used to perform the publish operation. msbuild files seem very flexible but more than a little complicated. I'm really struggling with this one.
If I had a batch file in the root of my project called finishPub.bat, how could I modify the above .pubxml file to cause the execute of the finishPub.bat file after the website has been copied to the output folder?
You can amend your publish profile with a custom target:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
...
</PropertyGroup>
<Target Name="ExecuteBatAfterPublish" AfterTargets="AfterPublish">
<Exec Command="example.bat" WorkingDirectory="$(publishUrl)" />
</Target>
</Project>

How to get jspm/webpack/browserify/requirejs bundling working with msdeploy?

I currently use jspm but the same issue applies with any other build-time bundling tool. I can't figure out how to get these to play well with msdeploy.
Here's the issue:
I run jspm to produce one or more bundle files (one for each "chain" that I want).
My application uses System.import (or require or just a script tag) to start these loading.
If I were to deploy everything to a directory and xcopy from there to the deployment server everything is copacetic. However, our devops team prefers to deploy using msdeploy. For this I'm supposed to point it at a csproj. If I do this then how does msdeploy know to deploy the generated bundles?
You have to create an MSBuild project to accomplish this - one which hooks into the MSDeploy pipeline. I've provided a sample (one I'm currently using for a project) below; I'm likely going to release this as a Nuget package (along with some other MSBuild scripts that were written to take advantage of npm, jspm, and gulp).
The props file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CopyAllFilesToSingleFolderForPackageDependsOn>
FrontendDeploymentFiles;
$(CopyAllFilesToSingleFolderForPackageDependsOn);
</CopyAllFilesToSingleFolderForPackageDependsOn>
</PropertyGroup>
</Project>
The targets file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="FrontendDeploymentFiles">
<ItemGroup>
<_CustomFiles Include="dist\**\*" />
<_CustomFiles Include="jspm_packages\**\*" />
<FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
<DestinationRelativePath>%(RelativeDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
</Project>
This isn't exactly a drop-in for you as you're bundling your files, but the takeaway here is that you can define a glob pattern for your copy methods. Replace jspm_packages with whatever your bundles are (as the scripts I've provided are only for publishing to a development environment) and you should be good.
Hope this is helpful to anyone else who runs into this issue.

MSBuild: Copy multiple directories in a generic way

I have a Visual Studio solution with multiple web projects (e.g. WebappA, WebappB, WebappC). When TFS builds the solution it puts the build results in a _PublishedWebsites folder. The folder structure may look like this:
$(OutDir)
|
+-- _PublishedWebsites
|
+-- WebappA
|
+-- WebappA_Package
|
+-- WebappB
|
+-- WebappB_Package
|
+-- WebappC
|
+-- WebappC_Package
I want to build a deployment package for our operations department in terms of a zip file. Therefore I let TFS run an MSBuild script which copies the _Package folders into a custom directory structure which is zipped in a subsequent step.
$(PackageDirectory)
|
+-- Web
|
+-- WebappA
|
+-- WebappB
|
+-- WebappB
I was able to create a bunch of MSBuild targets which do the copy operations. But I'm unhappy with my solution. I am referencing each webapp in an explicit way that's why I ended up with much repetitive code. To make matters worse each time a new webapp is added I have to extent the build script.
<Target Name="Pack" DependsOnTargets="Pack-WebappA;Pack-WebappB;Pack-WebappC" />
<Target Name="Pack-WebappA">
<ItemGroup>
<WebAppFile Include="$(OutDir)_PublishedWebsites\WebappA_Package\*.*" />
</ItemGroup>
<Copy SourceFiles="#(WebAppFile)" DestinationFolder="$(PackageDirectory)Web\WebappA\" />
</Target>
<Target Name="Pack-WebappB">
<ItemGroup>
<WebAppFile Include="$(OutDir)_PublishedWebsites\WebappB_Package\*.*" />
</ItemGroup>
<Copy SourceFiles="#(WebAppFile)" DestinationFolder="$(PackageDirectory)Web\WebappB\" />
</Target>
<Target Name="Pack-WebappC">
<ItemGroup>
<WebAppFile Include="$(OutDir)_PublishedWebsites\WebappC_Package\*.*" />
</ItemGroup>
<Copy SourceFiles="#(WebAppFile)" DestinationFolder="$(PackageDirectory)Web\WebappC\" />
</Target>
I'm searching for a solution which does the whole thing in a generic way without to referencing the concrete webapps. In essence all what MSBuild should do is to look into the _PublishedWebsites folder and copy each subfolder with a _Package suffix to another folder and remove the suffix. This sounds pretty easy but I was not able to come up with a working solution. I've tried it with batching without success.
You're pretty much right in that you're unhappy with the current solution: automating such things is a must. I agree MsBuild doesn't always make it straightforward though; batching is the right way but you have to add some filtering/manipulating of the items. Using property functions this isn't all that hard though:
In essence all what MSBuild should do is to look into the
_PublishedWebsites folder and copy each subfolder with a _Package suffix to another folder and remove the suffix.
We'll translate this to:
list all directories in _PublishedWebsites
filter the list and include only those ending in _Package
list all files in those directories and set destination for them to a subdirectory of PackageDirectory with the suffix removed
copy each file to corresponding directory
Step 3 is actually two things (list+specify dir) because that is the typical msbuild way of doing things. There are other ways to do this, but this one seems appropriate here.
<Target Name="BatchIt">
<PropertyGroup>
<SourceDir>$(OutDir)_PublishedWebsites\</SourceDir>
<DestDir>$(PackageDirectory)Web\</DestDir>
<PackageString>_Package</PackageString>
</PropertyGroup>
<ItemGroup>
<!-- step 1 -->
<Dirs Include="$([System.IO.Directory]::GetDirectories( `$(SourceDir)`, `*`, System.IO.SearchOption.AllDirectories ) )"/>
<!-- step 2 -->
<PackageDirs Include="%(Dirs.Identity)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch( %(Filename), '.*$(PackageString)' ) )"/>
<!-- step 3 -->
<PackageFiles Include="%(PackageDirs.Identity)\*.*">
<DestDir>$(PackageDirectory)$([System.IO.Path]::GetFilename( %(PackageDirs.Identity) ).Replace( $(PackageString), '' ) )</DestDir>
</PackageFiles>
</ItemGroup>
<!-- step 4 -->
<Copy SourceFiles="%(PackageFiles.Identity)" DestinationFolder="%(PackageFiles.DestDir)" />
</Target>
edit A more performant and maybe more logical way is to specify the DestDir directly when building the PackageDir list:
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::GetDirectories( `$(SourceDir)`, `*`, System.IO.SearchOption.AllDirectories))"/>
<PackageDirs Include="%(Dirs.Identity)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch( %(Filename), '.*$(PackageString)' ))">
<DestDir>$(DestDir)$([System.IO.Path]::GetFilename( %(Dirs.Identity) ).Replace( $(PackageString), '' ))</DestDir>
</PackageDirs>
<PackageFiles Include="%(PackageDirs.Identity)\*.*">
<DestDir>%(PackageDirs.DestDir)</DestDir>
</PackageFiles>
</ItemGroup>
stijn came up with a excellent answer. It worked for me almost. I changed only one or two things. This is the code which does the trick, at least in my case.
<Target Name="PackWeb">
<PropertyGroup>
<SourceDir>$(OutDir.TrimEnd('\'))\_PublishedWebsites\</SourceDir>
<PackDir>$(PackageDirectory.TrimEnd('\'))\Web\</PackDir>
<PackageString>_Package</PackageString>
</PropertyGroup>
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::GetDirectories( `$(SourceDir)`, `*` ) )"/>
<PackageDirs Include="%(Dirs.Identity)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch( %(FullPath), '.*$(PackageString)' ) )"/>
<PackageFiles Include="%(PackageDirs.Identity)\*.*">
<DestDir>$(PackDir)$([System.IO.Path]::GetFilename( %(PackageDirs.Identity) ).Replace( $(PackageString), '' ) )</DestDir>
</PackageFiles>
</ItemGroup>
<Copy SourceFiles="%(PackageFiles.Identity)" DestinationFolder="%(PackageFiles.DestDir)" />
</Target>

Working directory issue when importing msbuild file in another msbuild file

I am trying to specify some additional targets/tasks to an msbuild file by extending an existing msbuild file (a web applicartion .csproj file). The idea is to put configuration specific tasks in this "extended ms build file" and use this file in our build server (TeamCity). The way I tried to solve it at first was to add a folder "msbuildscripts" to my web project and put the extended ms build file there:
<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
<Import Project="../My.Web.csproj" />
...more stuff...
</Project>
and then build this file using something like:
c:\myweb\msbuild.exe msbuildscripts/extended.msbuild.file.xml
Now, this wont work because when importing the original ms build file, that csproj file will be "executed" in the "wrong" folder (msbuildscripts), and the csproj-build-file wont find any of its referenced folders/items.
Is there any way to tell msbuild.exe to use a specific working directory? I know it is possible to solve this problem using an execute task, but that doesnt seem like a good solution.
Use MSBuild task like this:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="MyBuild">
<ItemGroup>
<ProjectToBuild Include="../My.Web.csproj" />
</ItemGroup>
<Target Name="MyBuild">
<MSBuild Targets="Build" Projects="#(ProjectToBuild)"></MSBuild>
</Target>
</Project>