I'm looking into ways to execute python as part of a C# build.
Specifically, I want to create a Python package based on a C# project through python.net. My general idea was to build the C# project first. And then, as some sort of post-build step, invoke python to build a package based on the newly generated NET assemblies.
I can't presume python will installed on the build host, so ideally I want to include a "portable" - even more ideally, nuget-based - python distribution.
I have found a promising nuget package, but am not entirely sure of its usage. It incldues no C# code, but has all python binaries included, and has build props as copy/pasted below for reference.
Given on that package's props - can I somehow reference its binaries from my own project as a post-build step?
Say, for example, I want to add a post-build step to my own project, that simply just invokes "python.exe" after the build. How could I do that?
My own project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="python" Version="3.10.0-a6" />
</ItemGroup>
<Target Name="MyCustomStep" AfterTargets="Build">
<!-- .. now what? I can't seem to access. e.g. #(PythonHome) or $(PythonHome) from here --/>
<Target>
</Project>
Props of the python package from nuget:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="$(Platform) == 'X64'">
<PythonHome Condition="$(PythonHome) == ''">$([System.IO.Path]::GetFullPath("$(MSBuildThisFileDirectory)\..\..\tools"))</PythonHome>
<PythonInclude>$(PythonHome)\include</PythonInclude>
<PythonLibs>$(PythonHome)\libs</PythonLibs>
<PythonTag>3.10</PythonTag>
<PythonVersion>3.10.0-a6</PythonVersion>
<IncludePythonExe Condition="$(IncludePythonExe) == ''">true</IncludePythonExe>
<IncludeDistutils Condition="$(IncludeDistutils) == ''">false</IncludeDistutils>
<IncludeLib2To3 Condition="$(IncludeLib2To3) == ''">false</IncludeLib2To3>
<IncludeVEnv Condition="$(IncludeVEnv) == ''">false</IncludeVEnv>
<GetPythonRuntimeFilesDependsOn>_GetPythonRuntimeFilesDependsOn310_None;$(GetPythonRuntimeFilesDependsOn)</GetPythonRuntimeFilesDependsOn>
</PropertyGroup>
<ItemDefinitionGroup Condition="$(Platform) == 'X64'">
<ClCompile>
<AdditionalIncludeDirectories>$(PythonInclude);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<Target Name="GetPythonRuntimeFiles" Returns="#(PythonRuntime)" DependsOnTargets="$(GetPythonRuntimeFilesDependsOn)" />
<Target Name="_GetPythonRuntimeFilesDependsOn310_None" Returns="#(PythonRuntime)">
<ItemGroup>
<_PythonRuntimeExe Include="$(PythonHome)\python*.dll" />
<_PythonRuntimeExe Include="$(PythonHome)\python*.exe" Condition="$(IncludePythonExe) == 'true'" />
<_PythonRuntimeExe>
<Link>%(Filename)%(Extension)</Link>
</_PythonRuntimeExe>
<_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.pyd" />
<_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.dll" />
<_PythonRuntimeDlls>
<Link>DLLs\%(Filename)%(Extension)</Link>
</_PythonRuntimeDlls>
<_PythonRuntimeLib Include="$(PythonHome)\Lib\**\*" Exclude="$(PythonHome)\Lib\**\*.pyc;$(PythonHome)\Lib\site-packages\**\*" />
<_PythonRuntimeLib Remove="$(PythonHome)\Lib\distutils\**\*" Condition="$(IncludeDistutils) != 'true'" />
<_PythonRuntimeLib Remove="$(PythonHome)\Lib\lib2to3\**\*" Condition="$(IncludeLib2To3) != 'true'" />
<_PythonRuntimeLib Remove="$(PythonHome)\Lib\ensurepip\**\*" Condition="$(IncludeVEnv) != 'true'" />
<_PythonRuntimeLib Remove="$(PythonHome)\Lib\venv\**\*" Condition="$(IncludeVEnv) != 'true'" />
<_PythonRuntimeLib>
<Link>Lib\%(RecursiveDir)%(Filename)%(Extension)</Link>
</_PythonRuntimeLib>
<PythonRuntime Include="#(_PythonRuntimeExe);#(_PythonRuntimeDlls);#(_PythonRuntimeLib)" />
</ItemGroup>
<Message Importance="low" Text="Collected Python runtime from $(PythonHome):%0D%0A#(PythonRuntime->' %(Link)','%0D%0A')" />
</Target>
</Project>
That is for the use of internal nuget rather than your main project. You cannot get that property under main project.
You have to use my function:
1) edit csproj file and set this for your PackageReference python
<GeneratePathProperty>true</GeneratePathProperty>
Like this:
<ItemGroup>
<PackageReference Include="python" Version="3.10.0-a6">
<GeneratePathProperty>true</GeneratePathProperty>
</PackageReference>
</ItemGroup>
2) Then, you can use $(Pkgpython) to get that path.
<Target Name="MyCustomStep" AfterTargets="Build">
<Exec Command="$(Pkgpython)\tools\python.exe" />
</Target>
Related
I have project A that need to insert a TextId.cs before build. And, there is project B. TextId.cs would generated after project B is compiled and executed.
Now I'd like to integrate the compile and execute in Directory.Build.targets in project A. It is not worked as I expect. TextId.cs will generate but the build would still failed as no TextId.cs if I set BeforeTargets="BeforeBuild" as below.
Anyone knows that which target is OK? or, any other solution?
<Project>
<ItemGroup>
<ProjectReferences Include="c:\code\textidfilegenerator\*.*proj" />
</ItemGroup>
<Target Name="BuildOtherProjects">
<Message Importance="High" Text="-----------------------" />
<MSBuild
Projects="#(ProjectReferences)"
Targets="Build">
</MSBuild>
</Target>
<Target Name="CopyText" DependsOnTargets="BuildOtherProjects" BeforeTargets="BeforeBuild">
<Message Importance="High" Text="**********************" />
<Exec Command="C:\Code\TextIdFileGenerator\bin\Debug\net6.0\TextIdFileGenerator.exe C:\Code\Sys1500TestDriver\TextProvider\TextIds.cs" IgnoreExitCode="true"/>
</Target>
</Project>
Before you will dive into the comments below, go through these docs:
MSBuild reserved and well-known properties
Common MSBuild project properties
<Project>
<ItemGroup>
<ProjectReferences Include="c:\code\textidfilegenerator\*.*proj" /> <!--you should use relative path like <ProjectReference Include="../**/*.csproj or absolute with MSBuild well-known properties" />-->
</ItemGroup>
<Target Name="BuildOtherProjects"> <!--you don't need this target if you have project reference-->
<Message Importance="High" Text="-----------------------" />
<MSBuild
Projects="#(ProjectReferences)"
Targets="Build">
</MSBuild>
</Target>
<Target Name="CopyText" DependsOnTargets="BuildOtherProjects" BeforeTargets="BeforeBuild"><!-- 'BeforeBuild' will not work because TextIdFileGenerator.exe needs to be created before you will do anything with it so you should use 'AfterTargets="Build"'-->
<Message Importance="High" Text="**********************" />
<Exec Command="C:\Code\TextIdFileGenerator\bin\Debug\net6.0\TextIdFileGenerator.exe C:\Code\Sys1500TestDriver\TextProvider\TextIds.cs" IgnoreExitCode="true"/><!-- again, you should use relative paths or absolute ones with combination of MSBuildProjectDirectory and OutDir -->
</Target>
</Project>
I am working on source generator and I have problems with dependencies:
It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'Flurl.Http, Version=3.0.1.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.'
There is a lot of information on how to pack dependencies into nuget, but I reference analyzer project directly like this:
<ProjectReference Include="SG.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
In analyzer project I added <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> and all dependencies are available in output directory, but VS is not using that directory - it uses AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\[...] instead and it copies only one DLL there.
What can be done to make that work?
I found the way to make it work more or less reliably with some hacks.
Before that I also tried ILMerge, but it didn't work (missing method exceptions).
Solution:
First of all I embeded dependencies in source generator assembly like this:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" Visible="false" />
</ItemGroup>
Then I created AssemblyResolve handler for AppDomain (static constructor in generator class) like so:
AppDomain.CurrentDomain.AssemblyResolve += (_, args) =>
{
AssemblyName name = new(args.Name);
Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName);
if (loadedAssembly != null)
{
return loadedAssembly;
}
string resourceName = $"Namespace.{name.Name}.dll";
using Stream resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (resourceStream == null)
{
return null;
}
using MemoryStream memoryStream = new MemoryStream();
resourceStream.CopyTo(memoryStream);
return Assembly.Load(memoryStream.ToArray());
};
The way that this should work is outlined in the source-generators cook-book, with their example being:
<Project>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<!-- Take a private dependency on Newtonsoft.Json (PrivateAssets=all) Consumers of this generator will not reference it.
Set GeneratePathProperty=true so we can reference the binaries via the PKGNewtonsoft_Json property -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" PrivateAssets="all" GeneratePathProperty="true" />
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<!-- Package the Newtonsoft.Json dependency alongside the generator assembly -->
<None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
However, it is my experience that this is still not reliable or robust; this is acknowledged, and I get the impression that improving this experience is part of future planning, but: right now, honestly it is better off simply not using dependencies outside of the core framework and the Roslyn libraries that are already loaded. If your dependencies are in the same repo as the analyzer/generator, you might be able to simply suck in the code during build, for example from here:
<ItemGroup>
<!-- compile what we need from protobuf-net directly; package refs cause pure pain in anaylizers-->
<Compile Include="../protobuf-net.Core/**/*.cs" Link="protobuf-net.Core"/>
<Compile Remove="../protobuf-net.Core/obj/**/*.cs" />
<Compile Include="../protobuf-net.Reflection/**/*.cs" Link="protobuf-net.Reflection"/>
<Compile Remove="../protobuf-net.Reflection/obj/**/*.cs" />
</ItemGroup>
There is a definitive answer to this now. The Source Generator Samples contain an example .csproj which has the required setup.
I used this to make a source generator with Sprache, works great.
Pasting the example .csproj here for reference:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftNetCompilersToolsetVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<!-- Generator dependencies -->
<PackageReference Include="CsvTextFieldParser" Version="1.2.2-preview" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Handlebars.Net" Version="1.10.1" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn)</GetTargetPathDependsOn>;GetDependencyTargetPaths
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGCsvTextFieldParser)\lib\netstandard2.0\CsvTextFieldParser.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PKGHandlebars_Net)\lib\netstandard2.0\Handlebars.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
</Project>
ProjectReference is not the same as PackageReference!
The GeneratePathProperty will not take any action for ProjectReference because it is not a package.
So, you just have to add the TargetPathWithTargetPlatformMoniker pointing to your local DLL into Source Generator csproj.
Example with default output locations:
Folder structure
- Solution Folder
| - MySolution.sln
| + MyProject
| | - MyProject.csproj
| | + bin
| | | + Debug
| | | | + netstandard2.0
| | | | | - MyProject.dll
| | | + Release
| | | | + netstandard2.0
| | | | | - MyProject.dll
| + MySourceGenerator
| | - MySourceGenerator.csproj
MySourceGenerator.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyProject\MyProject.csproj" PrivateAssets="all"/>
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="..\MyProject\bin\$(Configuration)\netstandard2.0\MyProject.dll" IncludeRuntimeDependency="false"/>
</ItemGroup>
</Target>
</Project>
I want to embed local references in the assembly before compiling the main unit. But the written target does not work.
<Target Name="EmbedLocal" BeforeTargets="CoreCompile">
<Message Text="Run EmbedLocal for $(MSBuildProjectFullPath)..." Importance="high"/>
<ItemGroup>
<EmbeddedResource Include="#( ReferencePath->WithMetadataValue( 'CopyLocal', 'true' )->Metadata( 'FullPath' ) )"/>
</ItemGroup>
<Message Text="Embed local references complete for $(OutputPath)$(TargetFileName)." Importance="high" />
</Target>
#(EmbeddedResource) at this moment contains valid list of paths.
Update:
Now my import file contains:
<Project ToolsVersion="$(MSBuildToolsVersion)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<EmbedLocalReferences Condition=" '$(EmbedLocalReferences)' == '' ">True</EmbedLocalReferences>
</PropertyGroup>
<Target Name="EmbedLocal" BeforeTargets="ResolveReferences" Condition=" '$(EmbedLocalReferences)' == 'True' ">
<Message Text="Run EmbedLocal for $(MSBuildProjectFullPath)..." Importance="high"/>
<ItemGroup>
<EmbeddedResource Include="#(ReferenceCopyLocalPaths->WithMetadataValue( 'Extension', '.dll' )->Metadata( 'FullPath' ))">
<LogicalName>%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<Message Text="Embed local references complete for $(OutputPath)$(TargetFileName)." Importance="high" />
</Target>
</Project>
It works fine. Output assembly contains all .dll references as EmbeddedResource.
MSBuild. Create EmbeddedResource before build
You can try to use BeforeBuild action to the csproj file to include the embedded resources:
<Target Name="BeforeBuild">
...
<ItemGroup>
<EmbeddedResource Include="..."/>
</ItemGroup>
...
</Target>
Now MSBuild will add this file as embedded resource into your assembly.
Update:
Thanks #Martin Ullrich. He pointed out the correct direction, we could use <Target Name="EmbedLocal" BeforeTargets="PrepareForBuild"> in the Directory.Build.props to resolve this issue. You can check if it works for you.
<Target Name="EmbedLocal" BeforeTargets="PrepareForBuild">
...
<ItemGroup>
<EmbeddedResource Include="..."/>
</ItemGroup>
...
</Target>
I have an application which I want to publish with ClickOnce via command line. I have a test and a live version. It should be allowed to have both installed at the same time, which means that I need to change the assemble name (and preferably also the product name) for one of the builds. I would like to do this in the build settings.
I have managed to make some build settings, which works fine, but I cannot figure out how to change the assembly and product name, for just one of them.
I have added the following code to my .csproj file, which I call with the command msbuild /target:Test or msbuild /target:Live. But where do I implement the assembly and product name change?
<PropertyGroup>
<ProjLocation>$(ProjectDir)</ProjLocation>
<ProjLocationReleaseDir>$(ProjLocation)\bin\Debug</ProjLocationReleaseDir>
<ProjPublishLocation>$(ProjLocationReleaseDir)\app.publish</ProjPublishLocation>
<DeploymentFolder>C:\MyProjects\Software\Publish\</DeploymentFolder>
</PropertyGroup>
<!-- Build settings for live version -->
<Target Name="Live" DependsOnTargets="Clean">
<MSBuild Projects="$(ProjLocation)\$(ProjectName).csproj"
Properties="$(DefaultBuildProperties)"
Targets="Publish"/>
<ItemGroup>
<SetupFiles Include="$(ProjPublishLocation)\*.*"/>
<UpdateFiles Include="$(ProjPublishLocation)\Application Files\**\*.*"/>
</ItemGroup>
<Copy SourceFiles="#(SetupFiles)" DestinationFolder="$(DeploymentFolder)\Live\" />
<Copy SourceFiles="#(UpdateFiles)" DestinationFolder="$(DeploymentFolder)\Live\Application Files\%(RecursiveDir)"/>
</Target>
<!-- Build settings for test version -->
<Target Name="Test" DependsOnTargets="Clean">
<MSBuild Projects="$(ProjLocation)\$(ProjectName).csproj"
Properties="$(DefaultBuildProperties)"
Targets="Publish"/>
<ItemGroup>
<SetupFiles Include="$(ProjPublishLocation)\*.*"/>
<UpdateFiles Include="$(ProjPublishLocation)\Application Files\**\*.*"/>
</ItemGroup>
<Copy SourceFiles="#(SetupFiles)" DestinationFolder="$(DeploymentFolder)\Public Test\" />
<Copy SourceFiles="#(UpdateFiles)" DestinationFolder="$(DeploymentFolder)\Public Test\Application Files\%(RecursiveDir)"/>
</Target>
You can add "AssemblyName" property to the PropertyGroup. like this:
<PropertyGroup>
<AssemblyName>YourAppName</AssemblyName>
</PropertyGroup>
or you can use the MSBuild command line switch. like this :
msbuild /property:AssemblyName=YourAppName
I had almost the same task (need to distinguish between Staging and Production) and I solved it with the following MSBuild-Target:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- call "nuget restore Deployment\packages.config" before executing this script -->
<Import Project="..\packages\MSBuildTasks.1.5.0.235\build\MSBuildTasks.targets" />
<PropertyGroup>
<Configuration>Release</Configuration>
<ClientProject>..\MyProject\MyProject.vbproj</ClientProject>
<ClientPublishDir Condition="$(Environment) == 'Staging'">\\deployment-staging\MyProject\</ClientPublishDir>
<ClientPublishDir Condition="$(Environment) == 'Production'">\\deployment\MyProject\</ClientPublishDir>
</PropertyGroup>
<ItemGroup>
<ClientModifiedAppConfig Include="$(ClientProject)\..\App.$(Environment).config" />
</ItemGroup>
<Target Name="DeployClient">
<Error Condition="$(Environment) == ''" Text="The Property 'Environment' has not been set." />
<Error Condition="$(Environment) != 'Staging' AND $(Environment) != 'Production'" Text="The Property 'Environment' has not been set properly. Valid values are 'Staging' and 'Production'." />
<!-- Sets different assembly names for INT and PRD applications. Due to this, both INT and PRD applications with the
same version number can be installed on the same system. -->
<XmlUpdate Condition="$(Environment) == 'Staging'" Prefix="n" Namespace="http://schemas.microsoft.com/developer/msbuild/2003" XmlFileName="$(ClientProject)" Xpath="/n:Project/n:PropertyGroup/n:AssemblyName" Value="MyProjectStaging" />
<XmlUpdate Condition="$(Environment) == 'Staging'" Prefix="n" Namespace="http://schemas.microsoft.com/developer/msbuild/2003" XmlFileName="$(ClientProject)" Xpath="/n:Project/n:PropertyGroup/n:ProductName" Value="MyProject Staging" />
<XmlUpdate Condition="$(Environment) == 'Production'" Prefix="n" Namespace="http://schemas.microsoft.com/developer/msbuild/2003" XmlFileName="$(ClientProject)" Xpath="/n:Project/n:PropertyGroup/n:AssemblyName" Value="MyProject" />
<XmlUpdate Condition="$(Environment) == 'Production'" Prefix="n" Namespace="http://schemas.microsoft.com/developer/msbuild/2003" XmlFileName="$(ClientProject)" Xpath="/n:Project/n:PropertyGroup/n:ProductName" Value="MyProject" />
<!-- Overwrites the original App.config with the environment-dependent App.config.
Reason: ClickOnce only uses App.config and does not apply transformations, as it is done in Web Projects. -->
<Copy
SourceFiles="#(ClientModifiedAppConfig)"
DestinationFiles="$(ClientProject)\..\App.config"
OverwriteReadOnlyFiles="true"
/>
<!-- Publish -->
<MSBuild
Projects="$(ClientProject)"
Targets="Publish"
Properties="
PublishDir=$(ClientPublishDir);
Configuration=$(Configuration);
Platform=x86" />
</Target>
</Project>
To make this work, you have to restore the NuGet package MSBuildTasks.
Finally, all I have to call is msbuild Deployment.targets /t:DeployClient /p:Environment=Staging
Hope that helps!
I'm trying to make an item group of dll's and exe's but filtering out any msi's and test.dll's.
The following snippet doesn't include the .exe in the UniqueAssemblies itemgroup. It does contain all the dll's and removes the msi as expected though. UniqueCompiledFiles does contain all the expected output files (.test.dll, .dll, .msi, .exe)
<Target Name="CustomCompile">
<MSBuild
BuildInParallel="true"
Projects="#(ProjectFiles)"
Properties="$(ProjectProperties)"
>
<Output TaskParameter="TargetOutputs" ItemName="CompiledFiles" />
</MSBuild>
<RemoveDuplicates Inputs="#(CompiledFiles)">
<Output TaskParameter="Filtered" ItemName="UniqueCompiledFiles" />
</RemoveDuplicates>
<ItemGroup>
<UniqueAssemblies
Include="%(UniqueCompiledFiles.Identity)"
Condition=" '#(UniqueCompiledFiles->EndsWith('.dll'))' == 'true' " />
<UniqueAssemblies
Include="%(UniqueCompiledFiles.Identity)"
Condition=" '#(UniqueCompiledFiles->EndsWith('.exe'))' == 'true' " />
</ItemGroup>
I also figured out this workaround that does properly filter the .exe.
<ItemGroup>
<UniqueAssemblies2
Include="%(UniqueCompiledFiles.Identity)"
Condition=" $([System.String]::new('%(UniqueCompiledFiles.Identity)').EndsWith('.exe')) " />
</ItemGroup>
Found out the culprit lines that could be removed to fix the problem but it doesn't actually answer the question.
<ItemGroup>
<!-- Workaround for MSBuild defect: https://github.com/Microsoft/msbuild/issues/69 -->
<UniqueCompiledFiles Include="Project\bin\release\Project.exe">
<MSBuildSourceProjectFile>Project\Project.csproj</MSBuildSourceProjectFile>
<Platform>x86</Platform>
</UniqueCompiledFiles>
</ItemGroup>
Why is the #(UniqueCompiledFiles->EndsWith('.exe')) syntax not working as expected?
You're trying to use a Property Function on an item (instead of a property).
You should be able to get the desired result by using something like this:
<ItemGroup>
<UniqueAssemblies2
Include="%(UniqueCompiledFiles.Identity)"
Condition=" '%(Extension)' == '.exe' " />
</ItemGroup>