Skip to content

Replace dotnes.anese with dotnes.mesen emulator#369

Merged
jonathanpeppers merged 8 commits intomainfrom
feature/mesen-nuget-package
Mar 28, 2026
Merged

Replace dotnes.anese with dotnes.mesen emulator#369
jonathanpeppers merged 8 commits intomainfrom
feature/mesen-nuget-package

Conversation

@jonathanpeppers
Copy link
Copy Markdown
Owner

@jonathanpeppers jonathanpeppers commented Mar 28, 2026

Summary

Replace dotnes.anese with dotnes.mesen as the emulator for dotnet run.

Why

  • ANESE is MIT (bundleable) but x86-only -- no arm64 Windows/macOS support (see F5 in VS Code / C# Dev Kit #58)
  • Mesen2 is GPLv3 -- cannot bundle binaries in a NuGet package
  • Solution: the NuGet package ships dotnes.tasks.dll + MSBuild .targets that download Mesen from GitHub releases at build time

How it works

dotnet run  (in any sample)
  --> MSBuild imports build/dotnes.mesen.targets
  --> EnsureMesenInstalled task (in dotnes.tasks.dll):
      - Named mutex prevents parallel build races
      - Downloads Mesen zip from GitHub releases (with retries)
      - Verifies SHA256 against pinned hash
      - Extracts to NuGet package cache (shared across projects)
      - macOS: handles nested Mesen.app.zip extraction
  --> RunCommand points to cached Mesen executable
  --> ROM launches in Mesen

Not redistribution -- the NuGet package contains zero GPL code. The user's machine downloads Mesen directly from the original source.

Changes

  • New: EnsureMesenInstalled.cs in dotnes.tasks -- proper compiled MSBuild task with:
    • ICancelableTask + CancellationToken for Ctrl+C
    • Retry logic (configurable Retries + RetryDelayMilliseconds)
    • SHA256 hash verification (pinned per platform from GitHub API)
    • Named mutex for parallel build safety (try/finally)
    • AbandonedMutexException handling
    • Detailed logging for .binlog diagnostics
    • macOS nested zip extraction (Mesen.app.zip)
  • New: build/dotnes.mesen.targets -- platform detection + RunCommand
  • Simplified: dotnes.mesen.csproj -- NuGet package shipping dotnes.tasks.dll + .targets
  • Updated: all 45 sample .csproj + project template
  • Platforms: Windows, Linux x64/arm64, macOS x64/arm64

ANESE is MIT but x86-only (no arm64). Mesen2 is GPLv3, so we can't
bundle binaries. Instead, the dotnes.mesen NuGet package contains only
MSBuild .targets that download Mesen from GitHub releases at build
time and cache it in obj/mesen/. This is not redistribution — the
user's machine downloads directly from the original source.

- Created build/dotnes.mesen.targets with download/unzip + RunCommand
- Simplified dotnes.mesen.csproj to a NuGet package (13KB, no binaries)
- Updated all 45 sample .csproj files: dotnes.anese -> dotnes.mesen
- Updated project template to use dotnes.mesen
- Mesen download skipped on CI (local dev tool only)
- Supports Windows, Linux x64/arm64, macOS x64/arm64

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 28, 2026 21:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the ANESE-based dotnet run emulator integration with a new dotnes.mesen package that downloads/unzips Mesen2 at build time and updates samples/templates to reference it.

Changes:

  • Added dotnes.mesen.targets to download/unzip Mesen2 and set RunCommand/RunArguments for dotnet run.
  • Simplified dotnes.mesen.csproj to package MSBuild assets (no bundled emulator binaries).
  • Updated the project template + sample projects to reference dotnes.mesen instead of dotnes.anese.

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/dotnes.templates/templates/nes/hello.csproj Switch template emulator package reference to dotnes.mesen.
src/dotnes.mesen/dotnes.mesen.csproj Convert dotnes.mesen into a NuGet that packs MSBuild assets (no binaries).
src/dotnes.mesen/build/dotnes.mesen.targets Add MSBuild logic to download/unzip Mesen2 and wire dotnet run to launch the ROM.
samples/vrambuffer/vrambuffer.csproj Swap emulator package reference to dotnes.mesen.
samples/vertscroll/vertscroll.csproj Swap emulator package reference to dotnes.mesen.
samples/transtable/transtable.csproj Swap emulator package reference to dotnes.mesen.
samples/tint/tint.csproj Swap emulator package reference to dotnes.mesen.
samples/tileset1/tileset1.csproj Swap emulator package reference to dotnes.mesen.
samples/statusbar/statusbar.csproj Swap emulator package reference to dotnes.mesen.
samples/staticsprite/staticsprite.csproj Swap emulator package reference to dotnes.mesen.
samples/sprites/sprites.csproj Swap emulator package reference to dotnes.mesen.
samples/snake/snake.csproj Swap emulator package reference to dotnes.mesen.
samples/slideshow/slideshow.csproj Swap emulator package reference to dotnes.mesen.
samples/siegegame/siegegame.csproj Swap emulator package reference to dotnes.mesen.
samples/shoot2/shoot2.csproj Swap emulator package reference to dotnes.mesen.
samples/scroll/scroll.csproj Swap emulator package reference to dotnes.mesen.
samples/scoreboard/scoreboard.csproj Swap emulator package reference to dotnes.mesen.
samples/rletitle/rletitle.csproj Swap emulator package reference to dotnes.mesen.
samples/procgen/procgen.csproj Swap emulator package reference to dotnes.mesen.
samples/ppuhello/ppuhello.csproj Swap emulator package reference to dotnes.mesen.
samples/pong/pong.csproj Swap emulator package reference to dotnes.mesen.
samples/peekpoke/peekpoke.csproj Swap emulator package reference to dotnes.mesen.
samples/oamstatic/oamstatic.csproj Swap emulator package reference to dotnes.mesen.
samples/nestedloop/nestedloop.csproj Swap emulator package reference to dotnes.mesen.
samples/music/music.csproj Swap emulator package reference to dotnes.mesen.
samples/multifile/multifile.csproj Swap emulator package reference to dotnes.mesen.
samples/movingsprite/movingsprite.csproj Swap emulator package reference to dotnes.mesen.
samples/monobitmap/monobitmap.csproj Swap emulator package reference to dotnes.mesen.
samples/mmc1/mmc1.csproj Swap emulator package reference to dotnes.mesen.
samples/metatrigger/metatrigger.csproj Swap emulator package reference to dotnes.mesen.
samples/metasprites/metasprites.csproj Swap emulator package reference to dotnes.mesen.
samples/metacursor/metacursor.csproj Swap emulator package reference to dotnes.mesen.
samples/lols/lols.csproj Swap emulator package reference to dotnes.mesen.
samples/irq/irq.csproj Swap emulator package reference to dotnes.mesen.
samples/horizscroll/horizscroll.csproj Swap emulator package reference to dotnes.mesen.
samples/horizmask/horizmask.csproj Swap emulator package reference to dotnes.mesen.
samples/hello/hello.csproj Swap emulator package reference to dotnes.mesen.
samples/flicker/flicker.csproj Swap emulator package reference to dotnes.mesen.
samples/fami/fami.csproj Swap emulator package reference to dotnes.mesen.
samples/fade/fade.csproj Swap emulator package reference to dotnes.mesen.
samples/conio/conio.csproj Swap emulator package reference to dotnes.mesen.
samples/climber/climber.csproj Swap emulator package reference to dotnes.mesen.
samples/bigsprites/bigsprites.csproj Swap emulator package reference to dotnes.mesen.
samples/battery/battery.csproj Swap emulator package reference to dotnes.mesen.
samples/bankswitch/bankswitch.csproj Swap emulator package reference to dotnes.mesen.
samples/attributetable/attributetable.csproj Swap emulator package reference to dotnes.mesen.
samples/aputest/aputest.csproj Swap emulator package reference to dotnes.mesen.
samples/animation/animation.csproj Swap emulator package reference to dotnes.mesen.

Comment thread src/dotnes.mesen/build/dotnes.mesen.targets
Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
jonathanpeppers and others added 4 commits March 28, 2026 16:29
- Download to \../bin/ so Mesen is cached
  alongside the NuGet package, shared across all projects
- Remove _SkipMesenDownload — download on CI too
- Delete zip after extraction to save disk space
- Simpler .targets with fewer targets and conditions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify downloaded zip against known SHA256 digests from GitHub's
release asset API. Uses an inline MSBuild task with
SHA256.ComputeHash to check integrity before extraction.

Hashes sourced from: gh api repos/SourMesen/Mesen2/releases/tags/2.1.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use GetFullPath() to ensure _MesenDir is absolute so RunCommand
  works regardless of RunWorkingDirectory
- Add Error check after download (fail fast on download failure)
- Add Error check after unzip (fail fast if extraction produces no exe)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI confirmed the race: multiple samples building in parallel all hit
_MesenDownload simultaneously, causing 'file used by another process'
errors on the shared zip file.

Fix: acquire a Global named mutex before download/unzip. Second project
to acquire the lock re-checks Exists() and skips if already done.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 5 comments.

Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
Comment thread src/dotnes.mesen/build/dotnes.mesen.targets Outdated
Comment thread src/dotnes.mesen/dotnes.mesen.csproj
Move download/verify/unzip logic to EnsureMesenInstalled.cs in
dotnes.tasks — a proper compiled MSBuild task with:
- Named mutex for parallel build safety (try/finally guarantees release)
- SHA256 hash verification
- HttpClient for downloads
- ZipFile for extraction

Ship dotnes.tasks.dll in the dotnes.mesen nupkg's build/ folder so
the UsingTask can load it. No more fragile inline C# in XML.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/dotnes.tasks/MSBuild/EnsureMesenInstalled.cs Outdated
Comment thread src/dotnes.tasks/MSBuild/EnsureMesenInstalled.cs Outdated
Comment thread src/dotnes.tasks/MSBuild/EnsureMesenInstalled.cs Outdated
Comment thread src/dotnes.tasks/MSBuild/EnsureMesenInstalled.cs Outdated
jonathanpeppers and others added 2 commits March 28, 2026 17:10
Review comments addressed:
- ICancelableTask: implement ICancelableTask, pass CancellationToken
  through HttpClient.GetAsync for Ctrl+C support
- Logging: all steps logged with MessageImportance for .binlog visibility
- Retry logic: Retries + RetryDelayMilliseconds parameters with retry loop
- NETSTANDARD2_0 ifdef: removed, use ZipArchive entry-by-entry extraction
  with overwrite:true (works on all TFMs)
- AbandonedMutexException: caught and handled (safe to proceed)
- License metadata: added PackageLicenseExpression to csproj

CI fix:
- macOS: Mesen ships as nested zip (Mesen.app.zip inside outer zip),
  extract both layers. Exe path is Mesen.app/Contents/MacOS/Mesen.
- No more static HttpClient (caused issues with socket reuse across builds)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DownloadWithRetriesAsync is async with CancellationToken through
  GetAsync, ReadAsStreamAsync, and CopyToAsync
- Execute() calls .GetAwaiter().GetResult() at the sync boundary
- Single static HttpClient avoids socket exhaustion across retries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers requested a review from Copilot March 28, 2026 22:18
@jonathanpeppers jonathanpeppers merged commit d889175 into main Mar 28, 2026
1 check passed
@jonathanpeppers jonathanpeppers deleted the feature/mesen-nuget-package branch March 28, 2026 22:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 4 comments.

Comment on lines +119 to +146
for (int attempt = 1; attempt <= Retries; attempt++)
{
token.ThrowIfCancellationRequested();
try
{
Log.LogMessage(MessageImportance.High, "Downloading {0} (attempt {1}/{2})...", url, attempt, Retries);
using var response = await s_http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();
Log.LogMessage(MessageImportance.Normal, "Response: {0}, Content-Length: {1}",
response.StatusCode, response.Content.Headers.ContentLength);

using var content = await response.Content.ReadAsStreamAsync();
using var fs = File.Create(dest);
await content.CopyToAsync(fs, 81920, token);
Log.LogMessage(MessageImportance.Normal, "Downloaded {0} bytes to {1}.", fs.Length, dest);
return;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex) when (attempt < Retries)
{
Log.LogMessage(MessageImportance.High, "Download failed (attempt {0}/{1}): {2}", attempt, Retries, ex.Message);
Thread.Sleep(RetryDelayMilliseconds);
}
}
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DownloadWithRetriesAsync only logs failures for attempts < Retries; on the final attempt any exception (e.g., HTTP 404/403, IO errors) will bubble out of the task without a Log.LogError, producing a less actionable MSBuild failure. Catch/log the final failure as an error (include the URL and destination path) and have Execute() return false rather than relying on an unhandled exception for control flow.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +144
{
Log.LogMessage(MessageImportance.High, "Download failed (attempt {0}/{1}): {2}", attempt, Retries, ex.Message);
Thread.Sleep(RetryDelayMilliseconds);
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread.Sleep inside an async method blocks an MSBuild worker thread during retries and ignores cancellation while sleeping. Use an awaited delay (Task.Delay) that honors the cancellation token instead, so Ctrl+C cancels promptly and MSBuild remains responsive.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +75
DownloadWithRetries(DownloadUrl, zipPath);

if (!VerifySha256(zipPath))
return false;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This task implements ICancelableTask, but OperationCanceledException from the download path will currently escape Execute() as an unhandled exception, which tends to produce a noisy stack trace rather than a clean MSBuild cancellation. Catch OperationCanceledException in Execute(), log a low-importance message, and return false so Ctrl+C aborts cleanly.

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +176
foreach (var entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name))
continue;
var destPath = Path.Combine(DestDir, entry.FullName);
var destDir = Path.GetDirectoryName(destPath)!;
Directory.CreateDirectory(destDir);
entry.ExtractToFile(destPath, overwrite: true);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zip extraction uses Path.Combine(DestDir, entry.FullName) without validating that the resulting path stays under DestDir. A crafted zip entry (e.g., containing ../ segments or an absolute path) could write outside the intended directory (Zip Slip). Normalize the combined path and verify it is rooted under DestDir before extracting.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants