Skip to content
Open
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
123 changes: 123 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:

jobs:
test:
name: Test
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

- name: Install npm dependencies
run: npm ci

- name: Run tests
run: dotnet test src/XamlLanguageServer.Wpf.Tests/XamlLanguageServer.Wpf.Tests.csproj --logger "console;verbosity=normal"

package:
name: Package (${{ matrix.target }})
needs: test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: win32-x64
rid: win-x64
platformTarget: x64
os: windows-latest
- target: win32-arm64
rid: win-arm64
platformTarget: ARM64
os: windows-11-arm
- target: darwin-arm64
rid: osx-arm64
platformTarget: ''
os: macos-latest
- target: darwin-x64
rid: osx-x64
platformTarget: ''
os: macos-13
- target: linux-x64
rid: linux-x64
platformTarget: ''
os: ubuntu-latest
- target: linux-arm64
rid: linux-arm64
platformTarget: ''
os: ubuntu-24.04-arm

steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

- name: Install npm dependencies
run: npm ci

- name: Package VSIX
shell: pwsh
run: ./dist.all.ps1 -Target ${{ matrix.target }} -RuntimeIdentifier '${{ matrix.rid }}' -PlatformTarget '${{ matrix.platformTarget }}'

- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: vsix-${{ matrix.target }}
path: '*.vsix'
if-no-files-found: error

publish:
name: Publish to Marketplace
needs: package
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download VSIX artifacts
uses: actions/download-artifact@v4
with:
pattern: vsix-*
merge-multiple: true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Publish to VS Code Marketplace
run: npx "@vscode/vsce" publish --packagePath *.vsix
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
56 changes: 56 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ Both fail if compilation is Tier-1 (no user types).

## Build

### Prerequisites

Initialize submodules before building designer or language server components:

```bash
git submodule update --init --recursive
```

### Local development

```bash
# Build WPF language server
dotnet build src/XamlLanguageServer.Wpf/XamlLanguageServer.Wpf.csproj
Expand All @@ -80,3 +90,49 @@ dotnet build external/wxsg/external/XamlToCSharpGenerator/src/XamlToCSharpGenera
# Run tests
dotnet test src/XamlLanguageServer.Wpf.Tests/
```

### Release packaging

Platform-specific VSIX packages are built via [`dist.all.ps1`](dist.all.ps1). Architecture is handled with a hybrid model, both values derived automatically from `-Target`:

- **RID** (e.g. `win-x64`) controls the .NET Core apphost bitness — modern `XamlDesigner.exe`, `wpf-xaml-ls.exe`, `wpf-project-analyzer`. This is what makes cross-publishing work.
- **`PlatformTarget`** (e.g. `x64`/`ARM64`) controls the net481 (.NET Framework) designer, where a RID is not meaningful.
- The `WpfHotReload.Runtime` helper DLLs stay AnyCPU (no override) — they are injected into the user's process and must load on any arch.

```powershell
# Full Windows x64 release package (designer, hot reload, language server, analyzer)
pwsh ./dist.all.ps1 -Target win32-x64

# Windows ARM64
pwsh ./dist.all.ps1 -Target win32-arm64

# Generic local dev package (no --target; uses host architecture, no R2R)
pwsh ./dist.all.ps1
```

Non-Windows targets (`darwin-*`, `linux-*`) ship the TypeScript extension and cross-platform project analyzer only; Windows-only tools (designer, hot reload, language server) are omitted.

#### Local cross-publishing (before CI `VSCE_PAT` is configured)

Because the RID is derived from `-Target`, you can build any platform's VSIX from any host machine (e.g. produce a correct x64 package on an ARM laptop). Build every target in one command with `-All`:

```powershell
# Build all six platform VSIXes locally
pwsh ./dist.all.ps1 -All

# Build AND publish all six (authenticate first with `npx @vscode/vsce login <publisher>`,
# or set the VSCE_PAT environment variable)
pwsh ./dist.all.ps1 -All -Publish
```

`-All` and `-Target` are mutually exclusive. R2R is enabled only when a RID is present (so the generic no-target build skips it).

### CI

GitHub Actions ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs on every push/PR to `main`:

1. **test** — `dotnet test` on `windows-latest`
2. **package** — matrix builds six platform VSIXes (`win32-x64`, `win32-arm64`, `darwin-x64`, `darwin-arm64`, `linux-x64`, `linux-arm64`)
3. **publish** — on version tags (`v*`), publishes all VSIX artifacts to the Marketplace (requires `VSCE_PAT` secret)

All matrix legs must succeed before a platform-specific release is published; Marketplace routes each target to the matching VS Code host.
175 changes: 133 additions & 42 deletions dist.all.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Param(
[switch]$Publish
[switch]$Publish,
[ValidateSet('win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64', '')]
[string]$Target = '',
[switch]$All,
[string]$RuntimeIdentifier = '',
[string]$PlatformTarget = ''
)

