Skip to content

Commit 38c07d6

Browse files
committed
Pack console apps using publish output by default
We have an SDK-compatible mechanism for packing .NET tools: PackAsTool. This satisfies a very specific scenario for .NET tools, but doesn't work for regular CLI tools that don't need packing as standalone dotnet tools. For these CLI tools, packing the output of Publish makes much more sense, and results in way fewer customizations in package references. We introduce a new property, PackAsPublish, which is initialized to `true` whenever the project is publishable and a cross-platform executable (pending widening the definition upon feedback). In this case, we don't infer content from package dependencies and references with copy local and so on, but rather rely exclusively on Publish doing its work. We don't change the PackFolder treatment, so that property is orthogonal to PackAsPublish, although it's likely `tools` will be the most commonly used folder. The property's default `true` value is calculated dynamically in an initial target since we need to inspect `ProjectCapability` which is set by the SDK to include `CrossPlatformExecutable`. This is the more extensible approach, rather than checking the output type ourselves.
1 parent 6cd5a24 commit 38c07d6

File tree

4 files changed

+108
-5
lines changed

4 files changed

+108
-5
lines changed

readme.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ You can also add a reference to a CLI *tools* program like the following:
445445
<Project Sdk='Microsoft.NET.Sdk'>
446446
<PropertyGroup>
447447
<TargetFramework>net6.0</TargetFramework>
448+
<OutputType>Exe</OutputType>
448449
<PackFolder>tools</PackFolder>
449450
<!-- We don't need this particular tool in a framework-specific subfolder under /tools -->
450451
<BuildOutputFrameworkSpecific>false</BuildOutputFrameworkSpecific>
@@ -465,6 +466,20 @@ content inference.
465466
This section contains miscellaneous useful features that are typically used in advanced scenarios and
466467
are not necessarily mainstream.
467468

469+
### PackAsPublish for CLI tools
470+
471+
When a project's output type is `Exe` and it's not set to `PackAsTool=true` (used specifically for .NET tools),
472+
it will default to be use the `Publish` output for packing. This is typically what you want for a CLI
473+
project, since dependencies are included in the publish directory automatically without having to annotate
474+
any references with `PrivateAssets=all`.
475+
476+
This can be turned off by setting `PackAsPublish=false` on the project, which will cause the project
477+
to be packed as a regular class library, with the dependencies inference rules applied (such as
478+
`PrivateAssets=all` for package reference and `CopyLocal=true` for references).
479+
480+
When packing as publish, the output won't be framework-specific by default, and will just contribute
481+
the published contents to the specified `PackFolder`.
482+
468483
### Dynamically Extending Package Contents
469484

470485
If you need to calculate additional items to inject into the package dynamically, you can run a target

src/NuGetizer.Tasks/NuGetizer.Inference.targets

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ Copyright (c) .NET Foundation. All rights reserved.
248248
</Target>
249249

250250
<Target Name="InferPackageContents" DependsOnTargets="$(InferPackageContentsDependsOn);_CollectInferenceCandidates" Returns="@(PackageFile)">
251+
<Error Code="NG1004" Condition="'$(PackAsTool)' == 'true' and '$(PackAsPublish)' == 'true'" Text="PackAsTool and PackAsPublish are mutually exclusive." />
251252

252253
<!-- Even if all these conditions are false, the user can still explicitly pack the file, of course -->
253254
<ItemGroup Label="Readme" Condition="'$(PackReadme)' == 'true' and '$(PackageReadmeFile)' != '' and '$(IsPackable)' == 'true'">
@@ -294,7 +295,7 @@ Copyright (c) .NET Foundation. All rights reserved.
294295
'%(_InferenceCandidateWithTargetPath.CopyToOutputDirectory)' == 'Never'" />
295296
</ItemGroup>
296297

297-
<ItemGroup Label="BuildOutput Inference" Condition="'$(PackBuildOutput)' == 'true'">
298+
<ItemGroup Label="BuildOutput Inference" Condition="'$(PackBuildOutput)' == 'true' and '$(PackAsPublish)' != 'true'">
298299
<!-- Unfortunately, even with https://github.com/Microsoft/msbuild/pull/1115, when multi-targeting
299300
.NETFramework, the desktop WinFX.targets are imported which don't have the fix, so we need to
300301
do it "the old way" for this particular output group -->
@@ -318,9 +319,20 @@ Copyright (c) .NET Foundation. All rights reserved.
318319
<_InferredPackageFile Include="@(_InferredProjectOutput -> Distinct())" />
319320
</ItemGroup>
320321

