How do I stop MSBuild from replacing backslashes with forward slashes? - msbuild

MSBuild normally replaces backslashes in your build file (usually found in paths) with forward slashes, which helps when working on a cross-platform project. But I have one task where I don't want MSBuild to touch my backslashes: there's a custom task in our build that takes a regular expression and a replacement task, and updates a file. (I'm using it to update version numbers in AssemblyInfo.cs files, getting the version number from git describe. There's a TeamCity build feature that would work if only I could control when it runs, but that's a different story -- suffice it to say that doing this in my MSBuild file looks like the best approach for now).
My problem is that MSBuild is "helping" me by replacing backslashes with forward slashes in the attributes I pass to our custom task, wreaking havoc on my regular expression. What I wrote:
<FileUpdate File="$(RootDir)/GlobalAssemblyInfo.cs"
Regex='AssemblyFileVersion\("[^"]+"\)'
ReplacementText='AssemblyFileVersion("$(VersionNumber)")' />
And what I got in the build log:
error : Did not manage to replace 'AssemblyFileVersion/("[^"]+"/)' with
'AssemblyFileVersion/("1.1.0.92"/)'
Notice how the backslashes in my regex have become forward slashes? Yeah, that's not going to match anything in my AssemblyInfo.cs file.
I've been able to work around this by avoiding the use of backslashes entirely in my regular expression, like so:
<FileUpdate File="$(RootDir)/GlobalAssemblyInfo.cs"
Regex='AssemblyFileVersion[(]"[^"]+"[)]'
ReplacementText='AssemblyFileVersion("$(VersionNumber)")' />
But this won't work in every case. Sooner or later I'm going to need a regular expression with \ in it, or some other backslash expression, and then I'll be sunk. Before that happens, I'd really like to figure out how to tell MSBuild "Stop helping me! I said backslash, and I really meant BACKslash, not forward slash, in this ONE attribute. You can "help" me with other attributes all you like, but leave this one alone!" Any ideas?

You can create your own Replace Text Task and have control over wrongly replaced characters by simply changing / to \ or use other more unique marker.
<UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<InputFilename ParameterType="System.String" Required="true" />
<MatchExpression ParameterType="System.String" Required="true" />
<ReplacementText ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Text.RegularExpressions" />
<Code Type="Fragment" Language="cs">
<![CDATA[
File.WriteAllText(
InputFilename,
Regex.Replace(File.ReadAllText(InputFilename), MatchExpression.Replace('/', '\\'), ReplacementText)
);
]]>
</Code>
</Task>

Use %5C in place of \. This works in task attribute values as well as property values.

Related

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.

MSBUILD Splitting text file into lines

Note that I have already went through:
Is there a way to print a new-line when using <Message...>?
Read text file and split every line in MSBuild
But for some strange reason I can't make it work.
I have:
<ReadLinesFromFile File="$(OutputPath)myfile.log">
<Output PropertyName="FileOutput" TaskParameter="Lines" />
</ReadLinesFromFile>
<Message Text="$(FileOutput)"/>
-- This works, entire file content is shown on the screen.
Now I would like for each line in that file to report a warning/error.
<ItemGroup>
<SplitVersion Include="$(FileOutput.Split('%0A%0D'))"/>
</ItemGroup>
<Warning Text="%(SplitVersion.Identity)" />
Whatever combination I try in Split (e.g. \n, \r\n, %0A etc.) I get only one warning instead of getting one warning per line.
You are storing the lines in a property (typo maybe? Anyway I didn't even know was possible until now - it is also not mentioned in the documentation), store them in an item list instead and you'll get the lines, split already by ReadLinesFromFile so you don't have to bother with it, and after all that's the main way it is supposed to be used. Note the ItemName where you had PropertyName:
<ReadLinesFromFile File="$(OutputPath)myfile.log">
<Output ItemName="FileOutput" TaskParameter="Lines" />
</ReadLinesFromFile>
<Message Text="%(FileOutput.Identity)"/>

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.

MsBuild: Read part of the file using ReadLinesFromFile

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

MSBuild Annoyances (or blatant ignorance on my part)

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" />