Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ProjectReference: How to make one project to consume/import assets (content, props, targets) from another project? #5797

Open
Denis535 opened this issue Oct 12, 2020 · 10 comments
Labels
Milestone

Comments

@Denis535
Copy link

Denis535 commented Oct 12, 2020

I need to make project with some msbuild properties, tasks and targets which will be consumed by another project.

When my consumer-project references nuget package via PackageReference then consumer-projects can consume any assets: assemblies, content, props, targets and probably a lot more from package.

This is accomplished with MSBuildProjectExtensionsPath (project implicitly imports obj/MyProject.*.props and obj/MyProject.*.targets).
And NuGet creates MyProject.csproj.nuget.g.props and MyProject.csproj.nuget.g.targets which just adding content items and importing props and targets.

But when my consumer-project references another project then consumer-project can consume only assembly.
Is it possible to consume other assets?

I've found next ProjectReference's metadata, but there is a little information in documentation. So, I don't know if this can help me.

<IncludeAssets>All</IncludeAssets>
<ExcludeAssets>None</ExcludeAssets>
<PrivateAssets>None</PrivateAssets>
<Private>True</Private>
<OutputItemType>Content</OutputItemType>
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TreatAsPackageReference>True</TreatAsPackageReference>

Of course I can just manually write imports to my props and targets. But there must be a better way. Or not?

@Denis535 Denis535 added the needs-triage Have yet to determine what bucket this goes in. label Oct 12, 2020
@Denis535 Denis535 changed the title ProjectReference: How to make one project to consume/import content, props, targets from another project? ProjectReference: How to make one project to consume/import assets (content, props, targets) from another project? Oct 13, 2020
@benvillalobos
Copy link
Member

Team Triage: You can't do this with .props and .targets. You can transfer other assets like content to project references.

The problem here is that each project gets evaluated at the same time, and the outputs of your first project will not have generated such that your second project (at evaluation time) sees them. We also don't know where the outputs of a project are until build time. But in order to import and affect the second project we would need an import statement pointing to those outputs.

@benvillalobos benvillalobos removed the needs-triage Have yet to determine what bucket this goes in. label Oct 14, 2020
@benvillalobos benvillalobos added this to the Discussion milestone Oct 14, 2020
@Denis535
Copy link
Author

You can transfer other assets like content to project references.

Can you give more details?

Also what is happening when I write <OutputItemType>Content</OutputItemType> ? I don't notice any changes.

@yfital
Copy link

yfital commented May 17, 2021

Hey,

I've also stumbled across this, I've been wanted to change our project / nuget to start using BuildTransitive, but it seems that those will not take into account in project references.

In most places, you do both, its not one or the other, is there some sort of solution/workaround?

Example:
Project A reference Project B via project reference
Project A is later on outputted as a nuget

Project C reference project A

I expected both A and C projects to receive BuildTransitive .prop files

@benvillalobos
Copy link
Member

Can you give more details?

See #1054 (comment), there's a property that allows you to copy Content items transitively.

It's not possible for MSBuild to get .props and .targets from project references though. The issue is that MSBuild needs to first import these props and targets before it can start parsing your project. And so everything is imported by the time that a project reference is being looked at. Closing the issue, feel free to ask more questions though!

@yfital See the link above for copying over items from transitive project references. Copying over a nupkg that's a Content item is possible. Importing dynamic props and targets that affect the build, however, is not.

@ViktorHofer
Copy link
Member

Related: NuGet/Home#6624

It's not possible for MSBuild to get .props and .targets from project references though

It's possible when you declare the imports to these props and targets in the first restore evaluation pass. NuGet does this to import props and targets files from nuget packages. The evaluation kicked-off by NuGet restore doesn't include those as NuGet then creates import files to import for subsequent evaluations: nuget.g.props and nuget.g.targets.

As we already support this for PackageReferences, I don't see a strong reason why we couldn't do the same for ProjectReferences. That said, this might make more sense to fix on the NuGet side which is the component that triggers those separate restore evaluations.

@rainersigwald
Copy link
Member

@ViktorHofer I don't understand. How can restore import files that don't exist? If we wait for them to exist then it's not exactly restore any more.

@ViktorHofer
Copy link
Member

How can restore import files that don't exist? If we wait for them to exist then it's not exactly restore any more.

Based on the issue description I would expect those asset files (i.e. props and targets under a build / buildTransitive) folder to already exist. Do you mean the generated import files? NuGet creates those as part of the restore run, i.e. nuget.g.props and nuget.g.targets which are then getting imported by subsequent evaluations.

@rainersigwald
Copy link
Member

