Path settings in OpenApiReference (Nswag/Visual Studio) in csproj file (msbuild) - msbuild

I reference an OpenAPI service using the "Connected Services" feature in Visual Studio (2002 Prev 5, but same behavior in VS 2019). This tool is based on NSwag. The docs say that I can apply all settings from Nswag in certain elements of <OpenApiReference> element in *.csproj.
This works, but I want to use an output path different from the default (obj) and this setting can't handle .. paths and paths with spaces. The same setting on Nswag's command line tool works as expected.
My first approach in *.csproj with an output path without spaces:
<ItemGroup>
<OpenApiReference Include="swagger.json"
CodeGenerator="NSwagCSharp" Namespace="Demo.Proxy"
ClassName="ContractService"
OutputPath="ConnectedServices\Backend\ContractService.cs"
>
<Options>/DateTimeType:DateTime /GenerateClientInterfaces:true</Options>
</OpenApiReference>
</ItemGroup>
This results in a file obj\ConnectedServices\Backend\ContractService.cs. Correct.
But I want to use the "Connected Services" folder (with a space):
<ItemGroup>
<OpenApiReference Include="swagger.json"
CodeGenerator="NSwagCSharp" Namespace="Demo.Proxy"
ClassName="ContractService"
OutputPath="Connected Services\Backend\ContractService.cs"
>
<Options>/DateTimeType:DateTime /GenerateClientInterfaces:true</Options>
</OpenApiReference>
</ItemGroup>
This results in a file obj/Connected. It stops before the space. Not correct.
I tried:
%20 --> same result, stop before space
" from begin to end --> invalid character error
..\ path to get out of obj --> invalid character error
Also, just out of curiosity, I a used properties like this:
<PropertyGroup>
<ServicesPath>ConnectedServices\ContractService.cs</ServicesPath>
</PropertyGroup>
<ItemGroup>
<OpenApiReference Include="swagger.json" CodeGenerator="NSwagCSharp" Namespace="Demo.Proxy" ClassName="ContractService"
OutputPath="$([System.IO.Path]::Combine($(MSBuildProjectDirectory),$(ServicesPath)))"
>
<Options>/DateTimeType:DateTime /GenerateClientInterfaces:true</Options>
</OpenApiReference>
</ItemGroup>
Same result here: "ConnectedServices" works, "Connected Services" doesn't; the space kills the path and the result is just "Connected", rest ignored.
However, this fixes the "obj" folder issue and moves the file to the proper level.
I think it's a bug in OpenApiReference but I want to ask before file it whether I could have overlooked something. Just in case.

Related

How do I group files by the top folder they are in and have a task act on those files?

