In reworking our deployment process I moved over to using an MSBuild project in place of our existing batch files. All of the major elements are in place, and I was looking to cut out a step or two but ran into a snag.
I'm creating a property called OutputPath using the CombinePath task, and, while I can access it with no issues after it has been created I'm at a loss as for how to use it to my advantage. Consider:
<CombinePath BasePath ="$(DeployFolderRoot)" Paths ="$(DeployReleaseFolder)$(ReleaseFolderFormatted)" >
<Output TaskParameter ="CombinedPaths" ItemName ="OutputFolder"/>
</CombinePath>
<MakeDir Directories="#(OutputFolder)" />
<MakeDir Directories="#(OutputFolder)\Foo" />
<MakeDir Directories="#(OutputFolder)\Bar" />
Commands 2 and 3 fail because I'm referencing an array and attempting to concatenate with a string. Creating a property and assigning it #(OutputFolder) simply results in another item group, not a property I can reference with the $ accessor. I do have an ugly workaround but I'd love to clear this up somewhat.
Thanks,
-Jose
I'm not sure of the answer exactly but here is an idea:
<CombinePath BasePath ="$(DeployFolderRoot)" Paths ="$(DeployReleaseFolder)$(ReleaseFolderFormatted)" >
<Output TaskParameter ="CombinedPaths" ItemName ="OutputFolder"/>
</CombinePath>
<OutputFolder Include="$(DeployFolderRoot)$(DeployReleaseFolder)$(ReleaseFolderFormatted)\Foo" />
<OutputFolder Include="$(DeployFolderRoot)$(DeployReleaseFolder)$(ReleaseFolderFormatted)\Bar" />
<MakeDir Directories="#(OutputFolder)" />
Essentially, if you create OutputFolder items with the path they will just be appended to the list. This would have to be in an element btw, and you have to use Include="".
dOh! Definitely ignorance, used the wrong attribute on the Output element.
<CombinePath BasePath ="$(DeployFolderRoot)" Paths ="$(DeployReleaseFolder)$(ReleaseFolderFormatted)" >
<Output TaskParameter ="CombinedPaths" PropertyName="OutputFolder"/>
</CombinePath>
<MakeDir Directories="$(OutputFolder)" />
<MakeDir Directories="$(OutputFolder)\Foo" />
<MakeDir Directories="$(OutputFolder)\Bar" />
Related
For a C++ project, I want to autogenerate a defs.h file with project definitions, such as the date, git commit, ... to automate the versioning process of my application.
Therefore I am trying to create a MSBuild Target that will extract the latest git tag, git commit, and the current date and save it to a temporary gitinfo.txt file.
Another build target will depend on that file and generate a .h file.
In order to avoid unnecessary recompiles of my project, the .h file and for that reason the gitinfo.txt file shall only be rewritten, if any of the information has changes.
So my idea is the following:
Calculate git and date info
If available, read in the existing gitinfo.txt
Compare the calculated values to those in the txt file
If anything has changed, rewrite the gitinfo.txt
I've mastered steps 1. and 2., however I am not sure how to process the values after reading them.
<!-- The purpose of this target is to update gitinfo.txt if git information (commit...) has changed -->
<Target
Name="GetHeaderInfos"
BeforeTargets="ClCompile"
Outputs="$(IntDir)\gitinfo.txt"
>
<!-- Get information about the state of this repo-->
<GitDescribe>
<Output TaskParameter="Tag" PropertyName="NewGitTag" />
<Output TaskParameter="CommitHash" PropertyName="NewGitCommitHash" />
<Output TaskParameter="CommitCount" PropertyName="NewGitCommitCount" />
</GitDescribe>
<!-- Get the current date -->
<Time Format="dd.MM.yyyy">
<Output TaskParameter="FormattedTime" PropertyName="NewBuildDate" />
</Time>
<ReadLinesFromFile File="$(IntDir)\gitinfo.txt" Condition="Exists('$(IntDir)\gitinfo.txt')">
<Output TaskParameter="Lines" ItemName="Version" />
</ReadLinesFromFile>
<!-- Comparison here! HOW TO DO IT PROPERLY -->
<PropertyGroup>
<TagChanged> <!-- `$(NewGitTag)` == `$(Version)[0]` --> </TagChanged>
<!-- Other comparisons -->
</PropertyGroup>
</Target>
And this could be the content of gitinfo.txt
v4.1.4
04fe34ab
1
31.07.2016
I am not quite sure how to compare the values now. I need to compare $(NewGitTag) to the first value in the $(Version) version variable, and so on.
I haven't found an example, that actually accesses the variables after reading them from a file. The official documentation provides no help, nor have I found anything on stackoverflow or the likes.
I only know that the $(Version) variable holds a list, and I can batch process it. How can I compare its content to the defined variables $(NewGitTag), $(NewGitCommitHash), $(NewGitCommitCount) and $(NewBuildDate)?
Suppose we start with this data:
<ItemGroup>
<Version Include="v4.1.4;04fe34ab;1;31.07.2016"/>
</ItemGroup>
<PropertyGroup>
<GitTag>v4.1.4</GitTag>
<GitSHA>04fe34ab</GitSHA>
<Count>1</Count>
<Date>31.07.2016</Date>
</PropertyGroup>
Then here are at least 3 ways to achieve comparision (apart from the one mentioned in the comment) and there are probably other ways as well (I'll post them if I can come up with something else):
Just compare the items
I'm not sure why you want to compare everything seperately when this works just as well: compare the whole ItemGroup at once.
<Target Name="Compare1">
<PropertyGroup>
<VersionChanged>True</VersionChanged>
<VersionChanged Condition="'#(Version)' == '$(GitTag);$(GitSHA);$(Count);$(Date)'">False</VersionChanged>
</PropertyGroup>
<Message Text="VersionChanged = $(VersionChanged)" />
</Target>
Batch and check if there's one difference
Each item of Version is compared with e.g. GitTag via batching. The result will be False;False;False;False if there's a difference, else it will be True;False;False;False. Count the distinct elements and if it's 2 it means we got the latter so GitTag did not change. Note this obviousle only works if each of your source items can never have the same value as one of the other items.
<Target Name="Compare2">
<PropertyGroup>
<TagChanged>True</TagChanged>
<TagChanged Condition="'#(Version->Contains($(GitTag))->Distinct()->Count())' == '2'">False</TagChanged>
</PropertyGroup>
<Message Text="TagChanged = $(TagChanged)" />
</Target>
you can then compare the other items as well and combine the result.
Use an inline task to access items by index
This comes closest to what's in your question, but it does need a bit of inline code.
<UsingTask TaskName="IndexItemGroup" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<Items Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
<Index Required="true" ParameterType="System.Int32"/>
<Item Output="true" ParameterType="Microsoft.Build.Framework.ITaskItem"/>
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[Item = Items[ Index ];]]>
</Code>
</Task>
</UsingTask>
<Target Name="Compare3">
<IndexItemGroup Items="#(Version)" Index="1">
<Output PropertyName="OldGitSHA" TaskParameter="Item"/>
</IndexItemGroup>
<PropertyGroup>
<SHAChanged>True</SHAChanged>
<SHAChanged Condition="'$(GitSHA)' == '$(OldGitSHA)'">False</SHAChanged>
</PropertyGroup>
<Message Text="OldGitSHA = $(OldGitSHA), changed = $(SHAChanged)" />
</Target>
This is a snippet of my Publish Profile:
<Exec WorkingDirectory="$(_PackageTempDir)"
Command="uglifyjs ..\..\..\..\js\file1.js ..\..\..\..\js\file2.js --mangle --reserved "$" --compress > js\outfile.min.js" />
Certain files (say file1.js) is located outside my project and therefore is not copied to the _PackageTempDir. Here I have to ..\ up several levels to get there. I'm wondering if there is a good way to use an ItemGroup or full path that will allow me the same results.
The above code "works". It is just complicated and difficult to maintain. Looking for a better solution.
EDIT:
Based on Sayed's suggestions, I refined my profile to:
<ItemGroup>
<UglifyJSFiles Include="$(MSBuildThisFileDirectory)..\..\js\mcm\mcm.js" />
<UglifyJSFiles Include="$(_PackageTempDir)\js\main.js" />
</ItemGroup>
<Exec WorkingDirectory="$(_PackageTempDir)"
Command="uglifyjs #(UglifyJSFiles,' ') > js\app.min.js" />
But I am running into an issue because the paths contain spaces. How can I either quote the path strings or escape the spaces?
Here is an example showing a better approach
<PropertyGroup>
<JsFilesToUglifyRoot Condition=" '$(JsFilesToUglifyRoot)'=='' ">$(MSBuildThisFileDirectory)\..\..\..\..\js\</JsFilesToUglifyRoot>
</PropertyGroup>
<ItemGroup>
<JsFilesToUglify Include="$(JsFilesToUglifyRoot)**\*.js" />
</ItemGroup>
<Target Name="AfterBuild">
<Message Text="Files: [#(JsFilesToUglify,' ')]" Importance="high" />
<!-- If you need to quote the file paths use the transform below -->
<Message Text="Files: [#(JsFilesToUglify->'"%(FullPath)"',' ')]" Importance="high" />
</Target>
Here I define a new property JsFilesToUglify that is populated with the path you indicated above. Note the usage of the MSBuildThisFileDirectory reserved property. You should not rely on just ..\ as its value may be different in VS versus outside of VS. Also do not use the MSBuildProjectDirectory property, only MSBuildThisFileDirectory.
Then inside of the target I transform the list of files with #(JsFilesToUglify,' ') the ,' ' makes a space the separator between values like your command above.
I have a web app which I'm compiling using steal, and then I just want to copy the files from it needed for production use, but I need to preserve the directory structure. So for example, the directory looks like this after running steal's build (which compiles js/css into the production.js/css files):
\WebApp\index.html
\WebApp\app\img\a.png
\WebApp\app\img\b.png
\WebApp\app\js\foo.js
\WebApp\app\js\bar.js
\WebApp\app\css\base.css
\WebApp\app\css\app.css
\WebApp\app\css\widget1.css
\WebApp\app\production.js
\WebApp\app\production.css
\WebApp\steal\steal.js
\WebApp\steal\README.md
\WebApp\steal\build\build.js
Out of this, I want to copy only a few specific files to the same dir structure:
\artifacts\staging\www\index.html
\artifacts\staging\www\app\img\a.png
\artifacts\staging\www\app\img\b.png
\artifacts\staging\www\app\production.js
\artifacts\staging\www\app\production.css
\artifacts\staging\www\steal\steal.js
Ideally I'd have something like this:
<PropertyGroup>
<WorkingDir>WebApp\</WorkingDir>
<OutputDir>artifacts\staging\www\</OutputDir>
</PropertyGroup>
...
<ItemGroup>
<CopyFiles Remove="#(CopyFiles)" /> <!-- clean existing items -->
<CopyFiles Condition="'$(Configuration)'=='Debug'"
Include="$(WorkingDir)\**\*.*"
Exclude="$(WorkingDir)\**\.svn\**" />
<CopyFiles Condition="'$(Configuration)'=='Release'"
Include="$(WorkingDir)\index.html;$(WorkingDir)\app\img\**\*.*;$(WorkingDir)\app\production.*;$(WorkingDir)\steal\steal.js;"
Exclude="$(WorkingDir)\**\.svn\**" />
</ItemGroup>
<Copy SourceFiles="#(CopyFiles)"
DestinationFolder="#(CopyFiles->'$(OutputDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
The problem of course the directory structure isn't preserved, and I actually just get all of the files into the $(OutputDir) with no sub-directories. %(RecursiveDir) is the expansion of ** but since I've explicitly specified most paths, it doesn't actually take effect.
Now I know I can brute force this with a bunch of copy tasks and itemgroups, but that introduces its own problems, aside from being ugly. For one, it's error-prone, since if someone wants to add an item they have to be sure to use a unique itemgroup name (this build script is big and does many other tasks), and ensure several lines are all in sync.
There must be a better way than this?
<ItemGroup>
<IndexFiles Include="$(WorkingDir)\index.html" />
<ImgFiles Include="$(WorkingDir)\app\img\**\*.*" />
<AppFiles Include="$(WorkingDir)\app\production.*" />
...
</ItemGroup>
<Copy SourceFiles="#(IndexFiles)"
DestinationFolder="#(IndexFiles->'$(OutputDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="#(ImgFiles)"
DestinationFolder="#(ImgFiles->'$(OutputDir)\app\img\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="#(AppFiles)"
DestinationFolder="#(AppFiles->'$(OutputDir)\app\%(RecursiveDir)%(Filename)%(Extension)')" />
....
I had the same problem, and after some struggling I managed to do it. The key is to specify folders you want to include, after "**".
<ItemGroup>
<CopyFiles Include="$(WorkingDir)\index.html" />
<CopyFiles Include="$(WorkingDir)\**\app\img\**\*.*" />
<CopyFiles Include="$(WorkingDir)\**\app\production.*" />
...
</ItemGroup>
As a result, the output directory will contain the app folder with all subdirectories of "img", and all files named "production".
As a note- the part with "RecursiveDir" remains unchanged.
Just to add to the pot.
You can also go with an "Exclude Some Files" strategy.
The below will get all *.txt and *.doc files...but also exclude files of a specific name.
The question is.....are you more interested in including certain files...or.....excluding certain files.
Both "tricks" are needed from time to time.
<ItemGroup>
<MyExcludeFiles Include="$(WorkingDir)\**\SuperSecretStuff.txt" />
<MyExcludeFiles Include="$(WorkingDir)\**\SuperSecretStuff.doc" />
</ItemGroup>
<ItemGroup>
<MyIncludeFiles Include="$(WorkingDir)\**\*.txt" Exclude="#(MyExcludeFiles)"/>
<MyIncludeFiles Include="$(WorkingDir)\**\*.doc" Exclude="#(MyExcludeFiles)"/>
</ItemGroup>
<Copy
SourceFiles="#(MyIncludeFiles)"
DestinationFiles="#(MyIncludeFiles->'$(OutputDir)\%(RecursiveDir)%(Filename)%(Extension)')"
/>
Can you reverse your logic somewhat and include everything using ** and then exclude the files you don't want:
<CopyFiles Condition="'$(Configuration)'=='Debug'"
Include="$(WorkingDir)\**\*.*"
Exclude="$(WorkingDir)\**\.svn\**" />
<CopyFiles Condition="'$(Configuration)'=='Release'"
Include="$(WorkingDir)\**\*.*"
Exclude="$(WorkingDir)\**\.svn\**;$(WorkingDir)\app\css\*.*;$(WorkingDir)\app\js\*.*;$(WorkingDir)\steal\README.md;$(WorkingDir)\steal\build\*.*" />
You can then use the $(RecursiveDir) property.
I'm trying to copy a bunch of files whose names begin with the prefix DR__, but the copies must have that prefix removed. That is, DR__foo must be copied as foo. I'm trying this, which is based in the example provided in the documentation (the .chm):
<Target Name="CopyAuxiliaryFiles">
<MakeDir Directories="$(TargetDir)Parameters" Condition="!Exists('$(TargetDir)Parameters')" />
<ItemGroup>
<ContextVisionParameterFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Files to copy and rename: #(ContextVisionParameterFiles)"/>
<RegexReplace Input="#(ContextVisionParametersFiles)" Expression="DR__" Replacement="">
<Output ItemName ="DestinationFullPath" TaskParameter="Output" />
</RegexReplace>
<Message Text="Renamed Files: #(DestinationFullPath)"/>
<Copy SourceFiles="#(ContextVisionParameterFiles)" DestinationFiles="#(DestinationFullPath)" />
</Target>
DestinationFullPath comes out empty (or that's what I see when I display it with Message). Thus, Copy fails because no DestinationFiles are specified. What's wrong here?
Edit: ContextVisionParameterFiles is not empty, it contains this:
D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_bone.alut;D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_medium.gop
They're actually 40 files, but I trimmed it for the sake of clarity
Got it! It seems to have been the combination of a stupid error and a seemingly compulsory parameter. As for the first one, there were two Targets called CopyAuxiliaryFiles. As for the second one, it seems the Count parameter is needed.
The final, working version:
<Target Name="CopyCvParameters">
<ItemGroup>
<CvParamFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Input:
#(CvParamFiles, '
')"/>
<!-- Replaces first occurance of "foo." with empty string-->
<RegexReplace Input="#(CvParamFiles)" Expression="^.*DR__" Replacement="$(TargetDir)Parameters\" Count="1">
<Output ItemName ="RenamedCvParamFiles" TaskParameter="Output" />
</RegexReplace>
<Message Text="
Output RenamedCvParamFiles:
#(RenamedCvParamFiles, '
')" />
<Copy SourceFiles="#(CvParamFiles)" DestinationFiles="#(RenamedCvParamFiles)" SkipUnchangedFiles="True" />
</Target>
Notice that:
I renamed the Target to solve the name collision (Why doesn't Visual Studio detect this as an error?)
I pretty-printed the ItemGroups with the #(CvParamFiles, '
') syntax, which seems to replace ; with line breaks
My regex replaces the absolute path and the prefix
Count="1" is now passed to RegexReplace
I have a script that attempts to construct an ItemGroup out of all files in a certain directory while excluding files with certain names (regardless of extension).
The list of files to be excluded initially contains file extensions, and I am using Community Tasks' RegexReplace to replace the extensions with an asterisk. I then use this list in the item's Exclude attribute. For some reason the files do not get excluded properly, even though the list appears to be correct.
To try and find the cause I created a test script (below) which has two tasks: first one initialises two properties with the list of file patterns in two different ways. The second task prints both properties and the files resulting from using both these properties in the Exclude attribute.
The properties' values appear to be identical, however the resulting groups are different. How is this possible?
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="Init;Test" ToolsVersion="3.5">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<Target Name="Init">
<ItemGroup>
<OriginalFilenames Include="TestDir\SampleProj.exe"/>
<OriginalFilenames Include="TestDir\SampleLib1.dll"/>
</ItemGroup>
<RegexReplace Input="#(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
<Output TaskParameter="Output" ItemName="PatternedFilenames"/>
</RegexReplace>
<PropertyGroup>
<ExcludeFilesA>TestDir\SampleProj.*;TestDir\SampleLib1.*</ExcludeFilesA>
<ExcludeFilesB>#(PatternedFilenames)</ExcludeFilesB>
</PropertyGroup>
</Target>
<Target Name="Test">
<Message Text='ExcludeFilesA: $(ExcludeFilesA)' />
<Message Text='ExcludeFilesB: $(ExcludeFilesB)' />
<ItemGroup>
<AllFiles Include="TestDir\**"/>
<RemainingFilesA Include="TestDir\**" Exclude="$(ExcludeFilesA)"/>
<RemainingFilesB Include="TestDir\**" Exclude="$(ExcludeFilesB)"/>
</ItemGroup>
<Message Text="
**AllFiles**
#(AllFiles, '
')" />
<Message Text="
**PatternedFilenames**
#(PatternedFilenames, '
')" />
<Message Text="
**RemainingFilesA**
#(RemainingFilesA, '
')" />
<Message Text="
**RemainingFilesB**
#(RemainingFilesB, '
')" />
</Target>
</Project>
Output (reformatted somewhat for clarity):
ExcludeFilesA: TestDir\SampleProj.*;TestDir\SampleLib1.*
ExcludeFilesB: TestDir\SampleProj.*;TestDir\SampleLib1.*
AllFiles:
TestDir\SampleLib1.dll
TestDir\SampleLib1.pdb
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
TestDir\SampleProj.exe
TestDir\SampleProj.pdb
PatternedFilenames:
TestDir\SampleProj.*
TestDir\SampleLib1.*
RemainingFilesA:
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
RemainingFilesB:
TestDir\SampleLib1.dll
TestDir\SampleLib1.pdb
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
TestDir\SampleProj.exe
TestDir\SampleProj.pdb
Observe that both ExcludeFilesA and ExcludeFilesB look identical, but the resulting groups RemainingFilesA and RemainingFilesB differ.
Ultimately I want to obtain the list RemainingFilesA using the pattern generated the same way ExcludeFilesB is generated. Can you suggest a way, or do I have to completely rethink my approach?
The true cause of this was revealed accidentally when a custom task threw an exception.
The actual value of ExcludeFilesA is TestDir\SampleProj.*;TestDir\SampleLib1.* like one might expect. However the actual value of ExcludeFilesB is TestDir\SampleProj.%2a;TestDir\SampleLib1.%2a.
Presumably Message unescapes the string before using it, but Include and Exclude do not. That would explain why the strings look the same but behave differently.
Incidentally, the execution order doesn't seem to have anything to do with this, and I'm pretty sure (following extensive experimentation) that everything gets executed and evaluated exactly in the order in which it appears in this script.
ItemGroups need to be evaluated before targets execution, and the PatternedFilenames ItemGroup is being created on the fly within its target container.
You could workaround this using the CreateItem task, which will ensure the PatternedFilenames scope throughout the execution:
<RegexReplace Input="#(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
<Output TaskParameter="Output" ItemName="PatternedFilenames_tmp"/>
</RegexReplace>
<CreateItem Include="#(PatternedFilenames_tmp)">
<Output TaskParameter="Include" ItemName="PatternedFilenames"/>
</CreateItem>