From a5ed5e6d6e8ea43b8e60b3acf3f5ef5d6b0f68a9 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Sat, 11 Feb 2023 03:41:48 +0000 Subject: [PATCH] Support Include/Exclude attributes from Dependency 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. --- readme.md | 16 ++++--- src/NuGetizer.Tasks/CreatePackage.cs | 7 +-- src/NuGetizer.Tasks/MetadataName.cs | 18 ++++++++ src/NuGetizer.Tests/CreatePackageTests.cs | 52 +++++++++++++++++++++++ src/NuGetizer.Tests/InlineProjectTests.cs | 37 +++++++++++++--- 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/readme.md b/readme.md index 671e8c87..537baed8 100644 --- a/readme.md +++ b/readme.md @@ -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: @@ -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 `` to ``), 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 `` to ``), 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. diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index 4d4b9d9a..b43e2e5e 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -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 @@ -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 diff --git a/src/NuGetizer.Tasks/MetadataName.cs b/src/NuGetizer.Tasks/MetadataName.cs index 21dd612e..96f846e5 100644 --- a/src/NuGetizer.Tasks/MetadataName.cs +++ b/src/NuGetizer.Tasks/MetadataName.cs @@ -30,10 +30,28 @@ public static class MetadataName /// public const string PrivateAssets = nameof(PrivateAssets); + /// + /// Assets to include for a dependency/package reference + /// public const string IncludeAssets = nameof(IncludeAssets); + /// + /// Assets to exclude for a dependency/package reference + /// public const string ExcludeAssets = nameof(ExcludeAssets); + /// + /// Same as , but allows having a different value for the + /// included assets in pack vs build/restore of the referencing project. + /// + public const string PackInclude = nameof(PackInclude); + + /// + /// Same as , but allows having a different value for the + /// excluded assets in pack vs build/restore of the referencing project. + /// + public const string PackExclude = nameof(PackExclude); + /// /// Whether the project can be packed as a .nupkg. /// diff --git a/src/NuGetizer.Tests/CreatePackageTests.cs b/src/NuGetizer.Tests/CreatePackageTests.cs index 95ea9e36..bd8c8444 100644 --- a/src/NuGetizer.Tests/CreatePackageTests.cs +++ b/src/NuGetizer.Tests/CreatePackageTests.cs @@ -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() { diff --git a/src/NuGetizer.Tests/InlineProjectTests.cs b/src/NuGetizer.Tests/InlineProjectTests.cs index fe356ad7..5b07594b 100644 --- a/src/NuGetizer.Tests/InlineProjectTests.cs +++ b/src/NuGetizer.Tests/InlineProjectTests.cs @@ -722,11 +722,8 @@ public void when_packing_with_refs_then_includes_runtime_libs_for_private() """ - Exe - net472 - True - TestNuGetizer - Latest + true + netstandard2.0 @@ -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( + """ + + + Exe + net472 + true + Latest + + + + + + + """, output: output); + + result.AssertSuccess(output); + + Assert.Contains(result.Items, item => item.Matches(new + { + Identity = "Devlooped.SponsorLink", + PackFolder = "Dependency", + IncludeAssets = "analyzers", + ExcludeAssets = "build" + })); + } } }