diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c9d2732 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,150 @@ +name: Build + +on: + workflow_dispatch: + pull_request: + # Release runs this workflow with the version to pack, and publishes its packages. + workflow_call: + inputs: + version: + description: The version of the packages, .. Without it the version is marked local. + required: false + type: string + +permissions: + contents: read + +jobs: + desktop: + name: Generate, test and pack + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install the .NET 11 preview SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '11.0.x' + dotnet-quality: 'preview' + + # The checked-in bindings are generated with clang 22, and another clang can print the AST differently. + - name: Install clang 22 + run: | + wget -qO llvm.sh https://apt.llvm.org/llvm.sh + sudo bash llvm.sh 22 + + - name: Check that the generated code is current + run: dotnet run --project tools/XdlSharp.Generator -- --check --clang clang-22 + + - name: Test + run: dotnet test tests/XdlSharp.Tests + + - name: Pack + env: + VERSION: ${{ inputs.version }} + run: tools/pack.sh artifacts/packages "$VERSION" + + - name: Upload the packages + uses: actions/upload-artifact@v7 + with: + name: packages + path: artifacts/packages + if-no-files-found: error + + browser: + name: Test in the browser runtime + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install the .NET 11 preview SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '11.0.x' + dotnet-quality: 'preview' + + - name: Install the wasm-tools workload + run: dotnet workload install wasm-tools + + - name: Publish the browser tests + run: dotnet publish tests/XdlSharp.Browser.Tests + + - name: Run the browser tests + working-directory: tests/XdlSharp.Browser.Tests/bin/Release/net11.0/publish/wwwroot + run: node main.mjs + + # The packages as an app restores them, on each platform that has a hosted runner. osx-x64 and win-arm64 have none. + packages: + name: Run the packages on ${{ matrix.os }} + needs: desktop + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, windows-latest, macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download the packages + uses: actions/download-artifact@v8 + with: + name: packages + path: packages + + - name: Install the .NET 11 preview SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '11.0.x' + dotnet-quality: 'preview' + + - name: Run the packages + shell: bash + run: | + version=$(basename packages/XdlSharp.[0-9]*.nupkg .nupkg) + dotnet run --project tests/XdlSharp.Package.Tests "-p:XdlSharpPackageVersion=${version#XdlSharp.}" "-p:RestoreAdditionalProjectSources=$GITHUB_WORKSPACE/packages" + + packages-browser: + name: Run the packages in the browser runtime + needs: desktop + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download the packages + uses: actions/download-artifact@v8 + with: + name: packages + path: packages + + - name: Install the .NET 11 preview SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '11.0.x' + dotnet-quality: 'preview' + + - name: Install the wasm-tools workload + run: dotnet workload install wasm-tools + + - name: Publish the packages in a browser app + run: | + version=$(basename packages/XdlSharp.[0-9]*.nupkg .nupkg) + dotnet publish tests/XdlSharp.Package.Browser.Tests "-p:XdlSharpPackageVersion=${version#XdlSharp.}" "-p:RestoreAdditionalProjectSources=$GITHUB_WORKSPACE/packages" + + # The archives are linked into dotnet.native.wasm, so a copy in the output means the package puts them where NuGet copies files. + - name: Check that no archive is in the published app + run: | + if find tests/XdlSharp.Package.Browser.Tests/bin/Release/net11.0/publish -name '*.a' | grep .; then + exit 1 + fi + + - name: Run the browser app + working-directory: tests/XdlSharp.Package.Browser.Tests/bin/Release/net11.0/publish/wwwroot + run: node main.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..845fd5e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: Release + +# Publishes each push to main on nuget.org as . and tags it v. N is one more than the highest N tagged for that +# SDL version, so it starts at 1. A failed release is run again by hand: the push skips packages that nuget.org already has. +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# Two runs at once would take the same N. By default a new run cancels a pending one, so its merge would have no release of its own. +concurrency: + group: release + cancel-in-progress: false + queue: max + +jobs: + resolve: + # A run started from another branch would release unreviewed code. + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + # A shallow checkout has no tags, so every run would take N = 1. + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Find the version + id: version + run: | + sdl_version=$(cut -d- -f1 XDL_VERSION) + # The queue does not keep the order of the runs. A later commit released for the same SDL version contains this one, so this + # release would publish older code as a newer version. + released=$(git tag --contains HEAD "v$sdl_version.*") + if [[ -n "$released" ]]; then + echo "Released already in ${released//$'\n'/, }." + exit 0 + fi + + last=0 + for tag in $(git tag --list "v$sdl_version.*"); do + n=${tag#"v$sdl_version."} + if [[ "$n" =~ ^[0-9]+$ ]] && (( n > last )); then + last=$n + fi + done + echo "version=$sdl_version.$((last + 1))" >> "$GITHUB_OUTPUT" + + build: + needs: resolve + if: needs.resolve.outputs.version != '' + uses: ./.github/workflows/build.yml + with: + version: ${{ needs.resolve.outputs.version }} + + # Without a checkout or contents: write, so the steps that can get a push key from nuget.org hold no token that writes to the repository. + publish: + needs: [resolve, build] + runs-on: ubuntu-24.04 + permissions: + # Trusted Publishing: nuget.org exchanges this workflow's OIDC token for a short-lived API key. + id-token: write + env: + VERSION: ${{ needs.resolve.outputs.version }} + + steps: + - name: Download the packages + uses: actions/download-artifact@v8 + with: + name: packages + path: packages + + # A tag push that failed after the packages were published leaves N untagged, and the next merge would take it again. The push would + # skip every package nuget.org has as a duplicate, and the tag would name a commit whose code is not on nuget.org. + - name: Check that nuget.org has no other commit as this version + run: | + for package in packages/*.nupkg; do + id=$(basename "$package" ".$VERSION.nupkg") + id=${id,,} + status=$(curl -sS -o nuspec -w '%{http_code}' "https://api.nuget.org/v3-flatcontainer/$id/$VERSION/$id.nuspec") + if [[ "$status" == 200 ]] && ! grep -q "commit=\"$GITHUB_SHA\"" nuspec; then + echo "nuget.org has $id $VERSION from another commit. Tag that commit v$VERSION, then run Release on main again, not only its failed jobs." >&2 + exit 1 + elif [[ "$status" != 200 && "$status" != 404 ]]; then + echo "nuget.org answered $status for $id $VERSION." >&2 + exit 1 + fi + done + + - name: Install the .NET 11 preview SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '11.0.x' + dotnet-quality: 'preview' + + - name: Log in to nuget.org + id: login + uses: NuGet/login@v1 + with: + user: stanoddly + + - name: Push the packages + env: + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: dotnet nuget push "packages/*.nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + + # After the push, so a tag always names a published version. + tag: + needs: [resolve, publish] + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + TAG: v${{ needs.resolve.outputs.version }} + + steps: + # A rerun after the tag was created finds it on this commit. + - name: Tag the release + run: | + tagged=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" -q .object.sha 2>/dev/null || true) + if [[ "$tagged" == "$GITHUB_SHA" ]]; then + echo "$TAG is on this commit already." + else + gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$GITHUB_SHA" + fi diff --git a/Directory.Build.props b/Directory.Build.props index 745e8af..94c94a9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,18 @@ $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)XDL_VERSION').Trim()) + $(XdlVersion.Split('-')[0]) $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'artifacts', 'xdl', '$(XdlVersion)')) $(XdlReleaseDirectory)include/ + + + + $(XdlSdlVersion).0 + local + stanoddly + https://github.com/stanoddly/XDLSharp + https://github.com/stanoddly/XDLSharp + xdl;sdl;sdl3;bindings;interop + diff --git a/Directory.Build.targets b/Directory.Build.targets index 3b52d77..7a00fdf 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -2,10 +2,12 @@ $(XdlReleaseDirectory)runtimes/$(NETCoreSdkPortableRuntimeIdentifier)/native/ $(XdlReleaseDirectory)runtimes/browser-wasm/native/ + + headers;$(NETCoreSdkPortableRuntimeIdentifier);browser-wasm-net11 - + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cf869b --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +XdlSharp +Copyright (C) 2026 Stan (stanoddly) + +Portions of this software are generated from the headers of XDL, which are +derived from Simple DirectMedia Layer, and from the headers of SDL_image, +SDL_ttf and SDL_mixer +Copyright (C) 1997-2026 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md index 86a6cfa..f7afb28 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO); Pointer window = SDL_CreateWindow("Title"u8, 640, 480, SDL_WindowFlags.SDL_WINDOW_RESIZABLE); ``` +## Packages + +- `XdlSharp` holds the bindings. It is the same for every platform and holds no native library. +- `XdlSharp.Native.` holds XDL's runtime for one platform, from the XDL release: `linux-x64`, `linux-arm64`, `osx-x64`, `osx-arm64`, `win-x64`, `win-arm64` or `browser-wasm`. An app references the packages of the platforms it runs on. +- `XdlSharp.Native.browser-wasm` links its archives into `dotnet.native.wasm` and adds `--use-port=emdawnwebgpu` to the link. The link builds that Emscripten port into the Emscripten cache. The workload's own cache is read-only, so set `WasmCachePath` to a writable directory. + +The version is the SDL version and a release number, such as `3.4.20.1`. The package description names the XDL release, such as `3.4.20-4`. + ## Mapping - Functions, constants, hints and property names of all four libraries are static members of `XdlSharp.Xdl`; types are in `XdlSharp`. Each function loads from its own library: `IMG_` functions from `XDL3_image`, and so on. @@ -27,7 +35,7 @@ Pointer window = SDL_CreateWindow("Title"u8, 640, 480, SDL_WindowFla ## XDL release -`XDL_VERSION` pins the XDL release. Building the generator or a test project runs `tools/XdlRelease.proj`, which downloads that release's headers, the runtime for this machine and the browser archives into `artifacts/xdl/`. +`XDL_VERSION` pins the XDL release. Building the generator or a test project runs `tools/XdlRelease.proj`, which downloads that release's headers, the runtime for this machine and the browser archives into `artifacts/xdl/`. Packing a native package downloads the runtime it packs. ## Generating @@ -57,9 +65,21 @@ dotnet publish tests/XdlSharp.Browser.Tests cd tests/XdlSharp.Browser.Tests/bin/Release/net11.0/publish/wwwroot && node main.mjs ``` +## Releasing + +The Build workflow runs on every pull request. It checks the generated code with clang 22, runs the tests on the desktop and in the browser, and packs the packages. Then `tests/XdlSharp.Package.Tests` restores the packages and loads each library, as an app does, on linux-x64, linux-arm64, win-x64 and osx-arm64. `tests/XdlSharp.Package.Browser.Tests` does the same in the browser. + +The Release workflow publishes every push to `main`. For SDL 3.4.20, the first release is `3.4.20.1`, and each later one adds 1 to the highest `v3.4.20.` tag. The workflow pushes the packages to nuget.org through Trusted Publishing, and then tags the commit `v`. It stops when nuget.org already has that version from another commit. + +To pack locally, run `tools/pack.sh [version]`. Without a version, the version is marked local with the time, such as `3.4.20-local.20261109103000`, so a consumer never restores an older local pack from its package cache. + ## Limits - The headers are parsed for Linux, so declarations that only other platforms see, such as `SDL_SetWindowsMessageHook`, are missing. The generator runs only on Linux. - `SDL_main.h` is not bound: `SDL.h` does not include it. - `SDL_vulkan.h` is not bound: `SDL.h` does not include it, and `VkSurfaceKHR` is a pointer on 64-bit targets but a `uint64_t` on wasm32. - `wchar_t*` is `nint`, because `wchar_t` is 2 bytes on Windows and 4 elsewhere. + +## License + +XdlSharp uses the zlib license, as XDL does. See [LICENSE](https://github.com/stanoddly/XDLSharp/blob/main/LICENSE). Each `XdlSharp.Native.` package carries the release's `THIRD-PARTY-NOTICES.txt` as its license: XDL's license and the licenses of the third-party code in its libraries. diff --git a/XDL_VERSION b/XDL_VERSION index 7e6f81d..edb0958 100644 --- a/XDL_VERSION +++ b/XDL_VERSION @@ -1 +1 @@ -3.4.20-3 +3.4.20-4 diff --git a/XdlSharp.slnx b/XdlSharp.slnx index 2f47ef9..22c0fa6 100644 --- a/XdlSharp.slnx +++ b/XdlSharp.slnx @@ -1,4 +1,7 @@ + + + diff --git a/packaging/XdlSharp.Native/XdlSharp.Native.csproj b/packaging/XdlSharp.Native/XdlSharp.Native.csproj new file mode 100644 index 0000000..82c0521 --- /dev/null +++ b/packaging/XdlSharp.Native/XdlSharp.Native.csproj @@ -0,0 +1,47 @@ + + + + + XdlSharp.Native.$(XdlRuntimeIdentifier) + The $(XdlRuntimeIdentifier) runtime of XDL $(XdlVersion), the altered SDL $(XdlSdlVersion), with SDL_image, SDL_ttf and SDL_mixer built against it, for XdlSharp. + true + + $(XdlRuntimeIdentifier) + browser-wasm-net11 + $(XdlTarget) + + THIRD-PARTY-NOTICES.txt + README.md + false + runtimes/$(XdlRuntimeIdentifier)/native/ + buildTransitive/native/ + true + + $(NoWarn);NU5128 + $(TargetsForTfmSpecificContentInPackage);AddXdlRuntimeToPackage + + + + + + + + + + + + + + + <_XdlRuntimeFile Include="$(XdlReleaseDirectory)runtimes/$(XdlRuntimeIdentifier)/native/*" /> + + + + + + + diff --git a/packaging/XdlSharp.Native/buildTransitive/XdlSharp.Native.browser-wasm.targets b/packaging/XdlSharp.Native/buildTransitive/XdlSharp.Native.browser-wasm.targets new file mode 100644 index 0000000..eee05d4 --- /dev/null +++ b/packaging/XdlSharp.Native/buildTransitive/XdlSharp.Native.browser-wasm.targets @@ -0,0 +1,14 @@ + + + + + + + + $(EmccExtraLDFlags) --use-port=emdawnwebgpu + + diff --git a/src/XdlSharp/XdlSharp.csproj b/src/XdlSharp/XdlSharp.csproj index 6e495e9..ce4fb36 100644 --- a/src/XdlSharp/XdlSharp.csproj +++ b/src/XdlSharp/XdlSharp.csproj @@ -2,10 +2,14 @@ XdlSharp + Low-level C# bindings for XDL $(XdlVersion), the altered SDL $(XdlSdlVersion), and for SDL_image, SDL_ttf and SDL_mixer built against it. The native libraries are in XdlSharp.Native.<rid>. + README.md + Zlib + diff --git a/tests/XdlSharp.Package.Browser.Tests/XdlSharp.Package.Browser.Tests.csproj b/tests/XdlSharp.Package.Browser.Tests/XdlSharp.Package.Browser.Tests.csproj new file mode 100644 index 0000000..1141fc6 --- /dev/null +++ b/tests/XdlSharp.Package.Browser.Tests/XdlSharp.Package.Browser.Tests.csproj @@ -0,0 +1,26 @@ + + + + + Exe + false + false + false + $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', '..', '..', 'artifacts', 'em-cache')) + + + + + + + + + + + + + diff --git a/tests/XdlSharp.Package.Browser.Tests/wwwroot/main.mjs b/tests/XdlSharp.Package.Browser.Tests/wwwroot/main.mjs new file mode 100644 index 0000000..d5b3c77 --- /dev/null +++ b/tests/XdlSharp.Package.Browser.Tests/wwwroot/main.mjs @@ -0,0 +1,4 @@ +import { dotnet } from './_framework/dotnet.js'; + +const exitCode = await dotnet.run(); +process.exit(exitCode); diff --git a/tests/XdlSharp.Package.Tests/Program.cs b/tests/XdlSharp.Package.Tests/Program.cs new file mode 100644 index 0000000..5a3b44b --- /dev/null +++ b/tests/XdlSharp.Package.Tests/Program.cs @@ -0,0 +1,17 @@ +using static XdlSharp.Xdl; + +// Each version call loads one library of the native package, and the satellites load XDL3 next to them. +(string Name, int Loaded, int Header)[] libraries = +[ + ("XDL3", SDL_GetVersion(), SDL_VERSION), + ("XDL3_image", IMG_Version(), SDL_IMAGE_VERSION), + ("XDL3_ttf", TTF_Version(), SDL_TTF_VERSION), + ("XDL3_mixer", MIX_Version(), SDL_MIXER_VERSION), +]; + +foreach ((string name, int loaded, int header) in libraries) +{ + Console.WriteLine($"{name}: {loaded}, the header says {header}"); +} + +return libraries.All(library => library.Loaded == library.Header) ? 0 : 1; diff --git a/tests/XdlSharp.Package.Tests/XdlSharp.Package.Tests.csproj b/tests/XdlSharp.Package.Tests/XdlSharp.Package.Tests.csproj new file mode 100644 index 0000000..9acce11 --- /dev/null +++ b/tests/XdlSharp.Package.Tests/XdlSharp.Package.Tests.csproj @@ -0,0 +1,23 @@ + + + + + Exe + false + false + + + + + + + + + + + + diff --git a/tests/XdlSharp.Tests/XdlSharp.Tests.csproj b/tests/XdlSharp.Tests/XdlSharp.Tests.csproj index 16803b5..bf1e947 100644 --- a/tests/XdlSharp.Tests/XdlSharp.Tests.csproj +++ b/tests/XdlSharp.Tests/XdlSharp.Tests.csproj @@ -4,7 +4,7 @@ false true $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', '..', 'native')) - $([MSBuild]::NormalizeDirectory('$(BaseIntermediateOutputPath)', 'native')) + $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)', 'native')) diff --git a/tools/XdlRelease.proj b/tools/XdlRelease.proj index f8ec76e..31872f0 100644 --- a/tools/XdlRelease.proj +++ b/tools/XdlRelease.proj @@ -1,8 +1,8 @@ @@ -12,9 +12,10 @@ - + - <_XdlZip Include="xdl3-headers.zip;xdl3-$(XdlRuntimeIdentifier).zip;xdl3-browser-wasm-net11.zip" /> + <_XdlTarget Include="$(XdlTargets)" /> + <_XdlZip Include="@(_XdlTarget->'xdl3-%(Identity).zip')" /> <_MissingXdlZip Include="@(_XdlZip)" Condition="!Exists('$(_DownloadDirectory)%(Identity)')" /> diff --git a/tools/pack.sh b/tools/pack.sh new file mode 100755 index 0000000..fe369c3 --- /dev/null +++ b/tools/pack.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Packs XdlSharp and an XdlSharp.Native. package for each runtime of the XDL release into . +# usage: tools/pack.sh [version] +# Without a version, or with an empty one, the version is marked local with the time, such as 3.4.20-local.20261109103000, so a +# consumer never restores an older local pack from its package cache. A release passes .. +set -euo pipefail + +mkdir -p "${1:?usage: tools/pack.sh [version]}" +output=$(CDPATH= cd "$1" && pwd) +root=$(CDPATH= cd "$(dirname "$0")/.." && pwd) +if [[ -n "${2:-}" ]]; then + version_argument="-p:Version=$2" +else + version_argument="-p:VersionSuffix=local.$(date -u +%Y%m%d%H%M%S)" +fi + +# The runtimes of every XDL release; the browser one is the XDL target browser-wasm-net11. +runtimes=(linux-x64 linux-arm64 osx-x64 osx-arm64 win-x64 win-arm64 browser-wasm) + +dotnet pack "$root/src/XdlSharp" -c Release -o "$output" "$version_argument" +for runtime in "${runtimes[@]}"; do + dotnet pack "$root/packaging/XdlSharp.Native" -c Release -o "$output" "-p:XdlRuntimeIdentifier=$runtime" "$version_argument" +done