Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ When the `.nupkg` is created, these includes are resolved automatically so you k
minimum. Nested includes are also supported (i.e. `footer.md` might in turn include a `sponsors.md` file or
a fragment of it).

## Replacement Tokens

NuGetizer supports all the [replacement tokens](https://learn.microsoft.com/en-us/nuget/reference/nuspec#replacement-tokens)
provided by NuGet, such as `$id$`, `$version$`, `$author$` and so on. Replacements are applied to license
file, readme file (post-inclusions, if any) and string-based properties such as description, title and summary.

Morever, this replacement mechanism is extensible via MSBuild items `@(PackageReplacementToken)`, for example:

```xml
<ItemGroup>
<PackageReplacementToken Include="Company" Value="$(Company)" />
</ItemGroup>
```

The newly added token can be used (case-insensitively) in your license file or readme file as `$company$`.

## dotnet-nugetize

Carefully tweaking your packages until they look exactly the way you want them should not be a tedious and slow process. Even requiring your project to be built between changes can be costly and reduce the speed at which you can iterate on the packaging aspects of the project. Also, generating the final `.nupkg`, opening it in a tool and inspecting its content, is also not ideal for rapid iteration.
Expand Down
1 change: 1 addition & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dotnet_diagnostic.IDE1100.severity = none
67 changes: 54 additions & 13 deletions src/NuGetizer.Tasks/CreatePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Frameworks;
Expand All @@ -21,7 +22,9 @@ public class CreatePackage : Task
public ITaskItem Manifest { get; set; }

[Required]
public ITaskItem[] Contents { get; set; } = Array.Empty<ITaskItem>();
public ITaskItem[] Contents { get; set; } = [];

public ITaskItem[] ReplacementTokens { get; set; } = [];

[Required]
public string TargetPath { get; set; }
Expand All @@ -39,6 +42,8 @@ public class CreatePackage : Task
public ITaskItem OutputPackage { get; set; }

Manifest manifest;
Dictionary<string, string> tokens;
Regex tokensExpr;

public override bool Execute()
{
Expand Down Expand Up @@ -68,13 +73,17 @@ public override bool Execute()
}
}

public Manifest Execute(Stream output) => Execute(output, out _);

// Implementation for testing to avoid I/O
public Manifest Execute(Stream output)
public Manifest Execute(Stream output, out Manifest manifest)
{
GeneratePackage(output);
manifest = this.manifest;

output.Seek(0, SeekOrigin.Begin);
using var reader = new PackageArchiveReader(output);

return reader.GetManifest();
}

Expand All @@ -92,13 +101,13 @@ public Manifest CreateManifest()
metadata.DevelopmentDependency = true;

if (Manifest.TryGetMetadata("Title", out var title))
metadata.Title = title.TrimIndent();
metadata.Title = ReplaceTokens(title.TrimIndent());

if (Manifest.TryGetMetadata("Description", out var description))
metadata.Description = description.TrimIndent();
metadata.Description = ReplaceTokens(description.TrimIndent());

if (Manifest.TryGetMetadata("Summary", out var summary))
metadata.Summary = summary.TrimIndent();
metadata.Summary = ReplaceTokens(summary.TrimIndent());

if (Manifest.TryGetMetadata("Readme", out var readme))
metadata.Readme = readme;
Expand All @@ -107,17 +116,17 @@ public Manifest CreateManifest()
metadata.Language = language;

if (Manifest.TryGetMetadata("Copyright", out var copyright))
metadata.Copyright = copyright;
metadata.Copyright = ReplaceTokens(copyright);

if (Manifest.TryGetBoolMetadata("RequireLicenseAcceptance", out var requireLicenseAcceptance) &&
requireLicenseAcceptance)
metadata.RequireLicenseAcceptance = requireLicenseAcceptance;

if (!string.IsNullOrEmpty(Manifest.GetMetadata("Authors")))
metadata.Authors = Manifest.GetMetadata("Authors").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
metadata.Authors = Manifest.GetMetadata("Authors").Split([','], StringSplitOptions.RemoveEmptyEntries);

if (!string.IsNullOrEmpty(Manifest.GetMetadata("Owners")))
metadata.Owners = Manifest.GetMetadata("Owners").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
metadata.Owners = Manifest.GetMetadata("Owners").Split([','], StringSplitOptions.RemoveEmptyEntries);

if (!string.IsNullOrEmpty(Manifest.GetMetadata("LicenseUrl")))
metadata.SetLicenseUrl(Manifest.GetMetadata("LicenseUrl"));
Expand Down Expand Up @@ -171,7 +180,7 @@ public Manifest CreateManifest()
metadata.Icon = icon;

if (Manifest.TryGetMetadata("ReleaseNotes", out var releaseNotes))
metadata.ReleaseNotes = releaseNotes.TrimIndent();
metadata.ReleaseNotes = ReplaceTokens(releaseNotes.TrimIndent());

if (Manifest.TryGetMetadata("Tags", out var tags))
metadata.Tags = tags;
Expand All @@ -190,6 +199,21 @@ public Manifest CreateManifest()
return manifest;
}

string? ReplaceTokens(string? text)
{
tokens ??= ReplacementTokens
.Select(x => (x.ItemSpec.ToLowerInvariant(), x.GetMetadata("Value")))
.GroupBy(t => t.Item1)
.ToDictionary(g => g.Key, g => g.Last().Item2);

tokensExpr ??= new Regex(@"\$(" + string.Join("|", tokens.Keys.Select(Regex.Escape)) + @")\$", RegexOptions.IgnoreCase);

if (string.IsNullOrEmpty(text) || tokens.Count == 0)
return text;

return tokensExpr.Replace(text, match => tokens[match.Groups[1].Value.ToLower()]);
}

void GeneratePackage(Stream output = null)
{
manifest ??= CreateManifest();
Expand All @@ -205,10 +229,27 @@ void GeneratePackage(Stream output = null)
File.Exists(readmeFile.Source))
{
// replace readme with includes replaced.
var replaced = IncludesResolver.Process(readmeFile.Source, message => Log.LogWarningCode("NG001", message));
var temp = Path.GetTempFileName();
File.WriteAllText(temp, replaced);
readmeFile.Source = temp;
var replaced = ReplaceTokens(IncludesResolver.Process(readmeFile.Source, message => Log.LogWarningCode("NG001", message)));
if (!replaced.Equals(File.ReadAllText(readmeFile.Source), StringComparison.Ordinal))
{
var temp = Path.GetTempFileName();
File.WriteAllText(temp, replaced);
readmeFile.Source = temp;
}
}

if (manifest.Metadata.LicenseMetadata?.Type == LicenseType.File &&
manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.LicenseMetadata.License) is ManifestFile licenseFile &&
File.Exists(licenseFile.Source))
{
// replace readme with includes replaced.
var replaced = ReplaceTokens(IncludesResolver.Process(licenseFile.Source, message => Log.LogWarningCode("NG001", message)));
if (!replaced.Equals(File.ReadAllText(licenseFile.Source), StringComparison.Ordinal))
{
var temp = Path.GetTempFileName();
File.WriteAllText(temp, replaced);
licenseFile.Source = temp;
}
}

