Nexus-Mods / NexusMods.App

Home of the development of the Nexus Mods App
https://nexus-mods.github.io/NexusMods.App/
GNU General Public License v3.0
814 stars 43 forks source link

Consider using nested `Directory.Build.props` and `Directory.Build.targets` files #73

Closed erri120 closed 1 year ago

erri120 commented 1 year ago

Overview

MSBuild docs: Customize your build

The Directory.Build.props and Directory.Build.targets file are great for consolidating common properties, when separating projects into different folders (example: src and test).

Example

Using GameFinder as an example:

/
├─ Directory.Build.props
├─ Directory.Build.targets
├─ src/
│  ├─ Directory.Build.props
│  ├─ GameFinder.StoreHandlers.Steam/
│  │  ├─ GameFinder.StoreHandlers.Steam.csproj
├─ tests/
│  ├─ Directory.Build.props
│  ├─ Directory.Build.targets
│  ├─ GameFinder.StoreHandlers.Steam.Tests/
│  │  ├─ GameFinder.StoreHandlers.Steam.Tests.csproj

Top-Level

The top-level files can be used to configure properties that apply for every project:

Directory.Build.props:

<Project>
    <PropertyGroup>
        <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Meziantou.Analyzer" Version="2.0.13">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <PropertyGroup>
        <!-- https://github.com/meziantou/Meziantou.Analyzer/tree/main/docs/Rules -->
        <!-- MA0048: File name must match type name -->
        <NoWarn>MA0048</NoWarn>
    </PropertyGroup>
</Project>

Directory.Build.targets:

<Project>
    <PropertyGroup>
        <Nullable>enable</Nullable>
        <LangVersion>11</LangVersion>
        <ImplicitUsings>false</ImplicitUsings>
    </PropertyGroup>
</Project>

Every project will target both net6.0 and net7.0 have other properties already set for them. Every project also automatically references Meziantou.Analyzer and the dependency can me managed from a single file, making updating much easier.

src folder

src/Directory.Build.props:

<Project>
    <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

    <PropertyGroup>
        <Authors>erri120</Authors>

        <PackageReadmeFile>docs\README.md</PackageReadmeFile>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>

        <PackageProjectUrl>https://github.com/erri120/GameFinder</PackageProjectUrl>
        <RepositoryUrl>https://github.com/erri120/GameFinder.git</RepositoryUrl>
        <RepositoryType>git</RepositoryType>

        <IncludeSymbols>true</IncludeSymbols>
        <SymbolPackageFormat>snupkg</SymbolPackageFormat>
    </PropertyGroup>

    <ItemGroup>
        <None Include="../../README.md" Pack="true" PackagePath="docs"/>
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="all" />
    </ItemGroup>
</Project>

Since GameFinder is a library, all projects inside the src folder are going to be published and require package properties. These can be put into one Directory.Build.props file. Important is <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> which references the Directory.Build.props file from the root directory (see docs).

tests folder

tests/Directory.Build.props:

<Project>
    <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

    <PropertyGroup>
        <IsPackable>false</IsPackable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="AutoFixture" Version="4.17.0" />
        <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
        <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
        <PackageReference Include="FluentAssertions" Version="6.9.0" />
        <PackageReference Include="FluentAssertions.Analyzers" Version="0.17.2">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.1.5" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
        <PackageReference Include="Moq" Version="4.18.4" />
        <PackageReference Include="xunit" Version="2.4.2" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="coverlet.collector" Version="3.2.0">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
    </ItemGroup>
</Project>

Every test project has to reference common testing packages like xUnit.net, AutoFixture and FluentAssertions. Having all of those dependencies in the same file makes it very easy to upgrade.

Individual projects

Arriving at the individual .csproj files, these will contain unique properties for their project:

src/GameFinder.StoreHandlers.Steam/GameFinder.StoreHandlers.Steam.csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <Description>Library for finding games installed with Steam.</Description>
        <PackageTags>valve steam games</PackageTags>
    </PropertyGroup>

    <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
        <DocumentationFile>bin\Debug\GameFinder.StoreHandlers.Steam.xml</DocumentationFile>
    </PropertyGroup>

    <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
        <DocumentationFile>bin\Release\GameFinder.StoreHandlers.Steam.xml</DocumentationFile>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="ValveKeyValue" Version="0.8.2.162" />
        <PackageReference Include="System.IO.Abstractions" Version="19.1.5" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Roslyn.System.IO.Abstractions.Analyzers" Version="12.2.19">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="..\GameFinder.Common\GameFinder.Common.csproj" />
        <ProjectReference Include="..\GameFinder.RegistryUtils\GameFinder.RegistryUtils.csproj" />
    </ItemGroup>

    <ItemGroup>
        <InternalsVisibleTo Include="GameFinder.StoreHandlers.Steam.Tests" />
    </ItemGroup>
</Project>

tests/GameFinder.StoreHandlers.Steam.Tests/GameFinder.StoreHandlers.Steam.Tests.csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <ItemGroup>
        <ProjectReference Include="..\..\src\GameFinder.StoreHandlers.Steam\GameFinder.StoreHandlers.Steam.csproj" />
        <ProjectReference Include="..\TestUtils\TestUtils.csproj" />
    </ItemGroup>

    <ItemGroup>
        <Compile Include="..\Usings.cs">
            <Link>Usings.cs</Link>
        </Compile>
    </ItemGroup>
</Project>

(Having a common Usings.cs for tests is also possible with this approach.)

Conclusion

Putting common project properties and dependencies into Directory.Build.props and Directory.Build.targets files can drastically remove clutter from project files and make it easier to manage dependencies. The individual project files will then only contain the unique properties, required for the project, and nothing else.

Sewer56 commented 1 year ago

Thanks! I was planning on tackling this today actually. My plan is to first do the more maintenancy things for this sprint; so I can get better acquainted with the code little by little.

This means down the road with the more complicated things I'll have to study less of the codebase and as such I'll be more efficient in the long run.