diff --git a/.github/actions/setup-vcpkg/action.yml b/.github/actions/setup-vcpkg/action.yml new file mode 100644 index 00000000..58416aea --- /dev/null +++ b/.github/actions/setup-vcpkg/action.yml @@ -0,0 +1,43 @@ +name: Setup vcpkg +description: Setup vcpkg with fallback to submodule checkout + +runs: + using: composite + steps: + - name: Resolve or checkout vcpkg + shell: pwsh + run: | + # Ensure vcpkg cache and downloads directories exist + New-Item -ItemType Directory -Path $env:VCPKG_BINARY_SOURCES.Split(',')[1] -Force | Out-Null + New-Item -ItemType Directory -Path $env:VCPKG_DOWNLOADS -Force | Out-Null + + $vcpkgRoot = $env:VCPKG_INSTALLATION_ROOT + if (-not $vcpkgRoot) { + $vcpkgCommand = Get-Command vcpkg -ErrorAction SilentlyContinue + if ($vcpkgCommand) { + $vcpkgRoot = Split-Path -Parent $vcpkgCommand.Source + } + } + + if (-not $vcpkgRoot -or -not (Test-Path $vcpkgRoot)) { + Write-Host "vcpkg not found in runner image, checking out from submodule..." + git config --file .gitmodules --get-regexp path | while-object { $_.Split()[-1] | where-object { $_ -match 'vcpkg' } } | ForEach-Object { + git submodule update --init --recursive $_ + } + $vcpkgRoot = Join-Path $env:GITHUB_WORKSPACE "vcpkg" + if (-not (Test-Path $vcpkgRoot)) { + Write-Error "vcpkg submodule not found and vcpkg executable not available" + exit 1 + } + } + + if (Test-Path $vcpkgRoot) { + Write-Host "Bootstrapping vcpkg from: $vcpkgRoot" + if ($IsWindows) { + & "$vcpkgRoot/bootstrap-vcpkg.bat" -disableMetrics + } else { + & "$vcpkgRoot/bootstrap-vcpkg.sh" -disableMetrics + } + } + + "VCPKG_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 96ee3621..1bdd8c0f 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -7,15 +7,26 @@ on: - "[0-9]+.[0-9]+.[0-9]+-*" jobs: - build-and-publish: + build: + uses: ./.github/workflows/build.yml + + publish: + needs: build runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download Build Artifacts + uses: actions/download-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: NuGet Packages + path: artifacts/pkgs + merge-multiple: true + + - name: Download Documentation Artifacts + uses: actions/download-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: - fetch-depth: 0 - filter: tree:0 + name: Documentation Artifacts + path: artifacts/docs - name: Install Nix uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 @@ -24,76 +35,22 @@ jobs: shell: &dev-shell 'nix develop -c bash -eo pipefail {0}' run: 'true' - - name: Restore dependencies - shell: *dev-shell - run: dotnet restore - - - name: Build - shell: *dev-shell - run: dotnet build -c Release --no-restore --warnaserror - - - name: Test - shell: *dev-shell - run: dotnet test -c Release --no-build --verbosity normal - - - name: Pack Packages - shell: *dev-shell - run: | - dotnet pack NetCord -c Release --no-build - dotnet pack NetCord.Services -c Release --no-build - dotnet pack Hosting/NetCord.Hosting -c Release --no-build - dotnet pack Hosting/NetCord.Hosting.Services -c Release --no-build - dotnet pack Hosting/NetCord.Hosting.AspNetCore -c Release --no-build - - - name: Setup docs environment - shell: &docs-shell 'nix develop .#docs -c bash -eo pipefail {0}' - run: 'true' - - - name: Build Documentation - shell: *docs-shell - working-directory: Documentation - run: | - npm install - npm run lint - npm run test - npm run build - - name: Publish Packages shell: *dev-shell env: KEY: ${{ secrets.NUGET_API_KEY }} run: | - dotnet nuget push NetCord/bin/Release/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate - dotnet nuget push NetCord.Services/bin/Release/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate - dotnet nuget push Hosting/NetCord.Hosting/bin/Release/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate - dotnet nuget push Hosting/NetCord.Hosting.Services/bin/Release/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate - dotnet nuget push Hosting/NetCord.Hosting.AspNetCore/bin/Release/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push artifacts/pkgs/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push artifacts/pkgs/*.snupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate - name: Deploy Documentation - uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 + uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 with: username: ${{ secrets.SSH_USERNAME }} host: ${{ secrets.SSH_HOST }} port: ${{ secrets.SSH_PORT }} key: ${{ secrets.SSH_KEY }} rm: true - source: Documentation/_site + source: artifacts/docs strip_components: 2 target: ~/NetCord/html - - - name: Upload Build Artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: Build Artifacts - path: | - NetCord/bin/Release - NetCord.Services/bin/Release - Hosting/NetCord.Hosting/bin/Release - Hosting/NetCord.Hosting.Services/bin/Release - Hosting/NetCord.Hosting.AspNetCore/bin/Release - - - name: Upload Documentation Artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: Documentation Artifacts - path: Documentation/_site diff --git a/.github/workflows/build-natives.yml b/.github/workflows/build-natives.yml new file mode 100644 index 00000000..6cada1d3 --- /dev/null +++ b/.github/workflows/build-natives.yml @@ -0,0 +1,148 @@ +name: Build NetCord.Natives + +on: + workflow_call: + workflow_dispatch: + inputs: + GenerateBinLog: + type: boolean + default: false + push: + paths: + - 'NetCord.Natives/**' + - 'Tests/NetCord.Natives.Tests/**' + - '.github/workflows/build-natives.yml' + +jobs: + build-natives: + runs-on: ${{ matrix.runs-on }} + env: + CI: true + VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite + VCPKG_DOWNLOADS: ${{ github.workspace }}/.vcpkg-downloads + VcpkgOSTarget: ${{ matrix.vcpkg-os-target }} + VcpkgPlatformTarget: ${{ matrix.vcpkg-platform-target }} + Configuration: Release + RuntimeIdentifier: ${{ matrix.rid }} + DotnetVerbose: minimal + GenerateBinLog: ${{ github.event.inputs.GenerateBinLog || inputs.GenerateBinLog || false }} + + strategy: + fail-fast: false + matrix: + include: + - rid: win-x64 + runs-on: windows-latest + vcpkg-os-target: windows + vcpkg-platform-target: x64 + - rid: win-arm64 + runs-on: windows-11-arm + vcpkg-os-target: windows + vcpkg-platform-target: arm64 + - rid: osx-x64 + runs-on: macos-15-intel + vcpkg-os-target: osx + vcpkg-platform-target: x64 + - rid: osx-arm64 + runs-on: macos-15 + vcpkg-os-target: osx + vcpkg-platform-target: arm64 + - rid: linux-x64 + runs-on: ubuntu-latest + vcpkg-os-target: linux + vcpkg-platform-target: x64 + - rid: linux-arm64 + runs-on: ubuntu-24.04-arm + vcpkg-os-target: linux + vcpkg-platform-target: arm64 + + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + filter: tree:0 + + - name: Restore vcpkg cache + uses: actions/cache/restore@v5.0.5 + with: + path: | + .vcpkg-cache + .vcpkg-downloads + key: vcpkg-${{ runner.os }}-${{ matrix.rid }}-${{ hashFiles('NetCord.Natives/vcpkg.json', 'NetCord.Natives/natives-ports/**') }} + restore-keys: | + vcpkg-${{ runner.os }}-${{ matrix.rid }}- + vcpkg-${{ runner.os }}- + + - name: Setup vcpkg + uses: ./.github/actions/setup-vcpkg + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v5.2.0 + with: + global-json-file: global.json + + - name: Install native build tools (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + brew update + for pkg in autoconf automake libtool; do + brew list $pkg &>/dev/null || brew install $pkg + done + + - name: Restore + shell: pwsh + run: | + dotnet restore Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj -v ${{ env.DotnetVerbose }} ${{ env.GenerateBinLog == 'true' && '-bl:restore.binlog' || '' }} + + - name: Build natives projects for ${{ matrix.rid }} + shell: pwsh + run: > # Building tests will build dependencies including the native libraries + dotnet build Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj --no-restore -v ${{ env.DotnetVerbose }} + ${{ env.GenerateBinLog == 'true' && '-bl:build.binlog' || '' }} + -p:AppendNativeAotAppProps="RuntimeIdentifier=${{ env.RuntimeIdentifier }}" + -p:GeneratePackageOnBuild=false -p:CI=false + + - name: Test natives outputs for ${{ matrix.rid }} + shell: pwsh + run: > + dotnet test Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj --no-restore --no-build -v ${{ env.DotnetVerbose }} + ${{ env.GenerateBinLog == 'true' && '-bl:test.binlog' || '' }} + --logger "console;verbosity=detailed" + --logger GitHubActions + + - name: Pack NuGet Package for ${{ matrix.rid }} + if: ${{ always() }} + shell: pwsh + run: > + dotnet pack NetCord.Natives/NetCord.Natives.csproj --no-restore --no-build -v ${{ env.DotnetVerbose }} + ${{ env.GenerateBinLog == 'true' && '-bl:pack.binlog' || '' }} + -o NetCord.Natives/bin/${{ matrix.rid }} + -p:CI=false + + - name: Upload Binary Logs + if: ${{ env.GenerateBinLog == 'true' && always() }} + uses: actions/upload-artifact@v4.6.0 + with: + name: ${{ matrix.rid }}-binlogs + path: | + *.binlog + + - name: Upload NuGet Package Artifact + if: ${{ always() }} + uses: actions/upload-artifact@v7.0.1 + with: + name: NetCord.Natives.${{ matrix.rid }}.packages + path: | + NetCord.Natives/bin/${{ matrix.rid }}/*.nupkg + NetCord.Natives/bin/${{ matrix.rid }}/*.snupkg + + - name: Save vcpkg cache + if: ${{ always() }} + uses: actions/cache/save@v5.0.5 + with: + path: | + .vcpkg-cache + .vcpkg-downloads + key: vcpkg-${{ runner.os }}-${{ matrix.rid }}-${{ hashFiles('NetCord.Natives/vcpkg.json', 'NetCord.Natives/natives-ports/**') }} diff --git a/.github/workflows/build-publish-natives.yml b/.github/workflows/build-publish-natives.yml new file mode 100644 index 00000000..8850fb49 --- /dev/null +++ b/.github/workflows/build-publish-natives.yml @@ -0,0 +1,35 @@ +name: Build & Publish NetCord.Natives + +on: + workflow_dispatch: + push: + paths: + - 'NetCord.Natives/**' + - '.github/workflows/build-natives.yml' + - '.github/workflows/publish-natives.yml' + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-*" + +jobs: + build-natives: + uses: ./.github/workflows/build-natives.yml + + publish: + needs: [build-natives] + runs-on: ubuntu-latest + + steps: + - name: Download All NuGet Package Artifacts + uses: actions/download-artifact@v8.0.1 + with: + path: artifacts/pkgs + pattern: NetCord.Natives.*.packages + merge-multiple: true + + - name: Publish packages + env: + KEY: ${{ secrets.NUGET_API_KEY }} + run: | + dotnet nuget push artifacts/pkgs/*.nupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push artifacts/pkgs/*.snupkg -k $KEY -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 350387e0..175eebaf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: Build on: + workflow_call: pull_request: branches: - stable @@ -33,24 +34,28 @@ jobs: - name: Restore dependencies shell: *dev-shell - run: dotnet restore + run: | + dotnet restore NetCord.slnx + dotnet restore NetCord.Natives.slnx -p:VcpkgRoot=$VCPKG_INSTALLATION_ROOT -p:VcpkgEnabled=false - name: Build shell: *dev-shell - run: dotnet build -c Release --no-restore --warnaserror + run: | + dotnet build NetCord.slnx -c Release --no-restore --warnaserror + dotnet build NetCord.Natives.slnx -c Release --no-restore --warnaserror -p:VcpkgRoot=$VCPKG_INSTALLATION_ROOT -p:VcpkgEnabled=false - name: Test shell: *dev-shell - run: dotnet test -c Release --no-build --verbosity normal + run: dotnet test NetCord.slnx -c Release --no-build --verbosity normal - name: Pack Packages shell: *dev-shell run: | - dotnet pack NetCord -c Release --no-build - dotnet pack NetCord.Services -c Release --no-build - dotnet pack Hosting/NetCord.Hosting -c Release --no-build - dotnet pack Hosting/NetCord.Hosting.Services -c Release --no-build - dotnet pack Hosting/NetCord.Hosting.AspNetCore -c Release --no-build + dotnet pack NetCord -c Release --no-build -o pkgs/ + dotnet pack NetCord.Services -c Release --no-build -o pkgs/ + dotnet pack Hosting/NetCord.Hosting -c Release --no-build -o pkgs/ + dotnet pack Hosting/NetCord.Hosting.Services -c Release --no-build -o pkgs/ + dotnet pack Hosting/NetCord.Hosting.AspNetCore -c Release --no-build -o pkgs/ - name: Setup docs environment shell: &docs-shell 'nix develop .#docs -c bash -eo pipefail {0}' @@ -65,16 +70,13 @@ jobs: npm run test npm run build - - name: Upload Build Artifacts + - name: Upload NuGet Package Artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Build Artifacts + name: NuGet Packages path: | - NetCord/bin/Release - NetCord.Services/bin/Release - Hosting/NetCord.Hosting/bin/Release - Hosting/NetCord.Hosting.Services/bin/Release - Hosting/NetCord.Hosting.AspNetCore/bin/Release + pkgs/*.nupkg + pkgs/*.snupkg - name: Upload Documentation Artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c33f058f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "NetCord.Natives/vcpkg"] + path = NetCord.Natives/vcpkg + url = https://github.com/microsoft/vcpkg.git diff --git a/Directory.Packages.props b/Directory.Packages.props index 1a960262..ea6b070a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/Documentation/guides/basic-concepts/installing-native-dependencies.md b/Documentation/guides/basic-concepts/installing-native-dependencies.md index 9c9fe2d4..061d36f0 100644 --- a/Documentation/guides/basic-concepts/installing-native-dependencies.md +++ b/Documentation/guides/basic-concepts/installing-native-dependencies.md @@ -1,91 +1,109 @@ # Installing Native Dependencies -This is a hidden guide that is not visible in the guides index! If you are reading this, you are probably looking for information on how to install native dependencies for HTTP interactions or voice. +NetCord relies on several native libraries for high-performance audio processing and encryption. NetCord provides prebuilt native binaries via NuGet packages, which is the recommended way to manage these dependencies. -## HTTP Interactions +## Native Dependencies Context -For HTTP interactions, [Libsodium](https://doc.libsodium.org/installation) is required. +- **Libdave**: Essential for voice connection interactions. +- **Libsodium**: Used for encryption. While NetCord defaults to native AES-GCM (which does not require Libsodium), Libsodium is a highly recommended production dependency. It acts as a fallback for the XChaCha20-Poly1305 encryption mode, ensuring your bot remains compatible if Discord switches to this mode for your connection. +- **Opus**: A versatile audio codec required for any classes in NetCord prefixed with `Opus` (e.g., audio encoding/decoding). +- **Zstd**: Used for efficient payload compression. -## Voice +## NuGet Packages (Recommended) -For voice: -- [Libdave](https://github.com/discord/libdave) is required. -- [Libsodium](https://doc.libsodium.org/installation) **is not generally** required, but it **is highly recommended for production bots**. It is caused by the fact that generally @NetCord.Gateway.Voice.Encryption.Aes256GcmRtpSizeEncryption, which does not require Libsodium, is supported by Discord and is used by default. However, there is a small chance that Discord will not support this encryption mode for your connection. In this case, @NetCord.Gateway.Voice.Encryption.XChaCha20Poly1305RtpSizeEncryption, which does require Libsodium, is used by default. -- [Opus](https://opus-codec.org/downloads) is only required when you are using `Opus` prefixed classes. +NetCord distributes these dependencies as per-RID (Runtime Identifier) packages. This model automates binary resolution, ensuring the correct libraries are provided for your target platform. -## Installation +### Supported Platforms +Packages are available for: -### [Dynamic Linking](#tab/dynamic) +| Platform | RID | NativeAOT Support |
Size
![Storage Requirement](https://img.shields.io/badge/Runtime-Package-blue)
| +|----------|-----|-------------------------|-----------| +| Windows x64 | `win-x64` | ✓ (Static CRT /MT) | ![Windows x64](https://img.shields.io/badge/6_MB-42_MB_(176_MB)-blue) | +| Windows ARM64 | `win-arm64` | ✓ (Static CRT /MT) | ![Windows ARM64](https://img.shields.io/badge/6_MB-41_MB_(183_MB)-blue) | +| Linux x64 | `linux-x64` | ✓ (Dynamic CRT) | ![Linux x64](https://img.shields.io/badge/10_MB-9_MB_(28_MB)-blue) | +| Linux ARM64 | `linux-arm64` | ✓ (Dynamic CRT) | ![Linux ARM64](https://img.shields.io/badge/10_MB-8_MB_(26_MB)-blue) | +| ⓘ macOS x64 | `osx-x64` | ✓ (Dynamic CRT) | ![macOS x64](https://img.shields.io/badge/15_MB-11_MB_(28_MB)-blue) | +| ⓘ macOS ARM64 | `osx-arm64` | ✓ (Dynamic CRT) | ![macOS ARM64](https://img.shields.io/badge/13_MB-10_MB_(25_MB)-blue) | -For dynamic linking, you can install Libsodium for the most popular platforms by referencing the [official Libsodium NuGet package](https://www.nuget.org/packages/libsodium). +ⓘ Currently, the macOS packages are built and published, but functions verification tests are skipped due to limitations with `dyld`. +They are still expected to work correctly, but we recommend testing on macOS before production use. -### Manual or System-wide Installation +## Dynamic Linking +For standard .NET applications, you can simply reference the relevant packages for your target platforms. The runtimes will be correctly copied and loaded across platforms. -#### Windows - -For dynamic linking on Windows, you need to use the dynamic link libraries (`libdave`, `libsodium`, and/or `opus`). Here's how to set it up: -- Download or build the dynamic link libraries (`libdave`, `libsodium`, and/or `opus`) compatible with your development environment. - -- Place these files in the runtime directory of your application. This is the folder where your application's executable is located. - -#### Linux and MacOS - -Dynamic linking on Linux and MacOS involves using shared libraries (`libdave`, `libsodium`, and/or `opus`). You can install them using your system's package manager if available or follow these steps to install them manually: -- Download or build the shared libraries (`libdave`, `libsodium`, and/or `opus`) that are compatible with your development environment. - -- Place these files in the runtime directory of your application, which is the folder where your application's executable is located. - -### [Static Linking](#tab/static) - -> [!NOTE] -> Static linking requires [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot) compilation. - -#### Windows - -When using static linking on Windows, you need to link to the static libraries (`libdave`, `libsodium`, and/or `opus`). Here are the steps to set up static linking in your application: -- Download or build the static libraries (`libdave`, `libsodium`, and/or `opus`) compatible with your development environment. -- Link these libraries in your project settings. Ensure that you specify the correct paths to these libraries: - ```xml - - - - - - - - - - - ``` +```xml + + + + + +``` -You don't need to place any of these files in the runtime directory, as static linking embeds the library code directly into your application, eliminating the need for separate files. +## Usage with NativeAOT (Static Linking) -#### Linux and MacOS +When using [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot), static libraries are automatically linked from the NuGet package, embedding the necessary native code directly into your executable. -Static linking on Linux and MacOS involves linking your application with the static libraries (`libdave`, `libsodium`, and/or `opus`). You can install them using your system's package manager if available or download or build the static libraries (`libdave`, `libsodium`, and/or `opus`) compatible with your development environment manually. +### Requirement for DirectPInvoke +When targeting NativeAOT, you must ensure that all used native libraries are explicitly registered using `` in your project file to ensure they are properly included during the AOT compilation process: -Link these libraries in your project settings. Make sure you specify the correct paths to these libraries: ```xml - - - - - + ``` -Since you're statically linking, you won't need to place any of these files in the runtime directory. The necessary code from the libraries will be included directly in your application. +### Excluding Bundled Dependencies +If you have provided your own native binaries and need to exclude the ones bundled in the NetCord NuGet package to avoid conflicts, use the `NetCordExcludeNatives` property. + +```xml + + opus + +``` -*** +## Custom Manual/System-wide Installation -#### Installation External Links +If you are developing for an unsupported platform or require a custom-built native binary, you may choose to handle native dependencies manually. + +1. **Obtain Binaries**: Use your system's package manager (e.g., `apt`, `brew`, `dnf`) if available, or download the binaries from the official sources listed below. +2. **Placement**: Ensure the native libraries are available to your application at runtime. On Windows, place `.dll` files in your application's output directory. On Unix-like systems, ensure shared libraries are in your `LD_LIBRARY_PATH` or system standard paths. + +### Installation External Links | Library | Installation Link | |-----------|---------------------------------------------| | Libdave | https://github.com/discord/libdave/releases | | Libsodium | https://doc.libsodium.org/installation | | Opus | https://opus-codec.org/downloads | +| Zstd | https://github.com/facebook/zstd/releases | + +## Troubleshooting +- If you encounter issues with native dependencies, ensure that the correct versions are installed and that they are accessible to your application. +- Use tools like `ldd` (Linux) or `Dependency Walker` (Windows) to verify that your application is correctly linking against the native libraries. + +--- + +## Extra Notes on Native Dependencies + +The native packaging model is designed to make managing dependencies as transparent and reliable as possible. + +### What You Get +* **Ready-to-use binaries**: We provide prebuilt runtime binaries for standard .NET and static libraries for NativeAOT. +* **Automatic Configuration**: MSBuild files are included in the packages to automatically handle paths and linking requirements, so you don't have to manually configure build settings. + +### Built-in Compliance +All necessary licenses and copyright notices from the original native project sources are bundled automatically within each package under the `licenses/` directory. + +| Library | License | Remarks | +| :--- | :--- | :--- | +| **Libdave** | MIT | Discord voice communication | +| **Libsodium** | ISC | Used for encryption | +| **OpenSSL** | Apache 2.0 | Cryptographic operations | +| **Opus** | BSD-3-Clause | Audio codec | +| **Zstd** | BSD-3-Clause | Compression | + +### How It’s Built +* **Reproducible builds**: Dependencies are strictly pinned using [vcpkg](https://github.com/microsoft/vcpkg) baselines. This means every build result should be identical, regardless of who or what runs the build. diff --git a/NetCord.Natives.slnx b/NetCord.Natives.slnx new file mode 100644 index 00000000..8e306060 --- /dev/null +++ b/NetCord.Natives.slnx @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NetCord.Natives/.gitignore b/NetCord.Natives/.gitignore new file mode 100644 index 00000000..07db887c --- /dev/null +++ b/NetCord.Natives/.gitignore @@ -0,0 +1 @@ +*.tlog diff --git a/NetCord.Natives/NetCord.Natives.csproj b/NetCord.Natives/NetCord.Natives.csproj new file mode 100644 index 00000000..a13b761d --- /dev/null +++ b/NetCord.Natives/NetCord.Natives.csproj @@ -0,0 +1,224 @@ + + + + Pre-built native libraries for NetCord, $(Description) + false + false + false + true + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..')) + + + + + + + vcpkg\%(Filename)%(Extension) + + + + + + debug + + bin + + $(VCPKG_ROOT) + $(VcpkgRoot)\scripts\buildsystems\msbuild\vcpkg + + Release + true + + windows + osx + linux + + $(VcpkgOSTarget) + win + + + false + + x64 + $(Platform) + + $(VcpkgArtifactsRoot)\$(VcpkgBuildOS)-$(VcpkgPlatformTarget)\natives + $(VcpkgInstalledDirBase) + $(VcpkgInstalledDir)-static + + $(VcpkgInstalledDirBase)-static\$(VcpkgPlatformTarget)-$(VcpkgOSTarget) + $(VcpkgOutputPath_Static)-static + <_VcpkgStatusFile>$(VcpkgInstalledDir)\vcpkg\status + + + + + + + + + + + + <_VcpkgRelatedFile Include="native-ports\**" /> + + + + + <_StaticFullPath Include="$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\$(VcpkgOutputPath_Static)'))" /> + + + + + + + + + + + + + + + $(VcpkgPlatformTarget)-$(VcpkgOSTarget) + $(VcpkgTriplet)-dynamic + + <_ZVcpkgMSBuildStampFile>$(_ZVcpkgInstalledDir).msbuildstamp-$(VcpkgTriplet).$(_ZVcpkgHostTripletSuffix)stamp + + + $(VcpkgInstalledDir)\$(VcpkgTriplet) + + + + <_LibArchs Include="x64;arm64"> + %(NativeLibSet.Identity) + + <_BuiltRid Include="win-%(_LibArchs.Identity)"> + %(_LibArchs.Identity)-windows\bin\%(_LibArchs.LibName) + %(_LibArchs.Identity)-windows-static\lib\%(_LibArchs.LibName) + dll + + <_BuiltRid Include="linux-%(_LibArchs.Identity)"> + %(_LibArchs.Identity)-linux-dynamic\lib\lib%(_LibArchs.LibName) + %(_LibArchs.Identity)-linux\lib\lib%(_LibArchs.LibName) + so + + <_BuiltRid Include="osx-%(_LibArchs.Identity)"> + %(_LibArchs.Identity)-osx-dynamic\lib\lib%(_LibArchs.LibName) + %(_LibArchs.Identity)-osx\lib\lib%(_LibArchs.LibName) + dylib + + <_BuiltRid Remove="%(Identity)" Condition="'$(CI)' != 'true' and '%(Identity)' != '$(VcpkgBuildOS)-$(VcpkgPlatformTarget)'" /> + + + + + + + + + + + + + + + + + + + @(StatusLines, ' ') + + + + + <_VcpkgPkg Include="@(NativeLibLic)"> + + $([System.Text.RegularExpressions.Regex]::Match('$(StatusText)', 'Package: %(Identity)\n(?:.*\n)*?Version: (.*?)\n').Groups[1].Value) + + + + + + $(PackageDescription) (Includes: @(_VcpkgPkg->'%(Identity) v%(Version)', ', ')) + + + + + + + + + + <_CopyOutput Include="%(NativesRuntime.FullPath)"> + %(NativesRuntime.Filename)%(NativesRuntime.Extension) + PreserveNewest + + + + + + + + + + <_NativeLicenseSource Include="$(VcpkgOutputPath_Static)\share\%(NativeLibLic.Identity)\copyright" + Condition="Exists('$(VcpkgOutputPath_Static)\share\%(NativeLibLic.Identity)\copyright')"> + $(VcpkgOutputPath_Static)\licenses\%(NativeLibLic.Identity)_LICENSE.txt + + + + + + + $(PackageId).$(VcpkgBuildOS)-$(VcpkgPlatformTarget) + + + + + runtimes\%(_BuiltRid.Identity)\native\ + + + staticlibs\%(_BuiltRid.Identity)\ + + + + true + + + + true + licenses\ + + + <_BuildFiles Include="$(MSBuildProjectName).props;$(MSBuildProjectName).targets"> + true + + + + build\$(PackageId)%(Extension) + + + buildTransitive\$(PackageId)%(Extension) + + + + + + + + + + diff --git a/NetCord.Natives/NetCord.Natives.local.targets b/NetCord.Natives/NetCord.Natives.local.targets new file mode 100644 index 00000000..06e3c54d --- /dev/null +++ b/NetCord.Natives/NetCord.Natives.local.targets @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + %(_NetCordStaticDir.Identity)\lib + $([System.IO.Path]::GetDirectoryName('%(_NetCordRuntimesDir.FullPath)')) + + + + + diff --git a/NetCord.Natives/NetCord.Natives.props b/NetCord.Natives/NetCord.Natives.props new file mode 100644 index 00000000..c1b6ee94 --- /dev/null +++ b/NetCord.Natives/NetCord.Natives.props @@ -0,0 +1,16 @@ + + + + + + + $(MSBuildThisFileDirectory)..\runtimes\$(RuntimeIdentifier) + $(MSBuildThisFileDirectory)..\staticlibs\$(RuntimeIdentifier) + + + diff --git a/NetCord.Natives/NetCord.Natives.targets b/NetCord.Natives/NetCord.Natives.targets new file mode 100644 index 00000000..bdb1a6f6 --- /dev/null +++ b/NetCord.Natives/NetCord.Natives.targets @@ -0,0 +1,63 @@ + + + + + + + + <_NetCordStaticLink Include="libsodium;sodium" + Condition="'%(DirectPInvoke.Identity)' == 'libsodium'" /> + <_NetCordStaticLink Include="libdave;dave" + Condition="'%(DirectPInvoke.Identity)' == 'libdave'" /> + + + <_NetCordStaticLink Include="mlspp;hpke;bytes;tls_syntax" + Condition="'%(DirectPInvoke.Identity)' == 'libdave'" /> + + + <_NetCordStaticLink Include="opus;zstd" + Condition="'%(DirectPInvoke.Identity)' == 'opus' or '%(DirectPInvoke.Identity)' == 'zstd'" /> + + + <_NetCordStaticLink Include="libcrypto;crypto" + Condition="'%(DirectPInvoke.Identity)' == 'libdave'" /> + + + + + + + + + + true + + + + + + + <_NetCordExcluded Include="$(NetCordExcludeNatives)" + Condition="'$(NetCordExcludeNatives)' != ''" /> + <_NetCordExcluded Include="libcrypto" + Condition="$(NetCordExcludeNatives.Contains('libdave'))" /> + + <_NetCordExcluded Include="@(_NetCordStaticLink)" + Condition="'$(PublishAot)' == 'true' and '%(_NetCordStaticLink.Identity)' != ''"/> + + <_RunExclude Include="$(NetCordNativesRuntimesDir)\**\native\%(_NetCordExcluded.Identity)*.*" + Condition="'@(_NetCordExcluded.Identity)' != '' and '$(NetCordNativesRuntimesDir)' != ''" /> + <_RunExclude Include="$(NetCordNativesRuntimesDir)\**\native\lib%(_NetCordExcluded.Identity)*.*" + Condition="'@(_NetCordExcluded.Identity)' != '' and '$(NetCordNativesRuntimesDir)' != ''" /> + + + + + + + diff --git a/NetCord.Natives/natives-ports/libdave/fix-msvc-builtin-add-overflow.patch b/NetCord.Natives/natives-ports/libdave/fix-msvc-builtin-add-overflow.patch new file mode 100644 index 00000000..f6d3360a --- /dev/null +++ b/NetCord.Natives/natives-ports/libdave/fix-msvc-builtin-add-overflow.patch @@ -0,0 +1,24 @@ +diff --git a/cpp/src/frame_processors.cpp b/cpp/src/frame_processors.cpp +index 37c8d70..5f0460c 100644 +--- a/cpp/src/frame_processors.cpp ++++ b/cpp/src/frame_processors.cpp +@@ -13,6 +13,9 @@ + + #if defined(_MSC_VER) + #include ++#if defined(_M_ARM64) ++#include ++#endif + #endif + + namespace discord { +@@ -25,6 +28,9 @@ std::pair OverflowAdd(size_t a, size_t b) + bool didOverflow = _addcarry_u64(0, a, b, &res); + #elif defined(_MSC_VER) && defined(_M_IX86) + bool didOverflow = _addcarry_u32(0, a, b, &res); ++#elif defined(_MSC_VER) && defined(_M_ARM64) ++ res = a + b; ++ bool didOverflow = res < a; + #else + bool didOverflow = __builtin_add_overflow(a, b, &res); + #endif diff --git a/NetCord.Natives/natives-ports/libdave/portfile.cmake b/NetCord.Natives/natives-ports/libdave/portfile.cmake new file mode 100644 index 00000000..4f7517e7 --- /dev/null +++ b/NetCord.Natives/natives-ports/libdave/portfile.cmake @@ -0,0 +1,32 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO discord/libdave + REF "${VERSION}" + SHA512 78b4e5b8ddc6397775d403465e0da770ec7905d7913546b3aec161baf4478443e554f0ae7bd012af8bfd308639be2601d46da22c02aff2b756ff91878f1fc843 + HEAD_REF main + PATCHES fix-msvc-builtin-add-overflow.patch +) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}/cpp" + OPTIONS + -DTESTING=OFF + -DBUILD_TESTING=OFF + MAYBE_UNUSED_VARIABLES + BUILD_TESTING +) + +vcpkg_cmake_build() + +vcpkg_cmake_install() +vcpkg_copy_pdbs() + +file(INSTALL + "${SOURCE_PATH}/LICENSE" + DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" + RENAME copyright +) + +# Remove redundant debug directories to comply with vcpkg policy +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") diff --git a/NetCord.Natives/natives-ports/libdave/vcpkg.json b/NetCord.Natives/natives-ports/libdave/vcpkg.json new file mode 100644 index 00000000..9f74f4d3 --- /dev/null +++ b/NetCord.Natives/natives-ports/libdave/vcpkg.json @@ -0,0 +1,18 @@ +{ + "name": "libdave", + "version-string": "52cd56dc550f447fb354b3a06c9e2d2e2a4309c6", + "license": "MIT", + "dependencies": [ + "openssl", + "mlspp", + "nlohmann-json", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} \ No newline at end of file diff --git a/NetCord.Natives/natives-ports/mlspp/portfile.cmake b/NetCord.Natives/natives-ports/mlspp/portfile.cmake new file mode 100644 index 00000000..2ca219e3 --- /dev/null +++ b/NetCord.Natives/natives-ports/mlspp/portfile.cmake @@ -0,0 +1,33 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO cisco/mlspp + REF "${VERSION}" + SHA512 5d37631e2c47daae1133ef074e60cc09ca2d395f9e11c416f829060e374051cf219d2d7fe98dae49d1d045292e07d6a09f4814a5f16e6cc05e67e7cd96f146c4 +) + +if(VCPKG_TARGET_IS_OSX AND EXISTS "/usr/local/include/openssl/") + set(VCPKG_INCLUDE_OVERRIDE "-DCMAKE_CXX_FLAGS=-I${CURRENT_INSTALLED_DIR}/include") +endif() + +set(VCPKG_LIBRARY_LINKAGE static) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + ${VCPKG_INCLUDE_OVERRIDE} + -DDISABLE_GREASE=ON + -DTESTING=OFF + -DBUILD_TESTING=OFF + -DMLS_CXX_NAMESPACE="mlspp" + MAYBE_UNUSED_VARIABLES + BUILD_TESTING +) + +vcpkg_cmake_install() +vcpkg_copy_pdbs() + +vcpkg_cmake_config_fixup(PACKAGE_NAME "MLSPP" CONFIG_PATH "share/MLSPP") + +# Remove redundant debug directories to comply with vcpkg policy +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") diff --git a/NetCord.Natives/natives-ports/mlspp/vcpkg.json b/NetCord.Natives/natives-ports/mlspp/vcpkg.json new file mode 100644 index 00000000..fca86978 --- /dev/null +++ b/NetCord.Natives/natives-ports/mlspp/vcpkg.json @@ -0,0 +1,17 @@ +{ + "name": "mlspp", + "version-string": "1cc50a124a3bc4e143a787ec934280dc70c1034d", + "description": "Cisco MLS C++ library", + "dependencies": [ + "openssl", + "nlohmann-json", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/NetCord.Natives/vcpkg b/NetCord.Natives/vcpkg new file mode 160000 index 00000000..89dd0f4d --- /dev/null +++ b/NetCord.Natives/vcpkg @@ -0,0 +1 @@ +Subproject commit 89dd0f4d241136b843fb55813b2f0fa6448c204d diff --git a/NetCord.Natives/vcpkg-non-windows.targets b/NetCord.Natives/vcpkg-non-windows.targets new file mode 100644 index 00000000..134e697a --- /dev/null +++ b/NetCord.Natives/vcpkg-non-windows.targets @@ -0,0 +1,48 @@ + + + + <_ZVcpkgInstallManifestDependenciesInputs Include="$(_ZVcpkgManifestFileLocation)"/> + <_ZVcpkgInstallManifestDependenciesInputs Include="$(_ZVcpkgConfigurationFileLocation)" + Condition="Exists('$(_ZVcpkgConfigurationFileLocation)')"/> + + + + + + + <_ZVcpkgTLogFileLocation>$(TLogLocation)VcpkgInstallManifest$(VcpkgTriplet).$(_ZVcpkgHostTripletSuffix)read.1u.tlog + + <_ZVcpkgExecutable>$(_ZVcpkgRoot)vcpkg + + + + + + + + + + + + + + + + + + + diff --git a/NetCord.Natives/vcpkg.json b/NetCord.Natives/vcpkg.json new file mode 100644 index 00000000..a3af5a40 --- /dev/null +++ b/NetCord.Natives/vcpkg.json @@ -0,0 +1,20 @@ +{ + "dependencies": [ + "libdave", + "opus", + "libsodium", + "zstd" + ], + "vcpkg-configuration": { + "overlay-ports": [ + "natives-ports" + ] + }, + "builtin-baseline": "d07689ef165f033de5c0710e4f67c193a85373e1", + "overrides": [ + { + "name": "openssl", + "version": "3.0.7" + } + ] +} diff --git a/NetCord.slnx b/NetCord.slnx index 23417afd..30dd9aff 100644 --- a/NetCord.slnx +++ b/NetCord.slnx @@ -84,6 +84,7 @@ + diff --git a/README.md b/README.md index b3d29496..cdbb638f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ You can install NetCord packages via NuGet package manager: | **[NetCord.Hosting](https://www.nuget.org/packages/NetCord.Hosting)** | Provides .NET Generic Host extensions for the NetCord package. | | **[NetCord.Hosting.Services](https://www.nuget.org/packages/NetCord.Hosting.Services)** | Provides .NET Generic Host extensions for the NetCord.Services package. | | **[NetCord.Hosting.AspNetCore](https://www.nuget.org/packages/NetCord.Hosting.AspNetCore)** | Provides ASP.NET Core extensions for seamless handling of HTTP events. | +| **[NetCord.Natives](https://www.nuget.org/packages/NetCord.Natives)** | Provides pre-built native binaries dependencies. | ## 2. 🚀 Showcase @@ -116,6 +117,8 @@ NetCord's goal is to allow .NET developers to create fully customizable Discord This repository is released under the [MIT License](LICENSE.md). +The use of NetCord.Natives may subject to each libraries licenses. + ## 9. 🛠️ Development ### Versioning diff --git a/Resources/NuGet/README.md b/Resources/NuGet/README.md index 89a1d414..d7babe22 100644 --- a/Resources/NuGet/README.md +++ b/Resources/NuGet/README.md @@ -25,6 +25,7 @@ You can install NetCord packages via NuGet package manager: | **[NetCord.Hosting](https://www.nuget.org/packages/NetCord.Hosting)** | Provides .NET Generic Host extensions for the NetCord package. | | **[NetCord.Hosting.Services](https://www.nuget.org/packages/NetCord.Hosting.Services)** | Provides .NET Generic Host extensions for the NetCord.Services package. | | **[NetCord.Hosting.AspNetCore](https://www.nuget.org/packages/NetCord.Hosting.AspNetCore)** | Provides ASP.NET Core extensions for seamless handling of HTTP events. | +| **[NetCord.Natives](https://www.nuget.org/packages/NetCord.Natives)** | Provides pre-built native binaries dependencies. | ## 2. 🚀 Showcase @@ -95,6 +96,8 @@ NetCord's goal is to allow .NET developers to create fully customizable Discord This repository is released under the [MIT License](https://github.com/NetCordDev/NetCord/blob/alpha/LICENSE.md). +The use of NetCord.Natives may subject to each libraries licenses. + ## 9. 🛠️ Development ### Versioning diff --git a/Tests/NetCord.Natives.Tests/AssemblyInfo.cs b/Tests/NetCord.Natives.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..ae411c7a --- /dev/null +++ b/Tests/NetCord.Natives.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/NativeAotApp.csproj b/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/NativeAotApp.csproj new file mode 100644 index 00000000..d06d6424 --- /dev/null +++ b/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/NativeAotApp.csproj @@ -0,0 +1,31 @@ + + + + Exe + net10.0 + enable + enable + + true + true + true + true + false + $(NoWarn);IL2075;IL2026 + + ..\..\..\..\NetCord.Natives + + + + + + + + + + + + + + + diff --git a/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/Program.cs b/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/Program.cs new file mode 100644 index 00000000..6581e697 --- /dev/null +++ b/Tests/NetCord.Natives.Tests/Assets/NativeAotApp/Program.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; +using NetCord.Natives.Tests; + +Console.WriteLine("NetCord native ahead of time publish app."); + +Console.WriteLine($"Dave Max Supported Protocol Version: {NativeProbes.DaveMaxSupportedProtocolVersion()}"); +Console.WriteLine($"Sodium Init: {NativeProbes.SodiumInit()}. 0 = OK"); +Console.WriteLine($"Opus Version String: {Marshal.PtrToStringAnsi(NativeProbes.OpusGetVersionString())}"); +Console.WriteLine($"Zstd Version Number: {NativeProbes.ZstdVersionNumber()}"); diff --git a/Tests/NetCord.Natives.Tests/NativeProbes.cs b/Tests/NetCord.Natives.Tests/NativeProbes.cs new file mode 100644 index 00000000..4a03b6d2 --- /dev/null +++ b/Tests/NetCord.Natives.Tests/NativeProbes.cs @@ -0,0 +1,94 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace NetCord.Natives.Tests; + +public static partial class NativeProbes +{ + [LibraryImport("libdave", EntryPoint = "daveMaxSupportedProtocolVersion")] + internal static partial uint DaveMaxSupportedProtocolVersion(); + + [LibraryImport("libsodium", EntryPoint = "sodium_init")] + internal static partial int SodiumInit(); + + [LibraryImport("opus", EntryPoint = "opus_get_version_string")] + internal static partial IntPtr OpusGetVersionString(); + + [LibraryImport("zstd", EntryPoint = "ZSTD_versionNumber")] + internal static partial uint ZstdVersionNumber(); + + internal static IReadOnlyList GetMissingLibraryImports(string libName, string className, Assembly assemblyWithImports) + { + var typeWithImports = assemblyWithImports.GetType(className, true); + var libHandle = GetLoadedNativeModuleHandle(libName); + var missingExports = new List(); + + var methods = typeWithImports!.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .Select(m => new + { + MethodName = m.Name, + Attr = m.GetCustomAttribute(), + }) + .Where(x => x.Attr != null) + .ToArray(); + + if (methods.Length == 0) + throw new InvalidOperationException($"No methods with [LibraryImport] found in '{className}'."); + + foreach (var item in methods) + { + // Use EntryPoint if defined, otherwise fallback to method name. + string exportName = item.Attr!.EntryPoint ?? item.MethodName; + + if (!NativeLibrary.TryGetExport(libHandle, exportName, out _)) + { + missingExports.Add(exportName); + } + } + + return missingExports; + } + + /* + * This method is not working on macOS due to System.Diagnostics.ProcessModule + * not listing native libraries loaded by the runtime's dynamic library loader (dyld). + * This is likely because dyld does not expose these libraries to the process in the + * same way that LoadLibrary on Windows or dlopen on Linux does, and as a result they + * do not appear in the list of modules for the process. This is a known limitation + * when trying to inspect loaded native libraries on macOS using .NET, and may require + * platform-specific workarounds or tools to verify the presence of native libraries + * on that platform. + */ + internal static IntPtr GetLoadedNativeModuleHandle(string libName) + { + _ = libName switch + { + "libdave" => (object)DaveMaxSupportedProtocolVersion(), + "libsodium" => (object)SodiumInit(), + "opus" => (object)OpusGetVersionString(), + "zstd" => (object)ZstdVersionNumber(), + _ => throw new InvalidOperationException($"Unknown library name '{libName}' provided to test."), + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + throw new PlatformNotSupportedException("Retrieving loaded native module handles is not supported on macOS due to platform limitations."); + + var module = System.Diagnostics.Process.GetCurrentProcess().Modules + .Cast() + .FirstOrDefault(m => + { + var moduleName = Path.GetFileName(m.FileName); + return moduleName.StartsWith(libName, StringComparison.OrdinalIgnoreCase) + || moduleName.StartsWith($"lib{libName}", StringComparison.OrdinalIgnoreCase); + }) + ?? throw new InvalidOperationException($"Native module '{libName}' was not found in the current process."); + + if (NativeLibrary.TryLoad(module.FileName, out var libHandleByPath)) + { + Console.WriteLine($"Successfully obtained a handle for native module '{module.ModuleName}' using path '{module.FileName}'."); + return libHandleByPath; + } + + throw new InvalidOperationException($"Failed to obtain a handle for native module '{module.ModuleName}'."); + } +} diff --git a/Tests/NetCord.Natives.Tests/NativesBuildTests.cs b/Tests/NetCord.Natives.Tests/NativesBuildTests.cs new file mode 100644 index 00000000..8de10c24 --- /dev/null +++ b/Tests/NetCord.Natives.Tests/NativesBuildTests.cs @@ -0,0 +1,155 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace NetCord.Natives.Tests; + +[TestClass] +public class NativesBuildTests +{ + [TestMethod] + [DataRow("libdave")] + [DataRow("libsodium")] + [DataRow("opus")] + [DataRow("zstd")] + public void NativesLoaded(string libName) + { + _ = libName switch + { + "libdave" => (object)NativeProbes.DaveMaxSupportedProtocolVersion(), + "libsodium" => (object)NativeProbes.SodiumInit(), + "opus" => (object)NativeProbes.OpusGetVersionString(), + "zstd" => (object)NativeProbes.ZstdVersionNumber(), + _ => throw new InvalidOperationException($"Unknown library name '{libName}' provided to test."), + }; + } + + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.OSX)] + [DataRow("libdave", "NetCord.Gateway.Voice.Dave")] + [DataRow("libsodium", "NetCord.Gateway.Voice.Encryption.XChaCha20Poly1305")] + [DataRow("opus", "NetCord.Gateway.Voice.Opus")] + [DataRow("zstd", "NetCord.Gateway.Compression.Zstandard")] + public void AllLibraryImportsExistInBinary(string libName, string className) + { + var missingExports = NativeProbes.GetMissingLibraryImports(libName, className, typeof(NetCord.Application).Assembly); + + Assert.IsEmpty(missingExports, $"The following entry points were not found in '{libName}': {string.Join(", ", missingExports)}"); + } + + const string NativeAotAppLogTag = $"[{nameof(NativeAotStaticLinking)}]"; + [DoNotParallelize] + [TestMethod] + [DataRow("libdave;libsodium;opus;zstd")] + public void NativeAotStaticLinking(string libName) + { + var projectDirectory = Path.Combine(AppContext.BaseDirectory, "Assets", "NativeAotApp"); + var projectFile = Path.Combine(projectDirectory, "NativeAotApp.csproj"); + var generatedProjectFile = Path.Combine(projectDirectory, "NativeAotApp.g.csproj"); + + // get properties to be passed to the NativeAotApp build from AssemblyMetadata attribute + var assembly = typeof(NativesBuildTests).Assembly; + + var properties = assembly.GetCustomAttributes()? + .FirstOrDefault(a => a.Key == "NativeAotAppProps")?.Value; + Assert.IsNotNull(properties, "NativeAotAppProps metadata attribute is not defined."); + + // build asset NativeAotApp with Native AoT enabled + var buildProcess = new System.Diagnostics.Process(); + buildProcess.StartInfo.FileName = "dotnet"; + buildProcess.StartInfo.ArgumentList.Add("publish"); + buildProcess.StartInfo.ArgumentList.Add("NativeAotApp.csproj"); + buildProcess.StartInfo.ArgumentList.Add("-tl:off"); + buildProcess.StartInfo.ArgumentList.Add("-v:n"); + buildProcess.StartInfo.ArgumentList.Add($"-p:{properties}"); + + Console.WriteLine($"{NativeAotAppLogTag} Building Native AoT app in ({projectDirectory}): 'dotnet {buildProcess.StartInfo.ArgumentList.Aggregate((a, b) => $"{a} {b}")}'"); + + buildProcess.StartInfo.WorkingDirectory = projectDirectory; + buildProcess.StartInfo.RedirectStandardOutput = true; + buildProcess.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + }; + buildProcess.StartInfo.RedirectStandardError = true; + buildProcess.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + }; + var ok = buildProcess.Start(); + buildProcess.BeginOutputReadLine(); + buildProcess.BeginErrorReadLine(); + buildProcess.WaitForExit(); + + Assert.AreEqual(0, buildProcess.ExitCode, $"Native AoT build failed for '{libName}'."); + + // Obtain the generated RunCommand from a build so we launch the same command the SDK would. + var getRunCmd = new System.Diagnostics.Process(); + getRunCmd.StartInfo.FileName = "dotnet"; + getRunCmd.StartInfo.ArgumentList.Add("build"); + getRunCmd.StartInfo.ArgumentList.Add("NativeAotApp.csproj"); + getRunCmd.StartInfo.ArgumentList.Add($"-p:{properties}"); + getRunCmd.StartInfo.ArgumentList.Add("-t:GetTargetPath"); + getRunCmd.StartInfo.ArgumentList.Add("-getProperty:PublishDir"); + getRunCmd.StartInfo.ArgumentList.Add("--no-restore"); + + getRunCmd.StartInfo.ArgumentList.Add("--bl"); + + string? runCmdOutput = null; + + getRunCmd.StartInfo.WorkingDirectory = projectDirectory; + getRunCmd.StartInfo.RedirectStandardOutput = true; + getRunCmd.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + runCmdOutput = e.Data.Trim('\r', '\n', '"'); + } + }; + getRunCmd.StartInfo.RedirectStandardError = true; + getRunCmd.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + }; + getRunCmd.Start(); + getRunCmd.BeginErrorReadLine(); + getRunCmd.BeginOutputReadLine(); + getRunCmd.WaitForExit(); + + Assert.AreEqual(0, getRunCmd.ExitCode, $"Failed to obtain PublishDir for '{libName}'."); + Assert.IsFalse(string.IsNullOrEmpty(runCmdOutput), $"PublishDir is empty for '{libName}'."); + + runCmdOutput = Path.Combine(projectDirectory, runCmdOutput, + "NativeAotApp" + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")); + + // check that the library is running without errors, which indicates that it was statically linked successfully + var aotProcess = new System.Diagnostics.Process(); + aotProcess.StartInfo.FileName = runCmdOutput; + aotProcess.StartInfo.WorkingDirectory = Path.GetDirectoryName(runCmdOutput); + + Console.WriteLine($"{NativeAotAppLogTag} Running Native AoT app: '{runCmdOutput}'"); + + aotProcess.StartInfo.RedirectStandardOutput = true; + aotProcess.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + }; + aotProcess.StartInfo.RedirectStandardError = true; + aotProcess.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"{NativeAotAppLogTag} {e.Data}"); + }; + aotProcess.Start(); + aotProcess.BeginOutputReadLine(); + aotProcess.BeginErrorReadLine(); + aotProcess.WaitForExit(); + + Assert.AreEqual(0, aotProcess.ExitCode, $"Native AoT app failed to run for '{libName}'."); + } +} diff --git a/Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj b/Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj new file mode 100644 index 00000000..636b6f9a --- /dev/null +++ b/Tests/NetCord.Natives.Tests/NetCord.Natives.Tests.csproj @@ -0,0 +1,54 @@ + + + + net10.0 + enable + enable + true + + false + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + ..\..\NetCord.Natives\ + NetCordNativesDir=$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\$(NetCordNativesDir)')) + + $(VCPKG_ROOT) + $(NativeAotAppProps),VcpkgRoot=$(VcpkgRoot) + + $(NativeAotAppProps),$(AppendNativeAotAppProps) + + + + + + <_Parameter1>NativeAotAppProps + <_Parameter2>$(NativeAotAppProps) + + + + + + + +