Skip to content

nUpdate 5.0: rewrite, new formats, portable projects and a cross-platform installer - #118

Merged
begerdom merged 23 commits into
masterfrom
db/formats-v2-and-portable-projects
Oct 10, 2026
Merged

begerdom merged 23 commits into
masterfrom
db/formats-v2-and-portable-projects

Conversation

@begerdom

@begerdom begerdom commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

nUpdate 5.0. This branch contains db/refactorings-and-test-suite (a rewrite of the library, the installer and the Administration with a full test suite) and builds the 5.0 formats and the cross-platform installer on top.

What changes

  • Client library (nUpdate, .NET Standard 2.0): rewritten with a task-based API; a feed nupdate.json with one signed package file per platform (any, win, linux-arm64, …); RSA-PSS signatures with PEM keys, SHA-512 and manifest checks; canonical semantic versions; MinimumStability and rollout conditions.
  • Installer: nUpdate.UpdateInstaller is the base library (engine, InstallerHost, windowless mode, WindowProgressReporter for windows of your own); nUpdate.UpdateInstaller.UI.Avalonia is the built-in installer, self-contained for win, linux and osx on x64 and arm64, and replaces macOS app bundles atomically.
  • Administration: an Avalonia app for Windows, Linux and macOS with portable project folders, project passwords, SFTP/FTPS/FTP publishing, one package file per platform, statistics API v2 and a migration assistant for projects of nUpdate 3 and 4.
  • User interfaces: Windows Forms and WPF (also on .NET 8) and new Avalonia dialogs, all on UpdateFlow.
  • Tests and CI: one test project with unit, headless UI, Docker integration and published-installer tests; coverage gates; CI on Linux, Windows and macOS.

Breaking changes

nUpdate 5 does not read nUpdate 4 feeds, packages or projects. The "Migrating to 5.0" section of the README and the migration assistant cover the move; both versions can run side by side.

Verification

CI is green on 12d9a30: Linux build, tests and coverage gates, the packages with the Linux installer, Windows, and macOS including the bundle swap.

Plans: docs/plans/. Notes for coding agents: AGENTS.md.

- Remove what an SDK-style build no longer needs: MigrationBackup, .nuget,
  packages, the git submodule and the GhostDoc and ReSharper settings.
- global.json pins the .NET 10 SDK and runs tests on Microsoft.Testing.Platform.
- Directory.Build.props and .targets hold the shared settings: latest C#,
  nullable, warnings as errors, analyzers, documentation and symbols, package
  metadata and the package icon in assets/. Directory.Packages.props holds the
  central package versions, .editorconfig the code style and
  .config/dotnet-tools.json ReportGenerator.
- .gitignore no longer hides source folders named Packages.
- nUpdate.sln starts empty; the following commits add their projects.
nUpdate (netstandard2.0) is rewritten around an asynchronous UpdateManager
with injectable services (UpdateManagerServices: HttpClient, file system,
logger, system information, process launcher, terminator).

- API: CheckForUpdatesAsync, AvailableUpdates, DownloadAsync with progress,
  VerifyAsync, StartInstaller and DeleteDownloads; HttpTimeout, proxy,
  credentials and cancellation; failures surface as errors, never as a
  cancellation.
- Formats: the nupdate.json feed (format marker, camelCase, string enums, one
  file per platform with size, SHA-512, an RSA-PSS/SHA-512 signature and the
  areas its operations touch), a package manifest that VerifyAsync checks
  against the feed entry, installer-options.json format 2 and PEM keys.
- Versions: UpdateVersion has one canonical form (major.minor.patch, a fourth
  number when it is not 0, a SemVer pre-release label and build metadata).
  MinimumStability (Release, ReleaseCandidate, Beta, Any) and
  AcceptedPreReleaseLabels choose pre-releases; necessary updates, unsupported
  versions and rollout conditions choose the packages.
- Platforms: a client takes the file of its runtime identifier, else of its
  operating system, else any. The installer comes from
  nUpdate.Installer/<rid>/ or InstallerPath, with ShowInstallerWindow,
  InstallerIcon and InstallerAccentColor. On Linux and macOS the client checks
  write access, sets the execute bit, finds .app bundles and never elevates.
- Typed operations for files, registry, services and processes; the C# script
  operation is gone. AfterInstall and InstallerArgument say what happens to
  the application after the update.
- UpdateFlow and IUpdateFlowPresenter drive the update dialogs; UpdateSummary
  and the formatters are shared by the built-in UIs. Texts come from
  UpdateTexts in six languages, Culture and TextFiles.
- The installer contracts (InstallerOptions, IProgressReporter) live in
  nUpdate.Installer; statistics are best-effort JSON reports.
- nUpdate.UpdateInstaller (netstandard2.0) is the installer base library.
  InstallerHost.Run reads the options, shows a window or none (when asked, or
  without a display), waits for the application, runs the operations of every
  package and replaces its files: each one is staged next to its target and
  swapped in, and a locked file asks Retry, Skip or Abort. It restores Unix
  modes, refuses registry and service operations off Windows, swaps macOS
  .app bundles atomically, restarts the application with its arguments and
  writes install.log. Without a window it reports to the console and the
  Windows event log; WindowProgressReporter carries the threading protocol
  for windows of your own.
- nUpdate.UpdateInstaller.UI.Avalonia is the built-in installer window:
  progress, the locked-file question inline, errors with the log path, icon
  and accent colour from the options, light and dark like the system. It is
  published self-contained, single-file and trimmed for seven runtime
  identifiers; the package's build targets copy the ones the app needs to
  nUpdate.Installer/<rid>/.
- samples/CustomInstaller shows an installer of your own with a WPF window.
- The .NET Framework installer, nUpdate.UpdateInstaller.UIBase and the WPF
  plugin installer are removed.
- nUpdate.UI.WindowsForms and nUpdate.UI.WPF target net462 and
  net8.0-windows. Their dialogs present UpdateFlow (search, new update with
  changelog and touched areas, download, errors); WPF uses constructor
  injection and small view models instead of a service locator.
- nUpdate.UI.Avalonia (net8.0) is new: the same dialogs in the look of the
  installer window, modal to an owner window or on their own, with
  InstallerAccentColor.
dbforge and others added 9 commits October 10, 2026 17:44
- nUpdate.Administration.TransferInterface: an asynchronous ITransferProvider
  with protocol-aware settings and trust prompts for unknown certificates and
  host keys (UntrustedServerException).
- nUpdate.Administration.Core: project format 6 in portable project folders
  with project passwords, migration of every earlier project format, a
  package builder that writes one zip per platform with its manifest and the
  Unix mode of every file, RSA-PSS signing, the feed store, a compensating
  publish pipeline, FTP and FTPS (FluentFTP) and SFTP (SSH.NET) through an
  optional proxy, the statistics API v2 (nupdate-statistics.php on PHP and
  MySQL), and the migration of nUpdate 3 and 4 feeds and packages, which lets
  both versions run side by side.
- nUpdate.Administration (Avalonia 12, .NET 10): main window, new-project
  wizard, project window with packages, overview and client snippet,
  statistics and history, a package editor with platforms, files and typed
  operations, project settings, credentials and project passwords, and the
  migration assistant.
- nUpdate.Tests (xUnit v3 on Microsoft.Testing.Platform, NSubstitute,
  Shouldly, MockFileSystem) replaces the MSTest project. It holds unit tests
  for every library, headless Avalonia tests of the windows and dialogs,
  Windows-only adapter tests, Docker-backed integration tests (FTP/FTPS,
  SFTP, PHP and MySQL) with end-to-end updates, scenario tests that drive
  each Administration dialog against those servers (trait
  Category=Integration), and tests of the published installer (trait
  PublishedInstaller, skipped without NUPDATE_INSTALLER).
- tools/CoverageGate checks line and branch coverage per assembly: 100/100
  for nUpdate, nUpdate.UpdateInstaller, nUpdate.Administration.Core and
  nUpdate.Administration.TransferInterface; 90/90 for nUpdate.Administration,
  nUpdate.UpdateInstaller.UI.Avalonia and nUpdate.UI.Avalonia.
- Linux: format check, build, unit and Docker integration tests, the
  coverage gate on the merged report and a ReportGenerator summary.
- Packages: pack on Linux (the installer for all seven runtime
  identifiers), build an application against the packages and update it with
  the published linux-x64 installer.
- Windows: build, unit tests including the Windows-only adapter tests, the
  same consumer update with the win-x64 installer, and nUpdate
  Administration published for win-x64.
- macOS: check the signature of the osx-arm64 installer and swap a signed
  test bundle that must still verify and start.
- Runs on pushes to every branch and on pull requests from forks;
  Dependabot keeps the packages and actions current.
- README: features, requirements, installation, usage with examples, the
  installer, nUpdate Administration, migrating from nUpdate 3 and 4 and
  running both side by side, and the list of changes; screenshots in
  docs/images and a separate NuGet readme.
- BUILDING.md: building, testing, coverage and packaging. AGENTS.md lists the
  pitfalls for coding agents; CLAUDE.md points to it.
- docs/plans: the plans for the refactoring, the formats and portable
  projects, and the cross-platform installer.
- StartInstaller no longer fails when an earlier installer or a virus scanner
  still holds a file in the application's installer folder: the installer
  then runs from a folder next to it, which a later update removes again.
  Fixes #117.
- An executable path that is empty, relative or names no folder is refused
  where it is set, or when the installer is about to start, with a message
  that says to set ApplicationExecutablePath. It no longer reaches the
  installer as an empty application folder. Single-file builds already take
  the path from the process. Fixes #101.
AfterInstall.Close leaves the application closed after an update, and the
example now shows how to pass the arguments of the running instance, such as
a settings file from a shortcut, on to the restarted application. Closes #96.
vsftpd and ProFTPD require by default that every data connection resumes the
TLS session of the control connection. The TLS of .NET, which FluentFTP uses,
cannot do that, so the login works but listing and uploading fail with a bare
server reply.

The FTP provider now adds what to do when an FTPS data connection looks
refused (reply 425, 450 or 522, a failed TLS handshake, or a reply that
mentions reuse): allow fresh sessions on the server (vsftpd:
require_ssl_reuse=NO, ProFTPD: TLSOptions NoSessionReuseRequired) or publish
over SFTP. A failed handshake on a data connection now arrives as a
TransferException instead of an unhandled AuthenticationException. The README
notes the limitation.

The limitation itself stays: lifting it would take FluentFTP.GnuTLS, whose
LGPL-2.1 license is not an option. Refs #86.
A package can now ask to restart the application or to leave it closed after
the update, for example when the update needs a step before the next start.
The choice is stored in the feed entry ("afterInstall": "restart" or "close",
missing when the application decides) and set in the General section of the
package editor, also when editing a published package.

UpdateManager.AfterInstall becomes DefaultAfterInstall, which a package can
override; the read-only AfterInstall is the outcome for the updates found.
When several packages are installed together, leaving the application closed
wins. The update dialogs of Avalonia, WPF and Windows Forms say when the
application stays closed. "keepRunning" in a feed is rejected: whether the
application can stay open while its files are replaced only it knows.

Closes #96.
es-ES.json is built in now, from the translation by Guzzjakes in #106,
adapted to the texts of nUpdate 5: the keys follow UpdateTexts, the installer
messages lose their quotes, a registry subkey is a "subclave", a value is set
"a" another, and a locked file no longer asks to press OK. The texts nUpdate 4
did not have are new: the application staying closed, disk space, the
installer window, its log file and write access. Other Spanish regions such
as es-MX get these texts too. The English typo #106 fixed no longer exists.

Co-authored-by: Guzzjakes <kaxlolz@gmail.com>
@begerdom
begerdom force-pushed the db/formats-v2-and-portable-projects branch from 2cca78a to e8b55bf Compare October 10, 2026 15:48
@begerdom
begerdom force-pushed the db/formats-v2-and-portable-projects branch 3 times, most recently from c7bd5f8 to 36507f8 Compare October 10, 2026 18:22
@begerdom
begerdom force-pushed the db/formats-v2-and-portable-projects branch from 7c2dd41 to a85be98 Compare October 10, 2026 19:42
The sample application in the screenshots is Aurora now, as on the homepage,
instead of Trade Updater and BuildIt. Headless rendering draws no window
decorations, so WindowScreenshots adds them: the windows of nUpdate
Administration in a Windows 11 frame, the installer and the Avalonia update
dialog in a GNOME frame. The headless test application loads Inter like the
applications do, so the screenshots no longer depend on the fonts of the
machine.

tools/WinFormsScreenshots renders the real update dialog of
nUpdate.UI.WindowsForms on Windows, in the same Windows 11 frame; the Windows
job of CI uploads it as the artifact screenshots-windows. The README shows it
from docs/images instead of the BuildIt screenshot on nupdate.net.
Code clean-up across the solution. The acronyms keep the casing .NET uses
(UpdaterUI, PackagePlatform.MacOS, OperatingSystem.IsMacOS), and
nUpdate.sln.DotSettings declares OS and UI as abbreviations so that ReSharper
keeps them.
samples/Aurora is a small Avalonia application that checks a feed with the
dialogs of nUpdate.UI.Avalonia and installs what it finds. It references the
nUpdate projects of the repository, so changes to the library, the dialogs or
the installer are what gets tested.

publish.sh <version> publishes it self-contained as a single file, with the
installer for the same runtime identifier in nUpdate.Installer/<rid>/. On macOS
it builds Aurora.app, signed ad hoc; every file in Contents/MacOS is Mach-O with
an embedded signature, so the bundle still verifies after an update replaced
it. The feed URL and the public key are entered in the window and kept outside
the program folder. The README walks through an update from 1.0.0 to 1.1.0 with
a local web and FTP server.
Switching from Overview to Packages sometimes did nothing for a few clicks,
until another page had been opened. The pages were a Carousel bound to the
rail's selection; its page transition and the items it keeps realized could
leave the previous page in front. The pages are now a plain Panel, and the
code-behind shows the page of the selected entry and hides the others.
nUpdate 5 is published as a release candidate first: complete and tested, but
details may still change before 5.0.0. The README says so, installs
5.0.0-rc.1 and shows it in its release badge.

