diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index adc61a2..a093721 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,30 @@ concurrency:
cancel-in-progress: true
jobs:
+ version:
+ name: Calculate package version
+ runs-on: ubuntu-latest
+ outputs:
+ semver: ${{ steps.gitversion.outputs.semVer }}
+
+ steps:
+ - name: Check out repository history
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v4
+ with:
+ versionSpec: 6.x
+
+ - name: Calculate repository version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v4
+ with:
+ useConfigFile: true
+
parser:
name: Test Tree-sitter parser
runs-on: ubuntu-latest
@@ -31,9 +55,32 @@ jobs:
- name: Test parser
uses: tree-sitter/parser-test-action@v3
- csharp:
- name: Build and test C# binding
- runs-on: ubuntu-latest
+ native:
+ name: Build native library (${{ matrix.rid }})
+ needs: parser
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: windows-latest
+ rid: win-x64
+ library: tree-sitter-expressif.dll
+ - os: windows-11-arm
+ rid: win-arm64
+ library: tree-sitter-expressif.dll
+ - os: ubuntu-latest
+ rid: linux-x64
+ library: libtree-sitter-expressif.so
+ - os: ubuntu-24.04-arm
+ rid: linux-arm64
+ library: libtree-sitter-expressif.so
+ - os: macos-15-intel
+ rid: osx-x64
+ library: libtree-sitter-expressif.dylib
+ - os: macos-15
+ rid: osx-arm64
+ library: libtree-sitter-expressif.dylib
steps:
- name: Check out repository
@@ -41,130 +88,194 @@ jobs:
with:
persist-credentials: false
- - name: Set up .NET
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: 10.0.x
-
- name: Set up Tree-sitter
uses: tree-sitter/setup-action/cli@v2
- - name: Discover C# projects
- id: csharp-projects
+ - name: Build native grammar library
shell: pwsh
run: |
- $projectRoot = "bindings/csharp"
- $projects = if (Test-Path -LiteralPath $projectRoot -PathType Container) {
- @(Get-ChildItem -LiteralPath $projectRoot -Recurse -Filter *.csproj)
- } else {
- @()
- }
- "found=$($projects.Count -gt 0)" >> $env:GITHUB_OUTPUT
- if ($projects.Count -eq 0) {
- Write-Host "No C# projects exist yet; skipping .NET validation."
- }
-
- - name: Restore C# projects
- if: steps.csharp-projects.outputs.found == 'True'
- shell: pwsh
- run: |
- $projectRoot = "bindings/csharp"
- if (-not (Test-Path -LiteralPath $projectRoot -PathType Container)) {
- Write-Host "No C# project directory exists; skipping restore."
- exit 0
- }
- Get-ChildItem -LiteralPath $projectRoot -Recurse -Filter *.csproj | ForEach-Object {
- dotnet restore $_.FullName
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- }
-
- - name: Build C# projects
- if: steps.csharp-projects.outputs.found == 'True'
- shell: pwsh
- run: |
- $projectRoot = "bindings/csharp"
- if (-not (Test-Path -LiteralPath $projectRoot -PathType Container)) {
- Write-Host "No C# project directory exists; skipping build."
- exit 0
- }
- Get-ChildItem -LiteralPath $projectRoot -Recurse -Filter *.csproj | ForEach-Object {
- dotnet build $_.FullName --configuration Release --no-restore
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- }
+ $outputDirectory = Join-Path 'native-assets' '${{ matrix.rid }}'
+ New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
+ tree-sitter build -o (Join-Path $outputDirectory '${{ matrix.library }}') .
+ if ($LASTEXITCODE -ne 0) { throw 'Native grammar build failed.' }
- - name: Test C# projects
- if: steps.csharp-projects.outputs.found == 'True'
- shell: pwsh
- run: |
- $projectRoot = "bindings/csharp"
- if (-not (Test-Path -LiteralPath $projectRoot -PathType Container)) {
- Write-Host "No C# project directory exists; skipping tests."
- exit 0
- }
- Get-ChildItem -LiteralPath $projectRoot -Recurse -Filter *.csproj | ForEach-Object {
- dotnet test $_.FullName --configuration Release --no-build
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- }
+ - name: Upload native grammar library
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-${{ matrix.rid }}
+ path: native-assets/${{ matrix.rid }}/${{ matrix.library }}
+ if-no-files-found: error
package:
- name: Validate distributable packages
- needs: [parser, csharp]
+ name: Build C# package
+ needs: [version, native]
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
- fetch-depth: 0
persist-credentials: false
- - name: Install GitVersion
- uses: gittools/actions/gitversion/setup@v4
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v6
with:
- versionSpec: 6.x
+ dotnet-version: 10.0.x
- - name: Calculate repository version
- id: gitversion
- uses: gittools/actions/gitversion/execute@v4
+ - name: Download Windows native grammar library
+ uses: actions/download-artifact@v8
with:
- useConfigFile: true
+ name: native-win-x64
+ path: native-assets/win-x64
- - name: Set up Node.js
- uses: actions/setup-node@v6
+ - name: Download Linux native grammar library
+ uses: actions/download-artifact@v8
with:
- node-version: 24
- cache: npm
+ name: native-linux-x64
+ path: native-assets/linux-x64
- - name: Set up Python
- uses: actions/setup-python@v6
+ - name: Download Windows ARM64 native grammar library
+ uses: actions/download-artifact@v8
with:
- python-version: '3.12'
+ name: native-win-arm64
+ path: native-assets/win-arm64
- - name: Set up .NET
+ - name: Download Linux ARM64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-linux-arm64
+ path: native-assets/linux-arm64
+
+ - name: Download macOS x64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-osx-x64
+ path: native-assets/osx-x64
+
+ - name: Download macOS ARM64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-osx-arm64
+ path: native-assets/osx-arm64
+
+ - name: Build NuGet package
+ shell: pwsh
+ run: >-
+ ./scripts/Build-NuGetPackage.ps1
+ -Version '${{ needs.version.outputs.semver }}'
+ -NativeAssetsDirectory 'native-assets'
+
+ - name: Upload NuGet package
+ uses: actions/upload-artifact@v7
+ with:
+ name: nuget-${{ needs.version.outputs.semver }}
+ path: artifacts/*.nupkg
+ if-no-files-found: error
+
+ consume:
+ name: Test C# package (${{ matrix.rid }})
+ needs: [version, package]
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: windows-latest
+ rid: win-x64
+ - os: windows-11-arm
+ rid: win-arm64
+ - os: ubuntu-latest
+ rid: linux-x64
+ - os: ubuntu-24.04-arm
+ rid: linux-arm64
+ - os: macos-15-intel
+ rid: osx-x64
+ - os: macos-15
+ rid: osx-arm64
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - name: Set up .NET runtimes
uses: actions/setup-dotnet@v6
with:
- dotnet-version: 10.0.x
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Download NuGet package
+ uses: actions/download-artifact@v8
+ with:
+ name: nuget-${{ needs.version.outputs.semver }}
+ path: artifacts
- - name: Install packaging dependencies
+ - name: Restore installed package tests
+ shell: pwsh
run: |
- npm ci
- python -m pip install build
+ $packageSource = (Resolve-Path 'artifacts').Path
+ dotnet restore bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj `
+ -p:RestoreAdditionalProjectSources=$packageSource `
+ -p:PackageVersionUnderTest='${{ needs.version.outputs.semver }}'
+ if ($LASTEXITCODE -ne 0) { throw 'Package test restore failed.' }
+
+ - name: Test installed package
+ shell: pwsh
+ run: >-
+ dotnet test bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj
+ --configuration Release
+ --no-restore
+ -p:PackageVersionUnderTest='${{ needs.version.outputs.semver }}'
+
+ native-archive:
+ name: Build native C archive
+ needs: [version, parser]
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v7
+ with:
+ persist-credentials: false
- - name: Build packages with the repository version
+ - name: Build native C archive
shell: pwsh
- run: ./scripts/package.ps1 -Version '${{ steps.gitversion.outputs.semVer }}'
+ run: ./scripts/New-NativeArchive.ps1 -Version '${{ needs.version.outputs.semver }}'
+
+ - name: Upload native C archive
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-source-${{ needs.version.outputs.semver }}
+ path: artifacts/*.tar.gz
+ if-no-files-found: error
+
+ collect:
+ name: Collect validated packages
+ needs: [version, consume, native-archive]
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download validated packages
+ uses: actions/download-artifact@v8
+ with:
+ pattern: '*-${{ needs.version.outputs.semver }}'
+ path: artifacts
+ merge-multiple: true
- name: Upload package validation artifacts
uses: actions/upload-artifact@v7
with:
- name: packages-${{ steps.gitversion.outputs.semVer }}
+ name: packages-${{ needs.version.outputs.semver }}
path: artifacts/*
if-no-files-found: error
gate:
name: CI gate
if: always()
- needs: [parser, csharp, package]
+ needs: [version, parser, native, package, consume, native-archive, collect]
runs-on: ubuntu-latest
steps:
@@ -172,9 +283,13 @@ jobs:
shell: pwsh
run: |
$results = @{
+ version = '${{ needs.version.result }}'
parser = '${{ needs.parser.result }}'
- csharp = '${{ needs.csharp.result }}'
+ native = '${{ needs.native.result }}'
package = '${{ needs.package.result }}'
+ consume = '${{ needs.consume.result }}'
+ nativeArchive = '${{ needs.native-archive.result }}'
+ collect = '${{ needs.collect.result }}'
}
$failed = @($results.GetEnumerator() | Where-Object Value -ne 'success')
if ($failed.Count -gt 0) {
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 68427fb..19db245 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,9 +24,21 @@ jobs:
- os: windows-latest
rid: win-x64
library: tree-sitter-expressif.dll
+ - os: windows-11-arm
+ rid: win-arm64
+ library: tree-sitter-expressif.dll
- os: ubuntu-latest
rid: linux-x64
library: libtree-sitter-expressif.so
+ - os: ubuntu-24.04-arm
+ rid: linux-arm64
+ library: libtree-sitter-expressif.so
+ - os: macos-15-intel
+ rid: osx-x64
+ library: libtree-sitter-expressif.dylib
+ - os: macos-15
+ rid: osx-arm64
+ library: libtree-sitter-expressif.dylib
steps:
- name: Check out release commit
@@ -54,7 +66,7 @@ jobs:
retention-days: 7
validate:
- name: Validate release candidate
+ name: Build release packages
if: github.ref == 'refs/heads/main'
needs: native
runs-on: ubuntu-latest
@@ -91,34 +103,11 @@ jobs:
with:
useConfigFile: true
- - name: Set up Node.js
- uses: actions/setup-node@v6
- with:
- node-version: 24
- cache: npm
-
- - name: Set up Python
- uses: actions/setup-python@v6
- with:
- python-version: '3.12'
-
- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- - name: Install packaging dependencies
- run: |
- npm ci
- python -m pip install build pytest
-
- - name: Test parser and bindings
- run: |
- npm test
- python -m pip install '.[core]'
- python -m pytest bindings/python/tests
- dotnet test Expressif.Syntax.sln --configuration Release
-
- name: Download Windows native grammar library
uses: actions/download-artifact@v8
with:
@@ -131,13 +120,40 @@ jobs:
name: native-linux-x64
path: native-assets/linux-x64
- - name: Build all release packages
+ - name: Download Windows ARM64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-win-arm64
+ path: native-assets/win-arm64
+
+ - name: Download Linux ARM64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-linux-arm64
+ path: native-assets/linux-arm64
+
+ - name: Download macOS x64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-osx-x64
+ path: native-assets/osx-x64
+
+ - name: Download macOS ARM64 native grammar library
+ uses: actions/download-artifact@v8
+ with:
+ name: native-osx-arm64
+ path: native-assets/osx-arm64
+
+ - name: Build NuGet package
shell: pwsh
run: >-
- ./scripts/package.ps1
+ ./scripts/Build-NuGetPackage.ps1
-Version '${{ steps.gitversion.outputs.semVer }}'
-NativeAssetsDirectory 'native-assets'
- -RequirePublishablePythonVersion
+
+ - name: Build native C archive
+ shell: pwsh
+ run: ./scripts/New-NativeArchive.ps1 -Version '${{ steps.gitversion.outputs.semVer }}'
- name: Upload validated release packages
uses: actions/upload-artifact@v7
@@ -147,10 +163,69 @@ jobs:
if-no-files-found: error
retention-days: 7
+ consume:
+ name: Test release package (${{ matrix.rid }})
+ needs: validate
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: windows-latest
+ rid: win-x64
+ - os: windows-11-arm
+ rid: win-arm64
+ - os: ubuntu-latest
+ rid: linux-x64
+ - os: ubuntu-24.04-arm
+ rid: linux-arm64
+ - os: macos-15-intel
+ rid: osx-x64
+ - os: macos-15
+ rid: osx-arm64
+
+ steps:
+ - name: Check out release commit
+ uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+ ref: ${{ needs.validate.outputs.sha }}
+
+ - name: Set up .NET runtimes
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Download release package
+ uses: actions/download-artifact@v8
+ with:
+ name: release-packages-${{ needs.validate.outputs.version }}
+ path: artifacts
+
+ - name: Restore installed package tests
+ shell: pwsh
+ run: |
+ $packageSource = (Resolve-Path 'artifacts').Path
+ dotnet restore bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj `
+ -p:RestoreAdditionalProjectSources=$packageSource `
+ -p:PackageVersionUnderTest='${{ needs.validate.outputs.version }}'
+ if ($LASTEXITCODE -ne 0) { throw 'Package test restore failed.' }
+
+ - name: Test installed package
+ shell: pwsh
+ run: >-
+ dotnet test bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj
+ --configuration Release
+ --no-restore
+ -p:PackageVersionUnderTest='${{ needs.validate.outputs.version }}'
+
publish:
name: Publish patch-zero release
if: needs.validate.outputs.patch == '0'
- needs: validate
+ needs: [validate, consume]
runs-on: ubuntu-latest
permissions:
contents: write
diff --git a/bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj b/bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj
index 35e756e..e2aea39 100644
--- a/bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj
+++ b/bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj
@@ -22,12 +22,19 @@
-
+
+
+
+
+
+
+
+
-
+
tree-sitter-expressif.dll
libtree-sitter-expressif.so
diff --git a/bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj b/bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj
index 293fe7f..609bc0c 100644
--- a/bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj
+++ b/bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj
@@ -19,17 +19,41 @@
Pack="true"
PackagePath="runtimes/win-x64/native/"
Visible="false" />
+
+
+
+
+
+
+
+
diff --git a/docs/_docs/why-expressif-syntax-needs-platform-specific-native-builds.md b/docs/_docs/why-expressif-syntax-needs-platform-specific-native-builds.md
new file mode 100644
index 0000000..c2de817
--- /dev/null
+++ b/docs/_docs/why-expressif-syntax-needs-platform-specific-native-builds.md
@@ -0,0 +1,138 @@
+# Why Expressif.Syntax Needs Platform-Specific Native Builds
+
+For the C# version of `Expressif.Syntax`, the important distinction is between the **C# bindings** and the **Tree-sitter C parser** underneath.
+
+## C# bindings
+
+The C# part is compiled to .NET IL:
+
+```text
+C# source
+ ↓
+.NET IL
+ ↓
+.NET runtime / JIT
+ ↓
+x64 or ARM64 machine code
+```
+
+That means the same managed assembly can normally run on different CPU architectures.
+
+So `Expressif.Syntax.dll` does **not** need a separate build just because the machine is:
+
+```text
+Windows x64
+Windows ARM64
+Linux x64
+Linux ARM64
+macOS x64
+macOS ARM64
+```
+
+The .NET runtime handles the CPU-specific execution.
+
+## Tree-sitter C parser
+
+The Tree-sitter C code is different.
+
+C is compiled directly into machine code for a specific target:
+
+```text
+Tree-sitter C
+ ↓
+ compiler
+ ↓
+x64 machine code
+```
+
+or:
+
+```text
+Tree-sitter C
+ ↓
+ compiler
+ ↓
+ARM64 machine code
+```
+
+An x64 binary cannot simply run as ARM64.
+
+The operating system also matters because the resulting native library format and runtime environment are different between Windows, Linux, and macOS.
+
+So the native parser needs distinct builds for each supported combination.
+
+A reasonable build matrix is therefore:
+
+```text
+Windows
+ x64
+ ARM64
+
+Linux
+ x64
+ ARM64
+
+macOS
+ x64
+ ARM64
+```
+
+Or in RID form:
+
+```text
+win-x64
+win-arm64
+
+linux-x64
+linux-arm64
+
+osx-x64
+osx-arm64
+```
+
+## What the pipeline is really building
+
+The platform matrix should not be understood as:
+
+> Build the C# bindings six times.
+
+It is really:
+
+> Build the Tree-sitter C parser for each supported OS/CPU combination, and package those binaries so that the same C# binding can load the correct one at runtime.
+
+Conceptually:
+
+```text
+ Expressif.Syntax.dll
+ C# / .NET
+ │
+ ▼
+ Tree-sitter binding
+ │
+ ┌──────────────────┼──────────────────┐
+ ▼ ▼ ▼
+ Windows Linux macOS
+ C binary C binary C binary
+ x64/ARM64 x64/ARM64 x64/ARM64
+```
+
+## Rule for Expressif.Syntax
+
+For the C# implementation:
+
+> **The C# bindings are architecture-independent. The Tree-sitter C parser is not.**
+
+Therefore the OS/architecture matrix exists because of the **C parser**, not because of the C# code.
+
+The pipeline therefore produces and validates:
+
+```text
+win-x64
+win-arm64
+linux-x64
+linux-arm64
+osx-x64
+osx-arm64
+```
+
+This gives complete x64/ARM64 coverage across Windows, Linux, and macOS.
diff --git a/scripts/Build-NuGetPackage.ps1 b/scripts/Build-NuGetPackage.ps1
new file mode 100644
index 0000000..edb08aa
--- /dev/null
+++ b/scripts/Build-NuGetPackage.ps1
@@ -0,0 +1,39 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [ValidatePattern('^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$')]
+ [string] $Version,
+
+ [Parameter(Mandatory)]
+ [string] $NativeAssetsDirectory,
+
+ [string] $OutputDirectory = 'artifacts'
+)
+
+$ErrorActionPreference = 'Stop'
+$repositoryRoot = Split-Path -Parent $PSScriptRoot
+$projectPath = Join-Path $repositoryRoot 'bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj'
+$nativeAssetsPath = Join-Path $repositoryRoot $NativeAssetsDirectory
+$outputPath = Join-Path $repositoryRoot $OutputDirectory
+
+New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
+
+$properties = @(
+ "/p:Version=$Version"
+ "/p:PackageVersion=$Version"
+ "/p:NativeAssetsDirectory=$nativeAssetsPath"
+)
+
+dotnet restore $projectPath @properties
+if ($LASTEXITCODE -ne 0) { throw 'NuGet restore failed.' }
+
+dotnet build $projectPath --configuration Release --no-restore @properties
+if ($LASTEXITCODE -ne 0) { throw 'Managed assembly build failed.' }
+
+dotnet pack $projectPath `
+ --configuration Release `
+ --output $outputPath `
+ --no-build `
+ --no-restore `
+ @properties
+if ($LASTEXITCODE -ne 0) { throw 'NuGet packaging failed.' }
diff --git a/scripts/New-NativeArchive.ps1 b/scripts/New-NativeArchive.ps1
new file mode 100644
index 0000000..aa1a51b
--- /dev/null
+++ b/scripts/New-NativeArchive.ps1
@@ -0,0 +1,24 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [ValidatePattern('^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$')]
+ [string] $Version,
+
+ [string] $OutputDirectory = 'artifacts'
+)
+
+$ErrorActionPreference = 'Stop'
+$repositoryRoot = Split-Path -Parent $PSScriptRoot
+$outputPath = Join-Path $repositoryRoot $OutputDirectory
+$archivePath = Join-Path $outputPath "tree-sitter-expressif-c-$Version.tar.gz"
+
+New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
+
+Push-Location $repositoryRoot
+try {
+ tar -czf $archivePath CMakeLists.txt bindings/c src/parser.c src/tree_sitter
+ if ($LASTEXITCODE -ne 0) { throw 'Native parser packaging failed.' }
+}
+finally {
+ Pop-Location
+}