Skip to content

Auto-updater: stable and release candidate channels, with in-place install - #841

Merged
aidankinzett merged 17 commits into
nx2-experimentalfrom
updater/tagged-channels
Nov 14, 2026
Merged

aidankinzett merged 17 commits into
nx2-experimentalfrom
updater/tagged-channels

Conversation

@aidankinzett

@aidankinzett aidankinzett commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The update checker inherited from Eden was switched off in CI and still pointed at Eden's servers. This makes it work for Eglantine's tagged releases.

  • Releases are read from this repo's GitHub releases. Only vX.Y.Z and vX.Y.Z-rcN tags count; daily and branch builds are ignored and do not check for updates themselves. (Branch builds will be addressed in Tester builds: follow a branch or PR and build it on the user's machine #842)
  • Two update channels: Stable gets stable releases only, Release candidate gets both. The channel is a saved setting in Configure > General. The first tagged build a user runs sets it from its own tag, and after that only the user changes it, so someone who installed an rc keeps getting rcs after a stable release lands.
  • The update dialog can skip a version, and checks the download against the size and SHA-256 GitHub publishes for the asset.
  • Help > Check for Updates runs the check on demand.
  • Windows zip installs and AppImages update in place and restart. The previous version is kept and can be brought back from Help > Restore Previous Version. The portable user folder is never touched. Other packages (tar.gz, macOS) save the download to the Downloads folder.
  • CI turns the checker on for stable and rc builds only, and ships eglantine-updater.exe in the Windows zips.

The installer and the Windows helper are ported from Lichii (GPL-3.0-or-later), and the channel model follows Orchid's. Three fixes over Lichii's code: the helper now really links the C runtime statically, manifest lines like ./user/x can no longer get past the user folder protection, and an empty staging folder is refused instead of wiping the install.

Notes for reviewers:

  • The built-in news feed (builtin_news.cpp) used the same Eden release endpoints, so it now reads this repo's releases too.
  • rc3 shipped with the checker off, so rc3 users update by hand one last time.
  • The first in-place update has no manifest of the old version, so files the new version dropped are left behind. From the second update on they are removed.
  • The SHA-256 comes from the same GitHub release as the file. It catches broken downloads, not a compromised release. Nothing is signed.

Type of change

  • Bug fix
  • Game compatibility / game-support
  • Performance
  • New feature
  • Refactor / cleanup
  • CI / build
  • Documentation

Testing

  • Tested on Windows
  • Tested on Linux - see comment
  • Tested on macOS - see comment

Windows, MSVC build faked as release v0.1.0-rc2 in a portable folder:

  • offered v0.1.0-rc3 on the Release candidate channel, nothing on Stable, Skip This Version, download + unpack + restart into rc3, user folder unchanged all worked
  • Unit tests for tag parsing, channel selection and the installer pass (375 assertions in 26 cases). They were linked on their own, because the full tests target does not currently build with MSVC.
  • The Qt app builds with ENABLE_UPDATE_CHECKER both on and off.

Games tested:

Not applicable, no emulation changes.

Checklist

  • I built it locally and it compiles without new warnings
  • I did not add any copyrighted game data, keys or firmware
  • Docs updated if needed
  • I added AI Disclosure if one was used

Screenshots / logs

update available update config updated

AI Disclosure: Orchestrated by Fable 5.2 driving other Claude models

Summary by CodeRabbit

  • New Features
    • Added stable and release-candidate update channels, with manual update checks and options to skip a version.
    • Added update downloads and installation for supported platforms, plus the ability to restore a previous version on Linux and Windows.
    • Update downloads verify available file size and checksum information.
  • Improvements
    • Update checks now select releases and downloads based on the build channel and platform.
    • Improved secure connections by using available system certificate stores.

aidankinzett and others added 10 commits November 12, 2026 13:20
Stable and release candidate builds are configured with
ENABLE_UPDATE_CHECKER=ON and told which repository to check
(EGLANTINE_UPDATE_REPO) and which release asset they correspond to
(EGLANTINE_RELEASE_PLATFORM). Daily and branch builds keep it off.
The update checker and the built-in news feed still pointed at Eden's
servers. Releases now come from api.github.com for the repository in
EGLANTINE_UPDATE_REPO, and each asset carries the size and SHA-256
GitHub publishes for it.

