How can I split an ItemGroup's Excludes into multiple lines? - msbuild

I have a custom MSBuild task to minify some JavaScript files. I created an ItemGroup to define which files I want to be minified, and which should not be. I have the following in my .csproj file:
<ItemGroup>
<JS Include="**\*.js" Exclude="**\*.min.js;obj\**\*.*;**\_references.js;[snip]" />
</ItemGroup>
I want to split that property into several lines for better readability (the snipped part is long and could get longer in the future), so I tried this:
<ItemGroup>
<JS Include="**\*.js" Exclude="**\*.min.js" />
<JS Exclude="obj\**\*.*" />
<JS Exclude="**\_references.js" />
[snip]
</ItemGroup>
But that gave me this build error:
error MSB4066: The attribute "Exclude" in element <JS> is unrecognized.
The same occurred when I added an empty include (Include="") in those subsequent elements. (Putting something inside the quotes removed the error, but included extra "files" in the JS var.)
I then learned from the docs for MSBuild Items that the Exclude attribute only affects items added by the Include attribute in the same element.
I also tried using only one Exclude string, but splitting the string itself into multiple lines, like this:
<ItemGroup>
<JS Include="**\*.js"
Exclude="**\*.min.js;
obj\**\*.*;
**\_references.js;
[snip]" />
</ItemGroup>
That looks okay, but when I subsequently saved the project from Visual Studio, the line endings were mangled, so it turned into this:
<ItemGroup>
<JS Include="**\*.js" Exclude="**\*.min.js;
obj\**\*.*;
**\_references.js;
[snip]" />
</ItemGroup>
(This didn't break anything in the build, but defeats the purpose of splitting the string into multiple lines.)
How can I split these excludes into multiple lines?

I found this answer about excluding files from Content, which is also part of an ItemGroup. So I tried that:
<ItemGroup>
<JS Include="**\*.js" Exclude="**\*.min.js" />
<JS Remove="obj\**\*.*" />
<JS Remove="**\_references.js" />
[snip]
</ItemGroup>
That worked. JS now contains only the files I want, and the csproj file is a bit more readable.

Related

Use uglifyjs in Publish Profile

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.

Using the Zip task in MSBuild

I have been attempting to use the zip task of msbuild in a project I am working on at the moment.
My project file looks something like this:
<PropertyGroup> <MSBuildCommunityTasksPath>$(SolutionDir)\.build</MSBuildCommunityTasksPath> </PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" />
<ItemGroup>
<FileToZip include="C:\FilePath"></FilesToZip>
<FileToZip include="C:\FilePath"></FilesToZip>
</ItemGroup>
<Target Name="BeforeBuild">
<PropertyGroup>
<ReleasePath>\releasepath</ReleasePath>
<Zip Files="#(FilesToZip)" WorkingDirectory="$(ReleasePath)" ZipFileName="HTMLeditor.html" ZipLevel="9" />
</Target>
However, the zip file updates but does not contain the files specified in the item group FilesToZip. I cannot figure out why they aren't being recognised! I have double checked file paths and they are correct. Any ideas?
I think you want to do something like this:
<ItemGroup>
<FileToZip include="C:\FilePath;C:\FilePath"/>
</ItemGroup>
As I mentioned in my comment, simply creating a variable (FileToZip) and repeating it twice with different values does not give you an array that contains both of the values. You end up with only the last value (and not an array at all). Your include attribute is a selector which is used to build the array and it can contain multiple values, wildcards and other patterns which are used to build out that array for you.
Here's a link to MSDN that gives you more information on how to use the Include attribute: http://msdn.microsoft.com/en-us/library/ms171454.aspx
I ditched the ItemGroup in the end, and went with another way of doing it.
<Target Name="Zip">
<CreateItem Include="FilesToInclude" >
<Output ItemName="ZipFiles" TaskParameter="Include"/>
<Zip ZipFileName="ZipFile.zip" WorkingDirectory="FolderToWriteZipTo" Files="#(ZipFiles)" />
</Target>
This method seemed to be easier and wasn't adding files to the root of the file.
Thanks for the help though guys.

How do I cherry-pick files to copy in msbuild and preserve the directory structure?

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.

How do I dump a list of imported property sheets from MSBuild

I am constructing a hierarchy of property sheets (many that are conditionally included according to Platform and Configuration) and I am attempting to write a set of targets that can help diagnose any errors that may sneak in.
What I would like is a list of property sheets that have been included.
Example:
<ImportGroup Condition="$(Configuration.Contains('Debug'))">
<Import Project="ps.cpp.config.debug.props"/>
</ImportGroup>
<ImportGroup Condition="$(Configuration.Contains('Release'))">
<Import Project="ps.cpp.config.release.props"/>
</ImportGroup>
<ImportGroup Condition="'$(Platform)' == 'x64'">
<Import Project="ps.cpp.plat.x64.props"/>
</ImportGroup>
<ImportGroup Condition="'$(Platform)' == 'Win32'">
<Import Project="ps.cpp.plat.win32.props"/>
</ImportGroup>
And a target like this:
<Target Name="DumpPropertySheets">
<!-- This doesn't work! -->
<!-- <Message Text="%(Import.Project)"/> -->
</Target>
Which should result in console output like this when built with msbuild test.vcxproj /t:DumpPropertySheets /p:Platform=x64 /p:Configuration:Debug
DumpPropertySheets:
ps.cpp.config.debug.props
ps.cpp.plat.x64.props
There is not an obvious way to do what you are trying to do. Imports are pre-processed to aggregate all of the content into a single file, they are not a datatype like item arrays or properties that can be referenced later on.
The syntax %(Import.Project) doesn't work because that syntax is valid only for item arrays, and you are trying to use it on the Import keyword, which is not a populated item array.
Also note that your use of the ImportGroup elements surrounding the imports is optional (and probably a bit verbose). The following two constructs in an MSBuild file are equivalent...
<ImportGroup Condition="$(Configuration.Contains('Debug'))">
<Import Project="ps.cpp.config.debug.props"/>
</ImportGroup>
...and (line-wrapped for clarity)...
<Import
Condition="$(Configuration.Contains('Debug'))"
Project="ps.cpp.config.debug.props"
/>
If you are trying to diagnose property sheet import errors, don't forget about the /pp command-line switch, which will dump the complete preprocessed file. You also could (for your own files at least) give each import a unique entry into an item array, e.g.
<Import
Condition="$(Configuration.Contains('Debug'))"
Project="ps.cpp.config.debug.props"
/>
then inside ps.cpp.config.debug.props,
<ItemGroup>
<_Import Include="$(MSBuildThisFile)" />
</ItemGroup>
then later in your build you could get what you appear to be looking for, to some degree, with,
<Target Name="DumpPropertySheets">
<!-- This does work! -->
<Message Text="%(_Import.Identity)" />
</Target>

Including files with directory specified separately in MSBuild

This seems like it should be simple but I can't work it out from the reference and my google-fu is apparently weak.
I just want to specify the file names and base folder separately in the build file...
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TestFilesWithFolder>
B:\Root\Test1.*;
B:\Root\Test2.*
</TestFilesWithFolder>
<TestFiles>Test1.*;Test2.*</TestFiles>
<TestFileRoot>B:\Root</TestFileRoot>
</PropertyGroup>
<Target Name="Build">
<ItemGroup>
<TestFilesGroupWithFolder Include="$(TestFilesWithFolder)" />
<TestFilesGroup Include="$(TestFileRoot)\$(TestFiles)" />
</ItemGroup>
<Warning Text="Source files with folder: #(TestFilesGroupWithFolder)" />
<Warning Text="Source files: #(TestFilesGroup)" />
</Target>
</Project>
When I run this, the first warning shows both files as expected, but the second warning only shows the first file (since the straight string concat put the folder name on the first but not second).
How would I get the ItemGroup "TestFilesGroup" to include both the files given the "TestFiles" and "TestFileRoot" properties?
It is possible to convert a semicolon delimited list of things into an item, which would make this possible, except that the items in your property contain wildcards, so if you want to have MSBuild treat them as items in a list, at the moment MSBuild first sees it the path must be valid. There may be a way to do that but I can't think of one. In other words...
<ItemGroup>
<TestFiles Include="$(TestFiles)" />
</ItemGroup>
...only works if $(TestFiles) contains a delimited list of either things with no wildcards, or qualified paths that actually exist.
Further, MSBuild can't compose a path with a wildcard inside the Include attribute and evaluate it at the same time, so you need a trick to first compose the full path separately, then feed it into the Include attribute. The following will work, but it requires changing your delimited property into a set of items. It batches a dependent target on this item list, with each batched target execution calculating a meta value for one item, which is stored off in a new meta value. When the original target executes, it is able to use that meta value in a subsequent Include.
<PropertyGroup>
<TestFilesWithFolder>
D:\Code\Test1.*;
D:\Code\Test2.*
</TestFilesWithFolder>
<TestFileRoot>D:\Code</TestFileRoot>
</PropertyGroup>
<ItemGroup>
<TestFilePattern Include="TestFilePattern">
<Pattern>Test1.*</Pattern>
</TestFilePattern>
<TestFilePattern Include="TestFilePattern">
<Pattern>Test2.*</Pattern>
</TestFilePattern>
</ItemGroup>
<Target Name="Compose" Outputs="%(TestFilePattern.Pattern)">
<ItemGroup>
<TestFilePattern Include="TestFilePattern">
<ComposedPath>#(TestFilePattern->'$(TestFileRoot)\%(Pattern)')</ComposedPath>
</TestFilePattern>
</ItemGroup>
</Target>
<Target Name="Build" DependsOnTargets="Compose">
<ItemGroup>
<TestFilesGroupWithFolder Include="$(TestFilesWithFolder)" />
</ItemGroup>
<Warning Text="Source files with folder: #(TestFilesGroupWithFolder)" />
<ItemGroup>
<ComposedTestFiles Include="%(TestFilePattern.ComposedPath)" />
</ItemGroup>
<Warning Text="Source files: #(ComposedTestFiles)" />
</Target>
Which produces the following output:
(Build target) ->
d:\Code\My.proj(80,5): warning : Source files with folder:
D:\Code\Test1.txt;D:\Code\Test2.txt
d:\Code\My.proj(84,5): warning : Source files:
D:\Code\Test1.txt;D:\Code\Test2.txt