diff --git a/.github/workflows/dynamic-ci.yml b/.github/workflows/dynamic-ci.yml index 8b03e50e5a..8a404bd30c 100644 --- a/.github/workflows/dynamic-ci.yml +++ b/.github/workflows/dynamic-ci.yml @@ -60,6 +60,9 @@ jobs: - "CMakeLists.txt" - "vcpkg-configuration.json" - "vcpkg.json" + - "Cargo.toml" + - "Cargo.lock" + - "crates/**" hooks: - "git-hooks/**" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d58c56092d..f5721d3981 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,3 +77,21 @@ jobs: echo "Run '3rd-party/flutter/bin/dart format' to fix formatting." exit 1 fi + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Run Rust format check + shell: bash + run: | + if [ -f "Cargo.toml" ]; then + if ! cargo fmt --all --check; then + echo -n "##[error] Rust code is not properly formatted. " + echo "Run 'cargo fmt --all' to fix formatting." + exit 1 + fi + else + echo "No Cargo.toml found, skipping Rust format check" + fi diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index ac2bd5e14e..b76652c0be 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -198,12 +198,13 @@ jobs: /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c 'cat /proc/sys/kernel/core_pattern' # Create the directory for the coredumps /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c 'mkdir -p /coredump' - # Enable coredumps by setting the core dump size to "unlimited", and run the tests. + # Enable coredumps by setting the core dump size to "unlimited", and run the unified test target. /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ - ulimit -c unlimited && \ + source /root/.cargo/env && \ + ulimit -c unlimited && \ env CTEST_OUTPUT_ON_FAILURE=1 \ LD_LIBRARY_PATH=/root/stage/usr/lib/x86_64-linux-gnu/:/root/stage/lib/:/root/parts/multipass/build/lib/ \ - /root/parts/multipass/build/bin/multipass_tests" + ctest --test-dir /root/parts/multipass/build --output-on-failure" - name: Measure coverage id: measure-coverage @@ -231,18 +232,38 @@ jobs: /snap/bin/lxc --project snapcraft exec $instance_name -- \ sh -c "sudo sed -i \"s/use JSON::PP/use JSON::XS/\" \`which geninfo\`" + # Install cargo-llvm-cov for Rust coverage + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ + source /root/.cargo/env && \ + cargo install cargo-llvm-cov" + # Create the directory for the coredumps /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c 'mkdir -p /coredump' + + # Generate C++ coverage /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ ulimit -c unlimited && \ env CTEST_OUTPUT_ON_FAILURE=1 \ cmake --build /root/parts/multipass/build --target covreport" + # Generate Rust coverage in lcov format + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ + source /root/.cargo/env && \ + cargo llvm-cov --manifest-path /root/parts/multipass/src/Cargo.toml \ + --lcov --output-path /root/parts/multipass/build/rust-coverage.lcov" + + # Merge C++ and Rust coverage reports + /snap/bin/lxc --project snapcraft exec $instance_name -- bash -c "\ + lcov -a /root/parts/multipass/build/coverage.cleaned \ + -a /root/parts/multipass/build/rust-coverage.lcov \ + -o /root/parts/multipass/build/combined-coverage.info" + - name: Upload coverage if: ${{ matrix.build-type == 'Coverage' }} uses: codecov/codecov-action@v5 with: directory: ${{ steps.coverage-setup.outputs.build }} + files: ./combined-coverage.info env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/windows-macos.yml b/.github/workflows/windows-macos.yml index a004eb8bdc..de60d89cc8 100644 --- a/.github/workflows/windows-macos.yml +++ b/.github/workflows/windows-macos.yml @@ -137,7 +137,8 @@ jobs: pixman \ pkg-config \ python \ - wget + wget \ + rustup-init - name: Install dependencies from pip if: ${{ runner.os == 'macOS' }} @@ -154,7 +155,7 @@ jobs: if: ${{ runner.os == 'Windows' }} uses: crazy-max/ghaction-chocolatey@v3 with: - args: install --yes wget unzip + args: install --yes wget unzip rustup.install - name: Set up vcpkg id: setup-vcpkg @@ -255,6 +256,31 @@ jobs: if: ${{ runner.os == 'Windows' }} uses: ilammy/msvc-dev-cmd@v1 + - name: Initialize Rust toolchain + shell: bash + run: | + if [[ "${{ runner.os }}" == "Windows" ]]; then + # On Windows, rustup.install from chocolatey should be available + if command -v rustup >/dev/null 2>&1; then + rustup default stable + rustup update + else + echo "Error: rustup not found. Please ensure it is installed and in the PATH." + exit 1 + fi + elif [[ "${{ runner.os }}" == "macOS" ]]; then + # On macOS, initialize rustup-init + if command -v rustup-init >/dev/null 2>&1; then + rustup-init -y --default-toolchain stable + source ~/.cargo/env + else + echo "Error: rustup-init not found. Please ensure it is installed and in the PATH." + exit 1 + fi + fi + # Add cargo bin to PATH for subsequent steps + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Configure run: > ${{ env.ARCH_WRAPPER }} @@ -276,9 +302,8 @@ jobs: run: ccache --show-stats --zero-stats - name: Test - working-directory: ${{ env.BUILD_DIR }} - run: | - ${{ env.ARCH_WRAPPER }} bin/multipass_tests + run: |\ + ${{ env.ARCH_WRAPPER }} ctest --test-dir ${{ env.BUILD_DIR }} --output-on-failure - name: Package id: cmake-package diff --git a/.gitignore b/.gitignore index 0152c949dd..3e096b06d8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ packaging/windows/custom-actions/x64/* # Python artifacts *.pyc + + +# Added by cargo + +/target diff --git a/BUILD.linux.md b/BUILD.linux.md index 500440729f..de44441940 100644 --- a/BUILD.linux.md +++ b/BUILD.linux.md @@ -10,6 +10,10 @@ sudo apt install devscripts equivs mk-build-deps -s sudo -i ``` +### Rust toolchain + +Multipass requires Rust for building. Install the Rust toolchain from: https://rustup.rs/ + ## Building First, go into the repository root and get all the submodules: diff --git a/BUILD.macOS.md b/BUILD.macOS.md index f08253091e..1188944683 100644 --- a/BUILD.macOS.md +++ b/BUILD.macOS.md @@ -42,6 +42,10 @@ means to obtain these dependencies is with Homebrew . brew install cmake openssl@3 +### Rust toolchain + +Multipass requires Rust for building. Install the Rust toolchain from: https://rustup.rs/ + Building --------------------------------------- diff --git a/BUILD.windows.md b/BUILD.windows.md index dc7d699d6b..312fda9658 100644 --- a/BUILD.windows.md +++ b/BUILD.windows.md @@ -19,7 +19,7 @@ choco install cmake ninja qemu openssl git wget unzip -yfd ``` ```[pwsh] -choco install visualstudio2022buildtools visualstudio2022-workload-vctools -yfd +choco install visualstudio2022buildtools visualstudio2022-workload-vctools rustup.install -yfd ``` NOTE: visualcpp-build-tools is only the installer package. For this reason, choco cannot detect any @@ -43,6 +43,10 @@ For Windows 11: 1. Go to "Developer Settings" 2. Enable "Developer mode" +### Rust toolchain + +Multipass requires Rust for building. You can install it via Chocolatey (included in the command above) or from: https://rustup.rs/ + ### Qt6 To install Qt6, use `aqt`. First install it with chocolatey: diff --git a/CMakeLists.txt b/CMakeLists.txt index a6ec64092a..73fa0202c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,29 @@ find_package(gRPC CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) # targets: fmt::fmt, fmt::fmt-header-only +# Corrosion for Rust + CMake integration +include(FetchContent) + +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git + GIT_TAG v0.5 # Use stable version +) +FetchContent_MakeAvailable(Corrosion) + +# Register Rust build directories for cleaning +set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_CLEAN_FILES + "${CMAKE_BINARY_DIR}/cargo" + "${CMAKE_BINARY_DIR}/corrosion" +) + +# Add custom target to also run cargo clean when needed +add_custom_target(clean-rust + COMMAND cargo clean --manifest-path ${CMAKE_SOURCE_DIR}/Cargo.toml + COMMENT "Cleaning Cargo workspace artifacts" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} +) + # Needs to be here before we set further compilation options add_subdirectory(3rd-party) @@ -391,6 +414,20 @@ add_subdirectory(src) if(MULTIPASS_ENABLE_TESTS) enable_testing() add_subdirectory(tests) + + # Add convenience targets for running tests + add_custom_target(test-all + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target all_tests + COMMENT "Running all tests (C++ and Rust) via convenience target" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + + # Add alias for compatibility + add_custom_target(check + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target all_tests + COMMENT "Running all tests (alias for test-all)" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) endif() include(packaging/cpack.cmake OPTIONAL) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..a346657c1f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,427 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa144b12f11741f0dab5b4182896afad46faa0598b6a061f7b9d17a21837ba7" +dependencies = [ + "cc", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d3cbb84fb003242941c231b45ca9417e786e66e94baa39584bd99df3a270b6" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa36b7b249d43f67a3f54bd65788e35e7afe64bbc671396387a48b3e8aaea94" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77707c70f6563edc5429618ca34a07241b75ebab35bd01d46697c75d58f8ddfe" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6c0fb7e318f0a11799b86ee29dcf17b9be2960bd379a6c38e1a96a6010fff" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "link-cplusplus" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +dependencies = [ + "cc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustipass" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-build", + "rand", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[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-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_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 = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..757bc233fb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = ["crates/rustipass"] +resolver = "2" + +# Root workspace configuration +[workspace.dependencies] +cxx = "1.0" +rand = "0.8" + +[workspace.dependencies.cxx-build] +version = "1.0" diff --git a/crates/rustipass/Cargo.toml b/crates/rustipass/Cargo.toml new file mode 100644 index 0000000000..7960a40bab --- /dev/null +++ b/crates/rustipass/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rustipass" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[dependencies] +cxx = { workspace = true } +rand = { workspace = true } + +[build-dependencies] +cxx-build = { workspace = true } diff --git a/crates/rustipass/build.rs b/crates/rustipass/build.rs new file mode 100644 index 0000000000..d008bfd24a --- /dev/null +++ b/crates/rustipass/build.rs @@ -0,0 +1,5 @@ +fn main() { + cxx_build::bridge("src/lib.rs") + .flag_if_supported("-std=c++20") + .compile("rustipass"); +} diff --git a/crates/rustipass/src/lib.rs b/crates/rustipass/src/lib.rs new file mode 100644 index 0000000000..fbdae8e89c --- /dev/null +++ b/crates/rustipass/src/lib.rs @@ -0,0 +1,29 @@ +pub mod petname; + +// Re-export petname public API for FFI +pub use petname::{make_name, new_petname, Petname}; + +// CXX FFI Bridge for petname module +#[cxx::bridge(namespace = "multipass::petname")] +mod petname_ffi { + extern "Rust" { + type Petname; + fn new_petname(num_words: i32, separator: &str) -> Result>; + fn make_name(petname: &mut Petname) -> Result; + } +} + +// Future modules will have their own FFI bridges: +// #[cxx::bridge(namespace = "multipass::utils")] +// mod utils_ffi { +// extern "Rust" { +// // utils functions here +// } +// } + +// #[cxx::bridge(namespace = "multipass::network")] +// mod network_ffi { +// extern "Rust" { +// // network functions here +// } +// } diff --git a/src/petname/adjectives.txt b/crates/rustipass/src/petname/adjectives.txt similarity index 100% rename from src/petname/adjectives.txt rename to crates/rustipass/src/petname/adjectives.txt diff --git a/src/petname/adverbs.txt b/crates/rustipass/src/petname/adverbs.txt similarity index 100% rename from src/petname/adverbs.txt rename to crates/rustipass/src/petname/adverbs.txt diff --git a/crates/rustipass/src/petname/mod.rs b/crates/rustipass/src/petname/mod.rs new file mode 100644 index 0000000000..de83a31835 --- /dev/null +++ b/crates/rustipass/src/petname/mod.rs @@ -0,0 +1,95 @@ +use rand::seq::SliceRandom; +use rand::thread_rng; +use std::fmt; + +const ADJECTIVES: &str = include_str!("adjectives.txt"); +const ADVERBS: &str = include_str!("adverbs.txt"); +const NAMES: &str = include_str!("names.txt"); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PetnameError { + InvalidNumWords(i32), + EmptyWordList(String), +} + +impl fmt::Display for PetnameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PetnameError::InvalidNumWords(num) => { + write!(f, "Invalid number of words: {}. Must be 1, 2, or 3", num) + } + PetnameError::EmptyWordList(list_name) => { + write!(f, "Empty word list: {}", list_name) + } + } + } +} + +impl std::error::Error for PetnameError {} + +#[derive(Debug)] +pub enum NumWords { + One, + Two, + Three, +} + +#[derive(Debug)] +pub struct Petname { + num_words: NumWords, + separator: String, +} + +impl Petname { + pub fn new(num_words: NumWords, separator: &str) -> Self { + Self { + num_words, + separator: separator.to_string(), + } + } + + pub fn make_name(&self) -> Result { + let mut rng = thread_rng(); + let adjectives: Vec<&str> = ADJECTIVES.lines().collect(); + let adverbs: Vec<&str> = ADVERBS.lines().collect(); + let names: Vec<&str> = NAMES.lines().collect(); + + let adjective = adjectives + .choose(&mut rng) + .ok_or_else(|| PetnameError::EmptyWordList("adjectives".to_string()))?; + let adverb = adverbs + .choose(&mut rng) + .ok_or_else(|| PetnameError::EmptyWordList("adverbs".to_string()))?; + let name = names + .choose(&mut rng) + .ok_or_else(|| PetnameError::EmptyWordList("names".to_string()))?; + + let result = match self.num_words { + NumWords::One => name.to_string(), + NumWords::Two => format!("{}{}{}", adjective, self.separator, name), + NumWords::Three => format!( + "{}{}{}{}{}", + adverb, self.separator, adjective, self.separator, name + ), + }; + + Ok(result) + } +} + +pub fn new_petname(num_words: i32, separator: &str) -> Result, PetnameError> { + let num_words = match num_words { + 1 => NumWords::One, + 2 => NumWords::Two, + 3 => NumWords::Three, + _ => return Err(PetnameError::InvalidNumWords(num_words)), + }; + Ok(Box::new(Petname::new(num_words, separator))) +} + +pub fn make_name(petname: &mut Petname) -> Result { + petname.make_name() +} + +#[cfg(test)] +mod tests; diff --git a/src/petname/names.txt b/crates/rustipass/src/petname/names.txt similarity index 100% rename from src/petname/names.txt rename to crates/rustipass/src/petname/names.txt diff --git a/crates/rustipass/src/petname/tests.rs b/crates/rustipass/src/petname/tests.rs new file mode 100644 index 0000000000..9f0bd5cf1b --- /dev/null +++ b/crates/rustipass/src/petname/tests.rs @@ -0,0 +1,108 @@ +use super::*; +use std::collections::HashSet; + +fn split_name<'a>(name: &'a str, separator: &str) -> Vec<&'a str> { + name.split(separator).collect() +} + +#[test] +fn generates_the_requested_num_words() { + let mut gen1 = new_petname(1, "-").expect("Should create valid petname"); + let mut gen2 = new_petname(2, "-").expect("Should create valid petname"); + let mut gen3 = new_petname(3, "-").expect("Should create valid petname"); + + let one_word_name = make_name(&mut gen1).expect("Should generate name"); + let tokens = split_name(&one_word_name, "-"); + assert_eq!(tokens.len(), 1); + assert!(tokens.iter().all(|t| !t.is_empty())); + + let two_word_name = make_name(&mut gen2).expect("Should generate name"); + let tokens = split_name(&two_word_name, "-"); + assert_eq!(tokens.len(), 2); + assert!(tokens.iter().all(|t| !t.is_empty())); + + let three_word_name = make_name(&mut gen3).expect("Should generate name"); + let tokens = split_name(&three_word_name, "-"); + assert_eq!(tokens.len(), 3); + assert!(tokens.iter().all(|t| !t.is_empty())); +} + +#[test] +fn uses_custom_separator() { + let mut name_generator = new_petname(3, "_").expect("Should create valid petname"); + let name = make_name(&mut name_generator).expect("Should generate name"); + let tokens = split_name(&name, "_"); + assert_eq!(tokens.len(), 3); + assert!(tokens.iter().all(|t| !t.is_empty())); + assert!(name.contains("_")); +} + +#[test] +fn can_generate_two_token_name() { + let mut name_generator = new_petname(2, "-").expect("Should create valid petname"); + let name = make_name(&mut name_generator).expect("Should generate name"); + let tokens = split_name(&name, "-"); + assert_eq!(tokens.len(), 2); + assert!(tokens.iter().all(|t| !t.is_empty())); + + // Each token should be unique + let unique_tokens: HashSet<_> = tokens.iter().collect(); + assert_eq!(unique_tokens.len(), tokens.len()); +} + +#[test] +fn can_generate_at_least_hundred_unique_names() { + let mut name_generator = new_petname(3, "-").expect("Should create valid petname"); + let mut name_set = HashSet::new(); + let expected_num_unique_names = 100; + + // Generate many names to test uniqueness + for _ in 0..(10 * expected_num_unique_names) { + let name = make_name(&mut name_generator).expect("Should generate name"); + name_set.insert(name); + } + + assert!(name_set.len() >= expected_num_unique_names); +} + +#[test] +fn ffi_integration_test() { + let mut petname = new_petname(2, "-").expect("Should create valid petname"); + let name = make_name(&mut petname).expect("Should generate name"); + assert!(name.contains("-")); + assert!(!name.is_empty()); +} + +// error path tests +#[test] +fn new_petname_rejects_invalid_word_counts() { + assert!(new_petname(0, "-").is_err()); + assert!(new_petname(4, "-").is_err()); + assert!(new_petname(-1, "-").is_err()); + assert!(new_petname(100, "-").is_err()); +} + +#[test] +fn new_petname_returns_specific_error_for_invalid_counts() { + let result = new_petname(0, "-"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(matches!(error, PetnameError::InvalidNumWords(0))); + + let result = new_petname(5, "-"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(matches!(error, PetnameError::InvalidNumWords(5))); +} + +#[test] +fn error_display_formatting() { + let error = PetnameError::InvalidNumWords(42); + assert_eq!( + error.to_string(), + "Invalid number of words: 42. Must be 1, 2, or 3" + ); + + let error = PetnameError::EmptyWordList("test".to_string()); + assert_eq!(error.to_string(), "Empty word list: test"); +} diff --git a/include/multipass/name_generator.h b/include/multipass/name_generator.h index 7f607896e4..2f2dc67b5d 100644 --- a/include/multipass/name_generator.h +++ b/include/multipass/name_generator.h @@ -13,29 +13,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * Authored by: Alberto Aguirre - * */ #pragma once -#include "disabled_copy_move.h" - -#include #include namespace multipass { -class NameGenerator : private DisabledCopyMove + +class NameGenerator { public: - using UPtr = std::unique_ptr; virtual ~NameGenerator() = default; - virtual std::string make_name() = 0; -protected: - NameGenerator() = default; + virtual std::string make_name() = 0; }; -NameGenerator::UPtr make_default_name_generator(); } // namespace multipass diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 308199db62..7af102b4e6 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -203,6 +203,14 @@ parts: - -DMULTIPASS_UPSTREAM=origin - -DMULTIPASS_ENABLE_FLUTTER_GUI=on override-build: | + # Install Rust stable toolchain + if [ ! -f "$HOME/.cargo/env" ]; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source $HOME/.cargo/env + fi + # Ensure Rust is in PATH for this build + export PATH="$HOME/.cargo/bin:$PATH" + craftctl default set -e mkdir -p ${CRAFT_PART_INSTALL}/etc/bash_completion.d/ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7aa0f0c179..e25b8027ad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,10 +24,10 @@ add_subdirectory(daemon) add_subdirectory(iso) add_subdirectory(logging) add_subdirectory(network) -add_subdirectory(petname) add_subdirectory(platform) add_subdirectory(process) add_subdirectory(rpc) +add_subdirectory(rustipass) add_subdirectory(settings) add_subdirectory(simplestreams) add_subdirectory(ssh) diff --git a/src/client/gui/ffi/dart_ffi.cpp b/src/client/gui/ffi/dart_ffi.cpp index 7050de1ae0..e11fa35d81 100644 --- a/src/client/gui/ffi/dart_ffi.cpp +++ b/src/client/gui/ffi/dart_ffi.cpp @@ -1,14 +1,32 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + #include "multipass/dart_ffi.h" #include "multipass/cli/client_common.h" #include "multipass/logging/log.h" #include "multipass/memory_size.h" -#include "multipass/name_generator.h" #include "multipass/platform.h" #include "multipass/settings/settings.h" #include "multipass/standard_paths.h" #include "multipass/utils.h" #include "multipass/version.h" +#include + #include namespace mp = multipass; @@ -32,9 +50,10 @@ char* generate_petname() static constexpr auto error = "failed generating petname"; try { - static mp::NameGenerator::UPtr generator = mp::make_default_name_generator(); - const auto name = generator->make_name(); - return strdup(name.c_str()); + static rust::Box generator = + multipass::petname::new_petname(2, "-"); + const auto name = multipass::petname::make_name(*generator); + return strdup(std::string(name).c_str()); } catch (const std::exception& e) { diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index d436d25b0d..0206c614c7 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -37,7 +37,6 @@ #include #include #include -#include #include #include #include @@ -56,6 +55,8 @@ #include #include +#include + #include #include @@ -211,12 +212,15 @@ auto name_from(const std::string& requested_name, } else { - auto name = name_gen.make_name(); + std::string name = name_generator.make_name(); constexpr int num_retries = 100; for (int i = 0; i < num_retries; i++) { if (currently_used_names.find(name) != currently_used_names.end()) + { + name = name_generator.make_name(); continue; + } return name; } throw std::runtime_error("unable to generate a unique name"); diff --git a/src/daemon/daemon_config.cpp b/src/daemon/daemon_config.cpp index 776d1f4aec..0d2c9017a6 100644 --- a/src/daemon/daemon_config.cpp +++ b/src/daemon/daemon_config.cpp @@ -20,11 +20,12 @@ #include "custom_image_host.h" #include "ubuntu_image_host.h" +#include + #include #include #include #include -#include #include #include #include @@ -198,8 +199,6 @@ std::unique_ptr mp::DaemonConfigBuilder::build() factory->get_backend_directory_name()), days_to_expire); } - if (name_generator == nullptr) - name_generator = mp::make_default_name_generator(); if (server_address.empty()) server_address = platform::default_server_address(); if (ssh_key_provider == nullptr) @@ -237,17 +236,20 @@ std::unique_ptr mp::DaemonConfigBuilder::build() fs::perms::others_exec), server_name_from(server_address)); + if (name_generator == nullptr) + name_generator = std::make_unique(); + return std::unique_ptr(new DaemonConfig{std::move(url_downloader), std::move(factory), std::move(image_hosts), std::move(vault), - std::move(name_generator), std::move(ssh_key_provider), std::move(cert_provider), std::move(client_cert_store), std::move(update_prompt), multiplexing_logger, std::move(network_proxy), + std::move(name_generator), cache_directory, data_directory, server_address, diff --git a/src/daemon/daemon_config.h b/src/daemon/daemon_config.h index f91e5f748b..ea7bd5cb6d 100644 --- a/src/daemon/daemon_config.h +++ b/src/daemon/daemon_config.h @@ -46,13 +46,13 @@ struct DaemonConfig const std::unique_ptr factory; const std::vector> image_hosts; const std::unique_ptr vault; - const std::unique_ptr name_generator; const std::unique_ptr ssh_key_provider; const std::unique_ptr cert_provider; const std::unique_ptr client_cert_store; const std::unique_ptr update_prompt; const std::shared_ptr logger; const std::unique_ptr network_proxy; + const std::unique_ptr name_generator; const multipass::Path cache_directory; const multipass::Path data_directory; const std::string server_address; @@ -66,13 +66,13 @@ struct DaemonConfigBuilder std::unique_ptr factory; std::vector> image_hosts; std::unique_ptr vault; - std::unique_ptr name_generator; std::unique_ptr ssh_key_provider; std::unique_ptr cert_provider; std::unique_ptr client_cert_store; std::unique_ptr update_prompt; std::unique_ptr logger; std::unique_ptr network_proxy; + std::unique_ptr name_generator; multipass::Path cache_directory; multipass::Path data_directory; std::string server_address; diff --git a/src/petname/CMakeLists.txt b/src/petname/CMakeLists.txt deleted file mode 100644 index eb59e3e9d6..0000000000 --- a/src/petname/CMakeLists.txt +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) Canonical, Ltd. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Authored by: Alberto Aguirre - -set(PETNAME_GENERATED_SOURCE_DIR ${MULTIPASS_GENERATED_SOURCE_DIR}/multipass/petname) -set(PETNAME_GENERATED_HEADER ${PETNAME_GENERATED_SOURCE_DIR}/names.h) -file(MAKE_DIRECTORY ${PETNAME_GENERATED_SOURCE_DIR}) - -add_executable(text_to_string_array - text_to_string_array.cpp) - -add_custom_command( - OUTPUT "${PETNAME_GENERATED_HEADER}" - COMMAND $ - ARGS ${CMAKE_CURRENT_SOURCE_DIR}/adjectives.txt ${CMAKE_CURRENT_SOURCE_DIR}/adverbs.txt - ${CMAKE_CURRENT_SOURCE_DIR}/names.txt ${PETNAME_GENERATED_HEADER} - DEPENDS text_to_string_array - COMMENT "Converting petnames to c++ header" - VERBATIM) - -set_source_files_properties(${PETNAME_GENERATED_HEADER} PROPERTIES GENERATED TRUE) - -add_library(petname STATIC - ${PETNAME_GENERATED_HEADER} - petname.cpp - make_name_generator.cpp) diff --git a/src/petname/petname.cpp b/src/petname/petname.cpp deleted file mode 100644 index 2334532175..0000000000 --- a/src/petname/petname.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Alberto Aguirre - * - */ - -#include "petname.h" -#include "multipass/petname/names.h" - -#include - -namespace mp = multipass; -namespace -{ -constexpr auto num_names = std::extent::value; -constexpr auto num_adverbs = std::extent::value; -constexpr auto num_adjectives = std::extent::value; - -// Arbitrary but arrays should have at least 100 entries each -static_assert(num_names >= 100, ""); -static_assert(num_adverbs >= 100, ""); -static_assert(num_adjectives >= 100, ""); - -std::mt19937 make_engine() -{ - std::random_device device; - return std::mt19937(device()); -} -} // namespace - -mp::Petname::Petname(std::string separator) : Petname(NumWords::TWO, separator) -{ -} - -mp::Petname::Petname(NumWords num_words) : Petname(num_words, "-") -{ -} - -mp::Petname::Petname(NumWords num_words, std::string separator) - : separator{separator}, - num_words{num_words}, - engine{make_engine()}, - name_dist{1, num_names - 1}, - adjective_dist{0, num_adjectives - 1}, - adverb_dist{0, num_adverbs - 1} -{ -} - -std::string mp::Petname::make_name() -{ - std::string name = multipass::petname::names[name_dist(engine)]; - std::string adjective = multipass::petname::adjectives[adjective_dist(engine)]; - std::string adverb = multipass::petname::adverbs[adverb_dist(engine)]; - - switch (num_words) - { - case NumWords::ONE: - return name; - case NumWords::TWO: - return adjective + separator + name; - case NumWords::THREE: - return adverb + separator + adjective + separator + name; - default: - throw std::invalid_argument("Invalid number of words chosen"); - } -} diff --git a/src/petname/petname.h b/src/petname/petname.h deleted file mode 100644 index 43838cce85..0000000000 --- a/src/petname/petname.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Alberto Aguirre - * - */ - -#pragma once - -#include - -#include -#include -#include - -namespace multipass -{ -class Petname final : public NameGenerator -{ -public: - enum class NumWords - { - ONE, - TWO, - THREE - }; - - /// Constructs an instance that will generate names using - /// the requested separator and the requested number of words - Petname(NumWords num_words, std::string separator); - /// Constructs an instance that will generate names using - /// a default separator of "-" and the requested number of words - explicit Petname(NumWords num_words); - /// Constructs an instance that will generate names using - /// the requested separator and two words - explicit Petname(std::string separator); - - std::string make_name() override; - -private: - std::string separator; - NumWords num_words; - std::mt19937 engine; - std::uniform_int_distribution name_dist; - std::uniform_int_distribution adjective_dist; - std::uniform_int_distribution adverb_dist; -}; -} // namespace multipass diff --git a/src/petname/text_to_string_array.cpp b/src/petname/text_to_string_array.cpp deleted file mode 100644 index dd296f3b6e..0000000000 --- a/src/petname/text_to_string_array.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Alberto Aguirre - * - */ - -#include -#include -#include -#include - -namespace -{ -void usage(char* argv[]) -{ - std::cout << "Usage:\n "; - std::cout << argv[0] << " \n"; -} - -std::vector words_in(const std::string& filename) -{ - std::ifstream input_stream(filename); - std::string word; - std::vector words; - while (std::getline(input_stream, word)) - { - words.push_back(word); - } - return words; -} - -class Words -{ -public: - Words(const std::string& filename, std::string var_name) - : var_name{std::move(var_name)}, words{words_in(filename)} - { - } - - void print_to(std::ostream& out) - { - out << "const char* " << var_name << "[] =\n{\n"; - for (auto const& w : words) - { - out << " \"" << w << "\",\n"; - } - out << "};\n\n"; - } - -private: - std::string var_name; - std::vector words; -}; -} // namespace - -int main(int argc, char* argv[]) -try -{ - if (argc != 5) - { - usage(argv); - return EXIT_FAILURE; - } - - Words adjectives{argv[1], "adjectives"}; - Words adverbs{argv[2], "adverbs"}; - Words names{argv[3], "names"}; - - std::ofstream out(argv[4]); - - out << "//Auto Generated, any edits will be lost\n\n"; - out << "namespace multipass\n{\n"; - out << "namespace petname\n{\n"; - - adjectives.print_to(out); - adverbs.print_to(out); - names.print_to(out); - - out << "}\n}"; - - return EXIT_SUCCESS; -} -catch (const std::exception& e) -{ - std::cerr << "Error: " << e.what() << "\n"; -} diff --git a/src/rustipass/CMakeLists.txt b/src/rustipass/CMakeLists.txt new file mode 100644 index 0000000000..c9e3a327de --- /dev/null +++ b/src/rustipass/CMakeLists.txt @@ -0,0 +1,70 @@ +# Copyright (C) Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Set the crate directory (Corrosion is already available from root CMakeLists.txt) +set(RUSTIPASS_CRATE_DIR ${CMAKE_SOURCE_DIR}/crates/rustipass) + +# Configure Rust build profile based on CMAKE_BUILD_TYPE +if(CMAKE_BUILD_TYPE STREQUAL "Release") + set(RUST_PROFILE "release") +else() + set(RUST_PROFILE "dev") +endif() + +# Import the rustipass crate using Corrosion with the appropriate profile +corrosion_import_crate(MANIFEST_PATH ${RUSTIPASS_CRATE_DIR}/Cargo.toml PROFILE ${RUST_PROFILE}) + +# Add cxxbridge support for C++ interop +corrosion_add_cxxbridge(rustipass_cxx + CRATE rustipass + REGEN_TARGET rustipass_cxx_regen + FILES lib.rs +) + +# Fix macOS build issue: Apple Clang treats '$' in identifiers as non-standard extension +# cxxbridge generates identifiers with '$' from Rust namespaces (multipass::petname -> multipass$petname$...) +if(APPLE) + target_compile_options(rustipass_cxx PRIVATE -Wno-dollar-in-identifier-extension) +endif() + +# Add C++ wrapper for the Rust petname generator +add_library(rustipass_cpp STATIC + rust_petname_generator.cpp +) + +target_include_directories(rustipass_cpp + PUBLIC + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/src + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(rustipass_cpp + PUBLIC rustipass rustipass_cxx +) + +# Create an interface library that wraps everything together +add_library(rustipass_lib INTERFACE) + +# Link the Rust static library and C++ wrapper +target_link_libraries(rustipass_lib INTERFACE rustipass_cpp rustipass rustipass_cxx) + +# On Windows, link required system libraries for Rust std +if(WIN32) + target_link_libraries(rustipass_lib INTERFACE ntdll ws2_32 userenv bcrypt) +endif() + +# Create compatibility alias for petname (backward compatibility) +add_library(petname ALIAS rustipass_lib) diff --git a/src/rustipass/rust_petname_generator.cpp b/src/rustipass/rust_petname_generator.cpp new file mode 100644 index 0000000000..0e59099ad8 --- /dev/null +++ b/src/rustipass/rust_petname_generator.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "rust_petname_generator.h" + +#include + +namespace multipass +{ + +RustPetnameGenerator::RustPetnameGenerator(int num_words, const std::string& separator) +try + : petname_generator(multipass::petname::new_petname(num_words, separator.c_str())) +{ +} +catch (const rust::Error& e) +{ + throw std::runtime_error(std::string("Failed to create petname generator: ") + e.what()); +} + +std::string RustPetnameGenerator::make_name() +try +{ + return std::string(multipass::petname::make_name(*petname_generator)); +} +catch (const rust::Error& e) +{ + throw std::runtime_error(std::string("Failed to generate petname: ") + e.what()); +} + +} // namespace multipass diff --git a/src/rustipass/rust_petname_generator.h b/src/rustipass/rust_petname_generator.h new file mode 100644 index 0000000000..a53ef3fb98 --- /dev/null +++ b/src/rustipass/rust_petname_generator.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +#include + +// Forward declare the Rust type +namespace multipass::petname +{ +struct Petname; +rust::Box new_petname(int32_t num_words, rust::Str separator); +rust::String make_name(Petname& petname); +} // namespace multipass::petname + +namespace multipass +{ + +class RustPetnameGenerator : public NameGenerator +{ +public: + explicit RustPetnameGenerator(int num_words = 2, const std::string& separator = "-"); + + std::string make_name() override; + +private: + rust::Box petname_generator; +}; + +} // namespace multipass diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ecf3a9a1f0..2252eb97eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -92,7 +92,6 @@ add_executable(multipass_tests test_new_release_monitor.cpp test_output_formatter.cpp test_persistent_settings_handler.cpp - test_petname.cpp test_private_pass_provider.cpp test_qemuimg_process_spec.cpp test_remote_settings_handler.cpp @@ -129,6 +128,7 @@ add_executable(multipass_tests test_permission_utils.cpp test_client_logger.cpp test_standard_logger.cpp + test_rust_petname_generator.cpp ) target_include_directories(multipass_tests @@ -178,6 +178,21 @@ add_test(NAME multipass_tests COMMAND multipass_tests ) +# Platform-specific cargo path handling for Rust tests +if(WIN32) + # Windows: cargo should be in PATH, but ensure we use CMD to avoid bash conflicts + set(RUST_TEST_COMMAND cmd /c "cargo test --manifest-path ${CMAKE_SOURCE_DIR}/Cargo.toml") +else() + # Unix-like: cargo should be available in PATH + set(RUST_TEST_COMMAND cargo test --manifest-path ${CMAKE_SOURCE_DIR}/Cargo.toml) +endif() + +# Register Rust tests with CTest +add_test(NAME rust_tests + COMMAND ${CMAKE_COMMAND} -E env cargo test --manifest-path ${CMAKE_SOURCE_DIR}/Cargo.toml + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} +) + foreach(BACKEND IN LISTS MULTIPASS_BACKENDS) string(TOUPPER ${BACKEND}_ENABLED DEF) target_compile_definitions(multipass_tests PRIVATE ${DEF}) diff --git a/src/petname/make_name_generator.cpp b/tests/mock_name_generator.h similarity index 72% rename from src/petname/make_name_generator.cpp rename to tests/mock_name_generator.h index 9981db9869..0044142c72 100644 --- a/src/petname/make_name_generator.cpp +++ b/tests/mock_name_generator.h @@ -13,16 +13,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * Authored by: Alberto Aguirre - * */ -#include "petname.h" +#pragma once + #include -namespace mp = multipass; +#include -mp::NameGenerator::UPtr mp::make_default_name_generator() +namespace multipass::test { - return std::make_unique(mp::Petname::NumWords::TWO, "-"); -} + +class MockNameGenerator : public NameGenerator +{ +public: + MOCK_METHOD(std::string, make_name, (), (override)); +}; + +} // namespace multipass::test diff --git a/tests/test_daemon.cpp b/tests/test_daemon.cpp index b87b8f4006..58586135ff 100644 --- a/tests/test_daemon.cpp +++ b/tests/test_daemon.cpp @@ -27,6 +27,7 @@ #include "mock_image_host.h" #include "mock_json_utils.h" #include "mock_logger.h" +#include "mock_name_generator.h" #include "mock_permission_utils.h" #include "mock_platform.h" #include "mock_server_reader_writer.h" @@ -46,13 +47,14 @@ #include #include -#include #include #include #include #include #include +#include + #include #include @@ -93,17 +95,6 @@ const qint64 default_total_bytes{16'106'127'360}; // 15G const std::string csv_header{"Alias,Instance,Command,Working directory,Context\n"}; -struct StubNameGenerator : public mp::NameGenerator -{ - explicit StubNameGenerator(std::string name) : name{std::move(name)} - { - } - std::string make_name() override - { - return name; - } - std::string name; -}; } // namespace struct Daemon : public mpt::DaemonTestFixture @@ -658,14 +649,21 @@ TEST_P(DaemonCreateLaunchTestSuite, onCreationHandlesInstanceImagePreparationFai TEST_P(DaemonCreateLaunchTestSuite, generatesNameOnCreationWhenClientDoesNotProvideOne) { - const std::string expected_name{"pied-piper-valley"}; + // Create a mock name generator that returns a known name + auto mock_name_generator = std::make_unique>(); + const std::string expected_name = "test-petname"; + + EXPECT_CALL(*mock_name_generator, make_name()).WillOnce(Return(expected_name)); + + config_builder.name_generator = std::move(mock_name_generator); - config_builder.name_generator = std::make_unique(expected_name); + use_a_mock_vm_factory(); mp::Daemon daemon{config_builder.build()}; std::stringstream stream; send_command({GetParam()}, stream); + // Now we can check for the exact name we expect EXPECT_THAT(stream.str(), HasSubstr(expected_name)); } diff --git a/tests/test_petname.cpp b/tests/test_petname.cpp deleted file mode 100644 index 13dbc05817..0000000000 --- a/tests/test_petname.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Alberto Aguirre - * - */ - -#include "common.h" - -#include - -#include -#include -#include -#include - -namespace mp = multipass; - -using namespace testing; - -namespace -{ -std::vector split(const std::string& string, const std::string& delimiter) -{ - std::regex regex(delimiter); - return {std::sregex_token_iterator{string.begin(), string.end(), regex, -1}, - std::sregex_token_iterator{}}; -} -} // namespace -TEST(Petname, generatesTheRequestedNumWords) -{ - std::string separator{"-"}; - mp::Petname gen1{mp::Petname::NumWords::ONE, separator}; - mp::Petname gen2{mp::Petname::NumWords::TWO, separator}; - mp::Petname gen3{mp::Petname::NumWords::THREE, separator}; - - auto one_word_name = gen1.make_name(); - auto tokens = split(one_word_name, separator); - EXPECT_THAT(tokens.size(), Eq(1u)); - - auto two_word_name = gen2.make_name(); - tokens = split(two_word_name, separator); - EXPECT_THAT(tokens.size(), Eq(2u)); - - auto three_word_name = gen3.make_name(); - tokens = split(three_word_name, separator); - EXPECT_THAT(tokens.size(), Eq(3u)); -} - -TEST(Petname, usesDefaultSeparator) -{ - std::string expected_separator{"-"}; - mp::Petname name_generator{mp::Petname::NumWords::THREE}; - auto name = name_generator.make_name(); - auto tokens = split(name, expected_separator); - EXPECT_THAT(tokens.size(), Eq(3u)); -} - -TEST(Petname, generatesTwoTokensByDefault) -{ - std::string separator{"-"}; - mp::Petname name_generator{separator}; - auto name = name_generator.make_name(); - auto tokens = split(name, separator); - EXPECT_THAT(tokens.size(), Eq(2u)); - - // Each token should be unique - std::unordered_set set(tokens.begin(), tokens.end()); - EXPECT_THAT(set.size(), Eq(tokens.size())); -} - -TEST(Petname, canGenerateAtLeastHundredUniqueNames) -{ - std::string separator{"-"}; - mp::Petname name_generator{mp::Petname::NumWords::THREE, separator}; - std::unordered_set name_set; - const std::size_t expected_num_unique_names{100}; - - // TODO: fixme, randomness is involved in name generation hence there's a non-zero probability - // we will fail to generate the number of expected unique names. - for (std::size_t i = 0; i < 10 * expected_num_unique_names; i++) - { - name_set.insert(name_generator.make_name()); - } - - EXPECT_THAT(name_set.size(), Ge(expected_num_unique_names)); -} diff --git a/tests/test_rust_petname_generator.cpp b/tests/test_rust_petname_generator.cpp new file mode 100644 index 0000000000..509cda7e3e --- /dev/null +++ b/tests/test_rust_petname_generator.cpp @@ -0,0 +1,166 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include +#include + +namespace mpt = multipass::test; + +using namespace testing; + +struct RustPetnameGeneratorTests : public Test +{ +}; + +// Test valid constructor with 1 word +TEST_F(RustPetnameGeneratorTests, constructorWithOneWord) +{ + EXPECT_NO_THROW({ multipass::RustPetnameGenerator generator(1, "-"); }); +} + +// Test valid constructor with 2 words (default) +TEST_F(RustPetnameGeneratorTests, constructorWithTwoWords) +{ + EXPECT_NO_THROW({ multipass::RustPetnameGenerator generator(2, "-"); }); +} + +// Test valid constructor with 3 words +TEST_F(RustPetnameGeneratorTests, constructorWithThreeWords) +{ + EXPECT_NO_THROW({ multipass::RustPetnameGenerator generator(3, "-"); }); +} + +// Test default constructor +TEST_F(RustPetnameGeneratorTests, constructorWithDefaults) +{ + EXPECT_NO_THROW({ multipass::RustPetnameGenerator generator; }); +} + +// Test invalid constructor with 0 words - should throw +TEST_F(RustPetnameGeneratorTests, constructorWithZeroWordsThrows) +{ + MP_EXPECT_THROW_THAT(multipass::RustPetnameGenerator generator(0, "-"), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Failed to create petname generator"), + HasSubstr("Invalid number of words: 0")))); +} + +// Test invalid constructor with 4 words - should throw +TEST_F(RustPetnameGeneratorTests, constructorWithFourWordsThrows) +{ + MP_EXPECT_THROW_THAT(multipass::RustPetnameGenerator generator(4, "-"), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Failed to create petname generator"), + HasSubstr("Invalid number of words: 4")))); +} + +// Test invalid constructor with negative words - should throw +TEST_F(RustPetnameGeneratorTests, constructorWithNegativeWordsThrows) +{ + MP_EXPECT_THROW_THAT(multipass::RustPetnameGenerator generator(-1, "-"), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Failed to create petname generator"), + HasSubstr("Invalid number of words: -1")))); +} + +// Test invalid constructor with large number - should throw +TEST_F(RustPetnameGeneratorTests, constructorWithLargeNumberThrows) +{ + MP_EXPECT_THROW_THAT(multipass::RustPetnameGenerator generator(100, "-"), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Failed to create petname generator"), + HasSubstr("Invalid number of words: 100")))); +} + +// Test make_name returns non-empty string with 1 word +TEST_F(RustPetnameGeneratorTests, makeNameOneWordReturnsNonEmpty) +{ + multipass::RustPetnameGenerator generator(1, "-"); + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, Not(HasSubstr("-"))); // Single word should not have separator +} + +// Test make_name returns non-empty string with 2 words +TEST_F(RustPetnameGeneratorTests, makeNameTwoWordsReturnsNonEmpty) +{ + multipass::RustPetnameGenerator generator(2, "-"); + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, HasSubstr("-")); // Two words should have separator +} + +// Test make_name returns non-empty string with 3 words +TEST_F(RustPetnameGeneratorTests, makeNameThreeWordsReturnsNonEmpty) +{ + multipass::RustPetnameGenerator generator(3, "-"); + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, HasSubstr("-")); // Three words should have separator +} + +// Test custom separator works +TEST_F(RustPetnameGeneratorTests, customSeparatorWorks) +{ + multipass::RustPetnameGenerator generator(2, "_"); + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, HasSubstr("_")); + EXPECT_THAT(name, Not(HasSubstr("-"))); +} + +// Test empty separator works +TEST_F(RustPetnameGeneratorTests, emptySeparatorWorks) +{ + multipass::RustPetnameGenerator generator(2, ""); + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, Not(HasSubstr("-"))); + EXPECT_THAT(name, Not(HasSubstr("_"))); +} + +// Test make_name can be called multiple times +TEST_F(RustPetnameGeneratorTests, makeNameCanBeCalledMultipleTimes) +{ + multipass::RustPetnameGenerator generator(2, "-"); + + auto name1 = generator.make_name(); + auto name2 = generator.make_name(); + auto name3 = generator.make_name(); + + EXPECT_FALSE(name1.empty()); + EXPECT_FALSE(name2.empty()); + EXPECT_FALSE(name3.empty()); + + // Names should be random, but we can't guarantee they're different + // Just verify they're all valid + EXPECT_THAT(name1, HasSubstr("-")); + EXPECT_THAT(name2, HasSubstr("-")); + EXPECT_THAT(name3, HasSubstr("-")); +} + +// Test that make_name with default constructor works +TEST_F(RustPetnameGeneratorTests, makeNameWithDefaultConstructorWorks) +{ + multipass::RustPetnameGenerator generator; + auto name = generator.make_name(); + EXPECT_FALSE(name.empty()); + EXPECT_THAT(name, HasSubstr("-")); // Default is 2 words with "-" separator +}