nUpdate Administration shows its informational version, so the window title
and the About dialog read 5.0.0-rc.1 instead of the numeric assembly version
5.0.0. The consumer smoke test and CI's package listing take the version from
Directory.Build.props instead of naming 5.0.0.
…kage

codesign keeps the signature of a file in Contents/MacOS that is not Mach-O
code, such as a .NET assembly next to the executable, in extended attributes
(com.apple.cs.*). A zip cannot hold them, so after an update such a bundle no
longer passed codesign --verify.

nUpdate Administration reads these attributes when it builds a package on a
Mac and stores them, Base64 per entry, in the manifest (CodeSignatures). The
installer sets them on the installed files, for a bundle on the new copy
before the swap; a file it cannot set them on is a warning, as the file itself
works. When the files of a published package are edited, the files that stay
keep the attributes stored for them; new and replaced files are read again.

The macOS published-installer test now ships a bundle with a non-Mach-O file
in Contents/MacOS and checks that the swapped bundle still verifies.
Everything public in the packages is API and has to be documented, so the
types that are only public because another nUpdate assembly uses them are
internal now, with InternalsVisibleTo where nUpdate's own assemblies need
them:

- nUpdate: the default platform implementations behind UpdateManagerServices
  (ProcessLauncher, FilePermissions, SystemInformation, ...), the code
  signature attributes, PackagePlatform, PackageLayout, InstallerTexts and
  the JSON converters.
- nUpdate.UpdateInstaller: everything except InstallerHost, InstallerSession,
  WindowProgressReporter and InstallerServices. Of InstallerServices only the
  limits HostExitTimeout and MaxLockedFileAttempts stay public; the system
  services in it are seams for nUpdate's own tests.
- The UI packages: the view models, views and controls behind UpdaterUI.

The file formats stay public (UpdateFeed, PackageManifest, PackageRoot, the
operations and RegistryValue): nUpdate Administration's services expose them
in their public signatures, and they are the model of the feed and the package
manifest that the API page documents.

The public API shrinks from 143 types to 73.
A static site in website/: plain HTML, one stylesheet and a small script,
no framework and no build step.

- index.html: the homepage. The installer window cycles through Windows,
  Linux and macOS, the platform picker follows the real rule (runtime
  identifier, then operating system, then any), the code samples are the
  real API, and nUpdate Administration's screens are rebuilt after the real
  windows. It says that nUpdate 5 is a release candidate and installs
  5.0.0-rc.1.
- api.html: the API reference, every public type and member of the packages
  with its signature and examples for the types and main calls, including
  the file formats and the transfer plugin API. Each entry names the types
  and members it documents in data-api, as documentation comment IDs.
- imprint.html and privacy.html: the legal notice under § 5 DDG and the
  privacy policy, in German with an English translation.
- The fonts are served from website/fonts with their licenses instead of
  from Google Fonts, which would send every visitor's IP address to Google;
  the pages load nothing from other servers.

Colours are tokens with a dark variant that follows the system setting or
the theme switch, and the animations stop under "reduce motion".

tools/ApiDocsCheck keeps the API page complete. It reads the public types and
members of the packages from their metadata (System.Reflection.Metadata, no
new dependency) and compares them with the data-api IDs on api.html: it fails
on a public member without an entry and on an entry whose member no longer
exists, and a changed signature is both, because the IDs carry the member's
type. CI runs it after the Linux build through tools/ci/api-docs-check.sh,
which also runs locally after a Release build. BUILDING.md, AGENTS.md and the
website README describe the process.
CI publishes nUpdate Administration for every platform it supports and starts
each build once:

- Linux: tools/ci/administration-linux.sh publishes it self-contained for
  linux-x64 and linux-arm64 and packs nUpdate-Administration-<rid>.tar.gz; a
  tarball keeps the executable bit, which the zip of an Actions artifact
  would drop.
- macOS: the bundle is built for osx-x64 as well as osx-arm64.
- tools/ci/check-starts.sh starts a program and fails when it exits within
  15 seconds, showing its output. CI runs it for win-x64, linux-x64 under
  Xvfb and both macOS bundles, the Intel one under Rosetta.

The builds are uploaded as administration-win-x64, administration-linux-x64,
administration-linux-arm64, administration-osx-arm64 and
administration-osx-x64.
@begerdom
begerdom force-pushed the db/formats-v2-and-portable-projects branch from a85be98 to 3db276b Compare October 10, 2026 19:57
@begerdom
begerdom merged commit 3db276b into master Oct 10, 2026
8 checks passed
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