builder.Files.AddRange(manifest.Files.Select(file =>
Expand Down
13 changes: 12 additions & 1 deletion src/NuGetizer.Tasks/NuGetizer.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,23 @@ Copyright (c) .NET Foundation. All rights reserved.
<ItemGroup Condition="'@(NuspecFile)' == ''">
<NuspecFile Include="$(NuspecFile)" />
</ItemGroup>
<ItemGroup>
<PackageReplacementToken Include="Id" Value="$(PackageId)" />
<PackageReplacementToken Include="Version" Value="$(PackageVersion)" />
<PackageReplacementToken Include="Authors" Value="$(Authors)" Condition="'$(Authors)' != ''" />
<PackageReplacementToken Include="Title" Value="$(Title)" Condition="'$(Title)' != ''" />
<PackageReplacementToken Include="Description" Value="$(Description)" Condition="'$(Description)' != ''" />
<PackageReplacementToken Include="Copyright" Value="$(Copyright)" Condition="'$(Copyright)' != ''" />
<PackageReplacementToken Include="Configuration" Value="$(Configuration)" />
<PackageReplacementToken Include="Product" Value="$(Product)" Condition="'$(Product)' != ''" />
</ItemGroup>
<PropertyGroup>
<_NuspecFile>%(NuspecFile.FullPath)</_NuspecFile>
</PropertyGroup>
<CreatePackage Manifest="@(PackageTargetPath)" NuspecFile="$(_NuspecFile)" Contents="@(_PackageContent)"
EmitPackage="$(EmitPackage)" EmitNuspec="$(EmitNuspec)"
TargetPath="@(PackageTargetPath->'%(FullPath)')">
TargetPath="@(PackageTargetPath->'%(FullPath)')"
ReplacementTokens="@(PackageReplacementToken)">
<Output TaskParameter="OutputPackage" ItemName="_PackageTargetPath" />
<Output TaskParameter="OutputPackage" ItemName="FileWrites" Condition="'$(EmitPackage)' == 'true'" />
<Output TaskParameter="NuspecFile" ItemName="FileWrites" Condition="'$(EmitNuspec)' == 'true'" />
Expand Down
95 changes: 89 additions & 6 deletions src/NuGetizer.Tests/CreatePackageTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
Expand Down Expand Up @@ -33,7 +35,7 @@ public CreatePackageTests(ITestOutputHelper output)
{
{ MetadataName.PackageId, "package" },
{ MetadataName.Version, "1.0.0" },
{ "Title", "title" },
{ "Title", "title $id$" },
{ "Description",
"""

Expand All @@ -60,7 +62,13 @@ New paragraph preserved.
{ "ReleaseNotes", "release notes" },
{ "MinClientVersion", "3.4.0" },
{ "PackageTypes", PackageType.Dependency.Name }
})
}),
ReplacementTokens =
[
new TaskItem("Id", new Dictionary<string, string> { ["Value"] = "package" }),
new TaskItem("Version", new Dictionary<string, string> { ["Value"] = "1.0.0" }),
new TaskItem("Product", new Dictionary<string, string> { ["Value"] = "NuGetizer" }),
]
};

#if RELEASE
Expand All @@ -70,9 +78,16 @@ New paragraph preserved.
#endif
}

Manifest ExecuteTask() => createPackage ?
task.Execute(new MemoryStream()) :
task.CreateManifest();
Manifest ExecuteTask() => ExecuteTask(out _);

Manifest ExecuteTask(out Manifest sourceManifest)
{
if (createPackage)
return task.Execute(new MemoryStream(), out sourceManifest);

sourceManifest = null;
return task.CreateManifest();
}

[Fact]
public void when_output_path_not_exists_then_creates_it()
Expand Down Expand Up @@ -118,7 +133,7 @@ public void when_creating_package_then_contains_all_metadata()

Assert.Equal(task.Manifest.GetMetadata("PackageId"), metadata.Id);
Assert.Equal(task.Manifest.GetMetadata("Version"), metadata.Version.ToString());
Assert.Equal(task.Manifest.GetMetadata("Title"), metadata.Title);
Assert.Equal("title package", metadata.Title);
Assert.Equal(task.Manifest.GetMetadata("Summary"), metadata.Summary);
Assert.Equal(task.Manifest.GetMetadata("Language"), metadata.Language);
Assert.Equal(task.Manifest.GetMetadata("Copyright"), metadata.Copyright);
Expand Down Expand Up @@ -217,6 +232,74 @@ public void when_creating_package_has_license_file_then_manifest_has_license()
Assert.Null(metadata.LicenseMetadata.WarningsAndErrors);
}

[Fact]
public void when_license_file_has_tokens_then_replacements_applied()
{
var content = Path.GetTempFileName();
File.WriteAllText(content, "EULA for $product$ ($id$).");
task.Contents = new[]
{
new TaskItem(content, new Metadata
{
{ MetadataName.PackageId, task.Manifest.GetMetadata("Id") },
{ MetadataName.PackFolder, PackFolderKind.None },
{ MetadataName.PackagePath, "license.txt" }
}),
};

task.Manifest.SetMetadata("LicenseUrl", "");
task.Manifest.SetMetadata("LicenseFile", "license.txt");

createPackage = true;
ExecuteTask(out var manifest);

Assert.NotNull(manifest);

Assert.Equal("license.txt", manifest.Metadata.LicenseMetadata.License);
Assert.Equal(LicenseType.File, manifest.Metadata.LicenseMetadata.Type);

var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.LicenseMetadata.License);
Assert.NotNull(file);
Assert.True(File.Exists(file.Source));

var eula = File.ReadAllText(file.Source);

Assert.Equal("EULA for NuGetizer (package).", eula);
}

[Fact]
public void when_readme_has_include_and_tokens_then_replacements_applied()
{
var content = Path.GetTempFileName();
File.WriteAllText(content, "<!-- include https://github.com/devlooped/.github/blob/807335297e28cfe5a6dd00ecd72b2ca32c0f1ed8/osmf.md -->");
task.Contents = new[]
{
new TaskItem(content, new Metadata
{
{ MetadataName.PackageId, task.Manifest.GetMetadata("Id") },
{ MetadataName.PackFolder, PackFolderKind.None },
{ MetadataName.PackagePath, "readme.md" }
}),
};

task.Manifest.SetMetadata("Readme", "readme.md");

createPackage = true;
ExecuteTask(out var manifest);

Assert.NotNull(manifest);

Assert.Equal("readme.md", manifest.Metadata.Readme);

var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme);
Assert.NotNull(file);
Assert.True(File.Exists(file.Source));

var readme = File.ReadAllText(file.Source);

Assert.Contains("NuGetizer", readme);
}

[Fact]
public void when_creating_package_with_simple_dependency_then_contains_dependency_group()
{
Expand Down
18 changes: 18 additions & 0 deletions src/NuGetizer.Tests/given_a_library.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,23 @@ public void when_pack_excludes_additional_items_then_contains_only_matching_file

Assert.Single(compile);
}

[Fact]
public void when_packing_performs_token_replacement()
{
var result = Builder.BuildScenario(nameof(given_a_library),
new { PackCompile = "true", PackOnlyApi = "true" },
target: "Build,GetPackageContents,Pack");

Assert.True(result.BuildResult.HasResultsForTarget("GetPackageContents"));

var items = result.BuildResult.ResultsByTarget["GetPackageContents"];
var compile = items.Items.Where(item => item.Matches(new
{
BuildAction = "Compile",
})).ToArray();

Assert.Single(compile);
}
}
}