I am attempting to link resource files that were organized by locale folders into their own .resources.dll assembly. There are more than 750 locales that are dynamically generated, so it is not practical to hard code them like the docs show.
<ItemGroup>
<ResourceFiles Include="<path_to>/af/af.res;<path_to>/af/feature1.af.res;<path_to>/af/feature2.af.res">
<Culture>af<Culture>
</ResourceFiles>
<ResourceFiles Include="<path_to>/af-NA/af_NA.res;<path_to>/af-NA/feature1.af_NA.res;<path_to>/af-NA/feature2.af_NA.res">
<Culture>af-NA<Culture>
</ResourceFiles>
<ResourceFiles Include="<path_to>/af-ZA/af_ZA.res;<path_to>/af-ZA/feature1.af_ZA.res;<path_to>/af-ZA/feature2.af_ZA.res">
<Culture>af-ZA<Culture>
</ResourceFiles>
</ItemGroup>
The above structure can be used to execute the AL task multiple times for each group of files. As you can see, my files are arranged in folders that are named the same as the culture in .NET.
My question is, how do I build this structure dynamically based on the 750+ locale folders, many which contain multiple files?
What I Tried
I was able to get the grouping to function. However, for some odd reason the list of files is being evaluated as a String rather than ITaskItem[] like it should be. This is the structure that does the correct grouping. It is based on this Gist (although I am not sure whether I am misunderstanding how to use the last bit because the example is incomplete).
<PropertyGroup>
<SatelliteAssemblyTargetFramework>netstandard2.0</SatelliteAssemblyTargetFramework>
<TemplateAssemblyFilePath>$(MSBuildProjectDirectory)/bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll</TemplateAssemblyFilePath>
<ICU4JResourcesDirectory>$(SolutionDir)_artifacts/icu4j-transformed</ICU4JResourcesDirectory>
<ICU4NSatelliteAssemblyOutputDir>$(SolutionDir)_artifacts/SatelliteAssemblies</ICU4NSatelliteAssemblyOutputDir>
<PropertyGroup>
<Target
Name="GenerateOurSatelliteAssemblies"
DependsOnTargets="ExecICU4JResourceConverter"
AfterTargets="AfterBuild"
Condition=" '$(TargetFramework)' == '$(SatelliteAssemblyTargetFramework)' ">
<ItemGroup>
<EmbeddedResources Include="$(ICU4JResourcesDirectory)/*.*" />
<EmbeddedResourcesPaths Include="$([System.IO.Directory]::GetDirectories('$(ICU4JResourcesDirectory)'))" />
<!-- This groups each locale together along with its nested files and root path -->
<FolderInLocale Include="#(EmbeddedResourcesPaths)">
<Culture>$([System.IO.Path]::GetFileName('%(Identity)'))</Culture>
<Files>$([System.IO.Directory]::GetFiles('%(EmbeddedResourcesPaths.Identity)'))</Files>
</FolderInLocale>
</ItemGroup>
<!-- EmbedResources accepts ITaskItem[], but the result
of this transform is a ; delimited string -->
<AL EmbedResources="#(FolderInLocale->'%(Files)')"
Culture="%(FolderInLocale.Culture)"
TargetType="library"
TemplateFile="$(TemplateAssemblyFilePath)"
KeyFile="$(AssemblyOriginatorKeyFile)"
OutputAssembly="$(ICU4NSatelliteAssemblyOutputDir)/%(FolderInLocale.Culture)/$(AssemblyName).resources.dll" />
</Target>
I have attempted numerous ways to replace the semicolon characters with %3B and to split on semicolon (i.e. #(FolderInLocale->'%(Files.Split(';'))'), but in all cases, the transform fails to evaluate correctly.
I have also consulted the docs for MSBuild well-known item metadata to see if there is another way of grouping by folder. Unfortunately, there is no %(FolderName) metadata, which would solve my issue completely. While I was able to get it to group by folder using the below XML, it immediately flattened when trying to get the name of the top level folder, which is where the name of the culture is.
I am using GetFileName() to get the name of the top level folder after stripping the file name from it. But please do tell if there is a better way.
<ItemGroup>
<EmbeddedResourcesLocalizedFiles Include="$(ICU4JResourcesDirectory)/*/*.*"/>
<EmbeddedResourcesLocalized Include="#(EmbeddedResourcesLocalizedFiles)">
<Culture>%(RootDir)%(Directory)</Culture>
</EmbeddedResourcesLocalized>
<!-- Calling GetFileName() like this rolls the files into a single group,
but prior to this call it is grouped correctly. -->
<EmbeddedResourcesLocalized2 Include="#(EmbeddedResourcesLocalized)">
<Culture>$([System.IO.Path]::GetFileName('%(EmbeddedResourcesLocalized.Culture)'))</Culture>
</EmbeddedResourcesLocalized2>
</ItemGroup>

MSBuild: Filtering Items based on a part of the file path

I have a set of files I need to copy that are underneath a folder that contains the version of the package that deposited those files:
<ItemGroup>
<MyFiles Include="$(MyPackages)\foo.x64*\binaries\*.*"/>
</ItemGroup>
foo.x64* can resolve to more than one folder like foo.x64.17.5.50 and foo.x64.17.6.2 where the suffix represents the version of package. I want to filter my items based on that version but I can't seem to find a way to extract any part of the item file path as metadata on my item to then use it in batching or conditionals.
There are a few features of msbuild that can be used here. those are:
Defining custom item metadata. let's call themFooArch and FooVersion
MSBuild allows calling some .net APIs including regular expressions. So we can use RegEx.Match() here to extract the information you need to know.
When doing numeric comparison on strings, MSBuild parses them into Versions. This will allow you to do range conditions like '%(FooItems.FooVersion)' < '17.6.2'.
What's not possible out of the box is determining the highest version of a list of items. To achieve this, you'll need to write a custom msbuild task.
A more concrete example: To add the required metadata on the items and copy the right version and architecture, you can do the following:
<PropertyGroup>
<FooRegEx>[\\/]foo\.(?<arch>x(\d*))\.(?<version>((\d+)(\.\d+)+))</FooRegEx>
<FooVersionToUse>17.5.50</FooVersionToUse>
</PropertyGroup>
<ItemGroup>
<FooFiles Include="mypkgs\foo*\binaries\**\*">
<FooArch>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), $(FooRegEx)).get_Groups().get_Item("arch"))</FooArch>
<FooVersion>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), $(FooRegEx)).get_Groups().get_Item("version"))</FooVersion>
</FooFiles>
</ItemGroup>
<Target Name="IncludeFoo" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<FooArchToUse Condition="'$(FooArchToUse)' == '' and '$(Platform)' != 'AnyCPU'">$(Platform)</FooArchToUse>
<FooArchToUse Condition="'$(FooArchToUse)' == ''">x64</FooArchToUse>
</PropertyGroup>
<ItemGroup>
<Content Include="#(FooFiles)"
Link="foo\%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest"
Condition="'%(FooFiles.FooVersion)' == '$(FooVersionToUse)' and '%(FooFiles.FooArch)' == '$(FooArchToUse)'"/>
</ItemGroup>
</Target>
This will copy the items of the selected version and platform built for (defaulting to x64) into a foo subfolder of the output directory. (Tested on MSBuild 15.1). I've uploaded the example project to GitHub here.
You can you use the ItemGroup's Condition Attribute to filter out elements based on some boolean expression. You can use regex for instance as per this answer.