Set-StrictMode -Version Latest
Expand All @@ -8,10 +13,116 @@ $ErrorActionPreference = 'Stop'
$here = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
Set-Location $here

# Build a single generic .vsix for all platforms (no platform-specific targets)
# Previously we created platform-specific vsix packages; now produce one package.
# Keep the $targets variable empty for compatibility with older scripts.
$targets = @()
# RID drives .NET Core apphost bitness (XamlDesigner modern, language server, analyzer).
# Deriving it from the vsce target lets the author cross-publish locally: e.g. building
# win32-x64 on an ARM machine still emits x64 apphosts.
$targetToRid = @{
'win32-x64' = 'win-x64'
'win32-arm64' = 'win-arm64'
'darwin-x64' = 'osx-x64'
'darwin-arm64' = 'osx-arm64'
'linux-x64' = 'linux-x64'
'linux-arm64' = 'linux-arm64'
}

# PlatformTarget drives the net481 (.NET Framework) designer bitness, where RID is not meaningful.
$targetToPlatformTarget = @{
'win32-x64' = 'x64'
'win32-arm64' = 'ARM64'
}

$allTargets = @('win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64')

if ($All -and $Target) {
throw 'Specify either -All or -Target, not both.'
}

function Get-RidArgs {
param([string]$Rid)
if ($Rid) { return @('-r', $Rid) }
return @()
}

function Get-PlatformTargetArgs {
param([string]$Cpu)
if ($Cpu) { return @("-p:PlatformTarget=$Cpu") }
return @()
}

function Build-Target {
param(
[string]$Tgt,
[string]$Rid,
[string]$Cpu
)

$isWindowsTarget = $Tgt -match '^win32-'
$buildWindowsTools = (-not $Tgt) -or $isWindowsTarget

if ($buildWindowsTools) {
Write-Host "Building packaged XamlDesigner variants (modern RID '$Rid' / net481 PlatformTarget '$Cpu')..."
$designerArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass',
'-File', (Join-Path $here 'scripts\build-designer.ps1'),
'-Configuration', 'Release'
)
if ($Rid) { $designerArgs += @('-RuntimeIdentifier', $Rid) }
if ($Cpu) { $designerArgs += @('-PlatformTarget', $Cpu) }
pwsh @designerArgs
if ($LASTEXITCODE -ne 0) { throw 'Failed to build XamlDesigner variants' }

# Hot reload helper is an AnyCPU library injected into the user's process; no arch override.
Write-Host 'Building WpfHotReload.Runtime helper (netcoreapp3.0 + net462)...'
$helperProj = Join-Path $here 'src\WpfHotReload.Runtime\WpfHotReload.Runtime.csproj'
foreach ($tfm in @('netcoreapp3.0', 'net462')) {
$outDir = Join-Path $here "tools\WpfHotReload.Runtime\$tfm\"
dotnet build $helperProj -c Release -f $tfm -nologo "-p:OutDir=$outDir" '-p:EnableWindowsTargeting=true'
if ($LASTEXITCODE -ne 0) { throw "Failed to build WpfHotReload.Runtime for $tfm" }
}

Write-Host 'Building XAML Language Server...'
$lsProj = Join-Path $here 'src\XamlLanguageServer.Wpf\XamlLanguageServer.Wpf.csproj'
$lsOut = Join-Path $here 'tools\XamlLanguageServer'
$lsArgs = @('publish', $lsProj, '-c', 'Release', '--output', $lsOut, '--no-self-contained')
$lsArgs += Get-RidArgs -Rid $Rid
dotnet @lsArgs
if ($LASTEXITCODE -ne 0) { throw 'Failed to build XamlLanguageServer' }
}

Write-Host 'Building WPF Project Analyzer...'
$analyzerProj = Join-Path $here 'src\WpfProjectAnalyzer\WpfProjectAnalyzer.csproj'
$analyzerOut = Join-Path $here 'tools\WpfProjectAnalyzer'
$analyzerArgs = @('publish', $analyzerProj, '-c', 'Release', '--output', $analyzerOut, '--no-self-contained')
$analyzerArgs += Get-RidArgs -Rid $Rid
dotnet @analyzerArgs
if ($LASTEXITCODE -ne 0) { throw 'Failed to build WpfProjectAnalyzer' }

