MsBuild: Read part of the file using ReadLinesFromFile - msbuild

I need to read the XML from the file. I use following code:
<ItemGroup>
<SourceXsltFile Include="SourceFile.xml" />
</ItemGroup>
<ReadLinesFromFile File="#(SourceXsltFile)">
<Output TaskParameter="Lines" ItemName="FileContents" />
</ReadLinesFromFile>
But I need only the part of the file's content to be copied which resides inside the <XSL> tag.
Any ideas?

In 4.0+ there are XmlPeek, XmlPoke, and XslTransform tasks you can use here. See MSDN.

http://msbuildtasks.tigris.org/ - use RegexMatch task with something like - <XSL\b[^>]*>(.*?)</XSL> (not sure about exact correctness though).
Write your own custom task

Related

Can a task ItemGroup glob files?

I have an ItemGroup declared as follows:
<ItemGroup>
<MyCustomProjectType Include="..\path_to_my_project">
<Name>MyProjectName</Name>
</MyCustomProjectType>
</ItemGroup>
This is a custom project type that I want to perform some specific manipulations on.
Later I have a Target (example only but it communicates what I am after):
<Target Name="MyTarget">
<ItemGroup>
<CustomProjectReferenceFiles
KeepMetadata="Name"
Include="#(MyCustomProjectType->'%(Identity)\%(Name)\**\*')"
Exclude="**\*.x;**\*.y"
/>
</ItemGroup>
<Message Text="#(CustomProjectReferenceFiles)" />
</Target>
So I have a Target based ItemGroup where I am attempting, using a transform, to create a new Include. This does run, but it appears the Include is literally set to:
..\path_to_my_project\MyProjectName\**\*
AKA that glob/wildcards are not expanded.
I'm pretty new to MSBuild so maybe I am missing something in my search of the documentation. One solution I thought of here would be just just create a new Custom Task that handles pulling out the files I need and setting that Output on an intermediate Target.
I also found this SO question:
https://stackoverflow.com/a/3398872/1060314
Which brings up the point about CreateItem being deprecated which leaves me with not knowing what the alternatives are.
The easiest way is to use an intermediate property so that the actual text is used and not the escaped transformed items:
<PropertyGroup>
<_CustomProjectReferenceFileIncludes>#(MyCustomProjectType->'%(Identity)\%(Name)\**\*')</_CustomProjectReferenceFileIncludes>
</PropertyGroup>
<ItemGroup>
<CustomProjectReferenceFiles
KeepMetadata="Name"
Include="$(_CustomProjectReferenceFileIncludes)"
Exclude="**\*.x;**\*.y"
/>
</ItemGroup>

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

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.

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.

Pass list item to Properties when calling reusable msbuild target

I'm trying to create a reusable Target in msbuild, following the basic model outlined in How to invoke the same msbuild target twice?
I'm stuck trying to pass a property that I want interpreted as a list. I haven't found an example online that deals with this situation. As I understand it, the problem is that Properties is already treated as a list item, so it doesn't like having a list item passed in as well. Is there a way to get msbuild to pack and unpack the list correctly here?
Msbuild is complaining with:
error MSB4012: The expression "FilesToZip=#(Scripts)" cannot be used in this context. Item lists cannot be concatenated with other strings where an item list is expected. Use a semicolon to separate multiple item lists.
Here's an example caller:
<Target Name="BuildMigrationZip">
<MSBuild Projects="BuildZip.msbuild"
Targets="BuildZip"
Properties="FilesToZip=#(Scripts);OutputZipFile=$(MigrationPackageFilePath);OutputFolder=$(MigrationPackagePath);Flatten=true"/>
<Message Text="Created database migration zip: $(MigrationPackageFilePath)" Importance="high"/>
</Target>
And the base target:
<Target Name="BuildZip">
<MakeDir Directories="$(OutputFolder)"/>
<Zip Files="#(FilesToZip)"
ZipFileName="$(OutputZipFile)"
Flatten="$(Flatten)"
ParallelCompression="false" />
</Target>
I'm basically at the point of just going back to cut and paste for these, although I want to package up a number of zips here.
UPDATE: The same issue applies to setting Inputs on the reusable target. My question up to this point addresses the raw functionality, but it would be nice to keep dependencies working. So for example:
<Target Name="BuildZip"
Inputs="#(FilesToZip)"
Outputs="$(OutputZipFile)">
<MakeDir Directories="$(OutputFolder)"/>
<Zip Files="#(FilesToZip)"
ZipFileName="$(OutputZipFile)"
Flatten="$(Flatten)"
ParallelCompression="false" />
</Target>
They key is to pass the list around as a property. So when your Scripts list is defined as
<ItemGroup>
<Scripts Include="A"/>
<Scripts Include="B"/>
<Scripts Include="C"/>
</ItemGroup>
then you first convert it into a property (which just makes semicolon seperated items, but msbuild knows how to pass this via the Properties of the MSBuild target) then pass it to the target:
<Target Name="BuildMigrationZip">
<PropertyGroup>
<ScriptsProperty>#(Scripts)</ScriptsProperty>
</PropertyGroup>
<MSBuild Projects="$(MSBuildThisFile)" Targets="BuildZip"
Properties="FilesToZip=$(ScriptsProperty)" />
</Target>
(note I'm using $(MSBuildThisFile) here: you don't necessarily need to create seperate build files for every single target, in fact for small targets like yours it's much more convenient to put it in the same file)
Then in your destination target you turn the property into a list again:
<Target Name="BuildZip">
<ItemGroup>
<FilesToZipList Include="$(FilesToZip)"/>
</ItemGroup>
<Message Text="BuildZip: #(FilesToZipList)" />
</Target>
Output:
BuildZip: A;B;C
Update
When working with Inputs, you cannot pass #(FilesToZip) since that expands to nothing because is not a list: it's a property - which happens to be a number of semicolon-seperated strings. And as such, it is usable for Inputs you just have to expand it as the property it is i.e. $(FilesToZip):
<Target Name="BuildZip"
Inputs="$(FilesToZip)"
Outputs="$(OutputZipFile)">
...
</Target>
Output of second run:
BuildZip:
Skipping target "BuildZip" because all output files are up-to-date with respect to the input files.