From 6c0419e9fddf540f613290ed5757b5a28b3e2d2b Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 03:45:11 -0400 Subject: [PATCH 1/8] The packaging jobs cache their .NET tools, and the build rows stop restoring crates they never use The cargo cache cost every Windows and macOS row 21 to 60 seconds, most of it Windows tar unpacking 13,000 extracted crate sources, and in the runs looked at it was never read: every port came from the binary cache, so cargo never ran. The one port built with cargo, Velopack, is built only when that cache misses it, and then fetching its crates takes about ten seconds. Linux, which does not build Velopack, restored it too. vpk is a 147 MB package, and dotnet tool restore took 21 seconds of each Windows packaging job. setup-dotnet now caches the NuGet packages folder, keyed on the tool manifest, and the jobs point that folder into the workspace so the cache holds the two tools and nothing the image carries. pip stays uncached: the wheels download in under a second, and the time goes to starting pip and unpacking CMake, which a cache does not save. Co-Authored-By: Claude Opus 5.5 --- .github/actions/setup-build/action.yaml | 14 ++++---------- .github/workflows/build.yaml | 11 +++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup-build/action.yaml b/.github/actions/setup-build/action.yaml index 89e3dac3c8..56e9dfcc42 100644 --- a/.github/actions/setup-build/action.yaml +++ b/.github/actions/setup-build/action.yaml @@ -129,16 +129,10 @@ runs: shell: bash run: ./vcpkg/bootstrap-vcpkg.sh - # Ports built with cargo (Velopack) fetch their crates here. Keyed on the - # manifest and the registry baseline, which decide the crate versions. - - name: Cache Rust dependencies - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('indra/vcpkg.json', 'indra/vcpkg-configuration.json') }} - restore-keys: ${{ runner.os }}-cargo- + # No cargo cache. The one port built with cargo, Velopack, is built only + # when the binary cache misses it; every other row restored the crates + # for nothing, at up to a minute a row on Windows, where fetching them + # takes the rare row that needs them ten seconds. - name: Install AWS CLI on macOS if: inputs.r2-cache-mode != '' && runner.os == 'macOS' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2f09bfc73d..944bca7a56 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -546,6 +546,9 @@ jobs: runs-on: windows-2025-vs2026 env: SIGN_PACKAGES: ${{ github.event_name != 'pull_request' }} + # The .NET tools restore into a folder of their own, so the cache holds + # them and not whatever packages the image carries. + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: # The app and its metadata come from the build's artifacts; the tree # contributes only the .NET tool manifest. @@ -557,10 +560,14 @@ jobs: sparse-checkout: /dotnet-tools.json sparse-checkout-cone-mode: false + # vpk is a 147 MB package: it comes from the cache, keyed on the tool + # manifest, rather than from nuget.org. - name: Setup .NET for Velopack uses: actions/setup-dotnet@v6 with: dotnet-version: '10.x' + cache: true + cache-dependency-path: dotnet-tools.json - name: Install Velopack CLI and AzureSignTool run: dotnet tool restore @@ -672,6 +679,8 @@ jobs: env: DEVELOPER_DIR: "/Applications/Xcode.app/Contents/Developer" SIGN_PACKAGES: ${{ github.event_name != 'pull_request' }} + # As in the Windows packaging job. + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: # The app and its metadata come from the build's artifacts; the tree # contributes the .NET tool manifest, the signing script, the @@ -697,6 +706,8 @@ jobs: uses: actions/setup-dotnet@v6 with: dotnet-version: '10.x' + cache: true + cache-dependency-path: dotnet-tools.json - name: Install Velopack CLI run: dotnet tool restore From 37bc59e65f2fd68ef77f4473c607e9c6993a83cc Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 03:53:50 -0400 Subject: [PATCH 2/8] The Python tools come back from the cache as a venv, and no job reinstalls them pip took 10 to 14 seconds of every Windows row, and the time was not the download: the wheels came down in under a second, and the rest went to starting pip and unpacking CMake's 4,255 files. A download cache saves none of that. The tools now live in a venv in the runner's temp directory, restored whole from the Actions cache, so a row with a hit runs no pip at all. A restore costs what unpacking costs, file by file on Windows, so the venv carries no more files than it needs: no pip, since the setup's own pip installs into it, and none of CMake's documentation, which no build reads. That is 1,834 files where a plain venv holds 5,284. It is keyed on the runner, the exact interpreter it points at and the pinned requirements, and saved as soon as it is built, so a job that later fails still leaves it for the next. The venv is also the Python root. setup-python names its own interpreter in Python3_ROOT_DIR, which CMake's FindPython takes ahead of an active venv, and the tests that spawn a Python peer would have run on an interpreter without llsd. The setup's list gains sentry-cli, which the Sentry step installed for itself, and pins llsd, since a reused venv keeps whatever version it first got. The macOS packaging job takes CMake and dmgbuild the same way, CMake pinned there too. Co-Authored-By: Claude Opus 5.5 --- .github/actions/python-tools/action.yaml | 49 +++++++++ .github/actions/setup-build/action.yaml | 31 +++--- .github/workflows/build.yaml | 18 ++-- scripts/ci/python_tools.py | 130 +++++++++++++++++++++++ scripts/ci/test_python_tools.py | 50 +++++++++ 5 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 .github/actions/python-tools/action.yaml create mode 100644 scripts/ci/python_tools.py create mode 100644 scripts/ci/test_python_tools.py diff --git a/.github/actions/python-tools/action.yaml b/.github/actions/python-tools/action.yaml new file mode 100644 index 0000000000..d0718966fe --- /dev/null +++ b/.github/actions/python-tools/action.yaml @@ -0,0 +1,49 @@ +name: Python tools +description: >- + Puts a venv of pinned Python tools on the path, restored whole from the + Actions cache when the runner, the interpreter and the requirements match, + so a job with a hit runs no pip. Expects setup-python to have run and + scripts/ci/python_tools.py checked out. + +inputs: + requirements: + description: >- + pip requirements, one to a line. Pin each: the venv is reused until this + text or the interpreter changes, so an unpinned one, or anything one + depends on, stays at the version it first got + required: true + +runs: + using: composite + steps: + - name: Key the Python tools + id: key + shell: bash + env: + REQUIREMENTS: ${{ inputs.requirements }} + run: python scripts/ci/python_tools.py key + + - name: Restore the Python tools + id: restore + uses: actions/cache/restore@v6 + with: + path: ${{ steps.key.outputs.venv }} + key: ${{ steps.key.outputs.key }} + + - name: Install the Python tools + if: steps.restore.outputs.cache-hit != 'true' + shell: bash + run: python scripts/ci/python_tools.py build + + # As soon as it is built rather than when the job ends, which a failed + # job does not reach. + - name: Save the Python tools + if: steps.restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: ${{ steps.key.outputs.venv }} + key: ${{ steps.key.outputs.key }} + + - name: Put the Python tools on the path + shell: bash + run: python scripts/ci/python_tools.py activate diff --git a/.github/actions/setup-build/action.yaml b/.github/actions/setup-build/action.yaml index 56e9dfcc42..794fd98308 100644 --- a/.github/actions/setup-build/action.yaml +++ b/.github/actions/setup-build/action.yaml @@ -99,19 +99,6 @@ runs: with: python-version: "3.14" - # Linux builds with the CMake floor, so the floor is a floor; the Visual - # Studio 2026 generator needs a newer one. Both are pinned: vcpkg builds - # the ports with this CMake and hashes its version into every package - # ABI, so a CMake release on PyPI would otherwise miss the whole binary - # cache until a protected build filled it again. - - name: Install python dependencies - shell: bash - run: | - if [[ "$RUNNER_OS" == "Linux" ]] - then pip3 install llsd cmake==4.0.0 ninja==1.13.2 - else pip3 install llsd cmake==4.4.3 ninja==1.13.2 - fi - # What a Developer Command Prompt has, for the runner's architecture, # but Visual Studio's CMake, Ninja and vcpkg. - name: MSVC environment @@ -119,6 +106,24 @@ runs: shell: pwsh run: python scripts/ci/msvc_environment.py + # After the MSVC environment, so nothing it puts on the path comes ahead + # of these. Linux builds with the CMake floor, so the floor is a floor; + # the Visual Studio 2026 generator needs a newer one. Both are pinned: + # vcpkg builds the ports with this CMake and hashes its version into + # every package ABI, so a CMake release on PyPI would otherwise miss the + # whole binary cache until a protected build filled it again. llsd is + # for the tests that spawn a Python peer, sentry-cli uploads the debug + # files. + - name: Python tools + uses: ./.github/actions/python-tools + with: + requirements: | + cmake==4.0.0; sys_platform == "linux" + cmake==4.4.3; sys_platform != "linux" + ninja==1.13.2 + llsd==1.2.4 + sentry-cli==3.7.0 + - name: Bootstrap vcpkg Windows if: runner.os == 'Windows' shell: pwsh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 944bca7a56..103f7cd7da 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -515,8 +515,7 @@ jobs: exit 1 fi - pip3 install sentry-cli==3.7.0 - + # sentry-cli is one of the setup's Python tools. cmake --build "$BUILD_DIRECTORY" --config Release --target symbols sentry-cli debug-files upload --include-sources --wait "$BUILD_DIRECTORY/symbols/Release" @@ -683,8 +682,8 @@ jobs: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: # The app and its metadata come from the build's artifacts; the tree - # contributes the .NET tool manifest, the signing script, the - # entitlements and the disk image's layout. + # contributes the .NET tool manifest, the Python tools' action, the + # signing script, the entitlements and the disk image's layout. - name: Checkout code uses: actions/checkout@v7 with: @@ -692,6 +691,8 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} sparse-checkout: | /dotnet-tools.json + /.github/actions/python-tools/ + /scripts/ci/python_tools.py /indra/cmake/ViewerCodeSign.cmake /indra/newview/slplugin.entitlements /indra/newview/installers/darwin/ @@ -717,9 +718,14 @@ jobs: with: python-version: "3.14" - # The badge_icons extra badges the volume icon through Quartz. + # CMake runs the signing script; the badge_icons extra badges the + # volume icon through Quartz. - name: Install CMake and dmgbuild - run: pip install cmake "dmgbuild[badge_icons]==1.6.7" + uses: ./.github/actions/python-tools + with: + requirements: | + cmake==4.4.3 + dmgbuild[badge_icons]==1.6.7 - name: Fetch macOS app uses: actions/download-artifact@v8 diff --git a/scripts/ci/python_tools.py b/scripts/ci/python_tools.py new file mode 100644 index 0000000000..b09ed44f8b --- /dev/null +++ b/scripts/ci/python_tools.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Put a venv of pinned Python tools on a GitHub Actions job's path. + +The venv is built once for each runner, interpreter and list of +requirements, and restored whole from the Actions cache after that, so a +job with a hit runs no pip at all. The python-tools action calls this three +times: + + key the cache key and the venv's path, as step outputs; the + requirements, from REQUIREMENTS, go to a file beside the venv + build on a miss, the venv. Without pip, which a restored venv never + runs: the setup's own pip installs into it. Without the CMake + documentation either, which no build reads. Each file left out + is one fewer to unpack on every restore, which on Windows is + what a restore costs + activate the venv's scripts ahead on the path, and the venv as the + Python root. setup-python names its own interpreter in + Python_ROOT_DIR and Python3_ROOT_DIR, and CMake's FindPython + takes a root it is given ahead of an active venv: the tests + that spawn a Python peer would get an interpreter without llsd + +The key names the exact interpreter because a venv is not portable: it +points at the interpreter it was made from, by path. +""" + +import hashlib +import os +from pathlib import Path +import platform +import shutil +import subprocess +import sys +import venv + + +def scripts_dir(root, windows=os.name == "nt"): + return root / ("Scripts" if windows else "bin") + + +def interpreter(root, windows=os.name == "nt"): + return scripts_dir(root, windows) / ("python.exe" if windows else "python") + + +def normalized(requirements): + """The requirements as pip reads them, whatever line endings YAML gave.""" + lines = [line.strip() for line in requirements.splitlines()] + return "\n".join(line for line in lines if line) + "\n" + + +def cache_key(runner_os, runner_arch, version, base_prefix, requirements): + digest = hashlib.sha256() + for part in (base_prefix, normalized(requirements)): + digest.update(part.encode("utf-8")) + digest.update(b"\0") + return f"python-tools-{runner_os}-{runner_arch}-{version}-{digest.hexdigest()[:16]}" + + +def paths(env): + temp = Path(os.path.normpath(env["RUNNER_TEMP"])) + return temp / "python-tools", temp / "python-tools-requirements.txt" + + +def documentation(python): + """CMake's Help and doc trees, when CMake is one of the tools.""" + probe = subprocess.run( + [str(python), "-c", "import cmake; print(cmake.CMAKE_DOC_DIR); print(cmake.CMAKE_SHARE_DIR)"], + text=True, capture_output=True, + ) + if probe.returncode != 0: + return [] + doc, share = probe.stdout.splitlines() + return [Path(doc), *sorted(Path(share).glob("cmake-*/Help"))] + + +def file_count(root): + return sum(len(files) for _, _, files in os.walk(root)) + + +def key(env): + root, requirements = paths(env) + text = normalized(env["REQUIREMENTS"]) + requirements.write_text(text, encoding="utf-8") + name = cache_key(env["RUNNER_OS"], env["RUNNER_ARCH"], platform.python_version(), sys.base_prefix, text) + with open(env["GITHUB_OUTPUT"], "a", encoding="utf-8", newline="\n") as stream: + stream.write(f"key={name}\n") + stream.write(f"venv={root}\n") + print(f"{name}: {root}") + + +def build(env): + root, requirements = paths(env) + # A copied interpreter outside Windows would look for its shared + # library beside the copy. + venv.EnvBuilder(with_pip=False, symlinks=os.name != "nt").create(root) + python = interpreter(root) + subprocess.run( + [sys.executable, "-m", "pip", "--python", str(python), "install", + "--disable-pip-version-check", "--no-input", "--no-cache-dir", + "--requirement", str(requirements)], + check=True, + ) + installed = file_count(root) + for tree in documentation(python): + shutil.rmtree(tree, ignore_errors=True) + print(f"{root}: {file_count(root)} files, {installed} before the CMake documentation went") + + +def activate(env): + root, _ = paths(env) + # A venv whose interpreter is gone fails here rather than in the build. + subprocess.run([str(interpreter(root)), "-c", ""], check=True) + # The runner puts each line of GITHUB_PATH ahead of the ones before it. + with open(env["GITHUB_PATH"], "a", encoding="utf-8", newline="\n") as stream: + stream.write(f"{scripts_dir(root)}\n") + with open(env["GITHUB_ENV"], "a", encoding="utf-8", newline="\n") as stream: + for name in ("VIRTUAL_ENV", "Python_ROOT_DIR", "Python3_ROOT_DIR"): + stream.write(f"{name}={root}\n") + + +COMMANDS = {"key": key, "build": build, "activate": activate} + + +def main(argv): + if len(argv) != 2 or argv[1] not in COMMANDS: + sys.exit(f"usage: {argv[0]} {'|'.join(COMMANDS)}") + COMMANDS[argv[1]](dict(os.environ)) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/scripts/ci/test_python_tools.py b/scripts/ci/test_python_tools.py new file mode 100644 index 0000000000..b0faca60f1 --- /dev/null +++ b/scripts/ci/test_python_tools.py @@ -0,0 +1,50 @@ +from pathlib import Path, PureWindowsPath +import unittest + +import python_tools as tools + + +REQUIREMENTS = 'cmake==4.4.3; sys_platform != "linux"\nninja==1.13.2\n' +PREFIX = "/Users/runner/hostedtoolcache/Python/3.14.7/arm64" + + +class PythonToolsTests(unittest.TestCase): + def test_scripts_dir_follows_the_platform(self): + root = PureWindowsPath(r"D:\a\_temp\python-tools") + self.assertEqual(tools.scripts_dir(root, windows=True), root / "Scripts") + self.assertEqual(tools.interpreter(root, windows=True), root / "Scripts" / "python.exe") + root = Path("/home/runner/work/_temp/python-tools") + self.assertEqual(tools.interpreter(root, windows=False), root / "bin" / "python") + + def test_normalized_drops_blank_lines_and_line_endings(self): + self.assertEqual(tools.normalized(" a==1\r\n\r\nb==2 "), "a==1\nb==2\n") + + def test_key_names_the_runner_and_the_interpreter(self): + key = tools.cache_key("macOS", "ARM64", "3.14.7", PREFIX, REQUIREMENTS) + self.assertTrue(key.startswith("python-tools-macOS-ARM64-3.14.7-")) + + def test_key_ignores_what_pip_ignores(self): + self.assertEqual( + tools.cache_key("Linux", "X64", "3.14.7", PREFIX, REQUIREMENTS), + tools.cache_key("Linux", "X64", "3.14.7", PREFIX, "\r\n" + REQUIREMENTS.replace("\n", "\r\n\r\n")), + ) + + def test_key_changes_with_anything_the_venv_depends_on(self): + base = tools.cache_key("Linux", "X64", "3.14.7", PREFIX, REQUIREMENTS) + changed = [ + tools.cache_key("Linux", "ARM64", "3.14.7", PREFIX, REQUIREMENTS), + tools.cache_key("Linux", "X64", "3.14.8", PREFIX, REQUIREMENTS), + tools.cache_key("Linux", "X64", "3.14.7", "/opt/hostedtoolcache/Python/3.14.7/x64", REQUIREMENTS), + tools.cache_key("Linux", "X64", "3.14.7", PREFIX, REQUIREMENTS.replace("1.13.2", "1.13.3")), + ] + for key in changed: + self.assertNotEqual(key, base) + + def test_paths_sit_in_the_runner_temp(self): + root, requirements = tools.paths({"RUNNER_TEMP": "/home/runner/work/_temp/"}) + self.assertEqual(root, Path("/home/runner/work/_temp/python-tools")) + self.assertEqual(requirements, Path("/home/runner/work/_temp/python-tools-requirements.txt")) + + +if __name__ == "__main__": + unittest.main() From 7eba2774189695a86ec792ddf748318f5b818162 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 04:02:03 -0400 Subject: [PATCH 3/8] CMake 4.4.4 on Windows and macOS, and sentry-cli 3.8.0 The Python tools at their latest releases, but for Linux's CMake, which stays at the floor the Linux build holds to. Ninja, llsd and dmgbuild are current already. vcpkg hashes the CMake version into every package ABI, so Windows and macOS miss the whole binary cache once, until a protected build fills it with the 4.4.4 packages; pull requests only read it, and build every port until then. Co-Authored-By: Claude Opus 5.5 --- .github/actions/setup-build/action.yaml | 4 ++-- .github/workflows/build.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-build/action.yaml b/.github/actions/setup-build/action.yaml index 794fd98308..7f69f6c575 100644 --- a/.github/actions/setup-build/action.yaml +++ b/.github/actions/setup-build/action.yaml @@ -119,10 +119,10 @@ runs: with: requirements: | cmake==4.0.0; sys_platform == "linux" - cmake==4.4.3; sys_platform != "linux" + cmake==4.4.4; sys_platform != "linux" ninja==1.13.2 llsd==1.2.4 - sentry-cli==3.7.0 + sentry-cli==3.8.0 - name: Bootstrap vcpkg Windows if: runner.os == 'Windows' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 103f7cd7da..1e5558a461 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -724,7 +724,7 @@ jobs: uses: ./.github/actions/python-tools with: requirements: | - cmake==4.4.3 + cmake==4.4.4 dmgbuild[badge_icons]==1.6.7 - name: Fetch macOS app From 55fe65c07f707952aad15d2a18c5f03aae6acf79 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 04:21:31 -0400 Subject: [PATCH 4/8] Corrosion builds Velopack's C API from the crates.io velopack, not a port that runs cargo velopack_libc, the C API the viewer calls, is velopack's src/lib-cpp, which is not published; the velopack crate under it is. Its sources and headers now sit in indra/rust unchanged from 1.2.161, in a Cargo workspace whose lock pins every crate at the version velopack's own lock does, with only velopack itself taken from crates.io. Corrosion, a new port in the registry, imports it: a DLL on Windows, staged and installed with the viewer's other runtime DLLs, and a static library on macOS. The release build serves every configuration, as before, and only the viewer's link builds it. The port ran cargo inside vcpkg, where vcpkg's ABI hash never saw the Rust toolchain, rustup added targets from inside a port build, and every target triple came from a table the port kept, which mapped every Linux to x86-64. Corrosion takes the target from the CMake toolchain, and from CMAKE_OSX_ARCHITECTURES on a Mac, which it would otherwise not read. The build uses cargo's default release profile rather than velopack's, which optimised for size with LTO into one code unit: 16 seconds here where that took 74, now that every build tree builds it. The 36 functions it exports are the prebuilt library's. On macOS it needs only libSystem, as rustc reports, so the nine frameworks the port listed are gone, and so is the -lSystem Corrosion would add, which ld warns of as a duplicate. Rust is needed only where Velopack is, Windows and macOS: the Linux package lists drop rustup, and the macOS steps follow Homebrew's rustup, which no longer provides rustup-init and keeps its proxies off the path. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + CLAUDE.md | 3 +- doc/ARCHITECTURE.md | 1 + doc/BUILD.md | 32 +- indra/CMakeLists.txt | 1 + indra/cmake/Dependencies.cmake | 1 - indra/cmake/Velopack.cmake | 59 + indra/rust/Cargo.lock | 1542 +++++++++++++++++ indra/rust/Cargo.toml | 11 + indra/rust/velopack_libc/Cargo.toml | 35 + indra/rust/velopack_libc/LICENSE | 20 + indra/rust/velopack_libc/include/Velopack.h | 606 +++++++ indra/rust/velopack_libc/include/Velopack.hpp | 1149 ++++++++++++ indra/rust/velopack_libc/src/csource.rs | 292 ++++ indra/rust/velopack_libc/src/lib.rs | 890 ++++++++++ indra/rust/velopack_libc/src/raw.rs | 99 ++ indra/rust/velopack_libc/src/statics.rs | 152 ++ indra/rust/velopack_libc/src/types.rs | 709 ++++++++ indra/vcpkg-configuration.json | 4 +- indra/vcpkg.json | 2 +- 20 files changed, 5586 insertions(+), 23 deletions(-) create mode 100644 indra/cmake/Velopack.cmake create mode 100644 indra/rust/Cargo.lock create mode 100644 indra/rust/Cargo.toml create mode 100644 indra/rust/velopack_libc/Cargo.toml create mode 100644 indra/rust/velopack_libc/LICENSE create mode 100644 indra/rust/velopack_libc/include/Velopack.h create mode 100644 indra/rust/velopack_libc/include/Velopack.hpp create mode 100644 indra/rust/velopack_libc/src/csource.rs create mode 100644 indra/rust/velopack_libc/src/lib.rs create mode 100644 indra/rust/velopack_libc/src/raw.rs create mode 100644 indra/rust/velopack_libc/src/statics.rs create mode 100644 indra/rust/velopack_libc/src/types.rs diff --git a/.gitignore b/.gitignore index 14a4742c78..5454f73fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ indra/.distcc indra/out/* indra/packages/* +indra/rust/target/ build-*/ indra/CMakeFiles indra/build-vc[0-9]* diff --git a/CLAUDE.md b/CLAUDE.md index 7c1a6356e2..a8202959e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,8 @@ CMake with vcpkg for dependency management. The source root for CMake is `indra/ ### Prerequisites - CMake 4.0+, Visual Studio 2022/2026 (Windows) or Xcode (macOS) or GCC/Clang+Ninja (Linux) -- Rust and the .NET SDK (`dotnet tool restore`) only for Velopack installers +- Rust only for the Velopack update client (`AL_USE_VELOPACK`, Windows and macOS), whose C API Corrosion builds from `indra/rust` +- The .NET SDK (`dotnet tool restore`) only for Velopack installers - Python 3 with `llsd` (`pip install -r requirements.txt`) only for the four tests that spawn a Python peer; without it they are registered disabled - Python 3 with `PyYAML` and `llsd`, plus bison and flex, only for LL's LSL compiler (`indra/lscript`, a test-only reference); without them it is left out diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index f4bae96a5a..3e1e6e6deb 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -36,6 +36,7 @@ - **media_plugins** - Out-of-process media plugin implementations (CEF, VLC, GStreamer) - **alscript** - Script Studio's language services, free of UI (depends on llcommon, llmath, Tailslide and SLua's Luau). One folder a part, each on the include path: `core/` (problems, symbols, the lexicon, messages, weights, the formatter), `lint/` (the studio's lints and fixes), `lsl/` (the LSL analyzer over Tailslide, the SLua converter), `lsl/optimizer/` (the LSL optimizer, a pass a file, sharing `allsloptimizerpass.h`), `luau/` (the SLua analyzer) and `preprocessor/` (the preprocessor and its includes) - **lscript** - LL's LSL compiler and LSO VM, restored as a test-only reference for the optimizer and the weigher; built with tests, never linked into the viewer +- **rust** - The Rust the viewer builds: one Cargo workspace and lock file, imported by Corrosion, a vcpkg port. It holds velopack_libc, Velopack's C API over the `velopack` crate from crates.io, its sources and headers vendored unchanged from the release (`cmake/Velopack.cmake`): a DLL on Windows, a static library on macOS, the release build for every configuration - **newview** - The main viewer application. Contains all viewer-specific logic: the rendering pipeline, scene graph, UI floaters/panels, settings, object/avatar systems, and the application entry point. Links against all libraries above plus third-party: Tracy, OpenXR, Discord SDK, NVAPI, TinyEXR, Velopack ### Application Lifecycle diff --git a/doc/BUILD.md b/doc/BUILD.md index 73150ae670..59252cf82a 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -19,7 +19,8 @@ Every platform needs a C++ toolchain plus: - **CMake** 4.0+ - **Git** -- **Rust** and **.NET SDK** — only for Velopack installers (`-DAL_USE_VELOPACK=ON`) +- **Rust** — only for the Velopack update client (`-DAL_USE_VELOPACK=ON`, Windows and macOS), whose C API the build compiles from `indra/rust` +- **.NET SDK** — only for Velopack installers - **Python** 3 — only for the tests that spawn a Python peer (see [Running tests](#running-tests)) Install commands are platform-specific; see below. @@ -33,7 +34,7 @@ Install the following: - [Visual Studio 2026](https://visualstudio.microsoft.com/vs/community/) — select the **Desktop development with C++** workload - [CMake](https://cmake.org/download/) 4.0+ - [Git for Windows](https://git-scm.com/install/windows) -- [Rust](https://rust-lang.org/tools/install/) — run `rustup-init.exe` and accept defaults (packaging only) +- [Rust](https://rust-lang.org/tools/install/) — run `rustup-init.exe` and accept defaults (Velopack only) - [.NET SDK](https://dotnet.microsoft.com/en-us/download) (packaging only) Sanity-check in a fresh terminal: @@ -54,10 +55,11 @@ brew install git cmake zip unzip curl pkgconf automake autoconf autoconf-archive gettext libtool rustup dotnet ``` -Initialize the Rust toolchain (packaging only): +Put Homebrew's rustup on the path and install a stable toolchain (Velopack only). The formula is keg-only and no longer provides `rustup-init`; add the `export` to your shell profile too: ``` -rustup-init -y +export PATH="$(brew --prefix rustup)/bin:$PATH" +rustup default stable ``` ### Linux @@ -70,7 +72,7 @@ Install system packages for your distro: ``` sudo pacman -Syu automake autoconf autoconf-archive base-devel cmake fontconfig git glib2-devel \ gstreamer gst-plugins-base-libs ninja libglvnd libtool libvlc libx11 pkgconf python \ - wayland dotnet-sdk rustup zip nasm + wayland dotnet-sdk zip nasm ``` @@ -88,7 +90,7 @@ sudo apt install \ libthai-dev libtool libudev-dev libunwind-dev liburing-dev libvlc-dev libwayland-dev \ libx11-dev libxcursor-dev libxext-dev libxfixes-dev libxft-dev libxi-dev libxinerama-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxtst-dev linux-libc-dev ninja-build \ - pkgconf tar tex-common texinfo unzip zip dotnet-sdk-10.0 rustup nasm + pkgconf tar tex-common texinfo unzip zip dotnet-sdk-10.0 nasm ``` @@ -106,7 +108,7 @@ sudo apt install \ libthai-dev libtool libudev-dev libunwind-dev liburing-dev libvlc-dev libwayland-dev \ libx11-dev libxcursor-dev libxext-dev libxfixes-dev libxft-dev libxi-dev libxinerama-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxtst-dev linux-libc-dev ninja-build \ - pkgconf tar tex-common texinfo unzip zip dotnet-sdk-10.0 rustup nasm + pkgconf tar tex-common texinfo unzip zip dotnet-sdk-10.0 nasm ``` @@ -120,7 +122,7 @@ sudo apt install \ sudo dnf group install "Development Tools" sudo dnf install cmake fontconfig-devel git glib2-devel gstreamer1-devel \ gstreamer1-plugins-base-devel libX11-devel libglvnd-devel \ - ninja-build python3 vlc-devel wayland-devel dotnet-sdk-10.0 rustup + ninja-build python3 vlc-devel wayland-devel dotnet-sdk-10.0 ``` You may need to enable EPEL first: `sudo dnf install epel-release` @@ -131,7 +133,7 @@ You may need to enable EPEL first: `sudo dnf install epel-release` sudo dnf install @development-tools @c-development cmake fontconfig-devel git glib-devel \ gstreamer1-devel gstreamer1-plugins-base-devel libX11-devel \ libglvnd-devel ninja-build python3 vlc-devel \ - wayland-devel dotnet-sdk-10.0 rustup perl-IPC-Cmd perl-FindBin perl-Time-Piece \ + wayland-devel dotnet-sdk-10.0 perl-IPC-Cmd perl-FindBin perl-Time-Piece \ autoconf-archive perl-open libXcursor-devel wayland-protocols-devel dbus-devel \ ibus-devel mesa-libGLU-devel libxkbcommon-devel mesa-libEGL-devel mesa-libGL-devel \ libXtst-devel libXrandr-devel pipewire-devel pulseaudio-libs-devel alsa-lib-devel \ @@ -154,12 +156,6 @@ sudo zypper install cmake fontconfig-devel git glib2-devel gstreamer-devel \ -Initialize a stable Rust toolchain (packaging only): - -``` -rustup default stable -``` - ## Clone and bootstrap Alchemy vendors the [Dullahan](https://github.com/AlchemyViewer/dullahan) CEF wrapper — used by the in-world web media plugin — as a git submodule under `indra/dullahan`. It builds from source as part of the tree, so the submodule must be present before you configure. Clone with `--recurse-submodules`: @@ -521,15 +517,15 @@ Velopack needs the `vpk` .NET tool. Install it once per clone: dotnet tool restore ``` -### Rust / `cargo` missing during packaging +### `rustc` not found when configuring -Velopack invokes `cargo`. Install a stable Rust toolchain: +With `AL_USE_VELOPACK=ON` the build compiles Velopack's C API, `indra/rust/velopack_libc`, with cargo through [Corrosion](https://github.com/corrosion-rs/corrosion), which looks for `rustc` on the path and in `~/.cargo/bin`. Install a stable toolchain: ``` rustup default stable ``` -Only needed with `AL_USE_VELOPACK=ON`. +On macOS, Homebrew's rustup keeps `rustc` and `cargo` in `$(brew --prefix rustup)/bin`, which must be on the path. A target the toolchain lacks, such as `x86_64-apple-darwin` for an x86_64 build on Apple silicon, is added with rustup while configuring; `-DRust_RUSTUP_INSTALL_MISSING_TARGET=OFF` stops that. ### Warnings fail the build diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt index 0b9056352f..4ca83344ff 100644 --- a/indra/CMakeLists.txt +++ b/indra/CMakeLists.txt @@ -422,6 +422,7 @@ set( SDL3 Sentry Tracy + Velopack WebRTC ) diff --git a/indra/cmake/Dependencies.cmake b/indra/cmake/Dependencies.cmake index 8a57a6ff4a..9391efc7ad 100644 --- a/indra/cmake/Dependencies.cmake +++ b/indra/cmake/Dependencies.cmake @@ -110,7 +110,6 @@ al_import(ll::fontconfig PACKAGE Fontconfig TARGETS Fontconfig al_import(ll::faudio PACKAGE FAudio CONFIG TARGETS FAudio::FAudio DEFINES LL_FAUDIO=1 WHEN AL_USE_FAUDIO) al_import(ll::openal PACKAGE OpenAL CONFIG TARGETS OpenAL::OpenAL DEFINES LL_OPENAL=1 WHEN AL_USE_OPENAL) al_import(ll::openxr PACKAGE OpenXR CONFIG TARGETS OpenXR::headers OpenXR::openxr_loader WHEN AL_USE_OPENXR) -al_import(ll::velopack PACKAGE unofficial-velopack CONFIG TARGETS unofficial::velopack::velopack DEFINES LL_VELOPACK=1 WHEN AL_USE_VELOPACK) al_import(ll::nsspellchecker LINK "-framework AppKit" "-framework Foundation" WHEN AL_USE_NSSPELLCHECKER) al_import(ll::winspellcheck LINK ole32 WHEN AL_USE_WINSPELLCHECK) # gersemi: on diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake new file mode 100644 index 0000000000..afb4af6542 --- /dev/null +++ b/indra/cmake/Velopack.cmake @@ -0,0 +1,59 @@ +# -*- cmake -*- +# +# The Velopack update client: velopack_libc, Velopack's C API, built from +# rust/ by Corrosion over the velopack crate. A DLL on Windows, which the +# install carries beside the viewer with its other runtime DLLs; a static +# library on macOS, which needs nothing the standard library does not. The +# release build serves every configuration. +include_guard() +add_library(ll::velopack INTERFACE IMPORTED) + +if(AL_USE_VELOPACK) + # Corrosion takes the host's target outside Windows, and a Mac builds + # either architecture; with none named, it builds the host's. A target + # rustup lacks is added, as for any crate. + if(DARWIN AND CMAKE_OSX_ARCHITECTURES) + if(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + set(Rust_CARGO_TARGET x86_64-apple-darwin) + elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") + set(Rust_CARGO_TARGET aarch64-apple-darwin) + else() + message(FATAL_ERROR "Velopack builds for one architecture, not '${CMAKE_OSX_ARCHITECTURES}'") + endif() + endif() + set(Rust_RUSTUP_INSTALL_MISSING_TARGET ON CACHE BOOL "Install a missing Rust target with rustup") + find_package(Corrosion CONFIG REQUIRED) + + if(WINDOWS) + set(velopack_crate_type cdylib) + else() + set(velopack_crate_type staticlib) + endif() + corrosion_import_crate( + MANIFEST_PATH "${INDRA_SOURCE_DIR}/rust/Cargo.toml" + CRATES velopack_libc + CRATE_TYPES ${velopack_crate_type} + PROFILE release + LOCKED + ) + # Built for the viewer that links it, not by every build with Velopack on. + set_target_properties(cargo-build_velopack_libc PROPERTIES EXCLUDE_FROM_ALL TRUE) + if(DARWIN) + # What Corrosion links a static library with on macOS, System, c and m, + # is libSystem, which every link takes already: given again, ld warns of + # the duplicate. Nor is its CommandLineTools SDK directory wanted, which + # a Mac with only Xcode does not have. + set_target_properties( + velopack_libc-static + PROPERTIES INTERFACE_LINK_LIBRARIES "" INTERFACE_LINK_DIRECTORIES "" + ) + endif() + + target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1) + target_include_directories( + ll::velopack + SYSTEM + INTERFACE "${INDRA_SOURCE_DIR}/rust/velopack_libc/include" + ) + target_link_libraries(ll::velopack INTERFACE velopack_libc) +endif() diff --git a/indra/rust/Cargo.lock b/indra/rust/Cargo.lock new file mode 100644 index 0000000000..3533568846 --- /dev/null +++ b/indra/rust/Cargo.lock @@ -0,0 +1,1542 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "log-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a42526bb432bcd1b43571d5f163984effa25409a29f1a3242a54d0577d55bcf" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "log-panics" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" +dependencies = [ + "log", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "rand", + "wasm-bindgen", +] + +[[package]] +name = "velopack" +version = "1.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4295172ffaad78f9adbab35f7ffefa97e4ceefee9d2c38bda37fdce4d6c66e5a" +dependencies = [ + "anyhow", + "bitflags", + "derivative", + "glob", + "lazy_static", + "libc", + "log", + "log-panics", + "normpath", + "rand", + "regex", + "semver", + "serde", + "serde_json", + "sha1", + "sha2", + "simplelog", + "thiserror", + "time", + "ureq", + "url", + "uuid", + "wait-timeout", + "waitpid-any", + "windows", + "xml", + "zip", +] + +[[package]] +name = "velopack_libc" +version = "1.2.161" +dependencies = [ + "anyhow", + "lazy_static", + "libc", + "log", + "log-derive", + "semver", + "serde_json", + "simplelog", + "tempfile", + "velopack", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waitpid-any" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18aa3ce681e189f125c4c1e1388c03285e2fd434ef52c7203084012ac29c5e4a" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/indra/rust/Cargo.toml b/indra/rust/Cargo.toml new file mode 100644 index 0000000000..0396347a93 --- /dev/null +++ b/indra/rust/Cargo.toml @@ -0,0 +1,11 @@ +# The Rust the viewer builds, through Corrosion; see cmake/Velopack.cmake. +# One workspace, so one Cargo.lock pins every crate, and CI keys its crate +# cache on it. +# +# Cargo's default release profile. Velopack's own optimises for size with +# LTO into one code unit, which made velopack_libc a quarter the size and +# its build four times as long; every build tree builds it now, and the +# linker drops what the viewer does not call. +[workspace] +resolver = "2" +members = ["velopack_libc"] diff --git a/indra/rust/velopack_libc/Cargo.toml b/indra/rust/velopack_libc/Cargo.toml new file mode 100644 index 0000000000..30b7056efa --- /dev/null +++ b/indra/rust/velopack_libc/Cargo.toml @@ -0,0 +1,35 @@ +# Velopack's C API, velopack_libc: src/ and include/ are velopack's +# src/lib-cpp at the release this names, unchanged. It is not published, so +# it is built here over the velopack crate from crates.io, which its own +# workspace takes by path. build.rs, which writes include/Velopack.h again +# with cbindgen on every build, is left out: the header is the released one. +# +# To move to a new release: copy src/ and include/ from src/lib-cpp at its +# tag, set both versions below, and run `cargo update -p velopack` in +# indra/rust, which moves only what the new velopack needs. +[package] +name = "velopack_libc" +version = "1.2.161" +publish = false +description = "Installer and automatic update framework for cross-platform desktop applications" +license = "MIT" +edition = "2021" +rust-version = "1.75" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib", "staticlib"] + +[dependencies] +velopack = { version = "=1.2.161", features = ["file-logging", "public-utils"] } +anyhow = "1.0" +lazy_static = "1.5" +log = "0.4" +simplelog = "0.12" +log-derive = "0.4.1" +libc = "0.2" +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.27" +semver = "1.0" diff --git a/indra/rust/velopack_libc/LICENSE b/indra/rust/velopack_libc/LICENSE new file mode 100644 index 0000000000..e0ccfbd78c --- /dev/null +++ b/indra/rust/velopack_libc/LICENSE @@ -0,0 +1,20 @@ +Copyright © 2021 Caelan Sayler +Copyright © 2024 Velopack Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/indra/rust/velopack_libc/include/Velopack.h b/indra/rust/velopack_libc/include/Velopack.h new file mode 100644 index 0000000000..00b561532e --- /dev/null +++ b/indra/rust/velopack_libc/include/Velopack.h @@ -0,0 +1,606 @@ +#ifndef VELOPACK_H +#define VELOPACK_H + +/* Generated with cbindgen:0.29.4 */ + +/* THIS FILE IS AUTO-GENERATED - DO NOT EDIT */ + +#include +#include +#include +#include + +/** + * The result of a call to check for updates. This can indicate that an update is available, or that an error occurred. + */ +enum vpkc_update_check_t +#if defined(__cplusplus) || __STDC_VERSION__ >= 202311L + : int8_t +#endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L + { + UPDATE_ERROR = -1, + UPDATE_AVAILABLE = 0, + NO_UPDATE_AVAILABLE = 1, + REMOTE_IS_EMPTY = 2, +}; +#ifndef __cplusplus +#if __STDC_VERSION__ >= 202311L +typedef enum vpkc_update_check_t vpkc_update_check_t; +#else +typedef int8_t vpkc_update_check_t; +#endif // __STDC_VERSION__ >= 202311L +#endif // __cplusplus + +/** + * Opaque type for a Velopack UpdateSource. Must be freed with `vpkc_free_update_source`. + */ +typedef void vpkc_update_source_t; + +/** + * A single HTTP header (name and value pair) to be sent with a web request. + */ +typedef struct vpkc_http_header_t { + /** + * The name of the HTTP header (eg. "Authorization"). + */ + char *Name; + /** + * The value of the HTTP header. + */ + char *Value; +} vpkc_http_header_t; + +/** + * Options to customize HTTP requests (custom headers, timeout, etc). + */ +typedef struct vpkc_http_options_t { + /** + * Additional headers to send with each request. + */ + struct vpkc_http_header_t **Headers; + /** + * The number of elements in the Headers array. + */ + size_t HeadersCount; + /** + * Timeout applied to the entire request (connection + transfer), in milliseconds. + * The default of 0 means requests never time out. + */ + uint64_t TimeoutMilliseconds; +} vpkc_http_options_t; + +/** + * User delegate for to fetch a release feed. This function should return the raw JSON string of the release.json feed. + */ +typedef char *(*vpkc_release_feed_delegate_t)(void *p_user_data, const char *psz_releases_name); + +/** + * User delegate for freeing a release feed. This function should free the feed string returned by `vpkc_release_feed_delegate_t`. + */ +typedef void (*vpkc_free_release_feed_t)(void *p_user_data, char *psz_feed); + +/** + * An individual Velopack asset, could refer to an asset on-disk or in a remote package feed. + */ +typedef struct vpkc_asset_t { + /** + * The name or Id of the package containing this release. + */ + char *PackageId; + /** + * The version of this release. + */ + char *Version; + /** + * The type of asset (eg. "Full" or "Delta"). + */ + char *Type; + /** + * The filename of the update package containing this release. + */ + char *FileName; + /** + * The SHA1 checksum of the update package containing this release. + */ + char *SHA1; + /** + * The SHA256 checksum of the update package containing this release. + */ + char *SHA256; + /** + * The size in bytes of the update package containing this release. + */ + uint64_t Size; + /** + * The release notes in markdown format, as passed to Velopack when packaging the release. This may be an empty string. + */ + char *NotesMarkdown; + /** + * The release notes in HTML format, transformed from Markdown when packaging the release. This may be an empty string. + */ + char *NotesHtml; +} vpkc_asset_t; + +/** + * User delegate for downloading an asset file. This function is expected to download the provided asset + * to the provided local file path. Througout, you can use the progress callback to write progress reports. + * The function should return true if the download was successful, false otherwise. + * Progress + */ +typedef bool (*vpkc_download_asset_delegate_t)(void *p_user_data, + const struct vpkc_asset_t *p_asset, + const char *psz_local_path, + size_t progress_callback_id); + +/** + * Options to customise the behaviour of UpdateManager. + */ +typedef struct vpkc_update_options_t { + /** + * Allows UpdateManager to update to a version that's lower than the current version (i.e. downgrading). + * This could happen if a release has bugs and was retracted from the release feed, or if you're using + * ExplicitChannel to switch channels to another channel where the latest version on that + * channel is lower than the current version. + */ + bool AllowVersionDowngrade; + /** + * **This option should usually be left None**. + * Overrides the default channel used to fetch updates. + * The default channel will be whatever channel was specified on the command line when building this release. + * For example, if the current release was packaged with '--channel beta', then the default channel will be 'beta'. + * This allows users to automatically receive updates from the same channel they installed from. This options + * allows you to explicitly switch channels, for example if the user wished to switch back to the 'stable' channel + * without having to reinstall the application. + */ + char *ExplicitChannel; + /** + * Sets the maximum number of deltas to consider before falling back to a full update. + * The default is 10. Set to a negative number (eg. -1) to disable deltas. + */ + int32_t MaximumDeltasBeforeFallback; +} vpkc_update_options_t; + +/** + * VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). + */ +typedef struct vpkc_locator_config_t { + /** + * The root directory of the current app, or the path to the AppImage file on Linux. + */ + char *RootAppDir; + /** + * The path to the Update.exe binary. + */ + char *UpdateExePath; + /** + * The path to the packages' directory. + */ + char *PackagesDir; + /** + * The current app manifest. + */ + char *ManifestPath; + /** + * The directory containing the application's user binaries. + */ + char *CurrentBinaryDir; + /** + * Whether the current application is portable or installed. + */ + bool IsPortable; +} vpkc_locator_config_t; + +/** + * Opaque type for the Velopack UpdateManager. Must be freed with `vpkc_free_update_manager`. + */ +typedef void vpkc_update_manager_t; + +/** + * Holds information about the current version and pending updates, such as how many there are, and access to release notes. + */ +typedef struct vpkc_update_info_t { + /** + * The available version that we are updating to. + */ + struct vpkc_asset_t *TargetFullRelease; + /** + * The base release that this update is based on. This is only available if the update is a delta update. + */ + struct vpkc_asset_t *BaseRelease; + /** + * The list of delta updates that can be applied to the base version to get to the target version. + */ + struct vpkc_asset_t **DeltasToTarget; + /** + * The number of elements in the DeltasToTarget array. + */ + size_t DeltasToTargetCount; + /** + * True if the update is a version downgrade or lateral move (such as when switching channels to the same version number). + * In this case, only full updates are allowed, and any local packages on disk newer than the downloaded version will be + * deleted. + */ + bool IsDowngrade; +} vpkc_update_info_t; + +/** + * Progress callback function. + */ +typedef void (*vpkc_progress_callback_t)(void *p_user_data, size_t progress); + +/** + * VelopackApp startup hook callback function. + */ +typedef void (*vpkc_hook_callback_t)(void *p_user_data, const char *psz_app_version); + +/** + * Log callback function. + */ +typedef void (*vpkc_log_callback_t)(void *p_user_data, + const char *psz_level, + const char *psz_message); + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Create a new FileSource update source for a given file path. + * @param psz_file_path The path to a local directory containing updates. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_file(const char *psz_file_path); + +/** + * Create a new HttpSource update source for a given HTTP URL. + * @param psz_http_url The URL to a remote update server. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_http_url(const char *psz_http_url); + +/** + * Create a new HttpSource update source for a given HTTP URL, with custom HTTP options. + * @param psz_http_url The URL to a remote update server. + * @param p_options Optional HTTP options (custom headers, timeout). Can be null. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_http_url_with_options(const char *psz_http_url, + struct vpkc_http_options_t *p_options); + +/** + * Create a new GithubSource update source for a GitHub repository. + * @param psz_repo_url The GitHub repository URL (e.g. "https://github.com/user/repo"). + * @param psz_access_token Optional access token for private repositories (can be null). + * @param b_prerelease Whether to include pre-release versions. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_github(const char *psz_repo_url, + const char *psz_access_token, + bool b_prerelease); + +/** + * Create a new GitlabSource update source for a GitLab repository. + * @param psz_repo_url The GitLab repository URL (e.g. "https://gitlab.com/user/repo"). + * @param psz_access_token Optional access token for private repositories (can be null). + * @param b_prerelease Whether to include pre-release versions. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_gitlab(const char *psz_repo_url, + const char *psz_access_token, + bool b_prerelease); + +/** + * Create a new GiteaSource update source for a Gitea repository. + * @param psz_repo_url The Gitea repository URL (e.g. "https://gitea.example.com/user/repo"). + * @param psz_access_token Optional access token for private repositories (can be null). + * @param b_prerelease Whether to include pre-release versions. + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_gitea(const char *psz_repo_url, + const char *psz_access_token, + bool b_prerelease); + +/** + * Create a new VelopackFlowSource update source for Velopack Flow. + * @param psz_base_uri Optional base URI for the Velopack Flow API (can be null for default). + * @returns A new vpkc_update_source_t instance, or null on error. + */ +vpkc_update_source_t *vpkc_new_source_velopack_flow(const char *psz_base_uri); + +/** + * Create a new _CUSTOM_ update source with user-provided callbacks to fetch release feeds and download assets. + * You can report download progress using `vpkc_source_report_progress`. Note that the callbacks must be valid + * for the lifetime of any UpdateManager's that use this source. You should call `vpkc_free_source` to free the source, + * but note that if the source is still in use by an UpdateManager, it will not be freed until the UpdateManager is freed. + * Therefore to avoid possible issues, it is recommended to create this type of source once for the lifetime of your application. + * @param cb_release_feed A callback to fetch the release feed. + * @param cb_free_release_feed A callback to free the memory allocated by `cb_release_feed`. + * @param cb_download_entry A callback to download an asset. + * @param p_user_data Optional user data to be passed to the callbacks. + * @returns A new vpkc_update_source_t instance, or null on error. If null, the error will be available via `vpkc_get_last_error`. + */ +vpkc_update_source_t *vpkc_new_source_custom_callback(vpkc_release_feed_delegate_t cb_release_feed, + vpkc_free_release_feed_t cb_free_release_feed, + vpkc_download_asset_delegate_t cb_download_entry, + void *p_user_data); + +/** + * Sends a progress update to the callback with the specified ID. This is used by custom + * update sources created with `vpkc_new_source_custom_callback` to report download progress. + * @param progress_callback_id The ID of the progress callback to send the update to. + * @param progress The progress value to send (0-100). + */ +void vpkc_source_report_progress(size_t progress_callback_id, int16_t progress); + +/** + * Frees a vpkc_update_source_t instance. + * @param p_source The source to free. + */ +void vpkc_free_source(vpkc_update_source_t *p_source); + +/** + * Create a new UpdateManager instance. + * @param psz_url_or_path Location of the http update server url or path to the local update directory. + * @param p_options Optional extra configuration for update manager. + * @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. + * @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. + * @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_new_update_manager(const char *psz_url_or_path, + struct vpkc_update_options_t *p_options, + struct vpkc_locator_config_t *p_locator, + vpkc_update_manager_t **p_manager); + +/** + * Create a new UpdateManager instance which retrieves updates from the hosted Velopack Flow service (https://velopack.io). + * This is a convenience function which is equivalent to creating a VelopackFlowSource and calling vpkc_new_update_manager_with_source. + * @param p_options Optional extra configuration for update manager. + * @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. + * @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. + * @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_new_update_manager_flow(struct vpkc_update_options_t *p_options, + struct vpkc_locator_config_t *p_locator, + vpkc_update_manager_t **p_manager); + +/** + * Create a new UpdateManager instance with a custom UpdateSource. + * @param p_source A pointer to a custom UpdateSource. + * @param p_options Optional extra configuration for update manager. + * @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. + * @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. + * @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_new_update_manager_with_source(vpkc_update_source_t *p_source, + struct vpkc_update_options_t *p_options, + struct vpkc_locator_config_t *p_locator, + vpkc_update_manager_t **p_manager); + +/** + * Returns the currently installed version of the app. + * @param p_manager The update manager instance. + * @param psz_version A buffer to store the version string. + * @param c_version The size of the `psz_version` buffer. + * @returns The number of characters written to `psz_version` (including null terminator), or the required buffer size if the buffer is too small. + */ +size_t vpkc_get_current_version(vpkc_update_manager_t *p_manager, + char *psz_version, + size_t c_version); + +/** + * Returns the currently installed app id. + * @param p_manager The update manager instance. + * @param psz_id A buffer to store the app id string. + * @param c_id The size of the `psz_id` buffer. + * @returns The number of characters written to `psz_id` (including null terminator), or the required buffer size if the buffer is too small. + */ +size_t vpkc_get_app_id(vpkc_update_manager_t *p_manager, + char *psz_id, + size_t c_id); + +/** + * Returns whether the app is in portable mode. On Windows this can be true or false. + * On MacOS and Linux this will always be true. + * @param p_manager The update manager instance. + * @returns True if the app is in portable mode, false otherwise. + */ +bool vpkc_is_portable(vpkc_update_manager_t *p_manager); + +/** + * Returns an asset if there is an update downloaded which still needs to be applied. + * You can pass this asset to `vpkc_wait_exit_then_apply_updates` to apply the update. + * @param p_manager The update manager instance. + * @param p_asset A pointer to where the new vpkc_asset_t* instance will be stored. + * @returns True if there is an update pending restart, false otherwise. + */ +bool vpkc_update_pending_restart(vpkc_update_manager_t *p_manager, struct vpkc_asset_t **p_asset); + +/** + * Checks for updates. If there are updates available, this method will return an + * UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available. + * @param p_manager The update manager instance. + * @param p_update A pointer to where the new vpkc_update_info_t* instance will be stored if an update is available. + * @returns A `vpkc_update_check_t` value indicating the result of the check. If an update is available, the value will be `HasUpdate` and `p_update` will be populated. + */ +vpkc_update_check_t vpkc_check_for_updates(vpkc_update_manager_t *p_manager, + struct vpkc_update_info_t **p_update); + +/** + * Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional callback. + * This function will acquire a global update lock so may fail if there is already another update operation in progress. + * - If the update contains delta packages and the delta feature is enabled + * this method will attempt to unpack and prepare them. + * - If there is no delta update available, or there is an error preparing delta + * packages, this method will fall back to downloading the full version of the update. + * @param p_manager The update manager instance. + * @param p_update The update info object from `vpkc_check_for_updates`. + * @param cb_progress An optional callback to report download progress (0-100). + * @param p_user_data Optional user data to be passed to the progress callback. + * @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_download_updates(vpkc_update_manager_t *p_manager, + struct vpkc_update_info_t *p_update, + vpkc_progress_callback_t cb_progress, + void *p_user_data); + +/** + * This will launch the Velopack updater and tell it to wait for this program to exit gracefully. + * You should then clean up any state and exit your app. The updater will apply updates and then + * (if specified) restart your app. The updater will only wait for 60 seconds before giving up. + * @param p_manager The update manager instance. + * @param p_asset The asset to apply. This can be from `vpkc_update_pending_restart` or `vpkc_update_info_get_target_asset`. + * @param b_silent True to attempt to apply the update without showing any UI. + * @param b_restart True to restart the app after the update is applied. + * @param p_restart_args An array of command line arguments to pass to the new process when it's restarted. + * @param c_restart_args The number of arguments in `p_restart_args`. + * @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_wait_exit_then_apply_updates(vpkc_update_manager_t *p_manager, + struct vpkc_asset_t *p_asset, + bool b_silent, + bool b_restart, + char **p_restart_args, + size_t c_restart_args); + +/** + * This will launch the Velopack updater and optionally wait for a program to exit gracefully. + * This method is unsafe because it does not necessarily wait for any / the correct process to exit + * before applying updates. The `vpkc_wait_exit_then_apply_updates` method is recommended for most use cases. + * If dw_wait_pid is 0, the updater will not wait for any process to exit before applying updates (Not Recommended). + * @param p_manager The update manager instance. + * @param p_asset The asset to apply. This can be from `vpkc_update_pending_restart` or `vpkc_update_info_get_target_asset`. + * @param b_silent True to attempt to apply the update without showing any UI. + * @param dw_wait_pid The process ID to wait for before applying updates. If 0, the updater will not wait. + * @param b_restart True to restart the app after the update is applied. + * @param p_restart_args An array of command line arguments to pass to the new process when it's restarted. + * @param c_restart_args The number of arguments in `p_restart_args`. + * @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. + */ +bool vpkc_unsafe_apply_updates(vpkc_update_manager_t *p_manager, + struct vpkc_asset_t *p_asset, + bool b_silent, + uint32_t dw_wait_pid, + bool b_restart, + char **p_restart_args, + size_t c_restart_args); + +/** + * Frees a vpkc_update_manager_t instance. + * @param p_manager The update manager instance to free. + */ +void vpkc_free_update_manager(vpkc_update_manager_t *p_manager); + +/** + * Frees a vpkc_update_info_t instance. + * @param p_update_info The update info instance to free. + */ +void vpkc_free_update_info(struct vpkc_update_info_t *p_update_info); + +/** + * Frees a vpkc_asset_t instance. + * @param p_asset The asset instance to free. + */ +void vpkc_free_asset(struct vpkc_asset_t *p_asset); + +/** + * VelopackApp helps you to handle app activation events correctly. + * This should be used as early as possible in your application startup code. + * (eg. the beginning of main() or wherever your entry point is). + * This function will not return in some cases. + * @param p_user_data Optional user data to be passed to the callbacks. + */ +void vpkc_app_run(void *p_user_data); + +/** + * Set whether to automatically apply downloaded updates on startup. This is ON by default. + * @param b_auto_apply True to automatically apply updates, false otherwise. + */ +void vpkc_app_set_auto_apply_on_startup(bool b_auto_apply); + +/** + * Override the command line arguments used by VelopackApp. (by default this is env::args().skip(1)) + * @param p_args An array of command line arguments. + * @param c_args The number of arguments in `p_args`. + */ +void vpkc_app_set_args(char **p_args, size_t c_args); + +/** + * VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). + * @param p_locator The locator configuration to use. + */ +void vpkc_app_set_locator(struct vpkc_locator_config_t *p_locator); + +/** + * Sets a callback to be run after the app is installed. + * WARNING: FastCallback hooks are run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cb_after_install The callback to run after the app is installed. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_after_install(vpkc_hook_callback_t cb_after_install); + +/** + * Sets a callback to be run before the app is uninstalled. + * WARNING: FastCallback hooks are run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cb_before_uninstall The callback to run before the app is uninstalled. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_before_uninstall(vpkc_hook_callback_t cb_before_uninstall); + +/** + * Sets a callback to be run before the app is updated. + * WARNING: FastCallback hooks are run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cb_before_update The callback to run before the app is updated. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_before_update(vpkc_hook_callback_t cb_before_update); + +/** + * Sets a callback to be run after the app is updated. + * WARNING: FastCallback hooks are run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cb_after_update The callback to run after the app is updated. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_after_update(vpkc_hook_callback_t cb_after_update); + +/** + * This hook is triggered when the application is started for the first time after installation. + * @param cb_first_run The callback to run on first run. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_first_run(vpkc_hook_callback_t cb_first_run); + +/** + * This hook is triggered when the application is restarted by Velopack after installing updates. + * @param cb_restarted The callback to run after the app is restarted. The callback takes a user data pointer and the version of the app as a string. + */ +void vpkc_app_set_hook_restarted(vpkc_hook_callback_t cb_restarted); + +/** + * Get the last error message that occurred in the Velopack library. + * @param psz_error A buffer to store the error message. + * @param c_error The size of the `psz_error` buffer. + * @returns The number of characters written to `psz_error` (including null terminator). If the return value is greater than `c_error`, the buffer was too small and the message was truncated. + */ +size_t vpkc_get_last_error(char *psz_error, + size_t c_error); + +/** + * Set a custom log callback. This will be called for all log messages generated by the Velopack library. + * @param cb_log The callback to call with log messages. The callback takes a user data pointer, a log level, and the log message as a string. + * @param p_user_data Optional user data to be passed to the callback. + */ +void vpkc_set_logger(vpkc_log_callback_t cb_log, + void *p_user_data); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* VELOPACK_H */ diff --git a/indra/rust/velopack_libc/include/Velopack.hpp b/indra/rust/velopack_libc/include/Velopack.hpp new file mode 100644 index 0000000000..f0c0a0d27c --- /dev/null +++ b/indra/rust/velopack_libc/include/Velopack.hpp @@ -0,0 +1,1149 @@ +//! This header provides the C++ API for the Velopack library. +//! This C++ API is a thin wrapper around the C API, providing a more idiomatic C++ interface. +#ifndef VELOPACK_HPP +#define VELOPACK_HPP + +#include +#include +#include +#include +#include +#include + +#include "Velopack.h" + +#if !defined(_WIN32) +#include +#endif + +namespace Velopack { + +static inline void throw_last_error() +{ + size_t neededSize = vpkc_get_last_error(nullptr, 0); + std::string strError(neededSize, '\0'); + vpkc_get_last_error(&strError[0], neededSize); + throw std::runtime_error(strError); +} + +static inline std::optional to_cpp_string(const char* psz) +{ + return psz == nullptr ? std::optional("") : std::optional(psz); +} + +static inline char* alloc_c_string(const std::string& str) +{ + char* result = new char[str.size() + 1]; // +1 for null-terminator +#ifdef _WIN32 + strcpy_s(result, str.size() + 1, str.c_str()); // Copy string content +#else + strcpy(result, str.c_str()); // Copy string content +#endif + result[str.size()] = '\0'; // Null-terminate the string + return result; +} + +static inline char* alloc_c_string(const std::optional& str) +{ + if (!str.has_value()) { return nullptr; } + return alloc_c_string(str.value()); +} + +static inline void free_c_string(char* str) +{ + delete[] str; +} + +static inline char** alloc_c_string_vec(const std::vector& dto, size_t* count) +{ + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + char** arr = new char* [*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_string(dto[i]); + } + return arr; +} + +static inline void free_c_string_vec(char** arr, size_t size) +{ + for (size_t i = 0; i < size; ++i) { + free_c_string(arr[i]); + arr[i] = nullptr; + } + delete[] arr; +} + +template +inline T unwrap(const std::optional& opt, const std::string& message = "Expected value not present") { + if (!opt.has_value()) { + throw std::runtime_error(message); + } + return opt.value(); +} + +// !! AUTO-GENERATED-START CPP_TYPES + +/** A single HTTP header (name and value pair) to be sent with a web request. */ +struct HttpHeader { + /** The name of the HTTP header (eg. "Authorization"). */ + std::string Name; + /** The value of the HTTP header. */ + std::string Value; +}; + +static inline std::optional to_cpp_HttpHeader(const vpkc_http_header_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + unwrap(to_cpp_string(dto->Name), "Required property Name was null"), + unwrap(to_cpp_string(dto->Value), "Required property Value was null"), + }); +} + +static inline std::vector to_cpp_HttpHeader_vec(const vpkc_http_header_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_HttpHeader(dto))); + } + return result; +} + +static inline vpkc_http_header_t* alloc_c_HttpHeader_ptr(const HttpHeader* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_http_header_t* obj = new vpkc_http_header_t{}; + obj->Name = alloc_c_string(dto->Name); + obj->Value = alloc_c_string(dto->Value); + return obj; +} + +static inline vpkc_http_header_t* alloc_c_HttpHeader(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + HttpHeader obj = unwrap(dto); + return alloc_c_HttpHeader_ptr(&obj); +} + +static inline vpkc_http_header_t** alloc_c_HttpHeader_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_http_header_t** arr = new vpkc_http_header_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_HttpHeader(dto[i]); + } + return arr; +} + +static inline void free_c_HttpHeader(vpkc_http_header_t* obj) { + if (obj == nullptr) { return; } + free_c_string(obj->Name); + free_c_string(obj->Value); + delete obj; +} + +static inline void free_c_HttpHeader_vec(vpkc_http_header_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_HttpHeader(arr[i]); + } + delete[] arr; +} + +/** Options to customize HTTP requests (custom headers, timeout, etc). */ +struct HttpOptions { + /** Additional headers to send with each request. */ + std::vector Headers; + /** + * Timeout applied to the entire request (connection + transfer), in milliseconds. + * The default of 0 means requests never time out. + */ + uint64_t TimeoutMilliseconds; +}; + +static inline std::optional to_cpp_HttpOptions(const vpkc_http_options_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + to_cpp_HttpHeader_vec(dto->Headers, dto->HeadersCount), + dto->TimeoutMilliseconds, + }); +} + +static inline std::vector to_cpp_HttpOptions_vec(const vpkc_http_options_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_HttpOptions(dto))); + } + return result; +} + +static inline vpkc_http_options_t* alloc_c_HttpOptions_ptr(const HttpOptions* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_http_options_t* obj = new vpkc_http_options_t{}; + obj->Headers = alloc_c_HttpHeader_vec(dto->Headers, &obj->HeadersCount); + obj->TimeoutMilliseconds = dto->TimeoutMilliseconds; + return obj; +} + +static inline vpkc_http_options_t* alloc_c_HttpOptions(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + HttpOptions obj = unwrap(dto); + return alloc_c_HttpOptions_ptr(&obj); +} + +static inline vpkc_http_options_t** alloc_c_HttpOptions_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_http_options_t** arr = new vpkc_http_options_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_HttpOptions(dto[i]); + } + return arr; +} + +static inline void free_c_HttpOptions(vpkc_http_options_t* obj) { + if (obj == nullptr) { return; } + free_c_HttpHeader_vec(obj->Headers, obj->HeadersCount); + + delete obj; +} + +static inline void free_c_HttpOptions_vec(vpkc_http_options_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_HttpOptions(arr[i]); + } + delete[] arr; +} + +/** VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). */ +struct VelopackLocatorConfig { + /** The root directory of the current app, or the path to the AppImage file on Linux. */ + std::string RootAppDir; + /** The path to the Update.exe binary. */ + std::string UpdateExePath; + /** The path to the packages' directory. */ + std::string PackagesDir; + /** The current app manifest. */ + std::string ManifestPath; + /** The directory containing the application's user binaries. */ + std::string CurrentBinaryDir; + /** Whether the current application is portable or installed. */ + bool IsPortable; +}; + +static inline std::optional to_cpp_VelopackLocatorConfig(const vpkc_locator_config_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + unwrap(to_cpp_string(dto->RootAppDir), "Required property RootAppDir was null"), + unwrap(to_cpp_string(dto->UpdateExePath), "Required property UpdateExePath was null"), + unwrap(to_cpp_string(dto->PackagesDir), "Required property PackagesDir was null"), + unwrap(to_cpp_string(dto->ManifestPath), "Required property ManifestPath was null"), + unwrap(to_cpp_string(dto->CurrentBinaryDir), "Required property CurrentBinaryDir was null"), + dto->IsPortable, + }); +} + +static inline std::vector to_cpp_VelopackLocatorConfig_vec(const vpkc_locator_config_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_VelopackLocatorConfig(dto))); + } + return result; +} + +static inline vpkc_locator_config_t* alloc_c_VelopackLocatorConfig_ptr(const VelopackLocatorConfig* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_locator_config_t* obj = new vpkc_locator_config_t{}; + obj->RootAppDir = alloc_c_string(dto->RootAppDir); + obj->UpdateExePath = alloc_c_string(dto->UpdateExePath); + obj->PackagesDir = alloc_c_string(dto->PackagesDir); + obj->ManifestPath = alloc_c_string(dto->ManifestPath); + obj->CurrentBinaryDir = alloc_c_string(dto->CurrentBinaryDir); + obj->IsPortable = dto->IsPortable; + return obj; +} + +static inline vpkc_locator_config_t* alloc_c_VelopackLocatorConfig(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + VelopackLocatorConfig obj = unwrap(dto); + return alloc_c_VelopackLocatorConfig_ptr(&obj); +} + +static inline vpkc_locator_config_t** alloc_c_VelopackLocatorConfig_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_locator_config_t** arr = new vpkc_locator_config_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_VelopackLocatorConfig(dto[i]); + } + return arr; +} + +static inline void free_c_VelopackLocatorConfig(vpkc_locator_config_t* obj) { + if (obj == nullptr) { return; } + free_c_string(obj->RootAppDir); + free_c_string(obj->UpdateExePath); + free_c_string(obj->PackagesDir); + free_c_string(obj->ManifestPath); + free_c_string(obj->CurrentBinaryDir); + + delete obj; +} + +static inline void free_c_VelopackLocatorConfig_vec(vpkc_locator_config_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_VelopackLocatorConfig(arr[i]); + } + delete[] arr; +} + +/** An individual Velopack asset, could refer to an asset on-disk or in a remote package feed. */ +struct VelopackAsset { + /** The name or Id of the package containing this release. */ + std::string PackageId; + /** The version of this release. */ + std::string Version; + /** The type of asset (eg. "Full" or "Delta"). */ + std::string Type; + /** The filename of the update package containing this release. */ + std::string FileName; + /** The SHA1 checksum of the update package containing this release. */ + std::string SHA1; + /** The SHA256 checksum of the update package containing this release. */ + std::string SHA256; + /** The size in bytes of the update package containing this release. */ + uint64_t Size; + /** The release notes in markdown format, as passed to Velopack when packaging the release. This may be an empty string. */ + std::string NotesMarkdown; + /** The release notes in HTML format, transformed from Markdown when packaging the release. This may be an empty string. */ + std::string NotesHtml; +}; + +static inline std::optional to_cpp_VelopackAsset(const vpkc_asset_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + unwrap(to_cpp_string(dto->PackageId), "Required property PackageId was null"), + unwrap(to_cpp_string(dto->Version), "Required property Version was null"), + unwrap(to_cpp_string(dto->Type), "Required property Type was null"), + unwrap(to_cpp_string(dto->FileName), "Required property FileName was null"), + unwrap(to_cpp_string(dto->SHA1), "Required property SHA1 was null"), + unwrap(to_cpp_string(dto->SHA256), "Required property SHA256 was null"), + dto->Size, + unwrap(to_cpp_string(dto->NotesMarkdown), "Required property NotesMarkdown was null"), + unwrap(to_cpp_string(dto->NotesHtml), "Required property NotesHtml was null"), + }); +} + +static inline std::vector to_cpp_VelopackAsset_vec(const vpkc_asset_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_VelopackAsset(dto))); + } + return result; +} + +static inline vpkc_asset_t* alloc_c_VelopackAsset_ptr(const VelopackAsset* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_asset_t* obj = new vpkc_asset_t{}; + obj->PackageId = alloc_c_string(dto->PackageId); + obj->Version = alloc_c_string(dto->Version); + obj->Type = alloc_c_string(dto->Type); + obj->FileName = alloc_c_string(dto->FileName); + obj->SHA1 = alloc_c_string(dto->SHA1); + obj->SHA256 = alloc_c_string(dto->SHA256); + obj->Size = dto->Size; + obj->NotesMarkdown = alloc_c_string(dto->NotesMarkdown); + obj->NotesHtml = alloc_c_string(dto->NotesHtml); + return obj; +} + +static inline vpkc_asset_t* alloc_c_VelopackAsset(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + VelopackAsset obj = unwrap(dto); + return alloc_c_VelopackAsset_ptr(&obj); +} + +static inline vpkc_asset_t** alloc_c_VelopackAsset_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_asset_t** arr = new vpkc_asset_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_VelopackAsset(dto[i]); + } + return arr; +} + +static inline void free_c_VelopackAsset(vpkc_asset_t* obj) { + if (obj == nullptr) { return; } + free_c_string(obj->PackageId); + free_c_string(obj->Version); + free_c_string(obj->Type); + free_c_string(obj->FileName); + free_c_string(obj->SHA1); + free_c_string(obj->SHA256); + + free_c_string(obj->NotesMarkdown); + free_c_string(obj->NotesHtml); + delete obj; +} + +static inline void free_c_VelopackAsset_vec(vpkc_asset_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_VelopackAsset(arr[i]); + } + delete[] arr; +} + +/** Holds information about the current version and pending updates, such as how many there are, and access to release notes. */ +struct UpdateInfo { + /** The available version that we are updating to. */ + VelopackAsset TargetFullRelease; + /** The base release that this update is based on. This is only available if the update is a delta update. */ + std::optional BaseRelease; + /** The list of delta updates that can be applied to the base version to get to the target version. */ + std::vector DeltasToTarget; + /** + * True if the update is a version downgrade or lateral move (such as when switching channels to the same version number). + * In this case, only full updates are allowed, and any local packages on disk newer than the downloaded version will be + * deleted. + */ + bool IsDowngrade; +}; + +static inline std::optional to_cpp_UpdateInfo(const vpkc_update_info_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + unwrap(to_cpp_VelopackAsset(dto->TargetFullRelease), "Required property TargetFullRelease was null"), + to_cpp_VelopackAsset(dto->BaseRelease), + to_cpp_VelopackAsset_vec(dto->DeltasToTarget, dto->DeltasToTargetCount), + dto->IsDowngrade, + }); +} + +static inline std::vector to_cpp_UpdateInfo_vec(const vpkc_update_info_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_UpdateInfo(dto))); + } + return result; +} + +static inline vpkc_update_info_t* alloc_c_UpdateInfo_ptr(const UpdateInfo* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_update_info_t* obj = new vpkc_update_info_t{}; + obj->TargetFullRelease = alloc_c_VelopackAsset(dto->TargetFullRelease); + obj->BaseRelease = alloc_c_VelopackAsset(dto->BaseRelease); + obj->DeltasToTarget = alloc_c_VelopackAsset_vec(dto->DeltasToTarget, &obj->DeltasToTargetCount); + obj->IsDowngrade = dto->IsDowngrade; + return obj; +} + +static inline vpkc_update_info_t* alloc_c_UpdateInfo(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + UpdateInfo obj = unwrap(dto); + return alloc_c_UpdateInfo_ptr(&obj); +} + +static inline vpkc_update_info_t** alloc_c_UpdateInfo_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_update_info_t** arr = new vpkc_update_info_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_UpdateInfo(dto[i]); + } + return arr; +} + +static inline void free_c_UpdateInfo(vpkc_update_info_t* obj) { + if (obj == nullptr) { return; } + free_c_VelopackAsset(obj->TargetFullRelease); + free_c_VelopackAsset(obj->BaseRelease); + free_c_VelopackAsset_vec(obj->DeltasToTarget, obj->DeltasToTargetCount); + + delete obj; +} + +static inline void free_c_UpdateInfo_vec(vpkc_update_info_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_UpdateInfo(arr[i]); + } + delete[] arr; +} + +/** Options to customise the behaviour of UpdateManager. */ +struct UpdateOptions { + /** + * Allows UpdateManager to update to a version that's lower than the current version (i.e. downgrading). + * This could happen if a release has bugs and was retracted from the release feed, or if you're using + * ExplicitChannel to switch channels to another channel where the latest version on that + * channel is lower than the current version. + */ + bool AllowVersionDowngrade; + /** + * **This option should usually be left None**. + * Overrides the default channel used to fetch updates. + * The default channel will be whatever channel was specified on the command line when building this release. + * For example, if the current release was packaged with '--channel beta', then the default channel will be 'beta'. + * This allows users to automatically receive updates from the same channel they installed from. This options + * allows you to explicitly switch channels, for example if the user wished to switch back to the 'stable' channel + * without having to reinstall the application. + */ + std::optional ExplicitChannel; + /** + * Sets the maximum number of deltas to consider before falling back to a full update. + * The default is 10. Set to a negative number (eg. -1) to disable deltas. + */ + int32_t MaximumDeltasBeforeFallback; +}; + +static inline std::optional to_cpp_UpdateOptions(const vpkc_update_options_t* dto) { + if (dto == nullptr) { return std::nullopt; } + return std::optional({ + dto->AllowVersionDowngrade, + to_cpp_string(dto->ExplicitChannel), + dto->MaximumDeltasBeforeFallback, + }); +} + +static inline std::vector to_cpp_UpdateOptions_vec(const vpkc_update_options_t* const* arr, size_t c) { + if (arr == nullptr || c < 1) { return std::vector(); } + std::vector result; + result.reserve(c); + for (size_t i = 0; i < c; ++i) { + auto dto = arr[i]; + if (dto == nullptr) { continue; } + result.push_back(unwrap(to_cpp_UpdateOptions(dto))); + } + return result; +} + +static inline vpkc_update_options_t* alloc_c_UpdateOptions_ptr(const UpdateOptions* dto) { + if (dto == nullptr) { return nullptr; } + vpkc_update_options_t* obj = new vpkc_update_options_t{}; + obj->AllowVersionDowngrade = dto->AllowVersionDowngrade; + obj->ExplicitChannel = alloc_c_string(dto->ExplicitChannel); + obj->MaximumDeltasBeforeFallback = dto->MaximumDeltasBeforeFallback; + return obj; +} + +static inline vpkc_update_options_t* alloc_c_UpdateOptions(const std::optional& dto) { + if (!dto.has_value()) { return nullptr; } + UpdateOptions obj = unwrap(dto); + return alloc_c_UpdateOptions_ptr(&obj); +} + +static inline vpkc_update_options_t** alloc_c_UpdateOptions_vec(const std::vector& dto, size_t* count) { + if (dto.empty()) { + *count = 0; + return nullptr; + } + *count = dto.size(); + vpkc_update_options_t** arr = new vpkc_update_options_t*[*count]; + for (size_t i = 0; i < *count; ++i) { + arr[i] = alloc_c_UpdateOptions(dto[i]); + } + return arr; +} + +static inline void free_c_UpdateOptions(vpkc_update_options_t* obj) { + if (obj == nullptr) { return; } + + free_c_string(obj->ExplicitChannel); + + delete obj; +} + +static inline void free_c_UpdateOptions_vec(vpkc_update_options_t** arr, size_t count) { + if (arr == nullptr || count < 1) { return; } + for (size_t i = 0; i < count; ++i) { + free_c_UpdateOptions(arr[i]); + } + delete[] arr; +} +// !! AUTO-GENERATED-END CPP_TYPES + +/** + * VelopackApp helps you to handle app activation events correctly. + * This should be used as early as possible in your application startup code. + * (eg. the beginning of main() or wherever your entry point is) + * To use this class, you should create a new VelopackApp::Build() builder instance, + * and then chain calls to the builder to configure your app. + * Finally, call the Run() method to execute the Velopack logic. + */ +class VelopackApp { +private: + VelopackApp() {}; +public: + /** + * Create and return a new VelopackApp builder. + */ + static VelopackApp Build() { + return VelopackApp(); + }; + + /** + * Attach a custom callback to receive log messages from Velopack. + */ + VelopackApp& SetLogger(vpkc_log_callback_t cbInstall, void* p_user_data) { + vpkc_set_logger(cbInstall, p_user_data); + return *this; + }; + + /** + * Set whether to automatically apply downloaded updates on startup. This is ON by default. + */ + VelopackApp& SetAutoApplyOnStartup(bool bAutoApply) { + vpkc_app_set_auto_apply_on_startup(bAutoApply); + return *this; + }; + + /** + * Override the command line arguments used by VelopackApp. + * By default, Velopack will use the command line arguments from the current process. + * @param args The command line arguments to use. + * @returns A reference to the builder. + */ + VelopackApp& SetArgs(const std::vector& args) { + size_t c; + char** pArgs = alloc_c_string_vec(args, &c); + vpkc_app_set_args(pArgs, c); + free_c_string_vec(pArgs, c); + return *this; + }; + + /** + * Override the default VelopackLocator. The locator is used to find important paths for the application. + * @param locator The locator to use. + * @returns A reference to the builder. + */ + VelopackApp& SetLocator(const VelopackLocatorConfig& locator) { + vpkc_locator_config_t* vpkc_locator = alloc_c_VelopackLocatorConfig(locator); + vpkc_app_set_locator(vpkc_locator); + free_c_VelopackLocatorConfig(vpkc_locator); + return *this; + }; + + /** + * This hook is triggered after the app has been installed. + * WARNING: This hook is run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cbAfterInstall The callback to run after the app has been installed. + * @returns A reference to the builder. + */ + VelopackApp& OnAfterInstall(vpkc_hook_callback_t cbAfterInstall) { + vpkc_app_set_hook_after_install(cbAfterInstall); + return *this; + }; + + /** + * This hook is triggered before the app is uninstalled. + * WARNING: This hook is run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cbBeforeUninstall The callback to run before the app is uninstalled. + * @returns A reference to the builder. + */ + VelopackApp& OnBeforeUninstall(vpkc_hook_callback_t cbBeforeUninstall) { + vpkc_app_set_hook_before_uninstall(cbBeforeUninstall); + return *this; + }; + + /** + * This hook is triggered before the app is updated. + * WARNING: This hook is run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cbBeforeUpdate The callback to run before the app is updated. + * @returns A reference to the builder. + */ + VelopackApp& OnBeforeUpdate(vpkc_hook_callback_t cbBeforeUpdate) { + vpkc_app_set_hook_before_update(cbBeforeUpdate); + return *this; + }; + + /** + * This hook is triggered after the app is updated. + * WARNING: This hook is run during critical stages of Velopack operations. + * Your code will be run and then the process will exit. + * If your code has not completed within 30 seconds, it will be terminated. + * Only supported on windows; On other operating systems, this will never be called. + * @param cbAfterUpdate The callback to run after the app is updated. + * @returns A reference to the builder. + */ + VelopackApp& OnAfterUpdate(vpkc_hook_callback_t cbAfterUpdate) { + vpkc_app_set_hook_after_update(cbAfterUpdate); + return *this; + }; + + /** + * This hook is triggered when the application is started for the first time after installation. + * @param cbFirstRun The callback to run when the application is started for the first time. + * @returns A reference to the builder. + */ + VelopackApp& OnFirstRun(vpkc_hook_callback_t cbFirstRun) { + vpkc_app_set_hook_first_run(cbFirstRun); + return *this; + }; + + /** + * This hook is triggered when the application is restarted by Velopack after installing updates. + * @param cbRestarted The callback to run when the application is restarted. + * @returns A reference to the builder. + */ + VelopackApp& OnRestarted(vpkc_hook_callback_t cbRestarted) { + vpkc_app_set_hook_restarted(cbRestarted); + return *this; + }; + + /** + * Runs the Velopack startup logic. This should be the first thing to run in your app. + * In some circumstances it may terminate/restart the process to perform tasks. + * @param pUserData A pointer to user data that will be passed to any hooks that are executed. + */ + void Run(void* pUserData = 0) { + vpkc_app_run(pUserData); + }; +}; + +/** + * Progress callback function. Call with values between 0 and 100 inclusive. + */ +typedef std::function vpkc_progress_send_t; + +/** + * Base class for all update sources. Holds a pointer to a Rust-side update source. + * All built-in sources (FileSource, HttpSource, GithubSource, etc.) inherit from this directly. + * For custom sources with user-provided callbacks, inherit from IUpdateSource instead. + */ +class IUpdateSourcePointer { + friend class UpdateManager; +protected: + vpkc_update_source_t* m_pSource = nullptr; + explicit IUpdateSourcePointer(vpkc_update_source_t* pSource) : m_pSource(pSource) { } +public: + virtual ~IUpdateSourcePointer() { + if (m_pSource) vpkc_free_source(m_pSource); + } + // Non-copyable, movable + IUpdateSourcePointer(const IUpdateSourcePointer&) = delete; + IUpdateSourcePointer& operator=(const IUpdateSourcePointer&) = delete; + IUpdateSourcePointer(IUpdateSourcePointer&& other) noexcept : m_pSource(other.m_pSource) { other.m_pSource = nullptr; } + IUpdateSourcePointer& operator=(IUpdateSourcePointer&& other) noexcept { + if (this != &other) { + if (m_pSource) vpkc_free_source(m_pSource); + m_pSource = other.m_pSource; + other.m_pSource = nullptr; + } + return *this; + } +}; + +/** + * Abstract class for custom update sources. Subclass this and implement GetReleaseFeed and + * DownloadReleaseEntry to provide a custom way of fetching releases and downloading assets. + * SAFETY: It is your responsibility to ensure that a derived class instance is thread-safe, + * as Velopack may call methods on this class from multiple threads. + */ +class IUpdateSource : public IUpdateSourcePointer { +public: + /** + * Destructor for IUpdateSource. + */ + virtual ~IUpdateSource() = default; + /** + * Default constructor for IUpdateSource. This will create a new custom source that calls back into the virtual methods of this class. + */ + IUpdateSource() : IUpdateSourcePointer(nullptr) { + m_pSource = vpkc_new_source_custom_callback( + [](void* userData, const char* releasesName) { + IUpdateSource* source = reinterpret_cast(userData); + std::string json = source->GetReleaseFeed(releasesName); + return alloc_c_string(json); + }, + [](void* userData, char* pszFeed) { + free_c_string(pszFeed); + }, + [](void* userData, const struct vpkc_asset_t *pAsset, const char* pszLocalPath, size_t progressCallbackId) { + IUpdateSource* source = reinterpret_cast(userData); + VelopackAsset asset = to_cpp_VelopackAsset(pAsset).value(); + std::string localPath = to_cpp_string(pszLocalPath).value(); + std::function progress_callback = [progressCallbackId](int16_t progress) { + vpkc_source_report_progress(progressCallbackId, progress); + }; + return source->DownloadReleaseEntry(asset, localPath, progress_callback); + }, + this); + if (!m_pSource) { + throw_last_error(); + } + } + + /** + * Fetches the release feed json for the specified releases name, and returns it as a string. + */ + virtual const std::string GetReleaseFeed(const std::string releasesName) = 0; + + /** + * Downloads an asset to the specified local file path. Progress is reported back to Velopack via a callback. + */ + virtual bool DownloadReleaseEntry(const VelopackAsset& asset, const std::string localFilePath, vpkc_progress_send_t progress) = 0; +}; + +/** + * A built-in update source that reads release feeds and downloads assets from a local file path. + */ +class FileSource : public IUpdateSourcePointer { +public: + /** + * Creates a new FileSource. + * @param filePath The path to the directory containing the releases. + */ + FileSource(const std::string& filePath) : IUpdateSourcePointer(vpkc_new_source_file(filePath.c_str())) { } +}; + +/** + * A built-in update source that reads release feeds and downloads assets from a remote HTTP URL. + */ +class HttpSource : public IUpdateSourcePointer { +public: + /** + * Creates a new HttpSource. + * @param httpUrl The URL to the releases feed. + */ + HttpSource(const std::string& httpUrl) : IUpdateSourcePointer(vpkc_new_source_http_url(httpUrl.c_str())) { } + + /** + * Creates a new HttpSource with custom HTTP options. + * @param httpUrl The URL to the releases feed. + * @param options HTTP options (custom headers, timeout) applied to all requests made by this source. + */ + HttpSource(const std::string& httpUrl, const HttpOptions& options) : IUpdateSourcePointer(nullptr) { + vpkc_http_options_t* pOptions = alloc_c_HttpOptions_ptr(&options); + m_pSource = vpkc_new_source_http_url_with_options(httpUrl.c_str(), pOptions); + free_c_HttpOptions(pOptions); + } +}; + +/** + * A built-in update source that reads release feeds and downloads assets from a GitHub repository. + */ +class GithubSource : public IUpdateSourcePointer { +public: + /** + * Creates a new GithubSource. + * @param repoUrl The GitHub repository URL (e.g. "https://github.com/user/repo"). + * @param accessToken Optional access token for private repositories. + * @param prerelease Whether to include pre-release versions. + */ + GithubSource(const std::string& repoUrl, const std::string& accessToken = "", bool prerelease = false) + : IUpdateSourcePointer(vpkc_new_source_github(repoUrl.c_str(), accessToken.empty() ? nullptr : accessToken.c_str(), prerelease)) { } +}; + +/** + * A built-in update source that reads release feeds and downloads assets from a GitLab repository. + */ +class GitlabSource : public IUpdateSourcePointer { +public: + /** + * Creates a new GitlabSource. + * @param repoUrl The GitLab repository URL (e.g. "https://gitlab.com/user/repo"). + * @param accessToken Optional access token for private repositories. + * @param prerelease Whether to include pre-release versions. + */ + GitlabSource(const std::string& repoUrl, const std::string& accessToken = "", bool prerelease = false) + : IUpdateSourcePointer(vpkc_new_source_gitlab(repoUrl.c_str(), accessToken.empty() ? nullptr : accessToken.c_str(), prerelease)) { } +}; + +/** + * A built-in update source that reads release feeds and downloads assets from a Gitea repository. + */ +class GiteaSource : public IUpdateSourcePointer { +public: + /** + * Creates a new GiteaSource. + * @param repoUrl The Gitea repository URL (e.g. "https://gitea.example.com/user/repo"). + * @param accessToken Optional access token for private repositories. + * @param prerelease Whether to include pre-release versions. + */ + GiteaSource(const std::string& repoUrl, const std::string& accessToken = "", bool prerelease = false) + : IUpdateSourcePointer(vpkc_new_source_gitea(repoUrl.c_str(), accessToken.empty() ? nullptr : accessToken.c_str(), prerelease)) { } +}; + +/** + * A built-in update source that reads release feeds and downloads assets from Velopack Flow. + */ +class VelopackFlowSource : public IUpdateSourcePointer { +public: + /** + * Creates a new VelopackFlowSource. + * @param baseUri Optional base URI for the Velopack Flow API. If empty, the default URI is used. + */ + VelopackFlowSource(const std::string& baseUri = "") + : IUpdateSourcePointer(vpkc_new_source_velopack_flow(baseUri.empty() ? nullptr : baseUri.c_str())) { } +}; + +/** + * Provides functionality for checking for updates, downloading updates, and applying updates to the current application. + * This class is the main entry point for interacting with Velopack. + */ +class UpdateManager { +private: + vpkc_update_manager_t* m_pManager = 0; + std::unique_ptr m_pUpdateSource; + +public: + /** + * Create a new UpdateManager instance for a local or remote directory of releases. + * @param urlOrPath Location of the http update server or the local update directory path containing releases. + * @param options Optional extra configuration for update manager. + * @param locator Override the default locator configuration (usually used for testing / mocks). + */ + UpdateManager(const std::string& urlOrPath, const UpdateOptions* options = nullptr, const VelopackLocatorConfig* locator = nullptr) { + vpkc_update_options_t* pOptions = alloc_c_UpdateOptions_ptr(options); + vpkc_locator_config_t* pLocator = alloc_c_VelopackLocatorConfig_ptr(locator); + bool result = vpkc_new_update_manager(urlOrPath.c_str(), pOptions, pLocator, &m_pManager); + free_c_UpdateOptions(pOptions); + free_c_VelopackLocatorConfig(pLocator); + if (!result) { + throw_last_error(); + } + }; + + /** + * Create a new UpdateManager instance with any update source (built-in or custom). + * The UpdateManager takes ownership of the source. + * @param pUpdateSource The source to use for retrieving feed and downloading assets. + * @param options Optional extra configuration for update manager. + * @param locator Override the default locator configuration (usually used for testing / mocks). + */ + template >> + UpdateManager(std::unique_ptr pUpdateSource, const UpdateOptions* options = nullptr, const VelopackLocatorConfig* locator = nullptr) { + vpkc_update_options_t* pOptions = alloc_c_UpdateOptions_ptr(options); + vpkc_locator_config_t* pLocator = alloc_c_VelopackLocatorConfig_ptr(locator); + m_pUpdateSource = std::unique_ptr(static_cast(pUpdateSource.release())); + vpkc_update_source_t* pSource = m_pUpdateSource->m_pSource; + bool result = vpkc_new_update_manager_with_source(pSource, pOptions, pLocator, &m_pManager); + free_c_UpdateOptions(pOptions); + free_c_VelopackLocatorConfig(pLocator); + if (!result) { + throw_last_error(); + } + }; + + /** + * Create a new UpdateManager instance which retrieves updates from the hosted Velopack Flow service (https://velopack.io). + * This is a convenience constructor equivalent to creating a VelopackFlowSource and passing it to UpdateManager. + * @param options Optional extra configuration for update manager. + * @param locator Override the default locator configuration (usually used for testing / mocks). + */ + UpdateManager(const UpdateOptions* options = nullptr, const VelopackLocatorConfig* locator = nullptr) { + vpkc_update_options_t* pOptions = alloc_c_UpdateOptions_ptr(options); + vpkc_locator_config_t* pLocator = alloc_c_VelopackLocatorConfig_ptr(locator); + bool result = vpkc_new_update_manager_flow(pOptions, pLocator, &m_pManager); + free_c_UpdateOptions(pOptions); + free_c_VelopackLocatorConfig(pLocator); + if (!result) { + throw_last_error(); + } + }; + + /** + * Destructor for UpdateManager. + */ + ~UpdateManager() { + if (m_pManager != nullptr) { + vpkc_free_update_manager(m_pManager); + m_pManager = nullptr; + } + }; + + /** + * Returns whether the app is in portable mode. On Windows this can be true or false. + * On MacOS and Linux this will always be true. + */ + bool IsPortable() noexcept { + return vpkc_is_portable(m_pManager); + }; + + /** + * Returns the currently installed version of the app. + */ + std::string GetCurrentVersion() noexcept { + size_t neededSize = vpkc_get_current_version(m_pManager, nullptr, 0); + std::string strVersion(neededSize, '\0'); + vpkc_get_current_version(m_pManager, &strVersion[0], neededSize); + return strVersion; + }; + + /** + * Returns the currently installed app id. + */ + std::string GetAppId() noexcept { + size_t neededSize = vpkc_get_app_id(m_pManager, nullptr, 0); + std::string strId(neededSize, '\0'); + vpkc_get_app_id(m_pManager, &strId[0], neededSize); + return strId; + }; + + /** + * Returns a VelopackAsset object if there is an update downloaded which still needs to be applied. + * You can pass this object to WaitExitThenApplyUpdates to apply the update. + * @returns A VelopackAsset object if there is a pending update, otherwise null. + */ + std::optional UpdatePendingRestart() noexcept { + vpkc_asset_t* asset; + if (vpkc_update_pending_restart(m_pManager, &asset)) { + VelopackAsset cpp_asset = to_cpp_VelopackAsset(asset).value(); + vpkc_free_asset(asset); + return cpp_asset; + } + return std::nullopt; + }; + + /** + * Checks for updates, returning null if there are none available. If there are updates available, this method will return an + * UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available. + * @returns An UpdateInfo object if there is an update available, otherwise null. + */ + std::optional CheckForUpdates() { + vpkc_update_info_t* update; + vpkc_update_check_t result = vpkc_check_for_updates(m_pManager, &update); + switch (result) { + case vpkc_update_check_t::UPDATE_ERROR: + throw_last_error(); + return std::nullopt; + case vpkc_update_check_t::NO_UPDATE_AVAILABLE: + case vpkc_update_check_t::REMOTE_IS_EMPTY: + return std::nullopt; + case vpkc_update_check_t::UPDATE_AVAILABLE: + UpdateInfo cpp_info = to_cpp_UpdateInfo(update).value(); + vpkc_free_update_info(update); + return cpp_info; + } + return std::nullopt; + }; + + /** + * Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional callback. + * This function will acquire a global update lock so may fail if there is already another update operation in progress. + * - If the update contains delta packages and the delta feature is enabled + * this method will attempt to unpack and prepare them. + * - If there is no delta update available, or there is an error preparing delta + * packages, this method will fall back to downloading the full version of the update. + * @param update The update to download. + * @param progress A callback to report progress to. + * @param pUserData A pointer to user data that will be passed to the progress callback. + */ + void DownloadUpdates(const UpdateInfo& update, vpkc_progress_callback_t progress = nullptr, void* pUserData = 0) { + vpkc_update_info_t* vpkc_update = alloc_c_UpdateInfo(update); + bool result = vpkc_download_updates(m_pManager, vpkc_update, progress, pUserData); + free_c_UpdateInfo(vpkc_update); + if (!result) { + throw_last_error(); + } + }; + + /** + * This will launch the Velopack updater and tell it to wait for this program to exit gracefully. + * You should then clean up any state and exit your app. The updater will apply updates and then + * optionally restart your app. The updater will only wait for 60 seconds before giving up. + * @param asset The UpdateInfo object for the update to apply. + * @param silent If true, the updater will not show any UI. + * @param restart If true, the app will be restarted after the update is applied. + * @param restartArgs The arguments to pass to the app when it is restarted. + */ + void WaitExitThenApplyUpdates(const UpdateInfo& asset, bool silent = false, bool restart = true, std::vector restartArgs = {}) { + this->WaitExitThenApplyUpdates(asset.TargetFullRelease, silent, restart, restartArgs); + }; + + /** + * This will launch the Velopack updater and tell it to wait for this program to exit gracefully. + * You should then clean up any state and exit your app. The updater will apply updates and then + * optionally restart your app. The updater will only wait for 60 seconds before giving up. + * @param asset The update to apply. + * @param silent If true, the updater will not show any UI. + * @param restart If true, the app will be restarted after the update is applied. + * @param restartArgs The arguments to pass to the app when it is restarted. + */ + void WaitExitThenApplyUpdates(const VelopackAsset& asset, bool silent = false, bool restart = true, std::vector restartArgs = {}) { + size_t cRestartArgs; + char** pRestartArgs = alloc_c_string_vec(restartArgs, &cRestartArgs); + vpkc_asset_t* vpkc_asset = alloc_c_VelopackAsset(asset); + bool result = vpkc_wait_exit_then_apply_updates(m_pManager, vpkc_asset, silent, restart, pRestartArgs, cRestartArgs); + free_c_string_vec(pRestartArgs, cRestartArgs); + free_c_VelopackAsset(vpkc_asset); + if (!result) { + throw_last_error(); + } + }; + + /** + * This will launch the Velopack updater and optionally wait for a program to exit gracefully. + * This method is unsafe because it does not necessarily wait for any / the correct process to exit + * before applying updates. The `WaitExitThenApplyUpdates` method is recommended for most use cases. + * If waitPid is 0, the updater will not wait for any process to exit before applying updates (Not Recommended). + * @param asset The update to apply. + * @param silent If true, the updater will not show any UI. + * @param waitPid The process ID to wait for before applying updates. If 0, the updater will not wait. + * @param restart If true, the app will be restarted after the update is applied. + * @param restartArgs The arguments to pass to the app when it is restarted. + */ + void UnsafeApplyUpdates(const VelopackAsset& asset, bool silent, uint32_t waitPid, bool restart, std::vector restartArgs) { + size_t cRestartArgs; + char** pRestartArgs = alloc_c_string_vec(restartArgs, &cRestartArgs); + vpkc_asset_t* vpkc_asset = alloc_c_VelopackAsset(asset); + bool result = vpkc_unsafe_apply_updates(m_pManager, vpkc_asset, silent, waitPid, restart, pRestartArgs, cRestartArgs); + free_c_string_vec(pRestartArgs, cRestartArgs); + free_c_VelopackAsset(vpkc_asset); + if (!result) { + throw_last_error(); + } + }; +}; + +} // namespace Velopack + +#endif // VELOPACK_HPP diff --git a/indra/rust/velopack_libc/src/csource.rs b/indra/rust/velopack_libc/src/csource.rs new file mode 100644 index 0000000000..be09ecf765 --- /dev/null +++ b/indra/rust/velopack_libc/src/csource.rs @@ -0,0 +1,292 @@ +use crate::types::*; +use lazy_static::lazy_static; +use libc::{c_void, size_t}; +use std::{ + collections::HashMap, + ffi::CString, + path::Path, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::Sender, + RwLock, + }, +}; +use velopack::{bundle::Manifest, sources::UpdateSource, Error, VelopackAsset, VelopackAssetFeed}; + +lazy_static! { + static ref PROGRESS_CALLBACKS: RwLock>> = RwLock::new(HashMap::new()); + static ref PROGRESS_ID: AtomicUsize = AtomicUsize::new(1); +} + +pub fn report_csource_progress(callback_id: size_t, progress: i16) { + let progress_callbacks = PROGRESS_CALLBACKS.read().unwrap(); + if let Some(sender) = progress_callbacks.get(&callback_id) { + let _ = sender.send(progress); + } +} + +#[derive(Clone)] +pub struct CCallbackUpdateSource { + pub p_user_data: *mut c_void, + pub cb_get_release_feed: vpkc_release_feed_delegate_t, + pub cb_free_release_feed: vpkc_free_release_feed_t, + pub cb_download_release_entry: vpkc_download_asset_delegate_t, +} + +unsafe impl Send for CCallbackUpdateSource {} +unsafe impl Sync for CCallbackUpdateSource {} + +impl UpdateSource for CCallbackUpdateSource { + fn get_release_feed(&self, channel: &str, _: &Manifest, _staged_user_id: &str) -> Result { + let releases_name = format!("releases.{}.json", channel); + let releases_name_cstr = CString::new(releases_name).unwrap(); + + if let Some(cb_get_release_feed) = self.cb_get_release_feed { + let json_cstr_ptr = (cb_get_release_feed)(self.p_user_data, releases_name_cstr.as_ptr()); + let json = c_to_String(json_cstr_ptr) + .map_err(|_| Error::Other("User vpkc_release_feed_delegate_t returned a null pointer instead of an asset feed".to_string()))?; + if let Some(cb_free_release_feed) = self.cb_free_release_feed { + (cb_free_release_feed)(self.p_user_data, json_cstr_ptr); // Free the C string returned by the callback + } else { + log::error!("User vpkc_release_feed_delegate_t is null, this may be a memory leak"); + } + let feed: VelopackAssetFeed = serde_json::from_str(&json)?; + Ok(feed) + } else { + Err(Error::Other("User vpkc_release_feed_delegate_t is null".to_string())) + } + } + + fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option>) -> Result<(), Error> { + if let Some(cb_download_release_entry) = self.cb_download_release_entry { + let local_file = local_file.to_string_lossy().to_string(); + let local_file_cstr = CString::new(local_file).unwrap(); + let asset_ptr = unsafe { allocate_VelopackAsset(asset) }; + + let progress_callback_id = PROGRESS_ID.fetch_add(1, Ordering::SeqCst); + if let Some(progress_sender) = &progress_sender { + let _ = progress_sender.send(0); + PROGRESS_CALLBACKS.write().unwrap().insert(progress_callback_id, progress_sender.clone()); + } + + let success = (cb_download_release_entry)(self.p_user_data, asset_ptr, local_file_cstr.as_ptr(), progress_callback_id); + unsafe { free_VelopackAsset(asset_ptr) }; + + if let Some(sender) = PROGRESS_CALLBACKS.write().unwrap().remove(&progress_callback_id) { + let _ = sender.send(100); + } + + if !success { + return Err(Error::Other( + "User vpkc_download_asset_delegate_t returned false to indicate download failed".to_owned(), + )); + } + + Ok(()) + } else { + Err(Error::Other("User vpkc_download_asset_delegate_t is null".to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libc::{c_char, c_void, size_t}; + use std::ffi::CString; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::mpsc; + use velopack::sources::UpdateSource; + + fn test_manifest() -> Manifest { + Manifest { + id: "TestApp".to_string(), + version: semver::Version::new(1, 0, 0), + ..Default::default() + } + } + + fn test_asset() -> VelopackAsset { + VelopackAsset { + PackageId: "TestApp".to_string(), + Version: "2.0.0".to_string(), + Type: "Full".to_string(), + FileName: "TestApp-2.0.0-full.nupkg".to_string(), + SHA1: "abc123".to_string(), + SHA256: "def456".to_string(), + Size: 1048576, + NotesMarkdown: String::new(), + NotesHtml: String::new(), + } + } + + fn sample_feed_json_cstr() -> CString { + CString::new( + r#"{"Assets":[{"PackageId":"TestApp","Version":"2.0.0","Type":"Full","FileName":"TestApp-2.0.0-full.nupkg","SHA1":"abc123","SHA256":"def456","Size":1048576}]}"#, + ) + .unwrap() + } + + extern "C" fn mock_get_feed(_user_data: *mut c_void, _releases_name: *const c_char) -> *mut c_char { + let json = sample_feed_json_cstr(); + json.into_raw() + } + + extern "C" fn mock_free_feed(_user_data: *mut c_void, psz_feed: *mut c_char) { + if !psz_feed.is_null() { + unsafe { + drop(CString::from_raw(psz_feed)); + } + } + } + + extern "C" fn mock_download_success( + _user_data: *mut c_void, + _asset: *const vpkc_asset_t, + psz_local_path: *const c_char, + _progress_callback_id: size_t, + ) -> bool { + let path = c_to_String(psz_local_path).unwrap(); + std::fs::write(&path, b"downloaded content").unwrap(); + true + } + + extern "C" fn mock_download_failure( + _user_data: *mut c_void, + _asset: *const vpkc_asset_t, + _psz_local_path: *const c_char, + _progress_callback_id: size_t, + ) -> bool { + false + } + + #[test] + fn feed_success() { + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: Some(mock_get_feed), + cb_free_release_feed: Some(mock_free_feed), + cb_download_release_entry: None, + }; + + let manifest = test_manifest(); + let feed = source.get_release_feed("stable", &manifest, "").unwrap(); + assert_eq!(feed.Assets.len(), 1); + assert_eq!(feed.Assets[0].PackageId, "TestApp"); + assert_eq!(feed.Assets[0].Version, "2.0.0"); + } + + #[test] + fn feed_null_callback() { + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: None, + cb_free_release_feed: None, + cb_download_release_entry: None, + }; + + let manifest = test_manifest(); + let result = source.get_release_feed("stable", &manifest, ""); + assert!(result.is_err()); + let err = format!("{}", result.unwrap_err()); + assert!(err.contains("null"), "Unexpected error: {}", err); + } + + #[test] + fn free_callback_is_called() { + static FREE_COUNT: AtomicUsize = AtomicUsize::new(0); + + extern "C" fn counting_free(_user_data: *mut c_void, psz_feed: *mut c_char) { + FREE_COUNT.fetch_add(1, Ordering::SeqCst); + if !psz_feed.is_null() { + unsafe { + drop(CString::from_raw(psz_feed)); + } + } + } + + FREE_COUNT.store(0, Ordering::SeqCst); + + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: Some(mock_get_feed), + cb_free_release_feed: Some(counting_free), + cb_download_release_entry: None, + }; + + let manifest = test_manifest(); + let _ = source.get_release_feed("stable", &manifest, "").unwrap(); + assert_eq!(FREE_COUNT.load(Ordering::SeqCst), 1); + } + + #[test] + fn download_success() { + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: None, + cb_free_release_feed: None, + cb_download_release_entry: Some(mock_download_success), + }; + + let asset = test_asset(); + let dir = tempfile::tempdir().unwrap(); + let dest = dir.path().join("downloaded.nupkg"); + source.download_release_entry(&asset, &dest, None).unwrap(); + + let content = std::fs::read(&dest).unwrap(); + assert_eq!(content, b"downloaded content"); + } + + #[test] + fn download_failure() { + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: None, + cb_free_release_feed: None, + cb_download_release_entry: Some(mock_download_failure), + }; + + let asset = test_asset(); + let dir = tempfile::tempdir().unwrap(); + let dest = dir.path().join("downloaded.nupkg"); + let result = source.download_release_entry(&asset, &dest, None); + assert!(result.is_err()); + let err = format!("{}", result.unwrap_err()); + assert!(err.contains("false") || err.contains("failed"), "Unexpected error: {}", err); + } + + #[test] + fn progress_reporting() { + extern "C" fn download_with_progress( + _user_data: *mut c_void, + _asset: *const vpkc_asset_t, + psz_local_path: *const c_char, + progress_callback_id: size_t, + ) -> bool { + let path = c_to_String(psz_local_path).unwrap(); + // Report progress via the csource progress mechanism + report_csource_progress(progress_callback_id, 50); + std::fs::write(&path, b"data").unwrap(); + true + } + + let source = CCallbackUpdateSource { + p_user_data: std::ptr::null_mut(), + cb_get_release_feed: None, + cb_free_release_feed: None, + cb_download_release_entry: Some(download_with_progress), + }; + + let asset = test_asset(); + let dir = tempfile::tempdir().unwrap(); + let dest = dir.path().join("downloaded.nupkg"); + let (tx, rx) = mpsc::channel(); + source.download_release_entry(&asset, &dest, Some(tx)).unwrap(); + + let progress: Vec = rx.try_iter().collect(); + // Should have received: 0 (initial), 50 (from callback), 100 (final) + assert!(progress.contains(&0), "Should contain initial 0 progress"); + assert!(progress.contains(&50), "Should contain 50 progress from callback"); + assert!(progress.contains(&100), "Should contain final 100 progress"); + } +} diff --git a/indra/rust/velopack_libc/src/lib.rs b/indra/rust/velopack_libc/src/lib.rs new file mode 100644 index 0000000000..e0eeb5ccec --- /dev/null +++ b/indra/rust/velopack_libc/src/lib.rs @@ -0,0 +1,890 @@ +#![allow(dead_code)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +mod statics; +use statics::*; +mod types; +use types::*; +mod csource; +use csource::*; +mod raw; +use raw::*; + +use anyhow::{anyhow, bail}; +use libc::{c_char, c_void, size_t}; +use log_derive::{logfn, logfn_inputs}; +use std::{ffi::CString, ptr}; +use velopack::locator::LocationContext; +use velopack::logging::{default_logfile_path, init_logging}; +use velopack::{sources, ApplyWaitMode, Error as VelopackError, UpdateCheck, UpdateManager, VelopackApp}; + +/// Create a new FileSource update source for a given file path. +/// @param psz_file_path The path to a local directory containing updates. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_file(psz_file_path: *const c_char) -> *mut vpkc_update_source_t { + if let Ok(update_path) = c_to_String(psz_file_path) { + UpdateSourceRawPtr::new(Box::new(sources::FileSource::new(update_path))) + } else { + log::error!("psz_file_path is null"); + ptr::null_mut() + } +} + +/// Create a new HttpSource update source for a given HTTP URL. +/// @param psz_http_url The URL to a remote update server. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_http_url(psz_http_url: *const c_char) -> *mut vpkc_update_source_t { + if let Ok(update_url) = c_to_String(psz_http_url) { + UpdateSourceRawPtr::new(Box::new(sources::HttpSource::new(update_url))) + } else { + log::error!("psz_http_url is null"); + ptr::null_mut() + } +} + +/// Create a new HttpSource update source for a given HTTP URL, with custom HTTP options. +/// @param psz_http_url The URL to a remote update server. +/// @param p_options Optional HTTP options (custom headers, timeout). Can be null. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_http_url_with_options( + psz_http_url: *const c_char, + p_options: *mut vpkc_http_options_t, +) -> *mut vpkc_update_source_t { + let update_url = match c_to_String(psz_http_url) { + Ok(url) => url, + Err(_) => { + log::error!("psz_http_url is null"); + return ptr::null_mut(); + } + }; + let options = if p_options.is_null() { + Ok(velopack::HttpOptions::default()) + } else { + c_to_HttpOptions(p_options) + }; + match options { + Ok(options) => UpdateSourceRawPtr::new(Box::new(sources::HttpSource::new_with_options(update_url, options))), + Err(e) => { + log::error!("p_options is invalid: {}", e); + ptr::null_mut() + } + } +} + +/// Create a new GithubSource update source for a GitHub repository. +/// @param psz_repo_url The GitHub repository URL (e.g. "https://github.com/user/repo"). +/// @param psz_access_token Optional access token for private repositories (can be null). +/// @param b_prerelease Whether to include pre-release versions. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_github( + psz_repo_url: *const c_char, + psz_access_token: *const c_char, + b_prerelease: bool, +) -> *mut vpkc_update_source_t { + if let Ok(repo_url) = c_to_String(psz_repo_url) { + let access_token = c_to_String(psz_access_token).ok(); + UpdateSourceRawPtr::new(Box::new(sources::GithubSource::new(&repo_url, access_token, b_prerelease))) + } else { + log::error!("psz_repo_url is null"); + ptr::null_mut() + } +} + +/// Create a new GitlabSource update source for a GitLab repository. +/// @param psz_repo_url The GitLab repository URL (e.g. "https://gitlab.com/user/repo"). +/// @param psz_access_token Optional access token for private repositories (can be null). +/// @param b_prerelease Whether to include pre-release versions. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_gitlab( + psz_repo_url: *const c_char, + psz_access_token: *const c_char, + b_prerelease: bool, +) -> *mut vpkc_update_source_t { + if let Ok(repo_url) = c_to_String(psz_repo_url) { + let access_token = c_to_String(psz_access_token).ok(); + UpdateSourceRawPtr::new(Box::new(sources::GitlabSource::new(&repo_url, access_token, b_prerelease))) + } else { + log::error!("psz_repo_url is null"); + ptr::null_mut() + } +} + +/// Create a new GiteaSource update source for a Gitea repository. +/// @param psz_repo_url The Gitea repository URL (e.g. "https://gitea.example.com/user/repo"). +/// @param psz_access_token Optional access token for private repositories (can be null). +/// @param b_prerelease Whether to include pre-release versions. +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_gitea( + psz_repo_url: *const c_char, + psz_access_token: *const c_char, + b_prerelease: bool, +) -> *mut vpkc_update_source_t { + if let Ok(repo_url) = c_to_String(psz_repo_url) { + let access_token = c_to_String(psz_access_token).ok(); + UpdateSourceRawPtr::new(Box::new(sources::GiteaSource::new(&repo_url, access_token, b_prerelease))) + } else { + log::error!("psz_repo_url is null"); + ptr::null_mut() + } +} + +/// Create a new VelopackFlowSource update source for Velopack Flow. +/// @param psz_base_uri Optional base URI for the Velopack Flow API (can be null for default). +/// @returns A new vpkc_update_source_t instance, or null on error. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_velopack_flow(psz_base_uri: *const c_char) -> *mut vpkc_update_source_t { + let base_uri = c_to_String(psz_base_uri).ok(); + UpdateSourceRawPtr::new(Box::new(sources::VelopackFlowSource::new(base_uri.as_deref()))) +} + +/// Create a new _CUSTOM_ update source with user-provided callbacks to fetch release feeds and download assets. +/// You can report download progress using `vpkc_source_report_progress`. Note that the callbacks must be valid +/// for the lifetime of any UpdateManager's that use this source. You should call `vpkc_free_source` to free the source, +/// but note that if the source is still in use by an UpdateManager, it will not be freed until the UpdateManager is freed. +/// Therefore to avoid possible issues, it is recommended to create this type of source once for the lifetime of your application. +/// @param cb_release_feed A callback to fetch the release feed. +/// @param cb_free_release_feed A callback to free the memory allocated by `cb_release_feed`. +/// @param cb_download_entry A callback to download an asset. +/// @param p_user_data Optional user data to be passed to the callbacks. +/// @returns A new vpkc_update_source_t instance, or null on error. If null, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_source_custom_callback( + cb_release_feed: vpkc_release_feed_delegate_t, + cb_free_release_feed: vpkc_free_release_feed_t, + cb_download_entry: vpkc_download_asset_delegate_t, + p_user_data: *mut c_void, +) -> *mut vpkc_update_source_t { + if cb_release_feed.is_none() { + log::error!("cb_release_feed must not be null"); + set_last_error("cb_release_feed must not be null"); + return ptr::null_mut(); + } + + if cb_download_entry.is_none() { + log::error!("cb_download_entry must not be null"); + set_last_error("cb_download_entry must not be null"); + return ptr::null_mut(); + } + + if cb_free_release_feed.is_none() { + log::error!("cb_free_release_feed must not be null"); + set_last_error("cb_free_release_feed must not be null"); + return ptr::null_mut(); + } + + let source = CCallbackUpdateSource { + p_user_data, + cb_get_release_feed: cb_release_feed, + cb_download_release_entry: cb_download_entry, + cb_free_release_feed, + }; + + UpdateSourceRawPtr::new(Box::new(source)) +} + +/// Sends a progress update to the callback with the specified ID. This is used by custom +/// update sources created with `vpkc_new_source_custom_callback` to report download progress. +/// @param progress_callback_id The ID of the progress callback to send the update to. +/// @param progress The progress value to send (0-100). +#[no_mangle] +pub extern "C" fn vpkc_source_report_progress(progress_callback_id: size_t, progress: i16) { + report_csource_progress(progress_callback_id, progress); +} + +/// Frees a vpkc_update_source_t instance. +/// @param p_source The source to free. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_free_source(p_source: *mut vpkc_update_source_t) { + UpdateSourceRawPtr::free(p_source); +} + +/// Create a new UpdateManager instance. +/// @param psz_url_or_path Location of the http update server url or path to the local update directory. +/// @param p_options Optional extra configuration for update manager. +/// @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. +/// @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. +/// @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_update_manager( + psz_url_or_path: *const c_char, + p_options: *mut vpkc_update_options_t, + p_locator: *mut vpkc_locator_config_t, + p_manager: *mut *mut vpkc_update_manager_t, +) -> bool { + wrap_error(|| { + let update_url = c_to_String(psz_url_or_path)?; + let source = sources::AutoSource::new(&update_url); + let options = c_to_UpdateOptions(p_options).ok(); + let locator = c_to_VelopackLocatorConfig(p_locator).ok(); + let manager = UpdateManager::new(source, options, locator)?; + unsafe { *p_manager = UpdateManagerRawPtr::new(manager) }; + Ok(()) + }) +} + +/// Create a new UpdateManager instance which retrieves updates from the hosted Velopack Flow service (https://velopack.io). +/// This is a convenience function which is equivalent to creating a VelopackFlowSource and calling vpkc_new_update_manager_with_source. +/// @param p_options Optional extra configuration for update manager. +/// @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. +/// @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. +/// @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_update_manager_flow( + p_options: *mut vpkc_update_options_t, + p_locator: *mut vpkc_locator_config_t, + p_manager: *mut *mut vpkc_update_manager_t, +) -> bool { + wrap_error(|| { + let options = c_to_UpdateOptions(p_options).ok(); + let locator = c_to_VelopackLocatorConfig(p_locator).ok(); + let manager = UpdateManager::new_flow(options, locator)?; + unsafe { *p_manager = UpdateManagerRawPtr::new(manager) }; + Ok(()) + }) +} + +/// Create a new UpdateManager instance with a custom UpdateSource. +/// @param p_source A pointer to a custom UpdateSource. +/// @param p_options Optional extra configuration for update manager. +/// @param p_locator Optional explicit path configuration for Velopack. If null, the default locator will be used. +/// @param p_manager A pointer to where the new vpkc_update_manager_t* instance will be stored. +/// @returns True if the update manager was created successfully, false otherwise. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_new_update_manager_with_source( + p_source: *mut vpkc_update_source_t, + p_options: *mut vpkc_update_options_t, + p_locator: *mut vpkc_locator_config_t, + p_manager: *mut *mut vpkc_update_manager_t, +) -> bool { + wrap_error(|| { + let source = UpdateSourceRawPtr::get_source_boxed(p_source).ok_or(anyhow!("pSource must not be null"))?; + let options = c_to_UpdateOptions(p_options).ok(); + let locator = c_to_VelopackLocatorConfig(p_locator).ok(); + let manager = UpdateManager::new_boxed(source, options, locator)?; + unsafe { *p_manager = UpdateManagerRawPtr::new(manager) }; + Ok(()) + }) +} + +/// Returns the currently installed version of the app. +/// @param p_manager The update manager instance. +/// @param psz_version A buffer to store the version string. +/// @param c_version The size of the `psz_version` buffer. +/// @returns The number of characters written to `psz_version` (including null terminator), or the required buffer size if the buffer is too small. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_get_current_version(p_manager: *mut vpkc_update_manager_t, psz_version: *mut c_char, c_version: size_t) -> size_t { + match p_manager.to_opaque_ref() { + Some(manager) => { + let version = manager.get_current_version_as_string(); + return_cstr(psz_version, c_version, &version) + } + None => 0, + } +} + +/// Returns the currently installed app id. +/// @param p_manager The update manager instance. +/// @param psz_id A buffer to store the app id string. +/// @param c_id The size of the `psz_id` buffer. +/// @returns The number of characters written to `psz_id` (including null terminator), or the required buffer size if the buffer is too small. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_get_app_id(p_manager: *mut vpkc_update_manager_t, psz_id: *mut c_char, c_id: size_t) -> size_t { + match p_manager.to_opaque_ref() { + Some(manager) => { + let app_id = manager.get_app_id(); + return_cstr(psz_id, c_id, &app_id) + } + None => 0, + } +} + +/// Returns whether the app is in portable mode. On Windows this can be true or false. +/// On MacOS and Linux this will always be true. +/// @param p_manager The update manager instance. +/// @returns True if the app is in portable mode, false otherwise. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_is_portable(p_manager: *mut vpkc_update_manager_t) -> bool { + match p_manager.to_opaque_ref() { + Some(manager) => manager.get_is_portable(), + None => false, + } +} + +/// Returns an asset if there is an update downloaded which still needs to be applied. +/// You can pass this asset to `vpkc_wait_exit_then_apply_updates` to apply the update. +/// @param p_manager The update manager instance. +/// @param p_asset A pointer to where the new vpkc_asset_t* instance will be stored. +/// @returns True if there is an update pending restart, false otherwise. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_update_pending_restart(p_manager: *mut vpkc_update_manager_t, p_asset: *mut *mut vpkc_asset_t) -> bool { + match p_manager.to_opaque_ref() { + Some(manager) => match manager.get_update_pending_restart() { + Some(asset) => { + unsafe { *p_asset = allocate_VelopackAsset(&asset) }; + true + } + None => false, + }, + None => false, + } +} + +/// Checks for updates. If there are updates available, this method will return an +/// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available. +/// @param p_manager The update manager instance. +/// @param p_update A pointer to where the new vpkc_update_info_t* instance will be stored if an update is available. +/// @returns A `vpkc_update_check_t` value indicating the result of the check. If an update is available, the value will be `HasUpdate` and `p_update` will be populated. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_check_for_updates(p_manager: *mut vpkc_update_manager_t, p_update: *mut *mut vpkc_update_info_t) -> vpkc_update_check_t { + match p_manager.to_opaque_ref() { + Some(manager) => match manager.check_for_updates() { + Ok(UpdateCheck::UpdateAvailable(info)) => { + unsafe { *p_update = allocate_UpdateInfo(&*info) }; + vpkc_update_check_t::UPDATE_AVAILABLE + } + Ok(UpdateCheck::RemoteIsEmpty) => vpkc_update_check_t::REMOTE_IS_EMPTY, + Ok(UpdateCheck::NoUpdateAvailable) => vpkc_update_check_t::NO_UPDATE_AVAILABLE, + Err(e) => { + set_last_error(&format!("{:?}", e)); + vpkc_update_check_t::UPDATE_ERROR + } + }, + None => { + set_last_error("pManager must not be null"); + vpkc_update_check_t::UPDATE_ERROR + } + } +} + +/// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional callback. +/// This function will acquire a global update lock so may fail if there is already another update operation in progress. +/// - If the update contains delta packages and the delta feature is enabled +/// this method will attempt to unpack and prepare them. +/// - If there is no delta update available, or there is an error preparing delta +/// packages, this method will fall back to downloading the full version of the update. +/// @param p_manager The update manager instance. +/// @param p_update The update info object from `vpkc_check_for_updates`. +/// @param cb_progress An optional callback to report download progress (0-100). +/// @param p_user_data Optional user data to be passed to the progress callback. +/// @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_download_updates( + p_manager: *mut vpkc_update_manager_t, + p_update: *mut vpkc_update_info_t, + cb_progress: vpkc_progress_callback_t, + p_user_data: *mut c_void, +) -> bool { + wrap_error(|| { + let manager = match p_manager.to_opaque_ref() { + Some(manager) => manager, + None => bail!("pManager must not be null"), + }; + + let update = c_to_UpdateInfo(p_update)?; + + if let Some(cb_progress) = cb_progress { + let (progress_sender, progress_receiver) = std::sync::mpsc::channel::(); + let (completion_sender, completion_receiver) = std::sync::mpsc::channel::>(); + + // Move the download_updates call into a new thread + let manager = manager.clone(); + std::thread::spawn(move || { + let result = manager.download_updates(&update, Some(progress_sender)); + let _ = completion_sender.send(result); + }); + + // Process progress updates on the caller's thread + loop { + // Try to receive progress updates without blocking + match progress_receiver.try_recv() { + Ok(progress) => { + cb_progress(p_user_data, progress as size_t); + } + _ => { + // No progress updates available, sleep for a short time to avoid busy-waiting + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + + // Check if download is complete + match completion_receiver.try_recv() { + Ok(result) => { + // Download is complete, return the result (propagating any errors) + result?; + return Ok(()); + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + // Download is still in progress, continue processing progress updates + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + bail!("Download thread disconnected unexpectedly without returning a result"); + } + } + } + } else { + manager.download_updates(&update, None)?; + Ok(()) + } + }) +} + +/// This will launch the Velopack updater and tell it to wait for this program to exit gracefully. +/// You should then clean up any state and exit your app. The updater will apply updates and then +/// (if specified) restart your app. The updater will only wait for 60 seconds before giving up. +/// @param p_manager The update manager instance. +/// @param p_asset The asset to apply. This can be from `vpkc_update_pending_restart` or `vpkc_update_info_get_target_asset`. +/// @param b_silent True to attempt to apply the update without showing any UI. +/// @param b_restart True to restart the app after the update is applied. +/// @param p_restart_args An array of command line arguments to pass to the new process when it's restarted. +/// @param c_restart_args The number of arguments in `p_restart_args`. +/// @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_wait_exit_then_apply_updates( + p_manager: *mut vpkc_update_manager_t, + p_asset: *mut vpkc_asset_t, + b_silent: bool, + b_restart: bool, + p_restart_args: *mut *mut c_char, + c_restart_args: size_t, +) -> bool { + wrap_error(|| { + let manager = match p_manager.to_opaque_ref() { + Some(manager) => manager, + None => bail!("pManager must not be null"), + }; + + let asset = c_to_VelopackAsset(p_asset)?; + let restart_args = c_to_String_vec(p_restart_args, c_restart_args)?; + manager.wait_exit_then_apply_updates(&asset, b_silent, b_restart, &restart_args)?; + Ok(()) + }) +} + +/// This will launch the Velopack updater and optionally wait for a program to exit gracefully. +/// This method is unsafe because it does not necessarily wait for any / the correct process to exit +/// before applying updates. The `vpkc_wait_exit_then_apply_updates` method is recommended for most use cases. +/// If dw_wait_pid is 0, the updater will not wait for any process to exit before applying updates (Not Recommended). +/// @param p_manager The update manager instance. +/// @param p_asset The asset to apply. This can be from `vpkc_update_pending_restart` or `vpkc_update_info_get_target_asset`. +/// @param b_silent True to attempt to apply the update without showing any UI. +/// @param dw_wait_pid The process ID to wait for before applying updates. If 0, the updater will not wait. +/// @param b_restart True to restart the app after the update is applied. +/// @param p_restart_args An array of command line arguments to pass to the new process when it's restarted. +/// @param c_restart_args The number of arguments in `p_restart_args`. +/// @returns true on success, false on failure. If false, the error will be available via `vpkc_get_last_error`. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_unsafe_apply_updates( + p_manager: *mut vpkc_update_manager_t, + p_asset: *mut vpkc_asset_t, + b_silent: bool, + dw_wait_pid: u32, + b_restart: bool, + p_restart_args: *mut *mut c_char, + c_restart_args: size_t, +) -> bool { + wrap_error(|| { + let manager = p_manager.to_opaque_ref().ok_or(anyhow!("pManager must not be null"))?; + let asset = c_to_VelopackAsset(p_asset)?; + let restart_args = c_to_String_vec(p_restart_args, c_restart_args)?; + let wait_mode = if dw_wait_pid > 0 { + ApplyWaitMode::WaitPid(dw_wait_pid) + } else { + ApplyWaitMode::NoWait + }; + manager.unsafe_apply_updates(&asset, b_silent, wait_mode, b_restart, &restart_args)?; + Ok(()) + }) +} + +/// Frees a vpkc_update_manager_t instance. +/// @param p_manager The update manager instance to free. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_free_update_manager(p_manager: *mut vpkc_update_manager_t) { + UpdateManagerRawPtr::free(p_manager); +} + +/// Frees a vpkc_update_info_t instance. +/// @param p_update_info The update info instance to free. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_free_update_info(p_update_info: *mut vpkc_update_info_t) { + unsafe { free_UpdateInfo(p_update_info) }; +} + +/// Frees a vpkc_asset_t instance. +/// @param p_asset The asset instance to free. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_free_asset(p_asset: *mut vpkc_asset_t) { + unsafe { free_VelopackAsset(p_asset) }; +} + +/// VelopackApp helps you to handle app activation events correctly. +/// This should be used as early as possible in your application startup code. +/// (eg. the beginning of main() or wherever your entry point is). +/// This function will not return in some cases. +/// @param p_user_data Optional user data to be passed to the callbacks. +#[no_mangle] +#[logfn(Trace)] +#[logfn_inputs(Trace)] +pub extern "C" fn vpkc_app_run(p_user_data: *mut c_void) { + let app_options = VELOPACK_APP.read().unwrap(); + let mut app = VelopackApp::build(); + + if let Some(auto_apply) = app_options.auto_apply { + app = app.set_auto_apply_on_startup(auto_apply); + } + + if let Some(args) = &app_options.args { + app = app.set_args(args.clone()); + } + + if let Some(locator) = &app_options.locator { + app = app.set_locator(locator.clone()); + } + + #[cfg(windows)] + if let Some(hook) = &app_options.install_hook { + app = app.on_after_install_fast_callback(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + #[cfg(windows)] + if let Some(hook) = &app_options.uninstall_hook { + app = app.on_before_uninstall_fast_callback(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + #[cfg(windows)] + if let Some(hook) = &app_options.obsolete_hook { + app = app.on_before_update_fast_callback(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + #[cfg(windows)] + if let Some(hook) = &app_options.update_hook { + app = app.on_after_update_fast_callback(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + if let Some(hook) = &app_options.firstrun_hook { + app = app.on_first_run(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + if let Some(hook) = &app_options.restarted_hook { + app = app.on_restarted(|version| { + let c_string = CString::new(version.to_string()).unwrap(); + hook(p_user_data, c_string.as_ptr()); + }); + } + + // init logging + let log_file = if let Some(locator) = &app_options.locator { + default_logfile_path(locator) + } else { + default_logfile_path(LocationContext::FromCurrentExe) + }; + + init_logging("lib-cpp", Some(&log_file), false, false, Some(create_shared_logger())); + app.run(); +} + +/// Set whether to automatically apply downloaded updates on startup. This is ON by default. +/// @param b_auto_apply True to automatically apply updates, false otherwise. +#[no_mangle] +pub extern "C" fn vpkc_app_set_auto_apply_on_startup(b_auto_apply: bool) { + update_app_options(|opt| { + opt.auto_apply = Some(b_auto_apply); + }); +} + +/// Override the command line arguments used by VelopackApp. (by default this is env::args().skip(1)) +/// @param p_args An array of command line arguments. +/// @param c_args The number of arguments in `p_args`. +#[no_mangle] +pub extern "C" fn vpkc_app_set_args(p_args: *mut *mut c_char, c_args: size_t) { + update_app_options(|opt| { + opt.args = c_to_String_vec(p_args, c_args).ok(); + }); +} + +/// VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). +/// @param p_locator The locator configuration to use. +#[no_mangle] +pub extern "C" fn vpkc_app_set_locator(p_locator: *mut vpkc_locator_config_t) { + update_app_options(|opt| { + opt.locator = c_to_VelopackLocatorConfig(p_locator).ok(); + }); +} + +/// Sets a callback to be run after the app is installed. +/// WARNING: FastCallback hooks are run during critical stages of Velopack operations. +/// Your code will be run and then the process will exit. +/// If your code has not completed within 30 seconds, it will be terminated. +/// Only supported on windows; On other operating systems, this will never be called. +/// @param cb_after_install The callback to run after the app is installed. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_after_install(cb_after_install: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.install_hook = cb_after_install; + }); +} + +/// Sets a callback to be run before the app is uninstalled. +/// WARNING: FastCallback hooks are run during critical stages of Velopack operations. +/// Your code will be run and then the process will exit. +/// If your code has not completed within 30 seconds, it will be terminated. +/// Only supported on windows; On other operating systems, this will never be called. +/// @param cb_before_uninstall The callback to run before the app is uninstalled. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_before_uninstall(cb_before_uninstall: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.uninstall_hook = cb_before_uninstall; + }); +} + +/// Sets a callback to be run before the app is updated. +/// WARNING: FastCallback hooks are run during critical stages of Velopack operations. +/// Your code will be run and then the process will exit. +/// If your code has not completed within 30 seconds, it will be terminated. +/// Only supported on windows; On other operating systems, this will never be called. +/// @param cb_before_update The callback to run before the app is updated. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_before_update(cb_before_update: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.obsolete_hook = cb_before_update; + }); +} + +/// Sets a callback to be run after the app is updated. +/// WARNING: FastCallback hooks are run during critical stages of Velopack operations. +/// Your code will be run and then the process will exit. +/// If your code has not completed within 30 seconds, it will be terminated. +/// Only supported on windows; On other operating systems, this will never be called. +/// @param cb_after_update The callback to run after the app is updated. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_after_update(cb_after_update: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.update_hook = cb_after_update; + }); +} + +/// This hook is triggered when the application is started for the first time after installation. +/// @param cb_first_run The callback to run on first run. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_first_run(cb_first_run: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.firstrun_hook = cb_first_run; + }); +} + +/// This hook is triggered when the application is restarted by Velopack after installing updates. +/// @param cb_restarted The callback to run after the app is restarted. The callback takes a user data pointer and the version of the app as a string. +#[no_mangle] +pub extern "C" fn vpkc_app_set_hook_restarted(cb_restarted: vpkc_hook_callback_t) { + update_app_options(|opt| { + opt.restarted_hook = cb_restarted; + }); +} + +/// Get the last error message that occurred in the Velopack library. +/// @param psz_error A buffer to store the error message. +/// @param c_error The size of the `psz_error` buffer. +/// @returns The number of characters written to `psz_error` (including null terminator). If the return value is greater than `c_error`, the buffer was too small and the message was truncated. +#[no_mangle] +pub extern "C" fn vpkc_get_last_error(psz_error: *mut c_char, c_error: size_t) -> size_t { + let error = get_last_error(); + return_cstr(psz_error, c_error, &error) +} + +/// Set a custom log callback. This will be called for all log messages generated by the Velopack library. +/// @param cb_log The callback to call with log messages. The callback takes a user data pointer, a log level, and the log message as a string. +/// @param p_user_data Optional user data to be passed to the callback. +#[no_mangle] +pub extern "C" fn vpkc_set_logger(cb_log: vpkc_log_callback_t, p_user_data: *mut c_void) { + set_log_callback(cb_log, p_user_data); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn noteshtml_roundtrip_through_c_structs() { + let asset = velopack::VelopackAsset { + PackageId: "TestApp".to_string(), + Version: "2.0.0".to_string(), + Type: "Full".to_string(), + FileName: "TestApp-2.0.0-full.nupkg".to_string(), + SHA1: "abc123".to_string(), + SHA256: "def456".to_string(), + Size: 1048576, + NotesMarkdown: "# Release v2".to_string(), + NotesHtml: "

Release v2

".to_string(), + }; + + let c_asset = unsafe { allocate_VelopackAsset(&asset) }; + assert!(!c_asset.is_null()); + unsafe { + assert!(!(*c_asset).NotesMarkdown.is_null()); + assert!(!(*c_asset).NotesHtml.is_null()); + } + + let roundtripped = c_to_VelopackAsset(c_asset).unwrap(); + assert_eq!(roundtripped.NotesMarkdown, "# Release v2"); + assert_eq!(roundtripped.NotesHtml, "

Release v2

"); + + unsafe { free_VelopackAsset(c_asset) }; + } + + #[test] + fn wrap_error_catches_panic_without_crashing() { + let result = wrap_error(|| { + panic!("test panic"); + }); + assert!(!result); + let error = get_last_error(); + assert!(error.contains("panic"), "Error should mention panic: {}", error); + } + + #[test] + fn update_info_roundtrip_with_notes() { + let update = velopack::UpdateInfo { + TargetFullRelease: velopack::VelopackAsset { + PackageId: "App".to_string(), + Version: "3.0.0".to_string(), + Type: "Full".to_string(), + FileName: "App-3.0.0-full.nupkg".to_string(), + SHA1: "sha1".to_string(), + SHA256: "sha256".to_string(), + Size: 5000, + NotesMarkdown: "## v3 notes".to_string(), + NotesHtml: "

v3 notes

".to_string(), + }, + BaseRelease: None, + DeltasToTarget: Vec::new(), + IsDowngrade: false, + }; + + let c_update = unsafe { allocate_UpdateInfo(&update) }; + assert!(!c_update.is_null()); + + let roundtripped = c_to_UpdateInfo(c_update).unwrap(); + assert_eq!(roundtripped.TargetFullRelease.NotesMarkdown, "## v3 notes"); + assert_eq!(roundtripped.TargetFullRelease.NotesHtml, "

v3 notes

"); + assert!(roundtripped.BaseRelease.is_none()); + assert!(roundtripped.DeltasToTarget.is_empty()); + + unsafe { free_UpdateInfo(c_update) }; + } + + #[test] + fn new_update_manager_flow_uses_flow_source() { + let temp_dir = tempfile::tempdir().unwrap(); + let root_dir = temp_dir.path().join("root"); + let packages_dir = root_dir.join("packages"); + let current_binary_dir = root_dir.join("current"); + let update_exe_path = root_dir.join("Update.exe"); + + std::fs::create_dir_all(&packages_dir).unwrap(); + std::fs::create_dir_all(¤t_binary_dir).unwrap(); + std::fs::write(&update_exe_path, []).unwrap(); + + let manifest_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("test") + .join("fixtures") + .join("Test.Squirrel-App.nuspec"); + + let locator = velopack::locator::VelopackLocatorConfig { + RootAppDir: root_dir, + UpdateExePath: update_exe_path, + PackagesDir: packages_dir, + ManifestPath: manifest_path, + CurrentBinaryDir: current_binary_dir, + IsPortable: true, + }; + + let c_locator = unsafe { allocate_VelopackLocatorConfig(&locator) }; + assert!(!c_locator.is_null()); + + let mut manager: *mut vpkc_update_manager_t = ptr::null_mut(); + let ok = vpkc_new_update_manager_flow(ptr::null_mut(), c_locator, &mut manager); + unsafe { free_VelopackLocatorConfig(c_locator) }; + + assert!(ok, "vpkc_new_update_manager_flow failed: {}", get_last_error()); + assert!(!manager.is_null()); + + let mut buf = [0 as c_char; 256]; + let written = vpkc_get_app_id(manager, buf.as_mut_ptr(), buf.len()); + assert!(written > 0); + let app_id = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_string_lossy().to_string(); + assert_eq!(app_id, "Test.Squirrel-App"); + + vpkc_free_update_manager(manager); + } +} diff --git a/indra/rust/velopack_libc/src/raw.rs b/indra/rust/velopack_libc/src/raw.rs new file mode 100644 index 0000000000..a15b9236fa --- /dev/null +++ b/indra/rust/velopack_libc/src/raw.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use crate::types::*; +use velopack::{sources::UpdateSource, UpdateManager}; + +/// Wrapper around `Arc` that implements `UpdateSource` by delegation. +/// This allows sharing a single source across multiple `UpdateManager` instances +/// without requiring `clone_boxed` on the trait. +struct SharedSource(Arc); + +impl UpdateSource for SharedSource { + fn get_release_feed( + &self, + channel: &str, + app: &velopack::bundle::Manifest, + staged_user_id: &str, + ) -> Result { + self.0.get_release_feed(channel, app, staged_user_id) + } + + fn download_release_entry( + &self, + asset: &velopack::VelopackAsset, + local_file: &std::path::Path, + progress_sender: Option>, + ) -> Result<(), velopack::Error> { + self.0.download_release_entry(asset, local_file, progress_sender) + } +} + +pub trait RawPtrExt<'a, T>: Sized { + fn to_opaque_ref(self) -> Option<&'a T>; +} + +impl<'a> RawPtrExt<'a, UpdateManager> for *mut vpkc_update_manager_t { + fn to_opaque_ref(self) -> Option<&'a UpdateManager> { + if self.is_null() { + return None; + } + + let opaque = unsafe { &*(self as *mut UpdateManager) }; + Some(opaque) + } +} + +pub struct UpdateManagerRawPtr; + +impl UpdateManagerRawPtr { + #[allow(clippy::new_ret_no_self)] + pub fn new(obj: UpdateManager) -> *mut vpkc_update_manager_t { + log::debug!("vpkc_update_manager_t allocated"); + let boxed = Box::new(obj); + Box::into_raw(boxed) as *mut vpkc_update_manager_t + } + + pub fn free(p_manager: *mut vpkc_update_manager_t) { + if p_manager.is_null() { + return; + } + + // Convert the raw pointer back into a Box to deallocate it properly + log::debug!("vpkc_update_manager_t freed"); + let _ = unsafe { Box::from_raw(p_manager as *mut UpdateManager) }; + } +} + +pub struct UpdateSourceContainer { + source: Arc, +} + +pub struct UpdateSourceRawPtr; + +impl UpdateSourceRawPtr { + #[allow(clippy::new_ret_no_self)] + pub fn new(source: Box) -> *mut vpkc_update_source_t { + log::debug!("vpkc_update_source_t allocated"); + let boxed = Box::new(UpdateSourceContainer { source: Arc::from(source) }); + Box::into_raw(boxed) as *mut vpkc_update_source_t + } + + pub fn free(p_source: *mut vpkc_update_source_t) { + if p_source.is_null() { + return; + } + + // Convert the raw pointer back into a Box to deallocate it properly + log::debug!("vpkc_update_source_t freed"); + let _ = unsafe { Box::from_raw(p_source as *mut UpdateSourceContainer) }; + } + + pub fn get_source_boxed(p_source: *mut vpkc_update_source_t) -> Option> { + if p_source.is_null() { + return None; + } + + let opaque = unsafe { &*(p_source as *mut UpdateSourceContainer) }; + Some(Box::new(SharedSource(Arc::clone(&opaque.source)))) + } +} diff --git a/indra/rust/velopack_libc/src/statics.rs b/indra/rust/velopack_libc/src/statics.rs new file mode 100644 index 0000000000..3216a58291 --- /dev/null +++ b/indra/rust/velopack_libc/src/statics.rs @@ -0,0 +1,152 @@ +use anyhow::Result; +use log::{Level, LevelFilter, Log, Metadata, Record}; +use simplelog::{Config, SharedLogger}; +use std::ffi::{c_void, CString}; +use std::sync::{Mutex, RwLock}; +use velopack::locator::VelopackLocatorConfig; + +use crate::types::*; + +#[derive(Debug, Default, Clone)] +pub struct AppOptions { + pub install_hook: vpkc_hook_callback_t, + pub update_hook: vpkc_hook_callback_t, + pub obsolete_hook: vpkc_hook_callback_t, + pub uninstall_hook: vpkc_hook_callback_t, + pub firstrun_hook: vpkc_hook_callback_t, + pub restarted_hook: vpkc_hook_callback_t, + pub auto_apply: Option, + pub args: Option>, + pub locator: Option, +} + +lazy_static::lazy_static! { + static ref LAST_ERROR: RwLock = RwLock::new(String::new()); + static ref LOG_CALLBACK: Mutex<(vpkc_log_callback_t, usize)> = Mutex::new((None, 0)); + pub static ref VELOPACK_APP: RwLock = RwLock::new(Default::default()); +} + +pub fn update_app_options(op: F) +where + F: FnOnce(&mut AppOptions), +{ + let mut app_options = VELOPACK_APP.write().unwrap(); + op(&mut app_options); +} + +pub fn clear_last_error() { + let mut last_error = LAST_ERROR.write().unwrap(); + last_error.clear(); +} + +pub fn get_last_error() -> String { + let last_error = LAST_ERROR.read().unwrap(); + last_error.clone() +} + +pub fn set_last_error(message: &str) { + let mut last_error = LAST_ERROR.write().unwrap(); + *last_error = message.to_string(); +} + +pub fn wrap_error(op: F) -> bool +where + F: FnOnce() -> Result<()>, +{ + clear_last_error(); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(op)); + + match result { + Ok(Ok(_)) => true, + Ok(Err(e)) => { + let msg = format!("{:?}", e); + log::error!("{}", msg); + set_last_error(&msg); + false + } + Err(panic_info) => { + let msg = if let Some(s) = panic_info.downcast_ref::<&str>() { + format!("Internal error (panic): {}", s) + } else if let Some(s) = panic_info.downcast_ref::() { + format!("Internal error (panic): {}", s) + } else { + "Internal error (panic): unknown".to_string() + }; + log::error!("{}", msg); + set_last_error(&msg); + false + } + } +} + +pub fn set_log_callback(callback: vpkc_log_callback_t, user_data: *mut c_void) { + let mut log_callback = LOG_CALLBACK.lock().unwrap(); + *log_callback = (callback, user_data as usize); +} + +pub fn log_message(level: &str, message: &str) { + let log_callback = LOG_CALLBACK.lock().unwrap(); + let (callback, user_data) = *log_callback; + if let Some(callback) = callback { + let c_level = match CString::new(level) { + Ok(s) => s, + Err(_) => return, + }; + let c_message = match CString::new(message) { + Ok(s) => s, + Err(_) => match CString::new(message.replace('\0', "\\0")) { + Ok(s) => s, + Err(_) => return, + }, + }; + callback(user_data as *mut c_void, c_level.as_ptr(), c_message.as_ptr()); + } +} + +struct LoggerImpl {} + +impl SharedLogger for LoggerImpl { + fn level(&self) -> LevelFilter { + LevelFilter::max() + } + + fn config(&self) -> Option<&Config> { + None + } + + fn as_log(self: Box) -> Box { + Box::new(*self) + } +} + +impl Log for LoggerImpl { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let text = format!("{}", record.args()); + + let level = match record.level() { + Level::Error => "error", + Level::Warn => "warn", + Level::Info => "info", + Level::Debug => "debug", + Level::Trace => "trace", + } + .to_string(); + + log_message(&level, &text); + } + + fn flush(&self) {} +} + +pub fn create_shared_logger() -> Box { + Box::new(LoggerImpl {}) +} diff --git a/indra/rust/velopack_libc/src/types.rs b/indra/rust/velopack_libc/src/types.rs new file mode 100644 index 0000000000..b44b908599 --- /dev/null +++ b/indra/rust/velopack_libc/src/types.rs @@ -0,0 +1,709 @@ +#![allow(clippy::all)] + +use anyhow::{bail, Result}; +use libc::{c_char, c_void, size_t}; +use std::ffi::{CStr, CString}; +use std::mem::size_of; +use std::path::PathBuf; +use velopack::{locator::VelopackLocatorConfig, HttpHeader, HttpOptions, UpdateInfo, UpdateOptions, VelopackAsset}; + +/// The result of a call to check for updates. This can indicate that an update is available, or that an error occurred. +#[repr(i8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum vpkc_update_check_t { + UPDATE_ERROR = -1, + UPDATE_AVAILABLE = 0, + NO_UPDATE_AVAILABLE = 1, + REMOTE_IS_EMPTY = 2, +} + +/// Opaque type for the Velopack UpdateManager. Must be freed with `vpkc_free_update_manager`. +pub type vpkc_update_manager_t = c_void; + +/// Opaque type for a Velopack UpdateSource. Must be freed with `vpkc_free_update_source`. +pub type vpkc_update_source_t = c_void; + +/// Progress callback function. +pub type vpkc_progress_callback_t = Option; + +/// Log callback function. +pub type vpkc_log_callback_t = Option; + +/// VelopackApp startup hook callback function. +pub type vpkc_hook_callback_t = Option; + +/// User delegate for to fetch a release feed. This function should return the raw JSON string of the release.json feed. +pub type vpkc_release_feed_delegate_t = Option *mut c_char>; + +/// User delegate for freeing a release feed. This function should free the feed string returned by `vpkc_release_feed_delegate_t`. +pub type vpkc_free_release_feed_t = Option; + +/// User delegate for downloading an asset file. This function is expected to download the provided asset +/// to the provided local file path. Througout, you can use the progress callback to write progress reports. +/// The function should return true if the download was successful, false otherwise. +/// Progress +pub type vpkc_download_asset_delegate_t = Option< + extern "C" fn(p_user_data: *mut c_void, p_asset: *const vpkc_asset_t, psz_local_path: *const c_char, progress_callback_id: size_t) -> bool, +>; + +pub fn c_to_String(psz: *const c_char) -> Result { + if psz.is_null() { + bail!("Null pointer: String must be set."); + } + let cstr = unsafe { CStr::from_ptr(psz) }; + Ok(String::from_utf8_lossy(cstr.to_bytes()).to_string()) +} + +pub fn c_to_String_vec(p_args: *mut *mut c_char, c_args: size_t) -> Result> { + if p_args.is_null() || c_args == 0 { + return Ok(Vec::new()); + } + let mut args = Vec::with_capacity(c_args); + for i in 0..c_args { + let arg = c_to_String(unsafe { *p_args.add(i) })?; + args.push(arg); + } + Ok(args) +} + +pub fn c_to_PathBuf(psz: *const c_char) -> Result { + c_to_String(psz).map(PathBuf::from) +} + +pub fn allocate_String<'a, T: Into>>(s: T) -> *mut c_char { + let s = s.into(); + if s.is_none() { + return std::ptr::null_mut(); + } + let s = s.unwrap(); + let cstr = CString::new(s.clone()).unwrap(); + cstr.into_raw() +} + +pub fn allocate_PathBuf(p: &PathBuf) -> *mut c_char { + let st = p.to_string_lossy().to_string(); + allocate_String(&st) +} + +pub unsafe fn free_String(psz: *mut c_char) { + if !psz.is_null() { + drop(CString::from_raw(psz)); + } +} + +pub unsafe fn free_PathBuf(psz: *mut c_char) { + free_String(psz); +} + +pub fn return_cstr(psz: *mut c_char, c: size_t, s: &str) -> size_t { + if !psz.is_null() && c > 0 { + let cstr = CString::new(s).unwrap(); + let bytes = cstr.as_bytes_with_nul(); + let len = bytes.len().min(c); + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), psz as *mut u8, len); + *psz.add(len) = 0; + } + } + + return s.len(); +} + +// !! AUTO-GENERATED-START RUST_TYPES +#[rustfmt::skip] +#[repr(C)] +/// A single HTTP header (name and value pair) to be sent with a web request. +pub struct vpkc_http_header_t { + /// The name of the HTTP header (eg. "Authorization"). + pub Name: *mut c_char, + /// The value of the HTTP header. + pub Value: *mut c_char, +} + +#[rustfmt::skip] +pub fn c_to_HttpHeader(obj: *mut vpkc_http_header_t) -> Result { + if obj.is_null() { bail!("Null pointer: HttpHeader must be set."); } + let obj = unsafe { &*obj }; + let result = HttpHeader { + Name: c_to_String(obj.Name)?, + Value: c_to_String(obj.Value)?, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_HttpHeader_vec(obj: *mut *mut vpkc_http_header_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_HttpHeader(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_HttpHeader<'a, T: Into>>(dto: T) -> *mut vpkc_http_header_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_http_header_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_http_header_t; + (*obj).Name = allocate_String(&dto.Name); + (*obj).Value = allocate_String(&dto.Value); + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_HttpHeader_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_http_header_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_http_header_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_HttpHeader(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_HttpHeader(obj: *mut vpkc_http_header_t) { + if obj.is_null() { return; } + free_String((*obj).Name); + free_String((*obj).Value); + libc::free(obj as *mut c_void); + log::debug!("vpkc_http_header_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_HttpHeader_vec(obj: *mut *mut vpkc_http_header_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_HttpHeader(ptr); + } + log::debug!("vpkc_http_header_t vector freed"); +} + +#[rustfmt::skip] +#[repr(C)] +/// Options to customize HTTP requests (custom headers, timeout, etc). +pub struct vpkc_http_options_t { + /// Additional headers to send with each request. + pub Headers: *mut *mut vpkc_http_header_t, + /// The number of elements in the Headers array. + pub HeadersCount: size_t, + /// Timeout applied to the entire request (connection + transfer), in milliseconds. + /// The default of 0 means requests never time out. + pub TimeoutMilliseconds: u64, +} + +#[rustfmt::skip] +pub fn c_to_HttpOptions(obj: *mut vpkc_http_options_t) -> Result { + if obj.is_null() { bail!("Null pointer: HttpOptions must be set."); } + let obj = unsafe { &*obj }; + let result = HttpOptions { + Headers: c_to_HttpHeader_vec(obj.Headers, obj.HeadersCount)?, + TimeoutMilliseconds: obj.TimeoutMilliseconds, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_HttpOptions_vec(obj: *mut *mut vpkc_http_options_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_HttpOptions(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_HttpOptions<'a, T: Into>>(dto: T) -> *mut vpkc_http_options_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_http_options_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_http_options_t; + (*obj).Headers = allocate_HttpHeader_vec(&dto.Headers, &mut (*obj).HeadersCount); + (*obj).TimeoutMilliseconds = dto.TimeoutMilliseconds; + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_HttpOptions_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_http_options_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_http_options_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_HttpOptions(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_HttpOptions(obj: *mut vpkc_http_options_t) { + if obj.is_null() { return; } + free_HttpHeader_vec((*obj).Headers, (*obj).HeadersCount); + + libc::free(obj as *mut c_void); + log::debug!("vpkc_http_options_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_HttpOptions_vec(obj: *mut *mut vpkc_http_options_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_HttpOptions(ptr); + } + log::debug!("vpkc_http_options_t vector freed"); +} + +#[rustfmt::skip] +#[repr(C)] +/// VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). +pub struct vpkc_locator_config_t { + /// The root directory of the current app, or the path to the AppImage file on Linux. + pub RootAppDir: *mut c_char, + /// The path to the Update.exe binary. + pub UpdateExePath: *mut c_char, + /// The path to the packages' directory. + pub PackagesDir: *mut c_char, + /// The current app manifest. + pub ManifestPath: *mut c_char, + /// The directory containing the application's user binaries. + pub CurrentBinaryDir: *mut c_char, + /// Whether the current application is portable or installed. + pub IsPortable: bool, +} + +#[rustfmt::skip] +pub fn c_to_VelopackLocatorConfig(obj: *mut vpkc_locator_config_t) -> Result { + if obj.is_null() { bail!("Null pointer: VelopackLocatorConfig must be set."); } + let obj = unsafe { &*obj }; + let result = VelopackLocatorConfig { + RootAppDir: c_to_PathBuf(obj.RootAppDir)?, + UpdateExePath: c_to_PathBuf(obj.UpdateExePath)?, + PackagesDir: c_to_PathBuf(obj.PackagesDir)?, + ManifestPath: c_to_PathBuf(obj.ManifestPath)?, + CurrentBinaryDir: c_to_PathBuf(obj.CurrentBinaryDir)?, + IsPortable: obj.IsPortable, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_VelopackLocatorConfig_vec(obj: *mut *mut vpkc_locator_config_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_VelopackLocatorConfig(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_VelopackLocatorConfig<'a, T: Into>>(dto: T) -> *mut vpkc_locator_config_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_locator_config_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_locator_config_t; + (*obj).RootAppDir = allocate_PathBuf(&dto.RootAppDir); + (*obj).UpdateExePath = allocate_PathBuf(&dto.UpdateExePath); + (*obj).PackagesDir = allocate_PathBuf(&dto.PackagesDir); + (*obj).ManifestPath = allocate_PathBuf(&dto.ManifestPath); + (*obj).CurrentBinaryDir = allocate_PathBuf(&dto.CurrentBinaryDir); + (*obj).IsPortable = dto.IsPortable; + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_VelopackLocatorConfig_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_locator_config_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_locator_config_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_VelopackLocatorConfig(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_VelopackLocatorConfig(obj: *mut vpkc_locator_config_t) { + if obj.is_null() { return; } + free_PathBuf((*obj).RootAppDir); + free_PathBuf((*obj).UpdateExePath); + free_PathBuf((*obj).PackagesDir); + free_PathBuf((*obj).ManifestPath); + free_PathBuf((*obj).CurrentBinaryDir); + + libc::free(obj as *mut c_void); + log::debug!("vpkc_locator_config_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_VelopackLocatorConfig_vec(obj: *mut *mut vpkc_locator_config_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_VelopackLocatorConfig(ptr); + } + log::debug!("vpkc_locator_config_t vector freed"); +} + +#[rustfmt::skip] +#[repr(C)] +/// An individual Velopack asset, could refer to an asset on-disk or in a remote package feed. +pub struct vpkc_asset_t { + /// The name or Id of the package containing this release. + pub PackageId: *mut c_char, + /// The version of this release. + pub Version: *mut c_char, + /// The type of asset (eg. "Full" or "Delta"). + pub Type: *mut c_char, + /// The filename of the update package containing this release. + pub FileName: *mut c_char, + /// The SHA1 checksum of the update package containing this release. + pub SHA1: *mut c_char, + /// The SHA256 checksum of the update package containing this release. + pub SHA256: *mut c_char, + /// The size in bytes of the update package containing this release. + pub Size: u64, + /// The release notes in markdown format, as passed to Velopack when packaging the release. This may be an empty string. + pub NotesMarkdown: *mut c_char, + /// The release notes in HTML format, transformed from Markdown when packaging the release. This may be an empty string. + pub NotesHtml: *mut c_char, +} + +#[rustfmt::skip] +pub fn c_to_VelopackAsset(obj: *mut vpkc_asset_t) -> Result { + if obj.is_null() { bail!("Null pointer: VelopackAsset must be set."); } + let obj = unsafe { &*obj }; + let result = VelopackAsset { + PackageId: c_to_String(obj.PackageId)?, + Version: c_to_String(obj.Version)?, + Type: c_to_String(obj.Type)?, + FileName: c_to_String(obj.FileName)?, + SHA1: c_to_String(obj.SHA1)?, + SHA256: c_to_String(obj.SHA256)?, + Size: obj.Size, + NotesMarkdown: c_to_String(obj.NotesMarkdown)?, + NotesHtml: c_to_String(obj.NotesHtml)?, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_VelopackAsset_vec(obj: *mut *mut vpkc_asset_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_VelopackAsset(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_VelopackAsset<'a, T: Into>>(dto: T) -> *mut vpkc_asset_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_asset_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_asset_t; + (*obj).PackageId = allocate_String(&dto.PackageId); + (*obj).Version = allocate_String(&dto.Version); + (*obj).Type = allocate_String(&dto.Type); + (*obj).FileName = allocate_String(&dto.FileName); + (*obj).SHA1 = allocate_String(&dto.SHA1); + (*obj).SHA256 = allocate_String(&dto.SHA256); + (*obj).Size = dto.Size; + (*obj).NotesMarkdown = allocate_String(&dto.NotesMarkdown); + (*obj).NotesHtml = allocate_String(&dto.NotesHtml); + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_VelopackAsset_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_asset_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_asset_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_VelopackAsset(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_VelopackAsset(obj: *mut vpkc_asset_t) { + if obj.is_null() { return; } + free_String((*obj).PackageId); + free_String((*obj).Version); + free_String((*obj).Type); + free_String((*obj).FileName); + free_String((*obj).SHA1); + free_String((*obj).SHA256); + + free_String((*obj).NotesMarkdown); + free_String((*obj).NotesHtml); + libc::free(obj as *mut c_void); + log::debug!("vpkc_asset_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_VelopackAsset_vec(obj: *mut *mut vpkc_asset_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_VelopackAsset(ptr); + } + log::debug!("vpkc_asset_t vector freed"); +} + +#[rustfmt::skip] +#[repr(C)] +/// Holds information about the current version and pending updates, such as how many there are, and access to release notes. +pub struct vpkc_update_info_t { + /// The available version that we are updating to. + pub TargetFullRelease: *mut vpkc_asset_t, + /// The base release that this update is based on. This is only available if the update is a delta update. + pub BaseRelease: *mut vpkc_asset_t, + /// The list of delta updates that can be applied to the base version to get to the target version. + pub DeltasToTarget: *mut *mut vpkc_asset_t, + /// The number of elements in the DeltasToTarget array. + pub DeltasToTargetCount: size_t, + /// True if the update is a version downgrade or lateral move (such as when switching channels to the same version number). + /// In this case, only full updates are allowed, and any local packages on disk newer than the downloaded version will be + /// deleted. + pub IsDowngrade: bool, +} + +#[rustfmt::skip] +pub fn c_to_UpdateInfo(obj: *mut vpkc_update_info_t) -> Result { + if obj.is_null() { bail!("Null pointer: UpdateInfo must be set."); } + let obj = unsafe { &*obj }; + let result = UpdateInfo { + TargetFullRelease: c_to_VelopackAsset(obj.TargetFullRelease)?, + BaseRelease: c_to_VelopackAsset(obj.BaseRelease).ok(), + DeltasToTarget: c_to_VelopackAsset_vec(obj.DeltasToTarget, obj.DeltasToTargetCount)?, + IsDowngrade: obj.IsDowngrade, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_UpdateInfo_vec(obj: *mut *mut vpkc_update_info_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_UpdateInfo(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_UpdateInfo<'a, T: Into>>(dto: T) -> *mut vpkc_update_info_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_update_info_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_update_info_t; + (*obj).TargetFullRelease = allocate_VelopackAsset(&dto.TargetFullRelease); + (*obj).BaseRelease = allocate_VelopackAsset(&dto.BaseRelease); + (*obj).DeltasToTarget = allocate_VelopackAsset_vec(&dto.DeltasToTarget, &mut (*obj).DeltasToTargetCount); + (*obj).IsDowngrade = dto.IsDowngrade; + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_UpdateInfo_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_update_info_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_update_info_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_UpdateInfo(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_UpdateInfo(obj: *mut vpkc_update_info_t) { + if obj.is_null() { return; } + free_VelopackAsset((*obj).TargetFullRelease); + free_VelopackAsset((*obj).BaseRelease); + free_VelopackAsset_vec((*obj).DeltasToTarget, (*obj).DeltasToTargetCount); + + libc::free(obj as *mut c_void); + log::debug!("vpkc_update_info_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_UpdateInfo_vec(obj: *mut *mut vpkc_update_info_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_UpdateInfo(ptr); + } + log::debug!("vpkc_update_info_t vector freed"); +} + +#[rustfmt::skip] +#[repr(C)] +/// Options to customise the behaviour of UpdateManager. +pub struct vpkc_update_options_t { + /// Allows UpdateManager to update to a version that's lower than the current version (i.e. downgrading). + /// This could happen if a release has bugs and was retracted from the release feed, or if you're using + /// ExplicitChannel to switch channels to another channel where the latest version on that + /// channel is lower than the current version. + pub AllowVersionDowngrade: bool, + /// **This option should usually be left None**. + /// Overrides the default channel used to fetch updates. + /// The default channel will be whatever channel was specified on the command line when building this release. + /// For example, if the current release was packaged with '--channel beta', then the default channel will be 'beta'. + /// This allows users to automatically receive updates from the same channel they installed from. This options + /// allows you to explicitly switch channels, for example if the user wished to switch back to the 'stable' channel + /// without having to reinstall the application. + pub ExplicitChannel: *mut c_char, + /// Sets the maximum number of deltas to consider before falling back to a full update. + /// The default is 10. Set to a negative number (eg. -1) to disable deltas. + pub MaximumDeltasBeforeFallback: i32, +} + +#[rustfmt::skip] +pub fn c_to_UpdateOptions(obj: *mut vpkc_update_options_t) -> Result { + if obj.is_null() { bail!("Null pointer: UpdateOptions must be set."); } + let obj = unsafe { &*obj }; + let result = UpdateOptions { + AllowVersionDowngrade: obj.AllowVersionDowngrade, + ExplicitChannel: c_to_String(obj.ExplicitChannel).ok(), + MaximumDeltasBeforeFallback: obj.MaximumDeltasBeforeFallback, + }; + Ok(result) +} + +#[rustfmt::skip] +pub fn c_to_UpdateOptions_vec(obj: *mut *mut vpkc_update_options_t, count: size_t) -> Result> { + if obj.is_null() || count == 0 { return Ok(Vec::new()); } + let mut assets = Vec::with_capacity(count as usize); + for i in 0..count { + let ptr = unsafe { *obj.add(i as usize) }; + assets.push(c_to_UpdateOptions(ptr)?); + } + Ok(assets) +} + +#[rustfmt::skip] +pub unsafe fn allocate_UpdateOptions<'a, T: Into>>(dto: T) -> *mut vpkc_update_options_t { + let dto = dto.into(); + if dto.is_none() { + return std::ptr::null_mut(); + } + log::debug!("vpkc_update_options_t allocated"); + let dto = dto.unwrap(); + let obj = libc::malloc(size_of::()) as *mut vpkc_update_options_t; + (*obj).AllowVersionDowngrade = dto.AllowVersionDowngrade; + (*obj).ExplicitChannel = allocate_String(&dto.ExplicitChannel); + (*obj).MaximumDeltasBeforeFallback = dto.MaximumDeltasBeforeFallback; + obj +} + +#[rustfmt::skip] +pub unsafe fn allocate_UpdateOptions_vec(dto: &Vec, count: *mut size_t) -> *mut *mut vpkc_update_options_t { + if dto.is_empty() { + *count = 0; + return std::ptr::null_mut(); + } + log::debug!("vpkc_update_options_t vector allocated"); + let count_value = dto.len() as size_t; + *count = count_value; + let mut assets = Vec::with_capacity(count_value as usize); + for i in 0..count_value { + let ptr = allocate_UpdateOptions(&dto[i as usize]); + assets.push(ptr); + } + let ptr = assets.as_mut_ptr(); + std::mem::forget(assets); + ptr +} + +#[rustfmt::skip] +pub unsafe fn free_UpdateOptions(obj: *mut vpkc_update_options_t) { + if obj.is_null() { return; } + + free_String((*obj).ExplicitChannel); + + libc::free(obj as *mut c_void); + log::debug!("vpkc_update_options_t freed"); +} + +#[rustfmt::skip] +pub unsafe fn free_UpdateOptions_vec(obj: *mut *mut vpkc_update_options_t, count: size_t) { + if obj.is_null() || count == 0 { return; } + let vec = Vec::from_raw_parts(obj, count as usize, count as usize); + for i in 0..count { + let ptr = *vec.get_unchecked(i as usize); + free_UpdateOptions(ptr); + } + log::debug!("vpkc_update_options_t vector freed"); +} +// !! AUTO-GENERATED-END RUST_TYPES diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index f414475562..ef8f9faf80 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -2,7 +2,7 @@ "registries": [ { "kind": "git", - "baseline": "87d8ff664aac1a654ddc27b28e991888dc0fe123", + "baseline": "c6aa624da454ffd5a0e255254abc6f1c0c40855d", "reference": "main", "repository": "https://github.com/AlchemyViewer/alchemy-registry", "packages": [ @@ -15,6 +15,7 @@ "boost-fiber", "cef-bin", "collada-dom", + "corrosion", "curl", "dbus", "faudio", @@ -37,7 +38,6 @@ "tinygltf", "tracy", "v-hacd", - "velopack", "vlc-bin", "webrtc", "zlib" diff --git a/indra/vcpkg.json b/indra/vcpkg.json index f357e09011..458d9ad206 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -295,7 +295,7 @@ "velopack": { "description": "Build with support for the Velopack install and update framework", "dependencies": [ - "velopack" + "corrosion" ] }, "webrtc": { From 117148283f6aebd247a71b2cdc15c7b7d88c01c6 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 04:21:32 -0400 Subject: [PATCH 5/8] Windows and macOS rows restore the crates Velopack's C API builds from Corrosion runs cargo in every Windows and macOS build now, and fetching the crates cold takes nine seconds. They come from the Actions cache, keyed on indra/rust/Cargo.lock: the index and the .crate archives only, 330 files, since cargo unpacks the sources itself and the 8,000 unpacked files would be most of a restore on Windows. On a miss they are fetched and saved at once rather than when the job ends, as the rows that build no viewer never run cargo and finish first, and would save an empty registry under the key. Co-Authored-By: Claude Opus 5.5 --- .github/actions/setup-build/action.yaml | 41 ++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-build/action.yaml b/.github/actions/setup-build/action.yaml index 7f69f6c575..22cbcca284 100644 --- a/.github/actions/setup-build/action.yaml +++ b/.github/actions/setup-build/action.yaml @@ -1,8 +1,9 @@ name: Set up the build description: >- - Installs the host packages, Python tools and vcpkg that configuring the - viewer needs, and adds the R2 vcpkg binary cache when asked to. Expects the - repository checked out with its submodules and VCPKG_ROOT set. + Installs the host packages, Python tools, Rust crates and vcpkg that + configuring the viewer needs, and adds the R2 vcpkg binary cache when asked + to. Expects the repository checked out with its submodules and VCPKG_ROOT + set. inputs: msvc-environment: @@ -134,10 +135,36 @@ runs: shell: bash run: ./vcpkg/bootstrap-vcpkg.sh - # No cargo cache. The one port built with cargo, Velopack, is built only - # when the binary cache misses it; every other row restored the crates - # for nothing, at up to a minute a row on Windows, where fetching them - # takes the rare row that needs them ten seconds. + # Corrosion builds velopack_libc where Velopack ships, Windows and macOS, + # from the crates indra/rust/Cargo.lock names. They come from the cache: + # only what cargo downloads, the index and the .crate archives, as it + # unpacks the sources itself and the unpacked tree would be most of what + # a restore costs on Windows. On a miss they are fetched and saved at + # once, not when the job ends: the rows that build no viewer never run + # cargo, and would save an empty registry under the key. + - name: Restore the Rust crates + id: crates + if: runner.os != 'Linux' + uses: actions/cache/restore@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + key: cargo-${{ runner.os }}-${{ hashFiles('indra/rust/Cargo.lock') }} + + - name: Fetch the Rust crates + if: runner.os != 'Linux' && steps.crates.outputs.cache-hit != 'true' + shell: bash + run: cargo fetch --locked --manifest-path indra/rust/Cargo.toml + + - name: Save the Rust crates + if: runner.os != 'Linux' && steps.crates.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + key: ${{ steps.crates.outputs.cache-primary-key }} - name: Install AWS CLI on macOS if: inputs.r2-cache-mode != '' && runner.os == 'macOS' From a8f7439875889fce93b0f18708955ae6f5043451 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 05:56:20 -0400 Subject: [PATCH 6/8] cargo links velopack_libc with the link.exe CMake found, not Git's on the path Both Windows viewer rows failed building proc-macro2's and quote's build scripts: rustc runs link.exe by name, and the build step runs under Git Bash, whose /usr/bin, holding coreutils' link, comes first on the path. CMake's own links name the linker by its path. cargo now does the same, through CARGO_TARGET__LINKER, which covers the build scripts too, since they are built for the target the crate is. Co-Authored-By: Claude Opus 5.5 --- indra/cmake/Velopack.cmake | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake index afb4af6542..295717eab5 100644 --- a/indra/cmake/Velopack.cmake +++ b/indra/cmake/Velopack.cmake @@ -38,6 +38,14 @@ if(AL_USE_VELOPACK) ) # Built for the viewer that links it, not by every build with Velopack on. set_target_properties(cargo-build_velopack_libc PROPERTIES EXCLUDE_FROM_ALL TRUE) + if(WINDOWS AND CMAKE_LINKER) + # rustc runs link.exe by name, and a build from Git Bash finds Git's + # coreutils link first on the path. cargo takes the one CMake found, for + # the crate and for the build scripts it builds for the same target. + string(TOUPPER "${Rust_CARGO_TARGET}" velopack_rust_target) + string(REPLACE "-" "_" velopack_rust_target "${velopack_rust_target}") + corrosion_set_env_vars(velopack_libc "CARGO_TARGET_${velopack_rust_target}_LINKER=${CMAKE_LINKER}") + endif() if(DARWIN) # What Corrosion links a static library with on macOS, System, c and m, # is libSystem, which every link takes already: given again, ld warns of From abfbf1e3e21ea303c7e799b3eed845bdaaf0361d Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 05:56:20 -0400 Subject: [PATCH 7/8] Velopack's C API writes its strings inside the buffer it is given return_cstr, behind vpkc_get_last_error, vpkc_get_current_version and vpkc_get_app_id, copied the string and its terminator up to the buffer's size and then wrote another terminator after them: one byte past the buffer whenever the string did not leave two bytes spare. The viewer's 512-byte error buffers overflowed on a message of 511 characters. It now copies at most size - 1 bytes and terminates inside, and no longer panics on an interior NUL. Velopack.hpp's wrappers, which passed the string's length as the size and let the stray terminator land in std::string's, give it room for one. Checked with a canary after the buffer, clobbered before and intact after, at the exact length and at one more. The viewer's version read took the whole length returned even when the version had not fit its 64 bytes; it takes what was written. The lines are marked Alchemy, and the vendored copy's notes say to keep them until velopack fixes it. Co-Authored-By: Claude Opus 5.5 --- doc/ARCHITECTURE.md | 2 +- indra/newview/llvelopack.cpp | 3 ++- indra/rust/velopack_libc/Cargo.toml | 16 ++++++++++------ indra/rust/velopack_libc/include/Velopack.hpp | 16 ++++++++++------ indra/rust/velopack_libc/src/types.rs | 8 +++++--- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 3e1e6e6deb..442f1c2990 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -36,7 +36,7 @@ - **media_plugins** - Out-of-process media plugin implementations (CEF, VLC, GStreamer) - **alscript** - Script Studio's language services, free of UI (depends on llcommon, llmath, Tailslide and SLua's Luau). One folder a part, each on the include path: `core/` (problems, symbols, the lexicon, messages, weights, the formatter), `lint/` (the studio's lints and fixes), `lsl/` (the LSL analyzer over Tailslide, the SLua converter), `lsl/optimizer/` (the LSL optimizer, a pass a file, sharing `allsloptimizerpass.h`), `luau/` (the SLua analyzer) and `preprocessor/` (the preprocessor and its includes) - **lscript** - LL's LSL compiler and LSO VM, restored as a test-only reference for the optimizer and the weigher; built with tests, never linked into the viewer -- **rust** - The Rust the viewer builds: one Cargo workspace and lock file, imported by Corrosion, a vcpkg port. It holds velopack_libc, Velopack's C API over the `velopack` crate from crates.io, its sources and headers vendored unchanged from the release (`cmake/Velopack.cmake`): a DLL on Windows, a static library on macOS, the release build for every configuration +- **rust** - The Rust the viewer builds: one Cargo workspace and lock file, imported by Corrosion, a vcpkg port. It holds velopack_libc, Velopack's C API over the `velopack` crate from crates.io, its sources and headers vendored from the release with one buffer fix marked Alchemy (`cmake/Velopack.cmake`): a DLL on Windows, a static library on macOS, the release build for every configuration - **newview** - The main viewer application. Contains all viewer-specific logic: the rendering pipeline, scene graph, UI floaters/panels, settings, object/avatar systems, and the application entry point. Links against all libraries above plus third-party: Tracy, OpenXR, Discord SDK, NVAPI, TinyEXR, Velopack ### Application Lifecycle diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index ee6237b9af..be32bb86af 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -1510,7 +1510,8 @@ std::string velopack_get_current_version() size_t len = vpkc_get_current_version(sUpdateManager, version, sizeof(version)); if (len > 0) { - return std::string(version, len); + // The length returned is the whole version's, which may not have fit. + return std::string(version, std::min(len, sizeof(version) - 1)); } return ""; } diff --git a/indra/rust/velopack_libc/Cargo.toml b/indra/rust/velopack_libc/Cargo.toml index 30b7056efa..c5b751e2d0 100644 --- a/indra/rust/velopack_libc/Cargo.toml +++ b/indra/rust/velopack_libc/Cargo.toml @@ -1,12 +1,16 @@ # Velopack's C API, velopack_libc: src/ and include/ are velopack's -# src/lib-cpp at the release this names, unchanged. It is not published, so -# it is built here over the velopack crate from crates.io, which its own -# workspace takes by path. build.rs, which writes include/Velopack.h again -# with cbindgen on every build, is left out: the header is the released one. +# src/lib-cpp at the release this names, but for the lines marked Alchemy. +# return_cstr in src/types.rs wrote its terminator one past a buffer the +# string filled, and the three Velopack.hpp wrappers over it relied on that. +# It is not published, so it is built here over the velopack crate from +# crates.io, which its own workspace takes by path. build.rs, which writes +# include/Velopack.h again with cbindgen on every build, is left out: the +# header is the released one. # # To move to a new release: copy src/ and include/ from src/lib-cpp at its -# tag, set both versions below, and run `cargo update -p velopack` in -# indra/rust, which moves only what the new velopack needs. +# tag, keep the lines marked Alchemy unless the release has fixed them, set +# both versions below, and run `cargo update -p velopack` in indra/rust, +# which moves only what the new velopack needs. [package] name = "velopack_libc" version = "1.2.161" diff --git a/indra/rust/velopack_libc/include/Velopack.hpp b/indra/rust/velopack_libc/include/Velopack.hpp index f0c0a0d27c..b61307b87e 100644 --- a/indra/rust/velopack_libc/include/Velopack.hpp +++ b/indra/rust/velopack_libc/include/Velopack.hpp @@ -20,9 +20,11 @@ namespace Velopack { static inline void throw_last_error() { + // Alchemy: room for the terminator, which the C API writes inside the size it is given. size_t neededSize = vpkc_get_last_error(nullptr, 0); - std::string strError(neededSize, '\0'); - vpkc_get_last_error(&strError[0], neededSize); + std::string strError(neededSize + 1, '\0'); + vpkc_get_last_error(&strError[0], neededSize + 1); + strError.resize(neededSize); throw std::runtime_error(strError); } @@ -1013,8 +1015,9 @@ class UpdateManager { */ std::string GetCurrentVersion() noexcept { size_t neededSize = vpkc_get_current_version(m_pManager, nullptr, 0); - std::string strVersion(neededSize, '\0'); - vpkc_get_current_version(m_pManager, &strVersion[0], neededSize); + std::string strVersion(neededSize + 1, '\0'); + vpkc_get_current_version(m_pManager, &strVersion[0], neededSize + 1); + strVersion.resize(neededSize); return strVersion; }; @@ -1023,8 +1026,9 @@ class UpdateManager { */ std::string GetAppId() noexcept { size_t neededSize = vpkc_get_app_id(m_pManager, nullptr, 0); - std::string strId(neededSize, '\0'); - vpkc_get_app_id(m_pManager, &strId[0], neededSize); + std::string strId(neededSize + 1, '\0'); + vpkc_get_app_id(m_pManager, &strId[0], neededSize + 1); + strId.resize(neededSize); return strId; }; diff --git a/indra/rust/velopack_libc/src/types.rs b/indra/rust/velopack_libc/src/types.rs index b44b908599..cda93f0db4 100644 --- a/indra/rust/velopack_libc/src/types.rs +++ b/indra/rust/velopack_libc/src/types.rs @@ -97,9 +97,11 @@ pub unsafe fn free_PathBuf(psz: *mut c_char) { pub fn return_cstr(psz: *mut c_char, c: size_t, s: &str) -> size_t { if !psz.is_null() && c > 0 { - let cstr = CString::new(s).unwrap(); - let bytes = cstr.as_bytes_with_nul(); - let len = bytes.len().min(c); + // Alchemy: at most c - 1 bytes and the terminator, all inside the + // caller's c. Velopack's own wrote the terminator at psz[c] whenever + // the string filled the buffer, and panicked on an interior NUL. + let bytes = s.as_bytes(); + let len = bytes.len().min(c - 1); unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), psz as *mut u8, len); *psz.add(len) = 0; From 83a70d938721d90a5810ee845d80971470ee56f8 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 5 Oct 2026 05:56:20 -0400 Subject: [PATCH 8/8] Each list of Python tools has a venv of its own Every call of the python-tools action used one python-tools directory, so a job that asked for two lists would build the second over the first, and save the first's tools under the second's key. The venv and its requirements file are now named for the list. Co-Authored-By: Claude Opus 5.5 --- .github/actions/python-tools/action.yaml | 4 ++++ scripts/ci/python_tools.py | 8 ++++++-- scripts/ci/test_python_tools.py | 13 ++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/actions/python-tools/action.yaml b/.github/actions/python-tools/action.yaml index d0718966fe..9a56abcef9 100644 --- a/.github/actions/python-tools/action.yaml +++ b/.github/actions/python-tools/action.yaml @@ -33,6 +33,8 @@ runs: - name: Install the Python tools if: steps.restore.outputs.cache-hit != 'true' shell: bash + env: + REQUIREMENTS: ${{ inputs.requirements }} run: python scripts/ci/python_tools.py build # As soon as it is built rather than when the job ends, which a failed @@ -46,4 +48,6 @@ runs: - name: Put the Python tools on the path shell: bash + env: + REQUIREMENTS: ${{ inputs.requirements }} run: python scripts/ci/python_tools.py activate diff --git a/scripts/ci/python_tools.py b/scripts/ci/python_tools.py index b09ed44f8b..3ab08754ea 100644 --- a/scripts/ci/python_tools.py +++ b/scripts/ci/python_tools.py @@ -7,7 +7,9 @@ times: key the cache key and the venv's path, as step outputs; the - requirements, from REQUIREMENTS, go to a file beside the venv + requirements, from REQUIREMENTS, go to a file beside the venv. + Each list has a venv of its own, so a job that asks for two + never saves one's tools under the other's key build on a miss, the venv. Without pip, which a restored venv never runs: the setup's own pip installs into it. Without the CMake documentation either, which no build reads. Each file left out @@ -57,7 +59,9 @@ def cache_key(runner_os, runner_arch, version, base_prefix, requirements): def paths(env): temp = Path(os.path.normpath(env["RUNNER_TEMP"])) - return temp / "python-tools", temp / "python-tools-requirements.txt" + digest = hashlib.sha256(normalized(env["REQUIREMENTS"]).encode("utf-8")).hexdigest()[:12] + name = f"python-tools-{digest}" + return temp / name, temp / f"{name}.txt" def documentation(python): diff --git a/scripts/ci/test_python_tools.py b/scripts/ci/test_python_tools.py index b0faca60f1..278ebb1264 100644 --- a/scripts/ci/test_python_tools.py +++ b/scripts/ci/test_python_tools.py @@ -41,9 +41,16 @@ def test_key_changes_with_anything_the_venv_depends_on(self): self.assertNotEqual(key, base) def test_paths_sit_in_the_runner_temp(self): - root, requirements = tools.paths({"RUNNER_TEMP": "/home/runner/work/_temp/"}) - self.assertEqual(root, Path("/home/runner/work/_temp/python-tools")) - self.assertEqual(requirements, Path("/home/runner/work/_temp/python-tools-requirements.txt")) + root, requirements = tools.paths({"RUNNER_TEMP": "/home/runner/work/_temp/", "REQUIREMENTS": REQUIREMENTS}) + self.assertEqual(root.parent, Path("/home/runner/work/_temp")) + self.assertTrue(root.name.startswith("python-tools-")) + self.assertEqual(requirements, root.with_name(root.name + ".txt")) + + def test_each_list_has_its_own_venv(self): + env = {"RUNNER_TEMP": "/home/runner/work/_temp"} + first = tools.paths({**env, "REQUIREMENTS": REQUIREMENTS}) + self.assertEqual(first, tools.paths({**env, "REQUIREMENTS": "\r\n" + REQUIREMENTS})) + self.assertNotEqual(first[0], tools.paths({**env, "REQUIREMENTS": "dmgbuild==1.6.7\n"})[0]) if __name__ == "__main__":