322+
<ItemGroup Label="Publishable Inference" Condition="'$(PackAsPublish)' == 'true' and '$(PackAsTool)' != 'true'">
323+
<_InferredPublishItem Include="@(PublishItemsOutputGroupOutputs -> '%(OutputPath)')" />
324+
<_InferredPackageFile Include="@(_InferredPublishItem -> '%(FullPath)')">
325+
<PackFolder>$(PackFolder)</PackFolder>
326+
<FrameworkSpecific>false</FrameworkSpecific>
327+
<!-- NOTE: we don't set `BuildOutputFrameworkSpecific` since we're not packing the build output
328+
but rather the *publish* output. Users could change that by setting a TF-specific PackFolder -->
329+
</_InferredPackageFile>
330+
</ItemGroup>
331+
321332
<ItemGroup Label="References Inference">
322333
<_InferredPackageFile Include="@(PackageReference)"
323334
Condition="'$(PackAsTool)' != 'true' and
335+
'$(PackAsPublish)' != 'true' and
324336
'%(PackageReference.Identity)' != 'NuGetizer' and
325337
'%(PackageReference.Identity)' != 'NETStandard.Library' and
326338
'%(PackageReference.PrivateAssets)' != 'all' and
@@ -334,6 +346,7 @@ Copyright (c) .NET Foundation. All rights reserved.
334346
TBD: maybe include ResolvedFrom=ImplicitlyExpandDesignTimeFacades too? -->
335347
<_InferredPackageFile Include="@(ReferencePath->'%(OriginalItemSpec)')"
336348
Condition="'$(PackAsTool)' != 'true' and
349+
'$(PackAsPublish)' != 'true' and
337350
'$(PackFrameworkReferences)' == 'true' and
338351
'%(ReferencePath.ResolvedFrom)' == '{TargetFrameworkDirectory}' and
339352
'%(ReferencePath.Pack)' != 'false'">
@@ -385,7 +398,9 @@ Copyright (c) .NET Foundation. All rights reserved.
385398

386399
</Target>
387400

388-
<Target Name="_CollectPrimaryOutputDependencies" DependsOnTargets="ReferenceCopyLocalPathsOutputGroup;RunResolvePackageDependencies" Returns="@(ImplicitPackageReference)">
401+
<Target Name="_CollectPrimaryOutputDependencies"
402+
DependsOnTargets="ReferenceCopyLocalPathsOutputGroup;RunResolvePackageDependencies"
403+
Returns="@(ImplicitPackageReference)">
389404
<Error Code="NG1003" Text="Centrally managed package versions is only supported when using the Microsoft.NET.Sdk."
390405
Condition="'$(ManagePackageVersionsCentrally)' == 'true' and '$(UsingMicrosoftNETSdk)' != 'true'" />
391406
<ItemGroup>
@@ -416,6 +431,7 @@ Copyright (c) .NET Foundation. All rights reserved.
416431
<Target Name="_ResolvePackageDependencies" Condition="'$(UsingMicrosoftNETSdk)' == 'true'" DependsOnTargets="RunResolvePackageDependencies" />
417432

418433
<Target Name="InferPrimaryOutputDependencies"
434+
Condition="'$(PackAsTool)' != 'true' and '$(PackAsPublish)' != 'true'"
419435
Inputs="@(_PrimaryOutputRelatedFile)"
420436
Outputs="%(_PrimaryOutputRelatedFile.NuGetPackageId)"
421437
Returns="@(_InferredPackageFile)"
@@ -470,7 +486,7 @@ Copyright (c) .NET Foundation. All rights reserved.
470486
</ItemGroup>
471487
</Target>
472488

