Azure SDK 1.8 introduced a change in how CSPack locates virtual applications given the physicalDirectory attribute in the ServiceDefinition.csdef file while packaging an Azure project. This blog shows how to support build and packaging of virtual applications within both Visual Studio and TFS Build using Azure SDK 1.8.
Prior to SDK 1.8 my team used the build and packaging technique given on the kellyhpdx blog. After the SDK upgrade our build started throwing:
Cannot find the physical directory 'C:\...WebApplication1' for virtual path ... (in) ServiceDefinition.csdef
Tweaking the "..\" portion of the physicalDirectory attribute as suggested in other blog articles only worked for local Visual Studio builds but failed on our build server. I'll first cut to the case and show you what is needed then explain what is going on. Refer to the kellyhpdx blog article for background and details of what worked prior to SDK 1.8.
To add a virtual application to an Azure project:
- Add the virtual application's web project to your Visual Studio solution.
- Edit the web project's csproj file by right clicking the project and select "Unload Project" then right click the project again and select Edit.
- Locate the following import element:
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
- After this import element add this target:
<!-- Performs publishing prior to the Azure project packaging --> <Target Name="PublishToFileSystem" DependsOnTargets="PipelinePreDeployCopyAllFilesToOneFolder"> <Error Condition="'$(PublishDestination)'==''" Text="The PublishDestination property is not set." /> <MakeDir Condition="!Exists($(PublishDestination))" Directories="$(PublishDestination)" /> <ItemGroup> <PublishFiles Include="$(_PackageTempDir)\**\*.*" /> </ItemGroup> <Copy SourceFiles="@(PublishFiles)" DestinationFiles="@(PublishFiles->'$(PublishDestination)\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="True" /> </Target>
- Now edit the Azure project's ServiceDefinition.csdef file and add the <VirtualApplication> element with the physicalDirectory attribute's value set to "_PublishedSites\YourWebProjectName". Replace YourWebProjectName with the name of your web project's csproj file.
- Edit the Azure ccproj file by right clicking the project and select "Unload Project" then right click the project again and select Edit.
- At the end of the Azure ccproj file, right before the </Project> element closing tag, add the following:
<!-- Virtual applications to publish --> <ItemGroup> <!-- Manually add PublishToFileSystem target to each virtual application .csproj file --> <!-- For each virtual application add a VirtualApp item to this ItemGroup: <VirtualApp Include="Relative path to csproj file"> <PhysicalDirectory>Must match value in ServiceDefinition.csdef</PhysicalDirectory> </VirtualApp> --> <VirtualApp Include="..\WebApplication1\WebApplication1.csproj"> <PhysicalDirectory>_PublishedWebsites\WebApplication1</PhysicalDirectory> </VirtualApp> </ItemGroup> <!-- Executes before CSPack so that virtual applications are found --> <Target Name="PublishVirtualApplicationsBeforeCSPack" BeforeTargets="CorePublish;CsPackForDevFabric" Condition="'$(PackageForComputeEmulator)' == 'true' Or '$(IsExecutingPublishTarget)' == 'true' "> <Message Text="Start - PublishVirtualApplicationsBeforeCSPack" /> <PropertyGroup Condition=" '$(PublishDestinationPath)'=='' and '$(BuildingInsideVisualStudio)'=='true' "> <!-- When Visual Studio build --> <PublishDestinationPath>$(ProjectDir)$(OutDir)</PublishDestinationPath> </PropertyGroup> <PropertyGroup Condition=" '$(PublishDestinationPath)'=='' "> <!-- When TFS build --> <PublishDestinationPath>$(OutDir)</PublishDestinationPath> </PropertyGroup> <Message Text="Publishing '%(VirtualApp.Identity)' to '$(PublishDestinationPath)%(VirtualApp.PhysicalDirectory)'" /> <MSBuild Projects="%(VirtualApp.Identity)" ContinueOnError="false" Targets="PublishToFileSystem" Properties="Configuration=$(Configuration);PublishDestination=$(PublishDestinationPath)%(VirtualApp.PhysicalDirectory);AutoParameterizationWebConfigConnectionStrings=False" /> <!-- Delete files excluded from packaging; take care not to delete xml files unless there is a matching dll --> <CreateItem Include="$(PublishDestinationPath)%(VirtualApp.PhysicalDirectory)\**\*.dll"> <Output ItemName="DllFiles" TaskParameter="Include" /> </CreateItem> <ItemGroup> <FilesToDelete Include="@(DllFiles -> '%(RootDir)%(Directory)%(Filename).pdb')" /> <FilesToDelete Include="@(DllFiles -> '%(RootDir)%(Directory)%(Filename).xml')" /> </ItemGroup> <Message Text="Files excluded from packaging '@(FilesToDelete)'" /> <Delete Files="@(FilesToDelete)" /> <Message Text="End - PublishVirtualApplicationsBeforeCSPack" /> </Target>
- In the code you just added, locate this element:
<VirtualApp Include="..\WebApplication1\WebApplication1.csproj"> <PhysicalDirectory>_PublishedWebsites\WebApplication1</PhysicalDirectory> </VirtualApp>
- Change the three occurrences of WebApplication1 to the name of your web project's csproj file. See the comment above that element for a definition of what its values contain.
Your Azure solution should now build and package successfully in Visual Studio and on your TFS build server. I verified that this technique works in Visual Studio 2012 with the online Team Foundation Service. It was easy to create a TFS instance at http://tfs.visualstudio.com and perform continuous integration deployments to Azure.
The zip file attached below contains a sample project demonstrating this technique. Download and try it for yourself.
Explanation of Technique
Within your Azure ccproj file you define an ItemGroup of VirtualApp's that you want published and packaged inside the resulting .cspkg file. Define as many VirtualApp items as you need. The sample defines two.
The remainder of the MSBuild code in the ccproj file executes before targets that call CSPack. For each of the VirtualApp's the code launches another instance of MSBuild that executes the PublishToFileSystem target in each respective web project. The code then creates a list of dll filenames which were published and looks for .pdb and .xml files for each of those dll filenames. It then deletes the .pdb and .xml files found.
The code is written so that you can include other .xml files in your web project and just the .xml files corresponding to the dll's get deleted. Other .xml files are published and end up in the .cspkg file as expected.
The PublishToFileSystem target added to the virtual application's csproj file performs a simple recursive copy of the project's build output to the destination specified. Using the _PublishedSites directory isn't a strict requirement but follows the convention used by TFS Build so that the files get copied to the drop folder.
Explanation of Azure SDK 1.8 Changes
During packaging the physicalDirectory attribute in csdef is with respect to the location of the csdef file being used by CSPack, specifically the file identified by the ResolveServiceDefinition target as the Target Service Definition in the build output. See the definition of the ResolveServiceDefinition target, typically in "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\Windows Azure Tools\1.8\ Microsoft.WindowsAzure.targets".
Prior to SDK 1.8 the csdef being used was located in a subfolder within the csx folder under the Azure project and was named ServiceDefinition.build.csdef. The folder name matched the configuration being built, normally either Debug or Release. It is important to note that this caused the physicalDirectory attribute in csdef to be with respect to a folder in the source tree while building locally in Visual Studio and also on the TFS build server. Additionally, the csdef file was not copied to the build output folder.
Azure SDK 1.8 on the other hand copies the csdef file to the build output folder and changed the ResolveServiceDefinition target so that it looks for the file there. While doing a local Visual Studio build the output folder is a subfolder within the bin folder under the Azure project matching the configuration being built, typically Debug or Release. The important point for a local Visual Studio build is that this folder is an equal number of folders down in the source tree when compared to the previous csx subfolder so the SDK 1.8 change should be transparent to most developers.
Unfortunately the build output folder on a TFS build server is very different. A step in the TFS build process needs to copy the build output to a drop folder for archival purposes. To facilitate this the TFS build templates uses two folder, one for the source and another for the build output. The obj and csx folders continue to get created within the source folders but instead of the build output going to the bin folder as is done by Visual Studio, TFS uses a build output folder named Binaries that is at the same, sibling folder level as the Source. The build output folder contents is also slightly different in that there is a _PublishedWebsites folder.
If you want to verify what is packaged, inside the .cspkg file there is a sitesroot folder which contains a folder for each application; 0 is the root site and 1 is the virtual application. You can view the contents by renaming the .cspkg file to .zip then exporting it and renaming the .cssx file to .zip. The sitesroot folder is within the renamed .cssx file. The MSBuild code above results in the virtual application containing the files for the published site except that the pdb and xml files associated with dll's are deleted.