In MSBuild, why isn't Item Metadata, within a property, being resolved?

Below is a portion of a MSBuild file that I'm working on:
<ItemGroup>
<Tests Include="$(SolutionDir)\**\bin\$(TestPlatform)\$(Configuration)\*.Tests.dll" />
</ItemGroup>
<PropertyGroup>
<TestProperties>/testcontainer:%(Tests.FullPath)</TestProperties>
</PropertyGroup>
I want to have a property that holds a command line switch. However, when I try to use $(TestProperties) in an Exec Command string, %(Tests.FullPath) is never resolved to the absolute path of the Tests item. Instead, it's always processed literally, as "%(Tests.FullPath)".
Am I doing something wrong or is this a standard MSBuild behavior? If the latter, is there a way for me to workaround this?
Thanks
P.S. - I realize I probably don't need to access the FullPath property since my Include value is an absolute path. However, I'd still like to understand the issue, along with how to handle it.
You have a syntax error. Item lists are referenced via the # character and item meta data is referenced via %. Reference the MSBuild Special Character Reference for details. To access the well known item metadata, you need to apply a transform inside the Property itself.
<ItemGroup>
<Tests Include="MyFile.txt" />
</ItemGroup>
<PropertyGroup>
<TestProperties>/testcontainer:#(Tests->'%(FullPath)')</TestProperties>
</PropertyGroup>
You can find more help here

Get list of dlls in a directory with MSBuild

