Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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, <SDL version>.<N>. 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
132 changes: 132 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Release

# Publishes each push to main on nuget.org as <SDL version>.<N> and tags it v<version>. 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
11 changes: 11 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
<!-- The XDL release in XDL_VERSION, which tools/XdlRelease.proj downloads and unpacks into artifacts/xdl/<version>. -->
<PropertyGroup>
<XdlVersion>$([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)XDL_VERSION').Trim())</XdlVersion>
<XdlSdlVersion>$(XdlVersion.Split('-')[0])</XdlSdlVersion>
<XdlReleaseDirectory>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'artifacts', 'xdl', '$(XdlVersion)'))</XdlReleaseDirectory>
<XdlIncludeDirectory>$(XdlReleaseDirectory)include/</XdlIncludeDirectory>
</PropertyGroup>

<!-- A release passes -p:Version=<SDL version>.<N>; any other pack is marked local, so it never passes for a release. -->
<PropertyGroup>
<VersionPrefix>$(XdlSdlVersion).0</VersionPrefix>
<VersionSuffix>local</VersionSuffix>
<Authors>stanoddly</Authors>
<RepositoryUrl>https://github.com/stanoddly/XDLSharp</RepositoryUrl>
<PackageProjectUrl>https://github.com/stanoddly/XDLSharp</PackageProjectUrl>
<PackageTags>xdl;sdl;sdl3;bindings;interop</PackageTags>
</PropertyGroup>
</Project>
4 changes: 3 additions & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
<PropertyGroup>
<XdlRuntimeDirectory>$(XdlReleaseDirectory)runtimes/$(NETCoreSdkPortableRuntimeIdentifier)/native/</XdlRuntimeDirectory>
<XdlBrowserDirectory>$(XdlReleaseDirectory)runtimes/browser-wasm/native/</XdlBrowserDirectory>
<!-- The headers, the runtime for this machine and the browser archives, unless a project names other XDL targets. -->
<XdlReleaseTargets Condition="'$(XdlReleaseTargets)' == ''">headers;$(NETCoreSdkPortableRuntimeIdentifier);browser-wasm-net11</XdlReleaseTargets>
</PropertyGroup>

<!-- A project that sets UsesXdlRelease gets the release before it builds. -->
<Target Name="FetchXdlRelease" BeforeTargets="BeforeBuild" Condition="'$(UsesXdlRelease)' == 'true'">
<MSBuild Projects="$(MSBuildThisFileDirectory)tools/XdlRelease.proj" Properties="XdlRuntimeIdentifier=$(NETCoreSdkPortableRuntimeIdentifier)" />
<MSBuild Projects="$(MSBuildThisFileDirectory)tools/XdlRelease.proj" Properties="XdlTargets=$(XdlReleaseTargets)" />
</Target>
</Project>
23 changes: 23 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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 <slouken@libsdl.org>

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.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO);
Pointer<SDL_Window> 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.<rid>` 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.
Expand All @@ -27,7 +35,7 @@ Pointer<SDL_Window> 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/<version>`.
`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/<version>`. Packing a native package downloads the runtime it packs.

## Generating

Expand Down Expand Up @@ -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.<N>` tag. The workflow pushes the packages to nuget.org through Trusted Publishing, and then tags the commit `v<version>`. It stops when nuget.org already has that version from another commit.

To pack locally, run `tools/pack.sh <output> [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.<rid>` 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.
2 changes: 1 addition & 1 deletion XDL_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.4.20-3
3.4.20-4
3 changes: 3 additions & 0 deletions XdlSharp.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<Solution>
<Folder Name="/packaging/">
<Project Path="packaging/XdlSharp.Native/XdlSharp.Native.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/XdlSharp/XdlSharp.csproj" />
</Folder>
Expand Down
Loading
Loading