if ($Tgt) {
$vsixName = "$($script:pkgName)-$($script:pkgVersion)-$Tgt.vsix"
Write-Host "Packaging platform-specific .vsix as $vsixName (target: $Tgt)..."
npx -y "@vscode/vsce" package --target $Tgt --out $vsixName
}
else {
$vsixName = "$($script:pkgName)-$($script:pkgVersion).vsix"
Write-Host "Packaging generic .vsix as $vsixName..."
npx -y "@vscode/vsce" package --out $vsixName
}
if ($LASTEXITCODE -ne 0) { throw 'Failed to create .vsix package' }
Write-Host "Created $vsixName"

if ($Publish) {
Write-Host "Publishing $($script:pkgName) to Marketplace (requires vsce login or VSCE_PAT)..."
if ($Tgt) {
npx -y "@vscode/vsce" publish --packagePath $vsixName
}
else {
npx -y "@vscode/vsce" publish
}
if ($LASTEXITCODE -ne 0) { throw 'Failed to publish .vsix to Marketplace' }
}

return $vsixName
}

Write-Host 'Removing old .vsix files from destination folder...'
Get-ChildItem -Path $here -Filter *.vsix -File | Remove-Item -Force
Expand All @@ -22,46 +133,26 @@ Write-Host 'Syncing package.json version from latest git tag (if present)...'
Write-Host 'Building extension (esbuild)...'
npm run build

Write-Host 'Building packaged XamlDesigner variants (modern + net481)...'
pwsh -NoProfile -ExecutionPolicy Bypass -File (Join-Path $here 'scripts\build-designer.ps1') -Configuration Release
if ($LASTEXITCODE -ne 0) { throw 'Failed to build XamlDesigner variants' }

Write-Host 'Building WpfHotReload.Runtime helper (netcoreapp3.0 + net462)...'
$helperProj = Join-Path $here 'src\WpfHotReload.Runtime\WpfHotReload.Runtime.csproj'
foreach ($tfm in @('netcoreapp3.0', 'net462')) {
$outDir = Join-Path $here "tools\WpfHotReload.Runtime\$tfm\"
# EnableWindowsTargeting is required when building Windows-targeting TFMs on macOS/Linux
dotnet build $helperProj -c Release -f $tfm -nologo "-p:OutDir=$outDir" "-p:EnableWindowsTargeting=true"
if ($LASTEXITCODE -ne 0) { throw "Failed to build WpfHotReload.Runtime for $tfm" }
}

Write-Host 'Building XAML Language Server...'
$lsProj = Join-Path $here 'src\XamlLanguageServer.Wpf\XamlLanguageServer.Wpf.csproj'
$lsOut = Join-Path $here 'tools\XamlLanguageServer'
dotnet publish $lsProj -c Release --output $lsOut --no-self-contained
if ($LASTEXITCODE -ne 0) { throw 'Failed to build XamlLanguageServer' }

Write-Host 'Building WPF Project Analyzer...'
$analyzerProj = Join-Path $here 'src\WpfProjectAnalyzer\WpfProjectAnalyzer.csproj'
$analyzerOut = Join-Path $here 'tools\WpfProjectAnalyzer'
dotnet publish $analyzerProj -c Release --output $analyzerOut --no-self-contained
if ($LASTEXITCODE -ne 0) { throw 'Failed to build WpfProjectAnalyzer' }

$pkg = Get-Content package.json -Raw | ConvertFrom-Json
$script:pkgName = $pkg.name
$script:pkgVersion = $pkg.version
$createdVsix = @()

# Create one generic .vsix for all platforms
$vsixName = "$($pkg.name)-$($pkg.version).vsix"
Write-Host "Packaging generic .vsix as $vsixName..."
npx -y vsce package --out $vsixName
if ($LASTEXITCODE -ne 0) { throw 'Failed to create .vsix package' }
$createdVsix += $vsixName
Write-Host "Created $vsixName"

if ($Publish) {
Write-Host "Publishing $($pkg.name) to Marketplace (requires vsce login)..."
npx -y vsce publish
if ($LASTEXITCODE -ne 0) { throw 'Failed to publish .vsix to Marketplace' }
if ($All) {
foreach ($t in $allTargets) {
$rid = $targetToRid[$t]
$cpu = if ($targetToPlatformTarget.ContainsKey($t)) { $targetToPlatformTarget[$t] } else { '' }
$createdVsix += Build-Target -Tgt $t -Rid $rid -Cpu $cpu
}
}
else {
if ($Target -and -not $RuntimeIdentifier) {
$RuntimeIdentifier = $targetToRid[$Target]
}
if ($Target -and -not $PlatformTarget -and $targetToPlatformTarget.ContainsKey($Target)) {
$PlatformTarget = $targetToPlatformTarget[$Target]
}
$createdVsix += Build-Target -Tgt $Target -Rid $RuntimeIdentifier -Cpu $PlatformTarget
}

Write-Host ''
Expand Down
Loading