The following gives me only 1 file, the exe:
<ItemGroup>
<AssembliesToMerge Include="$(MSBuildProjectDirectory)\App\bin\Release\*.*" Condition="'%(Extension)'=='.dll'"/>
<AssembliesTomerge Include="$(MSbuildProjectDirectory)\App\bin\Release\App.exe"/>
</ItemGroup>
If I remove the Condition attribute, AssembliesToMerge contains all the files in the directory--dlls and otherwise. What am I doing wrong?
I am testing this via the ILMerge MSBuildCommunityExtensions Task. If there is a way to directly print the items in the ItemGroup, then that might help to ensure it's an issue with the Condition attribute.
Just use a wildcard in Include to filter dll files (Items wildcard)
<ItemGroup>
<AssembliesToMerge Include="$(MSBuildProjectDirectory)\App\bin\Release\*.dll"/>
<AssembliesTomerge Include="$(MSbuildProjectDirectory)\App\bin\Release\App.exe"/>
</ItemGroup>
I think it doesn't work using the Condition attribute because Item metadatas aren't set yet during creation, so %(Extension) is empty.

MSBuild OutputPath property and absolute paths

I'm trying to set the OutputPath value to an absolute path:
<OutputPath>c:\Projects\xxx\Deployment</OutputPath>
But I get this error:
Error 17 The expression "[System.IO.Path]::GetFullPath(D:\Projects\xxx\trunk\xxx.Web.Deployment\c:\Projects\xxx\Deployment\)" cannot be evaluated. The given path's format is not supported. 1 1 xxx.Web.Deployment
Is there a way to use an absolute path with the OutputPath property? I've tried experimenting with the BaseOutputPath property:
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Deployment|AnyCPU'">
<BaseOutputPath>C:\Projects\xxx\</BaseOutputPath>
<OutputPath>.\Deployment</OutputPath>
<EnableUpdateable>true</EnableUpdateable>
<UseMerge>true</UseMerge>
<SingleAssemblyName>xxx.Web.Deployment</SingleAssemblyName>
But it seems to get ignored. What are BaseOutputPath and BaseIntermediateOutputPath used for?
I'm not sure whether you can do what you're talking about, but you can add something similar to the following:
<PropertyGroup>
<CentralisedBinariesFolderLocation>c:\wherever</CentralisedBinariesFolderLocation>
</PropertyGroup>
<Target Name="AfterBuild">
<Exec Command="xcopy /Y /S /F /R "$(TargetPath)" "$(CentralisedBinariesFolderLocation)"" />
</Target>
Which will copy it to the relevant location after the build.
Try using OutDir instead of OutputPath :
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Deployment|AnyCPU'">
<OutDir>C:\Projects\xxx\$(Configuration)</OutDir>
<EnableUpdateable>true</EnableUpdateable>
<UseMerge>true</UseMerge>
<SingleAssemblyName>xxx.Web.Deployment</SingleAssemblyName>
</PropertyGroup>
Copy the .target and .dll files from the installer directory
Modify the lines at the top that look like <UsingTask TaskName="GetProjectProperties" AssemblyFile="../../ ..lallal/VisualStudio/v10.0/Microsoft.Web.Publishing.Tasks.dll"/> and c*opy those .target and .dll files to your vendors folder next to the copied Microsoft.WebDeployment.targets-file your are editing*. Set the attr, AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"
Add the line <EnablePackageProcessLoggingAndAssert Condition="'$(EnablePackageProcessLoggingAndAssert)' == ''">True</EnablePackageProcessLoggingAndAssert> to the initial PropertyGroup.
Set the OutputPath as you wish in the actual file/other tagets/other build-proj file.
Edit line ~290 to <WebPublishPipelineProjectDirectory Condition="'$(WebPublishPipelineProjectDirectory)'==''">$(OutputPath)</WebPublishPipelineProjectDirectory>
Instead of all the steps in the October answer, is it not possible just to define WebPublishPipelineProjectDirectory with the same path as OutputPath?
I tried it in my CI solution (using CruiseControl) and it seemed to work.
Does anyone know of any side effects that are not apparent to me from doing this?