Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

MSBuild Targets and Props in NuGet 3

NuGet 2.x supports the inclusion of .targets and .props files in NuGet packages. These files will be added as Imports in the csproj MSBuild file when the package is installed. For example, consider a package with the following files:

/
    build/
        net40/
            MyPackage.props
            MyPackage.targets

If this package is installed into an MSBuild project, the following lines are added to the MSBuild projects:

<Project ...>
    <Import Project="..\packages\MyPackage\1.0.0\build\net40\MyPackage.props" Condition="Exists(...)" />
    <!-- ... -->
    <Import Project="..\packages\MyPackage\1.0.0\build\net40\MyPackage.targets" Condition="Exists(...)" />
</Project>

This is not going to work well in the new transitive restore world. Instead of this, we will transition to a new model where the csproj will only import a single props and a single targets file if ANY packages contain targets/props file. These files will contain imports of the specific targets/props files from all of the packages found during the restore. Each restore may potentially recreate this file, as new versions are downloaded and new packages arrive.

For example, consider now this project.json referencing MyPackage:

MyProject\project.json

{
    "dependencies": {
        "MyPackage": "1.0.0",
        "SomethingElse": "1.0.0"
    }
}

Where SomethingElse depends on SomethingBuildy which has a .targets file but no .props file.

After restore, the following files would be generated:

MyProject\project.props (Name TBD)

<Project ...>
    <PropertyGroup>
        <!-- some code to find the Package Root based on USERPROFILE, NUGET_PACKAGES, etc. and put it in PackagesRoot -->
    </PropertyGroup>
    <Import Project="$(PackagesRoot)\MyPackage\1.0.0\build\net40\MyPackage.props" Condition="Exists(...)" />
</Project>

MyProject\project.targets (Name TBD)

<Project ...>
    <PropertyGroup>
        <!-- some code to find the Package Root based on USERPROFILE, NUGET_PACKAGES, etc. and put it in PackagesRoot -->
    </PropertyGroup>
    <Import Project="$(PackagesRoot)\MyPackage\1.0.0\build\net40\MyPackage.targets" Condition="Exists(...)" />
    <Import Project="$(PackagesRoot)\SomethingBuildy\1.0.0\build\net40\SomethingBuildy.targets" Condition="Exists(...)" />
</Project>

After this, something must update the csproj to reference these new targets/props files. The exact mechanism for this is TBD, but here are some ideas:

Templates

The UWP Project Templates should be augmented to include come with the following lines

<Project ...>
    <Import Project="$(MSBuildProjectDirectory)\$(MSBuildProjectName).nuget.props" Condition="Exists("$(MSBuildProjectDirectory)\$(MSBuildProjectName).nuget.props")" />
    <!-- ... -->
    <Import Project="$(MSBuildProjectDirectory)\$(MSBuildProjectName).nuget.targets" Condition="Exists("$(MSBuildProjectDirectory)\$(MSBuildProjectName).nuget.targets")" />
</Project>

This would mean that only UWP applications would get this functionality, but at RTM, they are the only MSBuild project type we intend to have support project.json restore fully. Other projects types could easily add support for this by adding the necessary imports manually, which we can (and should) document. Post-RTM we could provide a gesture for "upgrading" a project to use this model, as part of an upgrade process from packages.config to project.json. This proposal minimizes the amount of risky work we have to do for RTM.

Framework Re/Multi-Targeting

This model actually implements re-targeting in a much better way than we currently support. Now, once the target is changed in project.json, the developer need only re-restore and the new targets will be referenced.

Multi-targeting is not a supported scenario for MSBuild at this time. If a project.json defines more than one target framework, and MSBuild targets are found in a package, a Warning is emitted to the [projectname].nuget.targets file which notifies the user that this is not a supported scenario. In future releases, we can look at making groups of imports conditional on Target Framework to allow us to restore the full set correctly.

Errors

In this model, we are no longer able to produce an error if a props/targets file was expected to be present (because of a NuGet reference) but is not currently present (because 'restore' has not been run). However, many other issues will likely occur in this case, and MSBuild will likely complain because the lock file will be missing, so this does not seem like a major concern.

Paths in the Import file

The project.targets/props file contains a list of Import elements which import the individual targets/props files. The paths to these targets/props are generated by the restore command. At the top of the file, a PropertyGroup is added which defines the Packages Root path by reading from the generally-supported sources. Alternatively, or additionally, the nuget restore command could just place the exact absolute path in the file, since it knows where the packages were downloaded. However, this would make the file non-portable. This may be OK though, since we don't expect users to check this file in (it is a "build artifact" generated to prepare the project for build only).

A sample import file is as follows:

<Project ...>
    <!-- Allow the user to override this value
    <PropertyGroup Condition="'$(NuGetPackagesRoot)' == ''">
        <NuGetPackagesRoot Condition="'$(NUGET_PACKAGES)' != ''">$(NUGET_PACKAGES)</NuGetPackagesRoot>
        <!-- Should there be an MSBuild task to read from global.json?? -->
        <NuGetPackagesRoot Condition="'$(NuGetPackagesRoot)' == '' And '$(USERPROFILE)' != ''">$(USERPROFILE)\.nuget\packages</NuGetPackagesRoot>
        <NuGetPackagesRoot Condition="'$(NuGetPackagesRoot)' == '' And '$(HOME)' != ''">$(HOME)\.nuget\packages</NuGetPackagesRoot>
        <!-- Since restore knows the absolute path that was used for dropping packages, it could also hard-code the actual path. This would make the file non-portable, but the intent is that users will not check this in anyway... -->
    </PropertyGroup>

    <Import Project="$(NuGetPackagesRoot)\PackageId\1.0.0\build\net40\PackageId.targets" Condition="Exists(...)" />
    <!-- etc. -->
</Project>
@AArnott

This comment has been minimized.

Copy link

AArnott commented May 20, 2015

Your *.nuget.props and *.nuget.targets files should include this boilerplate xml:

<PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
@AArnott

This comment has been minimized.

Copy link

AArnott commented May 20, 2015

I'm not sure it's fair to say that MSBuild doesn't support multi-targeting. It's not particularly an MSBuild issue. It seems you could add the necessary Condition attributes to ImportGroups right away, and if the user builds their project with /p:TargetFrameworkVersion=4.5 then it will just import the right stuff.

@AArnott

This comment has been minimized.

Copy link

AArnott commented May 20, 2015

Some packages that add .props and .targets will not produce errors if they're not present -- the output will just be discretely wrong. Preserving errors when nuget package restore hasn't occurred is therefore important. Can we have an MSBuild target that NuGet already has on the machine check at build time whether the *.nuget.props and *.nuget.targets files are present and current compared to the project.json file and error out if not?

@AArnott

This comment has been minimized.

Copy link

AArnott commented May 20, 2015

Consider putting all these generated files in the .vs directory under the solution directory (sibling to where Packages used to be). That way they'll already be ignored by git and perhaps other SCC systems. We're trying to get away from producing 'turd files' in source directories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.