Skip to content

Commit

Permalink
Support Include/Exclude attributes from Dependency
Browse files Browse the repository at this point in the history
We currently surface the PackageReference's IncludeAssets/ExcludeAssets as the ultimate dependency Include/Exclude metadata attributes. This is not always what you want, though.

For example: if you need the compile dependency for compiling the project, but only want to surface the dependency as a build dependency, you cannot exclude the compile asset because your project depends on the lib to build.

In order to make this more intuitive and aligned with the other attributes, we introduce PackInclude/PackExclude attributes for that purpose, which map exactly to the .nuspec: https://learn.microsoft.com/en-us/nuget/reference/nuspec#dependencies-element.

Note that the existing behavior will be overriden if those attributes are provided, which provides backwards compatibility.
  • Loading branch information
kzu committed Feb 11, 2023
1 parent d6d9d89 commit 5fa3998
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 13 deletions.
16 changes: 11 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ The basic item metadata that drive pack inference are:

If the item does **not** provide a *PackagePath*, and *Pack* is not *false*, the inference targets wil try to determine the right value, based on the following additional metadata:

a. **PackFolder**: typically one of the [built-in package folders](https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/PackagingConstants.cs#L19), such as *build*, *lib*, etc.
b. **FrameworkSpecific**: *true*/*false*, determines whether the project's target framework is used when building the final *PackagePath*.
c. **TargetPath**: optional PackFolder-relative path for the item. If not provided, the relative path of the item in the project (or its *Link* metadata) is used.
* **PackFolder**: typically one of the [built-in package folders](https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/PackagingConstants.cs#L19), such as *build*, *lib*, etc.
* **FrameworkSpecific**: *true*/*false*, determines whether the project's target framework is used when building the final *PackagePath*.
* **TargetPath**: optional PackFolder-relative path for the item. If not provided, the relative path of the item in the project (or its *Link* metadata) is used.


When an item specifies *FrameworkSpecific=true*, the project's target framework is added to the final package path, such as `lib\netstandard2.0\My.dll`. Since the package folder itself typically determines whether it contains framework-specific files or not, the *FrameworkSpecific* value has sensible defaults so you don't have to specify it unless you want to override it. The [default values from NuGetizer.props](src/NuGetizer.Tasks/NuGetizer.props) are:
Expand Down Expand Up @@ -284,14 +284,20 @@ In addition, the resulting `PackageFile` items for these items point to the loca

### PackageReference

Package references are turned into package dependencies by default (essentially converting `<PackageReference>` to `<PackageFile ... PackFolder="Dependency">`), unless `PackDependencies` property is `false`. If the package reference specifies `PrivateAssets="all"`, however, it's not added as a dependency. Instead, in that case, all the files contributed to the compilation are placed in the same `PackFolder` as the project's build output (if packable, depending on `PackBuildOutput` property).
Package references are turned into package dependencies by default (essentially converting `<PackageReference>` to `<PackageFile ... PackFolder="Dependency">`), unless `PackDependencies` property is `false`. If the package reference specifies `PrivateAssets="all"`, however, it's not added as a dependency. Instead, in that case, all the files contributed to the compilation (more precisely: all copy-local runtime dependencies) are placed in the same `PackFolder` as the project's build output (if packable, depending on `PackBuildOutput` property).

Build-only dependencies that don't contribute assemblies to the output (i.e. analyzers or things like [GitInfo](https://github.com/kzu/GitInfo) or [ThisAssembly](https://github.com/kzu/ThisAssembly) won't cause any extra items.
Build-only dependencies that don't contribute assemblies to the output (i.e. analyzers or things like [GitInfo](https://github.com/devlooped/GitInfo) or [ThisAssembly](https://github.com/devlooped/ThisAssembly) won't cause any extra items.

This even works transitively, so if you use *PrivateAssets=all* on package reference *A*, which in turn has a package dependency on *B* and *B* in turn depends on *C*, all of *A*, *B* and *C* assets will be packed. You can opt out of the transitive packing with `PackTransitive=false` metadata on the `PackageReference`.

As usual, you can change this default behavior by using `Pack=false` metadata.

You can also control precisely what assets are surfaced from your dependencies, by
using `PackInclude` and `PackExclude` metadata on the `PackageReference`. This will
result in the corresponding `include`/`exclude` attributes as documented in the
[nuspec reference](https://learn.microsoft.com/en-us/nuget/reference/nuspec#dependencies-element). If not defined, both are defaulted to the package
reference `IncludeAssets` and `ExcludeAssets` metadata.

### ProjectReference

Unlike SDK Pack that [considers project references as package references by default](https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets#project-to-project-references), NuGetizer has an explicit contract between projects: the `GetPackageContents` target. This target is invoked when packing project references, and it returns whatever the referenced project exposes as package contents (including the inference rules above). If the project is *packable* (that is, it produces a package, denoted by the presence of a `PackageId` property or `IsPackable=true`, for compatibility with SDK Pack), it will be packed as a dependency/package reference instead.
Expand Down
7 changes: 4 additions & 3 deletions src/NuGetizer.Tasks/CreatePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public class CreatePackage : Task

public override bool Execute()
{
if (Environment.GetEnvironmentVariable("DEBUG_NUGETIZER") == "1")
if (Environment.GetEnvironmentVariable("DEBUG_NUGETIZER") == "1" ||
Environment.GetEnvironmentVariable("DEBUG_NUGETIZER_PACK") == "1")
Debugger.Launch();

try
Expand Down Expand Up @@ -244,8 +245,8 @@ where PackFolderKind.Dependency.Equals(item.GetMetadata(MetadataName.PackFolder)
Id = item.ItemSpec,
Version = VersionRange.Parse(item.GetMetadata(MetadataName.Version)),
TargetFramework = item.GetNuGetTargetFramework(),
Include = item.GetNullableMetadata(MetadataName.IncludeAssets),
Exclude = item.GetNullableMetadata(MetadataName.ExcludeAssets)
Include = item.GetNullableMetadata(MetadataName.PackInclude) ?? item.GetNullableMetadata(MetadataName.IncludeAssets),
Exclude = item.GetNullableMetadata(MetadataName.PackExclude) ?? item.GetNullableMetadata(MetadataName.ExcludeAssets)
};

var definedDependencyGroups = (from dependency in dependencies
Expand Down
18 changes: 18 additions & 0 deletions src/NuGetizer.Tasks/MetadataName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,28 @@ public static class MetadataName
/// </summary>
public const string PrivateAssets = nameof(PrivateAssets);

/// <summary>
/// Assets to include for a dependency/package reference
/// </summary>
public const string IncludeAssets = nameof(IncludeAssets);

/// <summary>
/// Assets to exclude for a dependency/package reference
/// </summary>
public const string ExcludeAssets = nameof(ExcludeAssets);

/// <summary>
/// Same as <see cref="IncludeAssets"/>, but allows having a different value for the
/// included assets in pack vs build/restore of the referencing project.
/// </summary>
public const string PackInclude = nameof(PackInclude);

/// <summary>
/// Same as <see cref="ExcludeAssets"/>, but allows having a different value for the
/// excluded assets in pack vs build/restore of the referencing project.
/// </summary>
public const string PackExclude = nameof(PackExclude);

/// <summary>
/// Whether the project can be packed as a .nupkg.
/// </summary>
Expand Down
52 changes: 52 additions & 0 deletions src/NuGetizer.Tests/CreatePackageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,58 @@ public void when_creating_package_with_dependency_and_without_exclude_assets_the
Assert.Equal(0, manifest.Metadata.DependencyGroups.First().Packages.First().Exclude.Count);
}

[Fact]
public void when_creating_package_with_dependency_packinclude_then_contains_dependency_include_attribute()
{
task.Contents = new[]
{
new TaskItem("Newtonsoft.Json", new Metadata
{
{ MetadataName.PackageId, task.Manifest.GetMetadata("Id") },
{ MetadataName.PackFolder, PackFolderKind.Dependency },
{ MetadataName.Version, "8.0.0" },
// NOTE: AssignPackagePath takes care of converting TFM > short name
{ MetadataName.TargetFramework, "net472" },
{ MetadataName.PackInclude, "build" }
}),
};

var manifest = ExecuteTask();

Assert.NotNull(manifest);
Assert.Single(manifest.Metadata.DependencyGroups);
Assert.Single(manifest.Metadata.DependencyGroups.First().Packages);
Assert.Equal("Newtonsoft.Json", manifest.Metadata.DependencyGroups.First().Packages.First().Id);
Assert.Equal(1, manifest.Metadata.DependencyGroups.First().Packages.First().Include.Count);
Assert.Equal("build", manifest.Metadata.DependencyGroups.First().Packages.First().Include[0]);
}

[Fact]
public void when_creating_package_with_dependency_packexclude_assets_then_contains_dependency_exclude_attribute()
{
task.Contents = new[]
{
new TaskItem("Newtonsoft.Json", new Metadata
{
{ MetadataName.PackageId, task.Manifest.GetMetadata("Id") },
{ MetadataName.PackFolder, PackFolderKind.Dependency },
{ MetadataName.Version, "8.0.0" },
// NOTE: AssignPackagePath takes care of converting TFM > short name
{ MetadataName.TargetFramework, "net472" },
{ MetadataName.PackExclude, "build" }
}),
};

var manifest = ExecuteTask();

Assert.NotNull(manifest);
Assert.Single(manifest.Metadata.DependencyGroups);
Assert.Single(manifest.Metadata.DependencyGroups.First().Packages);
Assert.Equal("Newtonsoft.Json", manifest.Metadata.DependencyGroups.First().Packages.First().Id);
Assert.Equal(1, manifest.Metadata.DependencyGroups.First().Packages.First().Exclude.Count);
Assert.Equal("build", manifest.Metadata.DependencyGroups.First().Packages.First().Exclude[0]);
}

[Fact]
public void when_creating_package_with_non_framework_secific_dependency_then_contains_generic_dependency_group()
{
Expand Down
37 changes: 32 additions & 5 deletions src/NuGetizer.Tests/InlineProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -722,11 +722,8 @@ public void when_packing_with_refs_then_includes_runtime_libs_for_private()
"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>TestNuGetizer</PackageId>
<LangVersion>Latest</LangVersion>
<IsPackable>true</IsPackable>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand All @@ -747,5 +744,35 @@ public void when_packing_with_refs_then_includes_runtime_libs_for_private()
PathInPackage = "lib/net461/System.Memory.dll",
}));
}

[Fact]
public void when_packing_dependencies_then_can_include_exclude_assets()
{
var result = Builder.BuildProject(
"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<IsPackable>true</IsPackable>
<LangVersion>Latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Devlooped.SponsorLink" Version="0.9.2" IncludeAssets="analyzers" ExcludeAssets="build" />
</ItemGroup>
</Project>
""", output: output);

result.AssertSuccess(output);

Assert.Contains(result.Items, item => item.Matches(new
{
Identity = "Devlooped.SponsorLink",
PackFolder = "Dependency",
IncludeAssets = "analyzers",
ExcludeAssets = "build"
}));
}
}
}

0 comments on commit 5fa3998

Please sign in to comment.