Release assets are picked by exact name from the platform the build was
made for (EGLANTINE_RELEASE_PLATFORM), with a PGO build falling back to
the standard asset.
Releases are compared by tag (vX.Y.Z or vX.Y.Z-rcN) instead of by string
inequality with the latest release. The stable channel is offered stable
releases only; the release candidate channel is offered both. A stable
release is newer than every candidate of the same version. Releases with
other tags (daily, branch) and releases without an asset for this
platform are skipped.

A failed check now says why (rate limited, HTTP error, no network).
The update channel (Stable or Release candidate) is a saved setting in
Configure > General. The first tagged release build to run sets it from
its own tag, and after that only the user changes it, so someone who
installed a release candidate keeps getting candidates after a stable
release lands.

The update dialog names the channel, can skip a version, saves to the
Downloads folder, follows GitHub's redirect, retries transient failures
and discards a download whose size or SHA-256 does not match what GitHub
published. Help > Check for Updates runs the check on demand and reports
the result either way.
UpdateInstaller installs a staged folder over an install folder: files
the previous update installed and the new one dropped are removed,
everything replaced is kept in .update-backup for one restore, a failed
step puts every file back, and the portable user folder is never
touched. eglantine-updater.exe runs it on Windows after the emulator has
closed, since a running exe and its DLLs cannot be replaced.

