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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.zip binary
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ All notable changes to this project are documented here. The format follows [Kee

- The documentation moved from the README into [docs/](./docs/README.md), one page per topic in reading order, with a [property reference](./docs/reference/properties.md) and a [code reference](./docs/reference/codes.md) that cover everything the kit sets, reads and reports. Corrected along the way: most packaging defaults apply to every project, not only packable ones; a Roslyn project imports its role's props itself; an update rewrites more than `.toolkit/msbuild/`; any tag build is a release build.

### Fixed

- A JPEG `PackageIconPath` no longer fails `dotnet pack` with NU5046: the icon is packed as `icon.<extension>`, lowercased (`icon.jpg`), and the nuspec names that file. `MSKIT_PKG015` accepts a 128×128 JPEG as well as a PNG, as nuget.org does ([#11](https://github.com/DragoAnt/MSBuildKit/pull/11)).

## [0.2.1] - 2026-10-05

### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For projects with `IsPackable=True` (the owner layer makes that the default; tes
| `PackageLicenseExpression` | `MIT` (owner layer), unless `PackageLicenseFile` is set | [licensing](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#licensing) |
| `PackageRequireLicenseAcceptance` | `false` | |
| `Authors`, `Copyright` | the owner; `Copyright (c) <year> <owner>` ([Build](./build.md#language-and-product-defaults)) | [copyright](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#copyright) |
| `PackageIcon` | `PackageIconPath` (owner layer: `.toolkit/res/package.icon.png`, 128×128) packed as `icon.png` | [icon](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#icon) |
| `PackageIcon` | `PackageIconPath` (owner layer: `.toolkit/res/package.icon.png`, 128×128; PNG or JPEG) packed as `icon<extension, lowercased>` (`icon.png`, `icon.jpg`); a `PackageIcon` the project sets is kept | [icon](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#icon) |
| `PackageReadmeFile` | generated from `MSKit_PackageReadmeFrom` ([Package readme](./package-readme.md)), else `package.readme.md` next to the csproj, else `README.md` next to it; packed as `readme.md`. `MSKit_PackageReadmeSourcePath` names another file | [README](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#readme) |
| `RepositoryType`, `RepositoryUrl`, `PackageProjectUrl` | `git`; `GITHUB_SERVER_URL/GITHUB_REPOSITORY` on GitHub Actions, else the git remote [Source Link](https://learn.microsoft.com/dotnet/standard/library-guidance/sourcelink) reads, without `.git` | [repository](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#repository-type-and-url) |
| `PackageReleaseNotes` | on a `https://github.com/` repository the tag's release page on a tag build, else its releases page; on other hosts the releases page the generated readme links (needs `MSKit_PackageReadmeFrom`). `MSKit_DefaultReleaseNotes=False` turns the default off | [release notes](https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#release-notes) |
Expand Down Expand Up @@ -54,7 +54,7 @@ Set `MSKit_PackageValidationBaselineVersion` to the last published version once
| [`MSKIT_PKG012`](./reference/codes.md#mskitpkg012) | the README loads images from a host nuget.org blocks |
| [`MSKIT_PKG013`](./reference/codes.md#mskitpkg013) | the version is not SemVer 2.0 (`MSKit_SemVerRegex`) |
| [`MSKIT_PKG014`](./reference/codes.md#mskitpkg014) | no repository or project URL |
| [`MSKIT_PKG015`](./reference/codes.md#mskitpkg015) | the icon is not a 128×128 PNG (`MSKit_PackageIconSize`) |
| [`MSKIT_PKG015`](./reference/codes.md#mskitpkg015) | the icon is not a 128×128 PNG or JPEG (`MSKit_PackageIconSize`) |
| [`MSKIT_PKG016`](./reference/codes.md#mskitpkg016) | no `PackageReleaseNotes` |
| [`MSKIT_PKG017`](./reference/codes.md#mskitpkg017) | the README has relative links |
| [`MSKIT_PKG018`](./reference/codes.md#mskitpkg018) | an open-source licence with an "All rights reserved" copyright |
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Every warning and error the kit reports, one section per code. The kit reports e

### MSKITPKG005

`MSKIT_PKG005` — no icon. Set `PackageIconPath` to a 128×128 PNG; the owner layer sets one for every package.
`MSKIT_PKG005` — no icon. Set `PackageIconPath` to a 128×128 PNG or JPEG, packed as `icon<extension, lowercased>`; the owner layer sets one for every package.

### MSKITPKG006

Expand Down Expand Up @@ -74,7 +74,7 @@ Every warning and error the kit reports, one section per code. The kit reports e

### MSKITPKG015

`MSKIT_PKG015` — the icon is not a PNG of `MSKit_PackageIconSize` × `MSKit_PackageIconSize` pixels (128).
`MSKIT_PKG015` — the icon is not a PNG or JPEG of `MSKit_PackageIconSize` × `MSKit_PackageIconSize` pixels (128); the size is checked on both formats.

### MSKITPKG016

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Every property and item the kit sets or reads, grouped by topic. **Set** = a val
| Name | Kind | Default | Meaning |
| --- | --- | --- | --- |
| `IsPackable` | set | `True` (owner layer); `False` for test projects and code fixes | |
| `PackageIconPath` | set | `.toolkit/res/package.icon.png` (owner layer) | The icon, packed as `icon.png` |
| `PackageIconPath` | set | `.toolkit/res/package.icon.png` (owner layer) | The icon (PNG or JPEG), packed as `icon<extension, lowercased>` |
| `MSKit_PackageIconSourcePath` | out | | The icon file the checks read |
| `MSKit_PackageReadmeSourcePath` | set | `package.readme.md`, else `README.md` next to the csproj | The README packed as `readme.md` |
| `MSKit_PackageValidationBaselineVersion` | set | empty | The release package validation compares against |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<Output TaskParameter="HtmlTags" PropertyName="_MSKit_HtmlTags" />
<Output TaskParameter="Alerts" PropertyName="_MSKit_Alerts" />
<Output TaskParameter="HasMermaid" PropertyName="_MSKit_HasMermaid" />
<Output TaskParameter="IconIsPng" PropertyName="_MSKit_IconIsPng" />
<Output TaskParameter="IconFormat" PropertyName="_MSKit_IconFormat" />
<Output TaskParameter="IconWidth" PropertyName="_MSKit_IconWidth" />
<Output TaskParameter="IconHeight" PropertyName="_MSKit_IconHeight" />
</MSKit_PackageAssets>
Expand Down Expand Up @@ -60,7 +60,7 @@
Text="Package '$(PackageId)' has no PackageTags. Add a few search terms that are not already in the package id."
Rule="$(_MSKit_PkgRules)#tags" />
<_MSKit_PkgFinding Include="MSKIT_PKG005" Condition="'$(PackageIcon)'==''"
Text="Package '$(PackageId)' has no icon. Set PackageIconPath to a 128x128 PNG (the company layer of the kit sets one for every package)."
Text="Package '$(PackageId)' has no icon. Set PackageIconPath to a 128x128 PNG or JPEG (the company layer of the kit sets one for every package)."
Rule="$(_MSKit_PkgRules)#icon" />
<_MSKit_PkgFinding Include="MSKIT_PKG006" Condition="'$(PackageLicenseExpression)'=='' AND '$(PackageLicenseFile)'==''"
Text="Package '$(PackageId)' has no licence. Without one nobody may use it. Set PackageLicenseExpression to an SPDX id (for example MIT)."
Expand Down Expand Up @@ -89,8 +89,8 @@
<_MSKit_PkgFinding Include="MSKIT_PKG014" Condition="'$(_MSKit_RepoUrlFinal)'=='' OR '$(PackageProjectUrl)'==''"
Text="Package '$(PackageId)' has no repository or project URL. Set RepositoryUrl, or build from a git clone whose origin remote Source Link can read."
Rule="$(_MSKit_PkgRules)#repository-type-and-url" />
<_MSKit_PkgFinding Include="MSKIT_PKG015" Condition="'$(MSKit_PackageIconSourcePath)'!='' AND Exists('$(MSKit_PackageIconSourcePath)') AND ('$(_MSKit_IconIsPng)'!='True' OR '$(_MSKit_IconWidth)'!='$(MSKit_PackageIconSize)' OR '$(_MSKit_IconHeight)'!='$(MSKit_PackageIconSize)')"
Text="Package '$(PackageId)' icon is not a $(MSKit_PackageIconSize)x$(MSKit_PackageIconSize) PNG (PNG: $(_MSKit_IconIsPng), size: $(_MSKit_IconWidth)x$(_MSKit_IconHeight))."
<_MSKit_PkgFinding Include="MSKIT_PKG015" Condition="'$(MSKit_PackageIconSourcePath)'!='' AND Exists('$(MSKit_PackageIconSourcePath)') AND (('$(_MSKit_IconFormat)'!='PNG' AND '$(_MSKit_IconFormat)'!='JPEG') OR '$(_MSKit_IconWidth)'!='$(MSKit_PackageIconSize)' OR '$(_MSKit_IconHeight)'!='$(MSKit_PackageIconSize)')"
Text="Package '$(PackageId)' icon is not a $(MSKit_PackageIconSize)x$(MSKit_PackageIconSize) PNG or JPEG (format: $(_MSKit_IconFormat), size: $(_MSKit_IconWidth)x$(_MSKit_IconHeight))."
Rule="$(_MSKit_PkgRules)#icon" />
<_MSKit_PkgFinding Include="MSKIT_PKG016" Condition="'$(PackageReleaseNotes)'==''"
Text="Package '$(PackageId)' has no PackageReleaseNotes. Set them, or a link to the changelog (the kit links the releases page by default on GitHub, and on any host the generated readme knows with MSKit_PackageReadmeFrom)."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
</PropertyGroup>

<!-- Icon: PackageIconPath (the kit's company layer points it at .toolkit/res/package.icon.png),
packed as icon.png. -->
packed as icon.<extension, lowercased> (icon.png, icon.jpg) unless the project sets PackageIcon. -->
<PropertyGroup Condition="'$(PackageIcon)'=='' AND '$(PackageIconPath)'!='' AND Exists('$(PackageIconPath)')">
<PackageIcon>icon.png</PackageIcon>
<PackageIcon>icon$([System.IO.Path]::GetExtension('$(PackageIconPath)').ToLowerInvariant())</PackageIcon>
<_MSKit_PacksIcon>True</_MSKit_PacksIcon>
</PropertyGroup>
<PropertyGroup>
Expand All @@ -63,7 +63,7 @@

<ItemGroup>
<None Condition="'$(_MSKit_PacksReadme)'=='True'" Include="$(MSKit_PackageReadmeSourcePath)" Pack="true" PackagePath="\$(PackageReadmeFile)" Visible="false" />
<None Condition="'$(_MSKit_PacksIcon)'=='True'" Include="$(PackageIconPath)" Pack="true" PackagePath="\icon.png" Visible="false" />
<None Condition="'$(_MSKit_PacksIcon)'=='True'" Include="$(PackageIconPath)" Pack="true" PackagePath="\$(PackageIcon)" Visible="false" />

<None Include="build/**" PackagePath="build/" Pack="true" />
<None Include="buildMultiTargeting/**" PackagePath="buildMultiTargeting/" Pack="true" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public sealed class MSKit_PackageAssets : Task
[Output] public string HtmlTags { get; set; }
[Output] public string Alerts { get; set; }
[Output] public bool HasMermaid { get; set; }
[Output] public bool IconIsPng { get; set; }
[Output] public string IconFormat { get; set; }
[Output] public int IconWidth { get; set; }
[Output] public int IconHeight { get; set; }

Expand Down Expand Up @@ -96,16 +96,18 @@ public sealed class MSKit_PackageAssets : Task

if (!string.IsNullOrEmpty(IconPath) && File.Exists(IconPath))
{
var header = new byte[24];
using (var stream = File.OpenRead(IconPath))
var icon = File.ReadAllBytes(IconPath);
IconFormat = "unknown";
if (icon.Length >= 24 && icon[0] == 0x89 && icon[1] == 0x50 && icon[2] == 0x4E && icon[3] == 0x47)
{
var read = stream.Read(header, 0, header.Length);
IconIsPng = read == 24 && header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47;
IconFormat = "PNG";
IconWidth = BigEndian(icon, 16, 4);
IconHeight = BigEndian(icon, 20, 4);
}
if (IconIsPng)
else if (icon.Length >= 4 && icon[0] == 0xFF && icon[1] == 0xD8)
{
IconWidth = (header[16] << 24) | (header[17] << 16) | (header[18] << 8) | header[19];
IconHeight = (header[20] << 24) | (header[21] << 16) | (header[22] << 8) | header[23];
IconFormat = "JPEG";
ReadJpegSize(icon);
}
}

Expand All @@ -116,6 +118,31 @@ public sealed class MSKit_PackageAssets : Task
Alerts = string.Join(", ", alerts.Distinct());
}

private static int BigEndian(byte[] bytes, int offset, int count)
{
var value = 0;
for (var i = 0; i < count; i++) value = (value << 8) | bytes[offset + i];
return value;
}

// Walks the JPEG segments to the first start-of-frame marker (SOF0..SOF15 except DHT, JPG, DAC).
private void ReadJpegSize(byte[] jpeg)
{
var i = 2;
while (i + 8 < jpeg.Length && jpeg[i] == 0xFF)
{
var marker = jpeg[i + 1];
if (marker == 0xFF) { i++; continue; }
if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC)
{
IconHeight = BigEndian(jpeg, i + 5, 2);
IconWidth = BigEndian(jpeg, i + 7, 2);
return;
}
i += 2 + BigEndian(jpeg, i + 2, 2);
}
}

public string OutputPath { get; set; }
public string FindingsBase { get; set; }
public string ReleasesUrlPath { get; set; }
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/PackageIcon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.toolkit/
12 changes: 12 additions & 0 deletions tests/fixtures/PackageIcon/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>A fixture package that checks which icon is packed, and under which name.</Description>
<PackageTags>fixture icon</PackageTags>
<PackageIconPath Condition="'$(FixtureIcon)'!=''">$(MSBuildThisFileDirectory)Icons/$(FixtureIcon)</PackageIconPath>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory).toolkit/msbuild/init.props" />

</Project>
5 changes: 5 additions & 0 deletions tests/fixtures/PackageIcon/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>

<Import Project="$(MSBuildThisFileDirectory).toolkit/msbuild/init.targets" />

</Project>
Binary file added tests/fixtures/PackageIcon/Icons/ICON.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions tests/fixtures/PackageIcon/Icons/Icons.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- The kit's icon by default; FixtureIcon picks another file in this folder. -->
<PropertyGroup>
<PackageId>DragoAnt.Fixture.Icons</PackageId>
</PropertyGroup>

</Project>
4 changes: 4 additions & 0 deletions tests/fixtures/PackageIcon/Icons/Placeholder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Icons;

/// <summary>Placeholder.</summary>
public static class Placeholder;
Binary file added tests/fixtures/PackageIcon/Icons/icon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/fixtures/PackageIcon/Icons/small.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/fixtures/PackageIcon/Own/Own.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- Sets its own PackageIcon: the kit must not replace it or pack its own icon next to it. -->
<PropertyGroup>
<PackageId>DragoAnt.Fixture.OwnIcon</PackageId>
<PackageIcon>logo.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<None Include="logo.png" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions tests/fixtures/PackageIcon/Own/Placeholder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Own;

/// <summary>Placeholder.</summary>
public static class Placeholder;
Binary file added tests/fixtures/PackageIcon/Own/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions tests/package-icon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Package icon: packed under icon<extension, lowercased>, and a project's own PackageIcon is kept.
# Sourced by tests/run.sh: uses its pass, bad, $out, $here, $clean_env and $icon_fixture.

rm -rf "$icon_fixture"/*/bin "$icon_fixture"/*/obj

ic_pack() {
name="$1"; proj="$2"; shift 2
rm -rf "$out/icon-$name"
: > "$out/icon-$name.nuspec"; : > "$out/icon-$name.files"
if $clean_env dotnet pack "$icon_fixture/$proj/$proj.csproj" -c Release -nologo -o "$out/icon-$name" \
-p:MSKit_PackageChecksAsErrors=False "$@" > "$out/icon-$name.log" 2>&1; then
pass "icon $name: pack succeeds"
nupkg=$(ls "$out/icon-$name"/*.nupkg | grep -v '\.snupkg$' | head -n 1)
unzip -p "$nupkg" '*.nuspec' | tr -d '\r' > "$out/icon-$name.nuspec"
unzip -l "$nupkg" | awk 'NR>3 {print $4}' | grep -v '^$' > "$out/icon-$name.files"
else
bad "icon $name: pack failed (see $out/icon-$name.log)"; tail -n 20 "$out/icon-$name.log"
fi
}
ic_icon() {
actual=$(sed -n 's:.*<icon>\(.*\)</icon>.*:\1:p' "$out/icon-$1.nuspec")
[ "$actual" = "$2" ] && pass "icon $1: nuspec <icon>$actual</icon>" || bad "icon $1: nuspec <icon> expected '$2', got '$actual'"
}
ic_entry() { grep -qx -- "$2" "$out/icon-$1.files" && pass "icon $1: nupkg contains $2" || bad "icon $1: nupkg lacks $2"; }
ic_no_entry() { grep -qx -- "$2" "$out/icon-$1.files" && bad "icon $1: nupkg still contains $2" || pass "icon $1: nupkg has no $2"; }
ic_warns() { grep -q "warning $2" "$out/icon-$1.log" && pass "icon $1 warns $2" || bad "icon $1 does not warn $2 (see $out/icon-$1.log)"; }
ic_quiet() { grep -q "$2" "$out/icon-$1.log" && bad "icon $1 reports $2 (see $out/icon-$1.log)" || pass "icon $1 does not report $2"; }

# The kit's PNG icon.
ic_pack default Icons
ic_icon default icon.png
ic_entry default icon.png
ic_quiet default MSKIT_PKG015

# A JPEG PackageIconPath keeps its extension.
ic_pack jpg Icons -p:FixtureIcon=icon.jpg
ic_icon jpg icon.jpg
ic_entry jpg icon.jpg
ic_no_entry jpg icon.png
ic_quiet jpg NU5046
ic_quiet jpg MSKIT_PKG015

# A JPEG of the wrong size is still reported.
ic_pack jpg-small Icons -p:FixtureIcon=small.jpg
ic_icon jpg-small icon.jpg
ic_warns jpg-small MSKIT_PKG015

# An upper-case extension is lowercased.
ic_pack upper Icons -p:FixtureIcon=ICON.PNG
ic_icon upper icon.png
ic_entry upper icon.png
ic_no_entry upper ICON.PNG
ic_no_entry upper icon.PNG

# A project that sets PackageIcon keeps it; the kit's icon is not added.
ic_pack own Own
ic_icon own logo.png
ic_entry own logo.png
ic_no_entry own icon.png
ic_quiet own MSKIT_PKG015
4 changes: 3 additions & 1 deletion tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sample="$here/samples/MinimalLibrary"
fixtures="$here/tests/fixtures/PackageChecks"
readme_fixture="$here/tests/fixtures/PackageReadme"
tfm_fixture="$here/tests/fixtures/TfmConstants"
icon_fixture="$here/tests/fixtures/PackageIcon"
out="$here/dist/selftest"
run_tests=1
[ "${1:-}" = "--skip-tests" ] && run_tests=0
Expand All @@ -19,7 +20,7 @@ failures=0
pass() { echo "PASS $*"; }
bad() { echo "FAIL $*"; failures=$((failures+1)); }

for root in "$sample" "$fixtures" "$readme_fixture" "$tfm_fixture"; do
for root in "$sample" "$fixtures" "$readme_fixture" "$tfm_fixture" "$icon_fixture"; do
sh "$kit/.toolkit/update.sh" --source "$kit" --root "$root" > "$out/install.log" || { cat "$out/install.log"; exit 1; }
done
pass "update.sh installed the kit into the sample and the fixtures"
Expand Down Expand Up @@ -109,6 +110,7 @@ grep -q "MSKIT_PKG" "$out/checks-skip.log" && bad "MSKit_SkipPackageChecks=All d

. "$here/tests/package-readme.sh"
. "$here/tests/tfm-constants.sh"
. "$here/tests/package-icon.sh"
. "$here/tests/docs.sh"

echo
Expand Down
Loading