From 14f8601c75406212bf5a6b5c995bebe09a1b18ef Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Fri, 17 Oct 2025 03:44:16 -0300 Subject: [PATCH 1/4] Expand image url as well as link url, use robust Markdig Rather than the brittle regex-based matching we were doing, switch to proper markdown processing so we can better support arbitrary content within the readme without potentially breaking due to the flaky nature of the regex. We were also not expanding the image url if it contained a link, so now we're more consistent. --- src/ILRepack.targets | 129 +++++++++++++++++++++ src/NuGetizer.Tasks/CreatePackage.cs | 44 +++---- src/NuGetizer.Tasks/NuGetizer.Tasks.csproj | 12 +- src/NuGetizer.Tests/CreatePackageTests.cs | 38 ++++++ 4 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 src/ILRepack.targets diff --git a/src/ILRepack.targets b/src/ILRepack.targets new file mode 100644 index 00000000..5c5b647b --- /dev/null +++ b/src/ILRepack.targets @@ -0,0 +1,129 @@ + + + + + true + false + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + %(ILRepackIncludeWildcard.Identity) + + + + + + + + + %(ILRepackExcludeWildcard.Identity) + + + + + + + + + ;@(ILRepackIncludeExact, ';'); + + + + + + + + + ;@(ILRepackExcludeExact, ';'); + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','$(AssemblyOriginatorKeyFile)')))) + /keyfile:"$(AbsoluteAssemblyOriginatorKeyFile)" /delaysign + $(ILRepackArgs) /internalize + $(ILRepackArgs) /union + + $(ILRepackArgs) @(LibDir -> '/lib:"%(Identity)."', ' ') + $(ILRepackArgs) /out:"@(IntermediateAssembly -> '%(FullPath)')" + $(ILRepackArgs) "@(IntermediateAssembly -> '%(FullPath)')" + $(ILRepackArgs) @(MergedAssemblies -> '"%(FullPath)"', ' ') + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index a8c624a4..2c3c67bb 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -4,7 +4,12 @@ using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Security.Policy; using System.Text.RegularExpressions; +using Markdig; +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NuGet.Frameworks; @@ -44,7 +49,6 @@ public class CreatePackage : Task Manifest manifest; Dictionary tokens; Regex tokensExpr; - Regex linkExpr; public override bool Execute() { @@ -237,33 +241,33 @@ void GeneratePackage(Stream output = null) Uri.TryCreate(manifest.Metadata.Repository.Url, UriKind.Absolute, out var uri) && uri.Host.EndsWith("github.com")) { - // expr to match markdown links with optional title. use named groups to capture the link text, url and optional title. - // Handle image links inside clickable badges: [![alt](img)](url) by explicitly matching the image pattern - linkExpr ??= new Regex(@"\[(?!\[[^\]]*\]\([^\)]*\)|[^\]]+)\]\((?[^\s)]+)(?:\s+""(?[^""]*)"")?\)", RegexOptions.None); - var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/'); - // Extract owner and repo from URL for raw.githubusercontent.com format var repoPath = uri.AbsolutePath.TrimStart('/'); var rawBaseUrl = $"https://raw.githubusercontent.com/{repoPath}"; - replaced = linkExpr.Replace(replaced, match => - { - var url = match.Groups["url"].Value; - var title = match.Groups["title"].Value; + var document = Markdown.Parse(replaced); + var links = document.Descendants<LinkInline>().ToList(); - // Check if the URL is already absolute - if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) - return match.Value; + foreach (var link in links) + { + if (string.IsNullOrEmpty(link.Url) || Uri.IsWellFormedUriString(link.Url, UriKind.Absolute)) + continue; - // Use raw.githubusercontent.com format for proper image display on nuget.org - var newUrl = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; + link.Url = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{link.Url.TrimStart('/')}"; - // Preserve the title if present - if (!string.IsNullOrEmpty(title)) - return $"[{match.Groups["text"].Value}]({newUrl} \"{title}\")"; + if (link.FirstChild is LinkInline img && + !string.IsNullOrEmpty(img.Url) && + !Uri.IsWellFormedUriString(img.Url, UriKind.Absolute)) + { + img.Url = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{img.Url.TrimStart('/')}"; + } + } - return $"[{match.Groups["text"].Value}]({newUrl})"; - }); + // render the document to console + using var writer = new StringWriter(); + var renderer = new NormalizeRenderer(writer); + renderer.Render(document); + replaced = writer.ToString(); } if (!replaced.Equals(File.ReadAllText(readmeFile.Source), StringComparison.Ordinal)) diff --git a/src/NuGetizer.Tasks/NuGetizer.Tasks.csproj b/src/NuGetizer.Tasks/NuGetizer.Tasks.csproj index 32d5cda5..4e614824 100644 --- a/src/NuGetizer.Tasks/NuGetizer.Tasks.csproj +++ b/src/NuGetizer.Tasks/NuGetizer.Tasks.csproj @@ -18,6 +18,8 @@ <DevelopmentDependency>true</DevelopmentDependency> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile> + + <ILRepack>true</ILRepack> </PropertyGroup> <ItemGroup> @@ -27,10 +29,18 @@ <PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" /> <PackageReference Include="ThisAssembly.Strings" Version="2.1.2" PrivateAssets="all" /> <PackageReference Include="Minimatch" Version="2.0.0" PrivateAssets="all" /> + <PackageReference Include="Markdig" Version="0.42.0" PrivateAssets="all" /> + <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" PrivateAssets="all" /> </ItemGroup> <ItemGroup> <ProjectCapability Include="Pack" /> + <ILRepackInclude Include="Markdig" /> + <ILRepackInclude Include="System.Buffers" /> + <ILRepackInclude Include="System.Memory" /> + <ILRepackInclude Include="System.Numerics.Vectors" /> + <ILRepackInclude Include="System.Runtime.CompilerServices.Unsafe" /> + <ILRepackInclude Include="Microsoft.NET.StringTools" /> </ItemGroup> <ItemGroup> @@ -52,5 +62,5 @@ <Import Project="NuGetizer.Tasks.Pack.targets" Condition="'$(GeneratePackageOnBuild)' == 'false' AND '$(NuGetize)' != 'true'" /> <Import Project="NuGetizer.Tasks.targets" /> - + <Import Project="..\ILRepack.targets" /> </Project> \ No newline at end of file diff --git a/src/NuGetizer.Tests/CreatePackageTests.cs b/src/NuGetizer.Tests/CreatePackageTests.cs index d265b9cb..f2e667ca 100644 --- a/src/NuGetizer.Tests/CreatePackageTests.cs +++ b/src/NuGetizer.Tests/CreatePackageTests.cs @@ -443,6 +443,44 @@ public void when_readme_has_image_link_then_uses_raw_url() Assert.DoesNotContain("/blob/", readme); } + [Fact] + public void when_readme_has_relative_image_url_and_link_then_expands_both() + { + var content = Path.GetTempFileName(); + File.WriteAllText(content, "[![Image](img/logo.png)](osmf.txt)"); + 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"); + task.Manifest.SetMetadata("RepositoryType", "git"); + task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer"); + task.Manifest.SetMetadata("RepositorySha", "abc123def"); + + createPackage = true; + ExecuteTask(out var manifest); + + Assert.NotNull(manifest); + + 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); + + // Should use raw.githubusercontent.com format for proper image display + Assert.Contains("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/img/logo.png", readme); + Assert.Contains("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/osmf.txt", readme); + Assert.DoesNotContain("/blob/", readme); + } + + [Fact] public void when_readme_has_clickable_image_badge_with_relative_url_then_replaces_url() { From f5355dc24b7861251daecb91c4d41b1442fbef9f Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino <daniel@cazzulino.com> Date: Fri, 17 Oct 2025 04:10:35 -0300 Subject: [PATCH 2/4] Attempt to fix the broken test from obsolete MAUI Mac --- .github/workflows/build.yml | 1 + .../Scenarios/given_multitargeting_libraries/common.csproj | 2 +- .../given_multitargeting_libraries/uilibrary.csproj | 3 +-- .../given_multitargeting_libraries/uishared.csproj | 2 +- src/NuGetizer.Tests/given_multitargeting_libraries.cs | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f987c515..ca83b455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,7 @@ jobs: working-directory: src/NuGetizer.Tests run: | dotnet restore Scenarios/given_a_packaging_project/a.nuproj + dotnet workload restore Scenarios/given_multitargeting_libraries/uilibrary.csproj # THIS IS IMPORTANT: WE NEED TO BUILD WITH DESKTOP MSBUILD msbuild -r diff --git a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/common.csproj b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/common.csproj index b209b14c..4e071437 100644 --- a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/common.csproj +++ b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/common.csproj @@ -2,7 +2,7 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" /> <PropertyGroup> - <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks> + <TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <IsPackable>true</IsPackable> </PropertyGroup> diff --git a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uilibrary.csproj b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uilibrary.csproj index 791fbef4..4d6fef1b 100644 --- a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uilibrary.csproj +++ b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uilibrary.csproj @@ -2,10 +2,9 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" /> <PropertyGroup> - <TargetFrameworks>net8.0;net8.0-windows;net8.0-maccatalyst</TargetFrameworks> + <TargetFrameworks>net9.0;net9.0-windows;net9.0-maccatalyst</TargetFrameworks> <PackOnBuild>true</PackOnBuild> <IsPackable>true</IsPackable> - <SupportedOSPlatformVersion Condition="'$(TargetFramework)' == 'net8.0-maccatalyst'">14.0</SupportedOSPlatformVersion> </PropertyGroup> <ItemGroup> diff --git a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uishared.csproj b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uishared.csproj index 913e3ed6..fe90b054 100644 --- a/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uishared.csproj +++ b/src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uishared.csproj @@ -2,7 +2,7 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" /> <PropertyGroup> - <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks> + <TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <IsPackable>true</IsPackable> </PropertyGroup> diff --git a/src/NuGetizer.Tests/given_multitargeting_libraries.cs b/src/NuGetizer.Tests/given_multitargeting_libraries.cs index 8e83952a..2e20dcdc 100644 --- a/src/NuGetizer.Tests/given_multitargeting_libraries.cs +++ b/src/NuGetizer.Tests/given_multitargeting_libraries.cs @@ -55,14 +55,14 @@ public void when_getting_content_then_multitargets() Assert.Contains(result.Items, item => item.Matches(new { - PackagePath = "lib/net8.0/uilibrary.dll" + PackagePath = "lib/net9.0/uilibrary.dll" })); Assert.Contains(result.Items, item => item.TryGetMetadata("PackagePath", out var path) && - path.StartsWith("lib/net8.0-windows") && path.EndsWith("uilibrary.dll")); + path.StartsWith("lib/net9.0-windows") && path.EndsWith("uilibrary.dll")); Assert.Contains(result.Items, item => item.TryGetMetadata("PackagePath", out var path) && - path.StartsWith("lib/net8.0-maccatalyst") && path.EndsWith("uilibrary.dll")); + path.StartsWith("lib/net9.0-maccatalyst") && path.EndsWith("uilibrary.dll")); } } } From 4dca6f052619935b073e4d8b419520a8c2cc9745 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino <daniel@cazzulino.com> Date: Fri, 17 Oct 2025 04:26:01 -0300 Subject: [PATCH 3/4] Split restore from build for better logging --- .github/workflows/build.yml | 7 ++++++- .github/workflows/publish.yml | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca83b455..dc66e3c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,12 +41,17 @@ jobs: - name: ⚙ msbuild uses: microsoft/setup-msbuild@v1.1 - - name: 🙏 build + - name: 🙏 restore shell: pwsh working-directory: src/NuGetizer.Tests run: | dotnet restore Scenarios/given_a_packaging_project/a.nuproj dotnet workload restore Scenarios/given_multitargeting_libraries/uilibrary.csproj + + - name: 🙏 build + shell: pwsh + working-directory: src/NuGetizer.Tests + run: | # THIS IS IMPORTANT: WE NEED TO BUILD WITH DESKTOP MSBUILD msbuild -r diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ebad5d1a..b89701fb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,11 +24,18 @@ jobs: - name: ⚙ msbuild uses: microsoft/setup-msbuild@v1.1 - - name: 🙏 build + - name: 🙏 restore shell: pwsh working-directory: src/NuGetizer.Tests run: | dotnet restore Scenarios/given_a_packaging_project/a.nuproj + dotnet workload restore Scenarios/given_multitargeting_libraries/uilibrary.csproj + + - name: 🙏 buildwhen_getting_content_then_multitargets + shell: pwsh + working-directory: src/NuGetizer.Tests + run: | + # THIS IS IMPORTANT: WE NEED TO BUILD WITH DESKTOP MSBUILD msbuild -r - name: 🐛 logs From 8769a0dc138c92b695a2e054ff6f808b9b2ee517 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino <daniel@cazzulino.com> Date: Fri, 17 Oct 2025 04:31:18 -0300 Subject: [PATCH 4/4] Avoid incompatible assembly from being copied to test output This assembly seems to be a new transitive dep from some prior updates. By excluding it explicitly, we prevent hard-to-diagnose test failures in local builds --- src/NuGetizer.Tests/NuGetizer.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NuGetizer.Tests/NuGetizer.Tests.csproj b/src/NuGetizer.Tests/NuGetizer.Tests.csproj index bd6be2f1..705739c1 100644 --- a/src/NuGetizer.Tests/NuGetizer.Tests.csproj +++ b/src/NuGetizer.Tests/NuGetizer.Tests.csproj @@ -11,6 +11,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Microsoft.NET.StringTools" Version="17.14.28" ExcludeAssets="all" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />