Replace dotnes.anese with dotnes.mesen emulator#369
Conversation
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>
There was a problem hiding this comment.
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.targetsto download/unzip Mesen2 and setRunCommand/RunArgumentsfordotnet run. - Simplified
dotnes.mesen.csprojto package MSBuild assets (no bundled emulator binaries). - Updated the project template + sample projects to reference
dotnes.meseninstead ofdotnes.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. |
- 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>
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>
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>
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| { | ||
| Log.LogMessage(MessageImportance.High, "Download failed (attempt {0}/{1}): {2}", attempt, Retries, ex.Message); | ||
| Thread.Sleep(RetryDelayMilliseconds); | ||
| } |
There was a problem hiding this comment.
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.
| DownloadWithRetries(DownloadUrl, zipPath); | ||
|
|
||
| if (!VerifySha256(zipPath)) | ||
| return false; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
Summary
Replace
dotnes.anesewithdotnes.mesenas the emulator fordotnet run.Why
dotnes.tasks.dll+ MSBuild.targetsthat download Mesen from GitHub releases at build timeHow it works
Not redistribution -- the NuGet package contains zero GPL code. The user's machine downloads Mesen directly from the original source.
Changes
EnsureMesenInstalled.csin dotnes.tasks -- proper compiled MSBuild task with:build/dotnes.mesen.targets-- platform detection + RunCommanddotnes.mesen.csproj-- NuGet package shipping dotnes.tasks.dll + .targets