I'm thinking of the copies of those files in the output folder, which may not match the ones that are available at restore time, depending on a variety of details.

@oformaniuk
Copy link

I have the same problem of referencing project as if it was a NuGet package (aka reference its props and targets automatically).

I come up with the following workaround to generate .proj.g.targets and .proj.g.props that can be recognized by the MSBuild.

<!-- Directory.Solution.targets -->

<Project>
    <!--
    This target is used to redirect to project-spesific restore targets on solution-level restore. 
    -->
    <Target Name="ImportProjectReferenceBuildFiles" AfterTargets="Restore">
        <MSBuild Projects="@(ProjectReference)" Targets="Restore" />
    </Target>

</Project>
<!-- Directory.Build.targets -->

<Project>
    <!--
    This target is designed to gather information about referenced projects and generate import statements for .props and .targets files. 
    It uses `MSBuildProjectExtensionsPath` auto import feature like NuGet does to import the generated .props file.
    -->
    <Target
            Name="ImportProjectReferenceBuildFiles"
            BeforeTargets="Restore;BeforeBuild"
            Inputs="@(ProjectReference);$(MSBuildThisFileFullPath)"
            Outputs="$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.props;$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.targets"
    >
        <!-- Collecting .props and .targets files for the ProjectReferences -->
        <ItemGroup>
            <!-- Check for .props files in the build directory of the ProjectReferences -->
            <_Props Include="@(ProjectReference->'%(RootDir)%(Directory)build\%(Filename).props')" Condition="Exists('%(RootDir)%(Directory)build\%(Filename).props')" />

            <!-- Check for .targets files in the build directory of the ProjectReferences -->
            <_Targets Include="@(ProjectReference->'%(RootDir)%(Directory)build\%(Filename).targets')" Condition="Exists('%(RootDir)%(Directory)build\%(Filename).targets')" />
        </ItemGroup>

        <!-- Creating groups to hold the XML content for import statements -->
        <ItemGroup Condition="@(_Props) != '' or @(_Targets) != ''">
            <_Lines Include="&lt;Project&gt;" />
        </ItemGroup>

        <ItemGroup>
            <!-- Include for .props files -->
            <_PropsLines Include="&lt;Project&gt;" />
            <_PropsLines Include="&lt;Import Project='" Condition="@(_Props) != ''" />
            <_PropsLines Include="@(_Props->'%(Identity);')" Condition="@(_Props) != ''" />
            <_PropsLines Include="' /&gt;" Condition="@(_Props) != ''" />
            <_PropsLines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <ItemGroup>
            <!-- Include for .targets files -->
            <_TargetsLines Include="&lt;Project&gt;" />
            <_TargetsLines Include="&lt;Import Project='" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="@(_Targets->'%(Identity);')" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="' /&gt;" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <ItemGroup Condition="@(_Props) != '' or @(_Targets) != ''">
            <_Lines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <!-- Write the collected props and targets imports to a file -->
        <WriteLinesToFile File="$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.props" Lines="@(_PropsLines)" Overwrite="true" />
        <WriteLinesToFile File="$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.targets" Lines="@(_TargetsLines)" Overwrite="true" />
        
    </Target>
    
    <!--
    Because most IDEs do not reload the project file up until after it has been modified, we need to force a reload of the project file by making a dummy change to it.
    -->
    <Target Name="ForceProjectReload" AfterTargets="ImportProjectReferenceBuildFiles">
        <Touch Files="$(MSBuildProjectFullPath)">
            <Output TaskParameter="TouchedFiles" ItemName="FilesTouched"/>
        </Touch>
    </Target>

</Project>

@oformaniuk
Copy link

oformaniuk commented Nov 14, 2023

Here's somewhat improved version to perform props and targets discovery recursively:

<Project>

    <!--
    This target is designed to gather information about referenced projects and generate import statements for .props and .targets files. 
    It uses `MSBuildProjectExtensionsPath` auto import feature like NuGet does to import the generated .props file.
    -->
    <Target
            Name="ImportProjectReferenceBuildFiles"
            BeforeTargets="Restore;BeforeBuild"
            Inputs="@(ProjectReference);$(MSBuildThisFileFullPath)"
            Outputs="$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.props;$(MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.targets"
    >
        <CallTarget
                Targets="CollectImportProjectReferenceProps"
                Condition="'@(ProjectReference)' != ''"
        >
            <Output TaskParameter="TargetOutputs" ItemName="_ProjectProps"/>
        </CallTarget>

        <CallTarget
                Targets="CollectImportProjectReferenceTargets"
                Condition="'@(ProjectReference)' != ''"
        >
            <Output TaskParameter="TargetOutputs" ItemName="_ProjectTargets"/>
        </CallTarget>
        
        <!-- Collecting .props and .targets files for the ProjectReferences tree -->
        <MSBuild
                Projects="@(ProjectReference)"
                Targets="CollectImportProjectReferenceProps"
                Properties="_MSBuildProjectExtensionsPath=$(MSBuildProjectExtensionsPath);ImportOutput='false'"
                Condition="'@(ProjectReference)' != ''"
        >
            <Output TaskParameter="TargetOutputs" ItemName="_ReferenceProps"/>
        </MSBuild>

        <MSBuild
                Projects="@(ProjectReference)"
                Targets="CollectImportProjectReferenceTargets"
                Properties="_MSBuildProjectExtensionsPath=$(MSBuildProjectExtensionsPath)"
                Condition="'@(ProjectReference)' != ''"
        >
            <Output TaskParameter="TargetOutputs" ItemName="_ReferenceTargets"/>
        </MSBuild>
        
        <ItemGroup>
            <!-- Deduplicating items -->
            <__Props Include="@(_ProjectProps);@(_ReferenceProps)" />
            <__Targets Include="@(_ProjectTargets);@(_ReferenceTargets)" />
            <_Props Include="@(__Props->Distinct())" />
            <_Targets Include="@(__Targets->Distinct())" />
        </ItemGroup>
        
        <!-- Creating groups to hold the XML content for import statements -->
        <ItemGroup Condition="@(_Props) != '' or @(_Targets) != ''">
            <_Lines Include="&lt;Project&gt;" />
        </ItemGroup>

        <ItemGroup>
            <!-- Include for .props files -->
            <_PropsLines Include="&lt;Project&gt;" />
            <_PropsLines Include="&lt;Import Project='" Condition="@(_Props) != ''" />
            <_PropsLines Include="@(_Props->'%(Identity);')" Condition="@(_Props) != ''" />
            <_PropsLines Include="' /&gt;" Condition="@(_Props) != ''" />
            <_PropsLines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <ItemGroup>
            <!-- Include for .targets files -->
            <_TargetsLines Include="&lt;Project&gt;" />
            <_TargetsLines Include="&lt;Import Project='" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="@(_Targets->'%(Identity);')" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="' /&gt;" Condition="@(_Targets) != ''" />
            <_TargetsLines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <ItemGroup Condition="@(_Props) != '' or @(_Targets) != ''">
            <_Lines Include="&lt;/Project&gt;" />
        </ItemGroup>

        <PropertyGroup>
            <_MSBuildProjectExtensionsPath Condition="'$(_MSBuildProjectExtensionsPath)' == ''">$(MSBuildProjectExtensionsPath)</_MSBuildProjectExtensionsPath>
        </PropertyGroup>
        
        <!-- Write the collected props and targets imports to a file -->
        <WriteLinesToFile File="$(_MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.props" Lines="@(_PropsLines)" Overwrite="true" />
        <WriteLinesToFile File="$(_MSBuildProjectExtensionsPath)\$(MSBuildProjectName).csproj.proj.g.targets" Lines="@(_TargetsLines)" Overwrite="true" />
        
    </Target>

    <Target Name="CollectImportProjectReferenceProps" Outputs="@(_ProjectProps)">
        
        <!-- Collecting .props and .targets files for the ProjectReferences -->
        <ItemGroup>
            <!-- Check for .props files in the build directory of the ProjectReferences -->
            <_ProjectProps Include="@(ProjectReference->'%(RootDir)%(Directory)build\%(Filename).props')" Condition="Exists('%(RootDir)%(Directory)build\%(Filename).props')" />
        </ItemGroup>
        
    </Target>

    <Target Name="CollectImportProjectReferenceTargets" Outputs="@(_ProjectTargets)">

        <!-- Collecting .props and .targets files for the ProjectReferences -->
        <ItemGroup>
            <!-- Check for .targets files in the build directory of the ProjectReferences -->
            <_ProjectTargets Include="@(ProjectReference->'%(RootDir)%(Directory)build\%(Filename).targets')" Condition="Exists('%(RootDir)%(Directory)build\%(Filename).targets')" />
        </ItemGroup>

    </Target>
    
    <!--
    Because most IDEs do not reload the project file up until after it has been modified, we need to force a reload of the project file by making a dummy change to it.
    -->
    <Target Name="ForceProjectReload" AfterTargets="ImportProjectReferenceBuildFiles">
        <Touch Files="$(MSBuildProjectFullPath)">
            <Output TaskParameter="TouchedFiles" ItemName="FilesTouched"/>
        </Touch>
    </Target>

</Project>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants