diff --git a/.github/actions/python-tools/action.yaml b/.github/actions/python-tools/action.yaml new file mode 100644 index 0000000000..9a56abcef9 --- /dev/null +++ b/.github/actions/python-tools/action.yaml @@ -0,0 +1,53 @@ +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 + 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 + # 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 + env: + REQUIREMENTS: ${{ inputs.requirements }} + 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 89e3dac3c8..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: @@ -99,19 +100,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 +107,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.4; sys_platform != "linux" + ninja==1.13.2 + llsd==1.2.4 + sentry-cli==3.8.0 + - name: Bootstrap vcpkg Windows if: runner.os == 'Windows' shell: pwsh @@ -129,16 +135,36 @@ 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 + # 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 - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('indra/vcpkg.json', 'indra/vcpkg-configuration.json') }} - restore-keys: ${{ runner.os }}-cargo- + ~/.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' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2f09bfc73d..1e5558a461 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" @@ -546,6 +545,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 +559,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,10 +678,12 @@ 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 - # 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: @@ -683,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/ @@ -697,6 +707,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 @@ -706,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.4 + dmgbuild[badge_icons]==1.6.7 - name: Fetch macOS app uses: actions/download-artifact@v8 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..442f1c2990 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 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/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..295717eab5 --- /dev/null +++ b/indra/cmake/Velopack.cmake @@ -0,0 +1,67 @@ +# -*- 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(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 + # 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/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/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..c5b751e2d0 --- /dev/null +++ b/indra/rust/velopack_libc/Cargo.toml @@ -0,0 +1,39 @@ +# Velopack's C API, velopack_libc: src/ and include/ are velopack's +# 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, 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" +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..b61307b87e --- /dev/null +++ b/indra/rust/velopack_libc/include/Velopack.hpp @@ -0,0 +1,1153 @@ +//! 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() +{ + // 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 + 1, '\0'); + vpkc_get_last_error(&strError[0], neededSize + 1); + strError.resize(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 + 1, '\0'); + vpkc_get_current_version(m_pManager, &strVersion[0], neededSize + 1); + strVersion.resize(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 + 1, '\0'); + vpkc_get_app_id(m_pManager, &strId[0], neededSize + 1); + strId.resize(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..cda93f0db4 --- /dev/null +++ b/indra/rust/velopack_libc/src/types.rs @@ -0,0 +1,711 @@ +#![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 { + // 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; + } + } + + 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": { diff --git a/scripts/ci/python_tools.py b/scripts/ci/python_tools.py new file mode 100644 index 0000000000..3ab08754ea --- /dev/null +++ b/scripts/ci/python_tools.py @@ -0,0 +1,134 @@ +#!/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. + 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 + 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"])) + 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): + """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..278ebb1264 --- /dev/null +++ b/scripts/ci/test_python_tools.py @@ -0,0 +1,57 @@ +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/", "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__": + unittest.main()