473-
<Target Name="_SetInferPackageContentsDependsOn" AfterTargets="_SetPropertiesFromCapabilities">
489+
<Target Name="_SetInferenceProperties" AfterTargets="_SetPropertiesFromCapabilities">
474490
<PropertyGroup>
475491
<!-- NOTE: this avoids taking dependencies on targets that are only available when the project supports the concept of references -->
476492
<_SupportsReferences Condition="
@@ -500,7 +516,26 @@ Copyright (c) .NET Foundation. All rights reserved.
500516
$(InferPackageContentsDependsOn);
501517
AllProjectOutputGroups
502518
</InferPackageContentsDependsOn>
519+
520+
<!-- Smarter inference for publishable console apps
521+
* PackAsTool should still do what SDK Pack does.
522+
* PackAsPublish=false turns off this inference.
523+
* IsPublishable is set by the SDK, but for everything, even libs :(,
524+
but we can use that as an alternative off switch too.
525+
* Finally, the CrossPlatformExecutable capability is set by the SDK for
526+
.NETCoreApp projects that are actually executable (Exe or WinExe)
527+
-->
528+
<PackAsPublish Condition="'$(PackAsPublish)' == '' and
529+
'$(PackAsTool)' != 'true' and
530+
'$(IsPublishable)' == 'true' and
531+
$(_AllProjectCapabilities.Contains('CrossPlatformExecutable'))">true</PackAsPublish>
532+
533+
<InferPackageContentsDependsOn Condition="'$(PackAsPublish)' == 'true'">
534+
$(InferPackageContentsDependsOn);
535+
PublishItemsOutputGroup
536+
</InferPackageContentsDependsOn>
503537
</PropertyGroup>
538+
504539
</Target>
505540

506541
</Project>

src/NuGetizer.Tests/given_a_tool_project.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,59 @@ public void when_pack_folder_tool_but_no_pack_as_tool_then_packs_dependencies_no
8585
Identity = "Microsoft.Extensions.DependencyModel"
8686
}));
8787
}
88+
89+
[Fact]
90+
public void when_pack_folder_tool_no_pack_as_tool_and_executable_then_packs_as_publish_with_no_dependencies()
91+
{
92+
var result = Builder.BuildProject(
93+
"""
94+
<Project Sdk="Microsoft.NET.Sdk">
95+
<PropertyGroup>
96+
<OutputType>Exe</OutputType>
97+
<PackageId>MyTool</PackageId>
98+
<TargetFramework>net6.0</TargetFramework>
99+
<PackageId>MyTool</PackageId>
100+
<PackFolder>tools</PackFolder>
101+
</PropertyGroup>
102+
<ItemGroup>
103+
<PackageReference Include='Microsoft.Extensions.DependencyModel' Version='6.0.0' />
104+
</ItemGroup>
105+
</Project>
106+
""",
107+
"GetPackageContents", output);
108+
109+
result.AssertSuccess(output);
110+
Assert.DoesNotContain(result.Items, item => item.Matches(new
111+
{
112+
Identity = "Microsoft.Extensions.DependencyModel"
113+
}));
114+
115+
Assert.All(result.Items, item => item.Matches(new
116+
{
117+
OutputGroup = "PublishItemsOutputGroup",
118+
}));
119+
}
120+
121+
[Fact]
122+
public void when_both_PackAsTool_and_PackAsPublish_true_then_fails()
123+
{
124+
var result = Builder.BuildProject(
125+
"""
126+
<Project Sdk="Microsoft.NET.Sdk">
127+
<PropertyGroup>
128+
<OutputType>Exe</OutputType>
129+
<TargetFramework>net6.0</TargetFramework>
130+
<PackageId>MyTool</PackageId>
131+
<PackFolder>tools</PackFolder>
132+
<PackAsTool>true</PackAsTool>
133+
<PackAsPublish>true</PackAsPublish>
134+
</PropertyGroup>
135+
</Project>
136+
""",
137+
"GetPackageContents", output);
138+
139+
Assert.Equal(Microsoft.Build.Execution.BuildResultCode.Failure, result.BuildResult.OverallResult);
140+
}
141+
88142
}
89143
}

src/dotnet-nugetize/Program.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ int Run(string[] args)
128128
"-p:SkipCompilerExecution=true",
129129
"-p:DesignTimeBuild=true",
130130
"-p:DesignTimeSilentResolution=true",
131-
"-p:ResolveAssemblyReferencesSilent=true",
132-
"-p:IsPublishable=false"
131+
"-p:ResolveAssemblyReferencesSilent=true"
133132
});
134133

135134
if (help)

0 commit comments

Comments
 (0)