This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Keep this CLAUDE.md file up to date. When the user tells you something about the project (conventions, patterns, warnings), eagerly add it here. If you make changes that conflict with what's documented here, update this file to match. The user will use git to revert if they disagree.
Velopack is an installation and auto-update framework for cross-platform desktop applications. It has a C#/.NET frontend (library + CLI tool called vpk) and a Rust backend (update binaries for Windows/macOS/Linux). The library supports C#, C++, Node.js, Python, and Rust clients.
The working directory is ALREADY the repo root. Never write cd /c/Source/velopack && or any variation — you are already there. Just run commands directly (e.g. cargo build, dotnet test test/Velopack.Tests). You may cd into a subdirectory if a specific task genuinely requires it, but never cd to the repo root itself.
# .NET (main solution)
dotnet build # Build all .NET projects (Debug)
dotnet build -c Release # Build all .NET projects (Release)
# Rust
cargo build # Build all Rust workspace members (Debug)
cargo build --release # Build all Rust workspace members (Release)
cargo bw # Alias for `cargo build --features windows` — required on Windows
# to also build stub.exe/setup.exe (needed by packaging tests)# Run all .NET tests
dotnet test
# Run a specific test project (must use --project)
dotnet test --project test/Velopack.Tests
dotnet test --project test/Velopack.CommandLine.Tests
# Velopack.Packaging.Tests runs in well under a minute since TestApp publishes are
# cached per process and collections parallelize; running the whole project is fine.
# Targeted subsets: tests use xunit v3 on Microsoft.Testing.Platform, so filters go
# after `--` and use --filter-class / --filter-method with * wildcards
# (NOT the old VSTest --filter "FullyQualifiedName~..." syntax):
dotnet test --project test/Velopack.Packaging.Tests -- --filter-class "*MsiTests"
# Run a single test by name
dotnet test --project test/Velopack.Tests -- --filter-method "*TestMethodName*"
# Rust tests
cargo testRun tests locally in Debug (the default), never -c Release. In Debug, HelperFile and
PathHelper.GetRustBuildOutputDir() resolve unsuffixed rust binaries from target/debug, so a plain
cargo build (add --features windows on Windows) is all the setup needed. Release-mode tests
expect CI's arch-suffixed vendored binaries (update_x64.exe etc.) — do not fake those locally by
copying files into target/release.
src/
├── lib-csharp/ # Core C# library (Velopack NuGet package)
│ ├── Sources/ # Update sources (Velopack Flow, GitHub, GitLab, Gitea, HTTP, file)
│ ├── Locators/ # Platform-specific app locators (Windows, Linux, OSX)
│ ├── NuGet/ # Package handling (ZipPackage, PackageManifest)
│ └── UpdateManager.cs, VelopackApp.cs # Primary public API
├── lib-rust/ # Core Rust library
├── bins/ # Rust update binaries (update.exe, UpdateNix, UpdateMac)
├── lib-cpp/ # C++ bindings via cbindgen
├── lib-nodejs/ # Node.js FFI bindings (neon)
├── lib-python/ # Python bindings via PyO3
├── vpk/ # CLI tool entry point
│ ├── Velopack.Core/ # Core CLI abstractions
│ ├── Velopack.Packaging/ # Base packaging logic
│ ├── Velopack.Packaging.Windows/ # MSI, setup.exe (Handlebars templates)
│ ├── Velopack.Packaging.Unix/ # AppImage, tarball
│ ├── Velopack.Deployment/ # Upload to GitHub, S3, Azure, etc.
│ ├── Velopack.Flow/ # Application flow/orchestration
│ └── Velopack.Vpk/ # CLI entry using System.CommandLine
├── code-generator/ # Cross-language type generator
└── wix-dll/ # WiX MSI integration
test/
├── Velopack.Tests/ # Core library unit tests
├── Velopack.Packaging.Tests/ # Packaging/CLI tests
├── Velopack.CommandLine.Tests/ # CLI command parsing tests
├── Velopack.Deployment.Tests/ # vpk destination + cross-language update-source tests (docker stack)
├── TestApp/ # Test application used by integration tests
├── fixtures/ # Test fixture files
├── PathHelper.cs # Shared test utilities (linked into all test projects)
└── GlobalUsings.cs # Shared usings (linked into all test projects)
samples/ # Example apps (C#, C++, Node.js, Python, Rust)
Core libraries: lib-csharp and lib-rust are the two core libraries and must be kept in sync. Other language bindings (Node.js, Python, C++) are built on top of lib-rust. The libraries invoke the update binaries (src/bins/ — e.g. update.exe) to perform actual install/update operations.
Backwards compatibility: Changes to the CLI/binaries must be backwards compatible, because older library versions shipped with apps in the field may call newer executables.
-
VelopackApp.Run()executes at app startup. It handles fast-exit lifecycle hooks invoked by the update binary (--veloapp-install,--veloapp-updated,--veloapp-obsolete,--veloapp-uninstall— each with a version arg). These hooks have strict time limits (15-30s) and the process is killed if exceeded. It also auto-applies pending updates if a newer local package exists, firesOnFirstRun/OnRestartedcallbacks based on environment variables (VELOPACK_FIRSTRUN,VELOPACK_RESTART), and cleans up old packages. -
UpdateManager.CheckForUpdatesAsync()queries anIUpdateSourcefor the remote release feed, compares against the installed version, and builds a delta strategy. It selects deltas if: they exist, there are ≤10 in the chain, and their total size < the full package size. ReturnsUpdateInfowithTargetFullReleaseandDeltasToTarget[]. -
UpdateManager.DownloadUpdatesAsync()acquires a lock (PackagesDir/.velopack_lock), downloads deltas (or falls back to full package), verifies checksums (SHA256 preferred, SHA1 fallback), then invokes the update binary'spatchcommand to reconstruct the full package from deltas. On Windows, it also extracts the newUpdate.exefrom the downloaded package. -
UpdateManager.ApplyUpdatesAndRestart()invokes the update binary'sapplycommand with--waitPid,--rootDir,--packageDir, and optional--packageand restart args, then exits the app. The update binary waits for the app to exit, extracts the package, calls the app's fast-exit hooks, and relaunches.
The vpk pack command (PackageBuilder<T> in Velopack.Packaging) runs platform-specific command runners:
-
Preprocessing: Locates the main executable, detects CPU architecture from the binary, copies files to a staging directory. On Windows: embeds icon in Update.exe, removes ClickOnce manifests, creates execution stubs. On Linux: builds an AppDir with
AppRunscript,.desktopfile, and icon hierarchy. On macOS: creates or validates.appbundle structure. -
Code signing (Windows and macOS): Signs all PE/Mach-O binaries. Supports
signtool.exe, custom sign templates, Azure Trusted Signing (Windows), andcodesignwith optional notarization (macOS). -
Package creation (parallel): Builds multiple outputs simultaneously:
- Release package (
.nupkg): ZIP withlib/app/containing all app files + update binary +sq.versionmanifest, plus a.nuspecwith metadata. This is the canonical package format. - Portable package: On Windows: ZIP with
Update.exe,current/dir, execution stub, and.portablemarker. On Linux: AppImage (squashfs appended to runtime binary). On macOS: ditto ZIP of.appbundle. - Setup installer: On Windows:
setup.exetemplate with the.nupkgappended as a bundle (offset+length header + signature). Optional MSI via WiX 5 compilation from Handlebars templates. On macOS:.pkgviapkgbuild. - Delta package (
.delta.nupkg): Created if a previous release exists. Compares files between old and new releases — unchanged files get zero-length.diffmarkers, changed files get zstd patches (.zsdiff), new files included as-is. zstd is the only supported patch format (creation and apply); zstd being unavailable fails delta creation. Legacy.bsdiff/msdelta patches are rejected with an "Unsupported patch format" error on apply.
- Release package (
-
Post-processing: Writes
releases.<channel>.json(asset feed for update clients) and legacyRELEASESfile.
Locators (IVelopackLocator in C#, VelopackLocator in Rust) resolve platform-specific paths and app metadata. Both implementations follow the same logic and must stay in sync. All locators read app identity (ID, version, channel) from a sq.version manifest file.
Windows (WindowsVelopackLocator / locator.rs):
- Discovers install by finding
Update.exein the parent directory of the running executable - Layout:
{RootAppDir}/Update.exe,{RootAppDir}/current/sq.version,{RootAppDir}/current/<app files> - Packages:
{RootAppDir}/packages/if writable, otherwise falls back to{LocalAppData}/{AppId}/packages/(copies Update.exe there too). This fallback handles MSI installs to read-only locations like Program Files. - Portable mode: detected by presence of
.portablefile in RootAppDir - Legacy fallback: if no manifest, tries parsing version from
app-{version}directory name
Linux (LinuxVelopackLocator / locator.rs):
- Apps are distributed as AppImage files. When an AppImage runs, it mounts a filesystem containing the app. The locator detects this by finding
/usr/bin/in the current exe path and extracting the mount root before it. - Requires
$APPIMAGEenvironment variable (set automatically by AppImage runtime) pointing to the .AppImage file on disk - Layout inside the mounted AppImage:
{root}/usr/bin/UpdateNix,{root}/usr/bin/sq.version,{root}/usr/bin/<app files> - Packages:
/var/tmp/velopack/{AppId}/packages/(persists across reboots) - Never portable
macOS (OsxVelopackLocator / locator.rs):
- Discovers install by finding
.app/in the current exe path, extracting the bundle root - Layout:
{App}.app/Contents/MacOS/UpdateMac,{App}.app/Contents/MacOS/sq.version - Packages:
~/Library/Caches/velopack/{AppId}/packages/ - Never portable
TestVelopackLocator (C# only): Mock locator for unit tests — all properties throw unless explicitly set in the constructor.
- Do not
cdto the repo root — you are already there. Never prefix commands withcd /c/Source/velopack &&. You maycdinto a subdirectory only if genuinely needed. - Test framework: xunit v3. Test helpers in
test/PathHelper.cs— usePathHelper.GetFixturesDir()for test fixture paths andPathHelper.IsCIto detect CI. - Versioning: Nerdbank.GitVersioning (NBGV) — version derived from git height, not manually set.
- Assembly signing: All assemblies signed with
Velopack.snk. - Build output: Goes to
build/{Configuration}/{TargetFramework}/(not defaultbin/). - Rust output:
target/debug/ortarget/release/. - Main branch:
develop(notmain/master). Releases frommaster. - Nullable reference types: Enabled across all C# projects.
- C# lang version:
latest. - Max line length: 150 characters (C# and Rust).
- Indent: 4 spaces for C#, 2 spaces for XML/csproj/props, spaces everywhere (no tabs).
- Rust formatting: Always run
rustfmton Rust files after finishing edits.
Covers every vpk upload/download destination (local, S3, Azure, Gitea x3 versions, GitHub) and
update-source tests run cross-language via small CLI harnesses in 5 languages (C# in-process,
Rust, C++, Node.js, Python — see harnesses/). Destinations/sources run against a local docker
stack (Gitea, GitLab, Azurite, S3Mock), except GitHub which is live and uses a 5-repo lock pool
(caesay/velopack-test-{1..5}).
- Start the stack once, then re-run tests freely (see
test/Velopack.Deployment.Tests/docker/README.md):GitLab takes several minutes to become healthy on first boot; other services are up in seconds.docker compose -f test/Velopack.Deployment.Tests/docker/docker-compose.yml up -d
- Tests self-skip (never fail) when a service, toolchain, or token is unavailable.
- Live GitHub tests need the
VELOPACK_DEPLOYMENT_TEST_TOKENenv var (on Windows a User-level variable works — tests also readEnvironmentVariableTarget.User). - Language harnesses are built once per test session (cargo / npm / maturin venv / cmake); a missing toolchain skips just that language's rows.
- Avoid running test projects in parallel processes — the C# TestApp is published once per
test process (cached in
Velopack.TestCommon.TestApp; the test string is injected via atest_string.txtfile at pack time, not compiled in), but that publish still writes referenced projects' outputs to the sharedbuild/{Configuration}/dir, so concurrent processes can race.
src/lib-python ships a committed velopack.pyi (PEP 561 stubs, generated by pyo3-stub-gen) plus a py.typed marker; maturin bundles both into the wheel via the [tool.maturin] include list. Regenerate after any change to the lib-python public API:
cd src/lib-python
cargo run --no-default-features --features stub-gen --bin stub_gen # writes velopack.pyi- The DTO stubs/docstrings come from
///comments on the Rust structs and are emitted bysrc/code-generator/Templates/python_asset.hbs— regeneratetypes.rswith the code-generator, do not hand-edit it. - Requires Python ≥ 3.10 for generation (pyo3-stub-gen references
Py_3_10types). The shipped wheel is stillabi3-py37— the generator just builds--no-default-features(abi3 OFF) against the real interpreter. If only an olderpython3is on PATH, point pyo3 at a newer one, e.g.PYO3_PYTHON=$(uv python find 3.12). - pyo3 features are split so this works: the default
extensionfeature enablespyo3/extension-module+abi3+abi3-py37;--no-default-features --features stub-genturns those off and pulls in the optionalpyo3-stub-gen. CI (build-python.yml→stub-check, on Linux) fails ifvelopack.pyiis stale.