Changes from Lichii's code: the helper really links the C runtime
statically (the project's /MD overrode the target property), manifest
lines containing "." are ignored like ".." ones, and an empty staging
folder is refused instead of removing the whole install.
On Windows a writable install with the helper beside it downloads the
release zip, unpacks it into .update-staging and hands over to
eglantine-updater.exe, which swaps the files once the emulator has
closed and starts it again. An AppImage is replaced directly and
restarted. Anything else (tar.gz, macOS, a read-only folder) keeps
saving the download to the Downloads folder.

Help > Restore Previous Version appears once an update has left a
backup, and goes back to it.
The Windows jobs build explicit targets, so eglantine-updater.exe was
never produced. It is only named for release builds, where the update
checker (and with it the target) exists.
"Check for updates" and "Update channel" sit together in an Updates box
instead of among the general settings. The box is hidden on builds
without the update checker.
Catch2 cannot stringify std::filesystem::file_time_type with libc++, so
decomposing the comparison failed to compile on macOS. Compare inside
double parentheses so it is not decomposed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@aidankinzett

Copy link
Copy Markdown
Collaborator Author

macOS testing

Apple Silicon, Release build with ENABLE_UPDATE_CHECKER=ON, EGLANTINE_RELEASE_PLATFORM=macos-arm64, faked as release v0.1.0-rc2 (GIT-TAG / GIT-RELEASE), run from a portable user folder and driven through the GUI.

Check Result
Offers v0.1.0-rc3 on the Release candidate channel Pass: "v0.1.0-rc3 is available. You are running v0.1.0-rc2. Update channel: Release candidate"
Nothing offered on Stable Pass: "You are running the newest version on the Stable channel"
Channel set from the build's own tag on first run Pass: update_channel=2 (Release candidate) after first launch
Skip This Version Pass: skipped_update=v0.1.0-rc3 saved, no dialog on next startup; a manual check still offers it
Help > Check for Updates Pass
Download on macOS Pass: save prompt defaults to Downloads; the .dmg size and SHA-256 match what GitHub publishes for the asset
Changing channel in Configure > General Pass: saved as Stable (update_channel=1); skipped_update was cleared
Updater unit tests (./bin/tests "[updater]") Pass: 387 assertions in 28 cases (after the compile fix below)

Not applicable / not tested: in-place install and restart, Restore Previous Version (Windows zip / AppImage only; there is no Restore entry in Help on macOS).

Notes:

  • src/tests/common/update_installer.cpp did not compile with Apple clang/libc++: Catch2 cannot stringify std::filesystem::file_time_type in the last_write_time CHECK. Fixed in a follow-up commit.
  • The full tests target still fails on macOS in src/tests/common/page_table.cpp (-Wshadow), which is unrelated to this PR. I linked the tests without it.

@aidankinzett

Copy link
Copy Markdown
Collaborator Author

Linux testing

x86_64 Arch-based Linux (CachyOS), Release build with ENABLE_UPDATE_CHECKER=ON, EGLANTINE_RELEASE_PLATFORM=linux-x86_64, faked as release v0.1.0-rc2 (GIT-TAG / GIT-RELEASE). Tested both as an AppImage (built with linuxdeploy + the Qt plugin, as build-linux.yml does) and as a tar.gz, each with its own throwaway config/data folder, driven through the GUI. Checked against the real v0.1.0-rc3 release.

Check tar.gz AppImage
Updater unit tests (./bin/tests "[updater]") Pass: 387 assertions in 28 cases same build
Offers v0.1.0-rc3 on the Release candidate channel Pass Pass
Nothing offered on Stable Pass Pass
Channel set from the build's own tag on first run Pass Pass
Skip This Version Pass Pass
Help > Check for Updates Pass Pass
Download: size and SHA-256 match GitHub's Pass (save prompt, no in-place install) n/a (installs in place)
In-place install replaces the AppImage, old one kept n/a Pass
Restart into rc3 n/a Pass
Help > Restore Previous Version n/a Pass

@aidankinzett aidankinzett changed the title Auto-updater: stable and release candidate channels, with in-place install- #838 Auto-updater: stable and release candidate channels, with in-place install Nov 12, 2026
@aidankinzett
aidankinzett added this pull request to stack #843 November 12, 2026 10:47
@aidankinzett
aidankinzett force-pushed the updater/tagged-channels branch 3 times, most recently from 6fe6bb0 to 0520003 Compare November 12, 2026 12:19
@kraibse

kraibse commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

I have not tested this myself yet but I think centering the three buttons and the "Would you like to download this update?" would be a good idea.

rdodoa pushed a commit to rdodoa/Eglantine that referenced this pull request Nov 13, 2026
…rrays (port Eglantine-Community#354, fixes the dark water grid) (#1485)

* shader: don't track bindless handles through texture results or float math

The guest compiler reuses registers: in Ocarina of Time's clustered
lighting shaders the register holding a light's shadow map handle holds
a shadow sample's result on another path, so the handle reaching a later
sample is a Phi of the handle and that result. Track then searched
through the sample's coordinates and returned the first constant buffer
word behind them: a float (0.001 in cbuf6+0x430), bound as a texture
handle (a TIC index far past the pool, a null view).

A handle is never made by a texture instruction or by floating-point
math, so the search no longer looks into either, and leaves such a
handle to the handle program builder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader, video_core: index a descriptor array with a handle table's per-invocation index

Ocarina of Time's clustered lighting (330 world fragment shaders) loops
over the lights of each pixel's cluster: it reads a light index from the
pixel's own list, then that light's shadow map handle from a table of
96-byte light entries (the first word of a 16-byte load), and samples
the shadow map with depth comparison. No single handle serves a draw,
so the handle program builder gave up (on CompositeExtractU32x4, and on
the per-pixel list address behind it) and the earlier search through
loads bound the constant buffer word holding the table's address
(cbuf0+0x520) as the handle: an ASTC color texture, an R32F or
A2B10G10R10_UINT render target, or null, depending on the address. A
depth-compare sample of a color or integer image is undefined. On AMD it
is the likely source of the neon green that the probe captures picked up
and the GI fed back into the scene, the tint growing over time.

Such a handle now gets a descriptor array, as a handle read from a
constant buffer at a dynamic offset does:

- The builder handles CompositeExtractU32x3/x4 like the x2 case.
- It may take one word as the array index: a word read at an address it
  can't compute (each invocation's own), scaled to a byte offset, that
  gives the address of the read holding the handle.
- The program computes the handle of each element through
  HandleOp::Index, so the draw binds the first 64 table entries
  (LICHII_BINDLESS_TABLE, 1 to 1024; 0 turns these arrays off).
- The shader indexes the array with the index word it read (NonUniform
  where supported), clamped to the array, provided the word is defined
  wherever the texture instruction runs. A dominator tree of the program
  decides that; otherwise every invocation takes the first element.
- The texture cap divides the descriptor budget by arrays rather than by
  the instructions sharing them.
- Programs drop trailing Tees, so instructions reading the same handle
  share a descriptor. Every light's samples in a shader now share one
  64-element array.

Per draw, the table is read in one go: each constant buffer word and
storage buffer address once, with one sampler lookup per run of equal
samplers. FillImageViews also skips an element repeating the view before
it. The first 64 changes in what a table holds are logged ("Handle
table: ... distinct: ticN@element"), as are any handles still left to
the earlier search, and the startup log reports LICHII_BINDLESS_TABLE.

An offline sweep of the game's 1768 fragment and 493 compute shaders
finds no handle left to the earlier search (4510 before). The 5670 shadow
samples now read the table, the other shaders' descriptors are
unchanged, and all the SPIR-V validates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* vulkan: bind the null image for depth-compare samples of uncomparable images

Sampling with depth comparison requires an image view format with
VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_DEPTH_COMPARISON_BIT. Without it the
result is undefined and differs between vendors, and a garbage value
(even a NaN) can survive a shader multiplying the shadow factor by zero.
A depth-compare descriptor now gets the null image instead in two cases:

- The handle was read from memory (a handle program, such as an unused
  entry of a light table).
- The image is an integer or block-compressed color texture, which no
  game compares on purpose.

A single-channel color texture behind a constant buffer handle keeps its
view, since a game may compare it deliberately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* texture_cache: visit a repeated view again once the pass has deleted views

FillImageViews gives an element that repeats the one before it that
element's view instead of visiting it again. A deletion earlier in the
same pass can take that view with it: scaling a blacklisted image down
erases the image's views (InvalidateScale), and a following element with
the same index, blacklisted too, then read the erased slot. Once the
pass has deleted views, such an element is visited again, as every
element was before. The pass repeats after a deletion anyway, and the
next one takes the shortcut again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: make handle table arrays a host capability, which OpenGL lacks

OpenGL pipelines bind at most 64 textures across their stages
(MAX_TEXTURES), in fixed-size arrays, while the OpenGL host's descriptor
limits are the 1024 defaults, so the texture pass doesn't hold its arrays
to that. A 64-element handle table array and any other texture would fail
the pipeline's assert and overrun those arrays. Handle table arrays are
now made only for a host with HostTranslateInfo::support_handle_table_arrays:
Vulkan and the offline shader sweep. On OpenGL such a handle keeps the
constant buffer word the earlier search finds, as before them. A test
covers both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: fit texture arrays to the sampler limits of the stage and pipeline

A texture is a combined image sampler in Vulkan, so it counts against
maxPerStageDescriptorSamplers and maxDescriptorSetSamplers as well as
the sampled image limits. LimitSampledTextureArrays checked only the
sampled image and resource limits, so a 64-element handle table array
could exceed a device's sampler limits and make an invalid pipeline
layout. The descriptor set limits also count the descriptors of every
stage, while each stage sized its arrays as if it had the set to itself.

Texture arrays are now capped by the sampler limits too (texel buffers
still count only as sampled images), and before its stages are emitted
a graphics pipeline shrinks the arrays of all its stages together until
their textures fit the set limits. The Vulkan host passes
maxPerStageDescriptorSamplers in HostTranslateInfo. A test covers the
per-stage and pipeline limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader-sweep: index sampled image arrays nonuniformly, as Vulkan does

The offline profile enables handle table arrays like the Vulkan host,
but not sampled image array nonuniform indexing, so it emitted their
per-invocation accesses without the NonUniform decoration. It now enables
it, matching the SPIR-V the Vulkan backend emits for these arrays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* vulkan: make handle table arrays only with nonuniform sampled image indexing

A handle table array is indexed with a word each invocation reads for
itself. Without shaderSampledImageArrayNonUniformIndexing the index must
be dynamically uniform, and the emitter can't decorate the access
NonUniform, so on such a device the array would be read with undefined
results. The Vulkan host now enables handle table arrays only when the
device supports that feature; otherwise such a handle keeps the constant
buffer word the earlier search finds, as on OpenGL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* vulkan: fit a dual vertex program's texture arrays to the stage limits

Vertex A and Vertex B each fit their texture arrays to the per-stage
descriptor limits when they are translated, but merged they are one
stage, and nothing checked that stage: the pipeline pass only checks
the descriptor set limits. The merged program now goes through
LimitSampledTextureArrays as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: join the handle programs of a dual vertex program's halves

MergeDualVertexPrograms starts from Vertex A's Info and adds Vertex B's
descriptors, which kept the indices of Vertex B's handle programs but
not the programs. Such a descriptor indexed Vertex A's list, past its
end when Vertex A has none, and Vertex B's programs read storage buffers
at Vertex B's bindings, which JoinStorageInfo renumbers.

JoinStorageInfo now returns the binding each buffer gets in the merged
program, and JoinTextureInfo appends Vertex B's handle programs with
those bindings, once each as the texture pass keeps them, and remaps
the program index of Vertex B's descriptors. A test merges two programs
whose storage buffers swap bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: point Vertex B's instructions at its joined descriptors and buffers

Joining Vertex B's descriptors and storage buffers onto Vertex A's can
give them new indices, but Vertex B's instructions kept the ones from
its own lists. Whenever Vertex A had descriptors or buffers of the same
kind, Vertex B's samples, image accesses, rescaling tests and storage
loads, stores and atomics read Vertex A's.

JoinTextureInfo now returns the index each of Vertex B's descriptors
gets in the merged lists, and MergeDualVertexPrograms renumbers Vertex
B's blocks with those and the bindings JoinStorageInfo returns. Vertex
A's instructions keep their indices. Tests merge programs whose
descriptors and buffers move, and remap each kind directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: count texture arrays as the descriptors they become

The texture pass's first cap counted arrays by their constant buffer
word, handle program, type and depth, so arrays that Descriptors::Add
keeps apart, such as one table sampled with two samplers, counted once
and each got the whole budget. They are now counted by the fields
Descriptors::Add compares, all but the count, which the cap sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: count texture arrays by the type the texture pass gives them

The first cap counted texture arrays by the type each instruction had
before the replace loop, which sets some types only later: a size query
carries none until it takes the type of another instruction with the
same handle program or the one its handle reads as, and a 1D fetch of a
texel buffer becomes a buffer. A size query and a 2D sample of one table
counted as two arrays and halved the cap. The count now uses the type
the replace loop gives, from one helper both share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* vulkan: log handle tables only when EDEN_NX2_HANDLE_TABLE_NOTES is set

HandleTableNotes logged until 64 lines were written, but a table that
doesn't change writes no line, so in steady play every draw and dispatch
with a handle table kept copying its texture indices and taking the notes'
lock for a log that never came. The notes are a diagnostic: they are now
off unless the switch is set, and the 64-line limit stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: count texture arrays cut to one element, merge descriptors a cut makes equal

A texture array that the limits cut to a single element looked like a single texture. The
stage check (LimitSampledTextureArrays) skipped a program whose arrays the pass had already
cut to one element, and the pipeline check did the same for arrays a stage had cut, so a
stage (compute included) or a pipeline over the limits reached layout creation unchecked.
TextureDescriptor::is_descriptor_array now records that the shader indexes the descriptor,
and both checks count such arrays whatever their count. A program that reads its textures
only on their own is left as before.

A cut also left equal descriptors that the pass would have merged: an array cut to the
elements of another descriptor of its texture, or to the single texture a descriptor of its
own reads. The limits counted both and could fail a stage that fits. One element has no
stride, so an array cut to one takes a single texture's, and CapTextureArrays merges the
descriptors that are then equal, pointing the instructions and the rescaling tests at the
one kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader, vulkan: fit handle table arrays to nx2's mesh fallback and cache records

Merging nx2 brought code that the handle table arrays meet for the first
time:

- Mesh and task emulation through compute (Eglantine-Community#569, Eglantine-Community#841). Task and mesh
  programs run in one host compute stage together with the fallback's
  storage buffers. Their arrays now fit that stage's sampler, sampled
  image and resource limits (LimitSharedStageSampledTextureArrays), and
  the fallback also checks the stage's sampler limit. The pipeline-wide
  limit runs before the fallback, because cutting arrays can merge
  descriptors.
- The fallback numbered its output buffer's binding by descriptor
  elements. An array takes one binding (DefineTextures and the
  descriptor layout builder), so it now counts descriptors. With arrays
  of one element, as before this branch, both counts agree.
- The environment read properties of cache records (Eglantine-Community#815) now include
  support_handle_table_arrays. A record made with handle table arrays
  reads other environment words than one made without them.
- The shader sweep fits the arrays of a pipeline's emitted stages to one
  descriptor set, as the runtime does.

Tests: handle table support changes the environment read properties;
task and mesh arrays share the compute stage budget (limited by
resources, by samplers, and too small even after cutting); joined
storage descriptors keep Eglantine-Community#816's write bounds of both halves. The handle
unit tests pass with a small Catch2 runner on Linux: 23 cases, 386
assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* video_core: track all 64 storage buffer bases when reading a handle table

EvaluateTextureHandles reads each storage buffer's base once for all the
elements of a table, and kept which bases it had read in a 32-bit mask.
Native programs bind up to Info::MAX_SSBOS (64) storage buffers, and
Ocarina of Time binds 37. 1U << binding is undefined from binding 32 on;
on x86 it wraps, so binding 33 took binding 1's bit, counted its base as
read, and read the handle from an uninitialized base. EvaluateTextureHandle,
for one element, has no mask and was right.

The mask is 64 bits now. A test reads binding 1, then binding 33, for two
elements: it got a wrong handle before and passes now.

Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* texture_pass: fix the handle table size at 64 entries, drop LICHII_BINDLESS_TABLE

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: drop branch hints from the handle table notes checks

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: jpkto <255363096+jpkto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
@aidankinzett

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 053dd5bf-f8e1-4518-8af7-92dbd8b8541d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
Walkthrough

This change adds GitHub-based release checks with stable and release-candidate channels, platform asset selection, and update prompts. It also adds download verification, platform-specific installation and rollback, shared certificate-store configuration, and release-build configuration for the update checker.

Changes: Release Update and Installation

Layer / File(s) Summary
Release metadata and network support
.github/workflows/build-linux.yml, .github/workflows/build-macos.yml, .github/workflows/build-windows.yml, CMakeLists.txt, CMakeModules/GenerateSCMRev.cmake, src/common/net/*, src/common/scm_rev.*, src/core/hle/service/bcat/news/builtin_news.cpp, src/core/hle/service/ssl/ssl_backend_openssl.cpp, src/qt_common/discord/discord_impl.cpp, src/web_service/web_backend.cpp, src/yuzu/configuration/nvidia_dlss_panel.cpp, src/yuzu/util/icon_search.cpp
Build configuration supplies the update repository and release platform. Network code fetches GitHub releases and matches assets by platform and package, using asset size and SHA-256 metadata when available. HTTP clients use shared CA-store configuration.
Update channels and selection
src/common/settings_enums.h, src/frontend_common/update_checker.*, src/frontend_common/CMakeLists.txt, src/qt_common/config/*, src/tests/CMakeLists.txt, src/tests/common/update_checker.cpp, src/yuzu/configuration/configure_general.*, src/yuzu/CMakeLists.txt
The checker parses stable and release-candidate tags, selects newer releases by channel and platform assets, and classifies request failures. Settings and configuration UI expose update channels, and tests cover parsing, selection, and failure classification.
Transactional installer and backup
src/frontend_common/update_installer.*, src/tests/common/update_installer.cpp
The installer adds locking, manifest validation, file replacement and rollback, backup creation, and protection for user paths. Tests cover successful installation, restoration, and failure cases.
Platform installation helper
.github/workflows/build-windows.yml, src/CMakeLists.txt, src/updater/*
Windows release builds include the updater executable. The helper waits for the application process, installs staged files, and launches the requested program.
Application update and restore flow
src/yuzu/main.ui, src/yuzu/main_window.*, src/yuzu/updater/update_dialog.*
The application adds manual and automatic update checks, skipped-version handling, update prompts, downloads with optional size and SHA-256 checks, platform-specific installation, and previous-version restore actions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant MainWindow
  participant UpdateChecker
  participant GitHubReleases
  participant UpdateDialog
  MainWindow->>UpdateChecker: Check selected channel
  UpdateChecker->>GitHubReleases: Request repository releases
  GitHubReleases-->>UpdateChecker: Release list and HTTP status
  UpdateChecker-->>MainWindow: Selected update and check result
  MainWindow->>UpdateDialog: Show release and channel
Loading

Possibly related issues

  • Eglantine-Community/Eglantine issue 746: The update prompt and release information align with the request, though the issue’s startup-check scope is not established by the supplied change summary.

Suggested labels: windows

Suggested reviewers: crueter, kraibse

Security architecture risk: Moderate · up to 31aaa

The updater gains authority to replace and restart installed application files. Download handling improves, but installation ownership does not remain exclusive through the Windows handoff, and recovery can discard files before successful restoration is confirmed. These risks are primarily contained to the affected application installation; no new privilege-escalation path was established.

Retained concerns

  • Medium · reliability · inferred: Recovery can discard the files needed to restore a consistent installation. Install ignores errors while moving files back, then removes the pending backup regardless of restoration success. An interrupted install also leaves originals in that directory, which the next Install invocation deletes before reconciling the interrupted state. AppImage replacement and reversal similarly span multiple renames without durable recovery state. Consequently, a failed or interrupted transition can leave a mixed or missing executable installation rather than the promised previous version.
  • Medium · reliability · inferred: Windows installation ownership is not preserved across the process handoff. InstallOnRestart holds a function-local lock while preparing the shared staging directory, but releases it after launching the helper. The helper waits for the parent and then installs without acquiring that lock; Windows restore also launches it without locking. Another update or restore targeting the same installation can therefore clear staging or interleave file and backup mutations. Waiting for one parent process is not mutual exclusion across installation operations.
Security review details

Security Blast Radius

  • inferred — Each installation operation mutates one local application tree or AppImage. Release-publishing or authenticated-transport compromise could influence executable content across participating desktop installations. The inspected handoff uses ordinary process creation rather than requesting elevation, so no additional OS privilege gain was established.

Trust Boundaries and Controls

  • observed — Downloaded bytes are checked against metadata from the same release authority. These conditional size/hash checks improve transfer integrity but do not independently authenticate a compromised publisher. CA discovery takes only environment-presence checks and fixed host paths; no network-supplied CA selection is visible in that helper.

Resilience and Maintainability Implications

  • inferred — The principal retained concerns affect executable-state ownership and rollback containment, not a demonstrated unauthenticated remote attack. Unchecked undo failures and unlocked helper mutation weaken the guarantee that a failed update leaves a coherent previous version available.

Hardening Proposals

  • proposed — Make one installation identity own mutual exclusion continuously across staging, helper handoff, installation, and restore. A helper should validate or acquire that ownership before reading or deleting shared staging and backup state.
  • proposed — Represent installation and reversal as recoverable transitions. Preserve originals when undo fails, reconcile interrupted states before cleanup, and report incomplete recovery rather than restarting a potentially mixed installation as though restoration succeeded.
Pre-merge checks: Passed 4

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "Auto-updater: stable and release candidate channels, with in-place install" matches the main change in the changeset. The summaries show a release-channel-aware update checker (Stable and R…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "Auto-updater: stable and release candidate channels, with in-place install" matches the main change in the changeset. The summaries show a release-channel-aware update checker (Stable and ReleaseCandidate in update_checker.h and settings_enums.h), a channel setting in configure_general and uisettings, and an update_installer with in-place install logic. The title is specific, one sentence, and omits only minor details such as the CI, news feed, and Help menu restore action. No part of the title is unrelated to the change.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Review coverage is incomplete: 8 files could not be fully reviewed. Findings from completed review steps are included; see review info for details.

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 19f8a9dc-ed0a-4082-b7d7-62b93af73373
Commits

Reviewing files that changed from the base of the PR and between 1f6bb92 and 31aaad5.

Files selected for processing: 40

  • .github/workflows/build-linux.yml
  • .github/workflows/build-macos.yml
  • .github/workflows/build-windows.yml
  • CMakeLists.txt
  • CMakeModules/GenerateSCMRev.cmake
  • src/CMakeLists.txt
  • src/common/CMakeLists.txt
  • src/common/net/ca_store.h
  • src/common/net/net.cpp
  • src/common/net/net.h
  • src/common/scm_rev.cpp.in
  • src/common/scm_rev.h
  • src/common/settings_enums.h
  • src/core/hle/service/bcat/news/builtin_news.cpp
  • src/core/hle/service/ssl/ssl_backend_openssl.cpp
  • src/frontend_common/CMakeLists.txt
  • src/frontend_common/update_checker.cpp
  • src/frontend_common/update_checker.h
  • src/frontend_common/update_installer.cpp
  • src/frontend_common/update_installer.h
  • src/qt_common/config/shared_translation.cpp
  • src/qt_common/config/uisettings.h
  • src/qt_common/discord/discord_impl.cpp
  • src/tests/CMakeLists.txt
  • src/tests/common/update_checker.cpp
  • src/tests/common/update_installer.cpp
  • src/updater/CMakeLists.txt
  • src/updater/main.cpp
  • src/web_service/web_backend.cpp
  • src/yuzu/CMakeLists.txt
  • src/yuzu/configuration/configure_general.cpp
  • src/yuzu/configuration/configure_general.ui
  • src/yuzu/configuration/nvidia_dlss_panel.cpp
  • src/yuzu/main.ui
  • src/yuzu/main_window.cpp
  • src/yuzu/main_window.h
  • src/yuzu/updater/update_dialog.cpp
  • src/yuzu/updater/update_dialog.h
  • src/yuzu/updater/update_dialog.ui
  • src/yuzu/util/icon_search.cpp
Files not reviewed due to moderation or processing errors: 8

  • src/frontend_common/update_installer.h
  • src/frontend_common/update_installer.cpp
  • src/tests/common/update_installer.cpp
  • src/CMakeLists.txt
  • src/updater/CMakeLists.txt
  • src/updater/main.cpp
  • src/yuzu/main_window.cpp
  • src/yuzu/main_window.h

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

When an install failed and a file could not be moved back, the pending
backup holding its only copy was deleted anyway, and the helper reported
the previous version as restored. The backup is now kept in that case and
the helper says where the files are instead of starting the program.

The Windows helper also takes the install lock before it installs or
restores. Eglantine releases it at the handoff, so a second window could
clear the staging folder while the helper worked from it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@aidankinzett

Copy link
Copy Markdown
Collaborator Author

I have not tested this myself yet but I think centering the three buttons and the "Would you like to download this update?" would be a good idea.

I've centred the buttons and added a bit more spacing

- Hide the install lock so it never lands in the old version's file list, and only list
  top-level folders the new version ships, so a restore never makes the next update remove
  the user's own folders.
- Read every page of the release list, so tagged releases behind the rolling builds are still
  found.
- Refuse to install while .update-backup.tmp holds files a failed undo could not put back, and
  tell the user, instead of deleting them.
- Hand the bundled CA bundle to httplib as a file, so redirects to another host trust it too.
  Look for the system CA bundle on macOS as well.
- News: show only tagged releases, stop fetching images from eden-emu.dev, and link to GitHub.
- Open a lock created by another user read-only, fail the check on a release list that is not
  JSON, reject root-relative manifest lines on Windows, and translate the restore action.
aidankinzett and others added 2 commits November 14, 2026 11:07
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Daily builds carry their daily-YYYYMMDD tag in the build id, so the
checker now reads it from there and offers the newest later daily build.
Daily builds and tagged releases come from different branches and cannot
be ordered, so a build on the other kind's channel is offered that
channel's newest release as a switch.

CI turns the update checker on for daily builds and ships the Windows
updater helper with them, so they install in place like tagged releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aidankinzett
aidankinzett merged commit 28edf9f into nx2-experimental Nov 14, 2026
8 checks passed
@aidankinzett
aidankinzett deleted the updater/tagged-channels branch November 14, 2026 04:00
Octelly added a commit to Octelly/Eglantine that referenced this pull request Nov 14, 2026
…ter off

g_release_tag had no reader: the updater derives the tag itself, from
g_build_version or (for a daily) g_build_id.

The Nix package's reason for keeping the update checker off is no longer "the
endpoints point at another project" - they point at this one now, and release
and daily tarballs ship a working updater. The reason is the package manager: an
in-app download would put a tarball inside a store-managed tree.

nx2 carries no updater code (PR Eglantine-Community#841 is nx2-experimental only), so the two UI
call sites this commit touches on that branch do not exist here.
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