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
108 changes: 108 additions & 0 deletions sdk_v2/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# sdk_v2 — Developer Setup

A first-time contributor should be able to install the tools listed below,
clone the repo, then run the one-shot build/test script from this directory
and watch all four SDKs go green:

```powershell
pwsh ./build_and_test_all.ps1
```

If that passes, your machine is correctly configured.

## Prerequisites

All four SDKs (C++, C#, Python, JS/TS) build on **Windows**, **Linux**, and
**macOS**. The WinML variant (`-UseWinml`) is Windows-only.

### All platforms

| Tool | Minimum version | Notes |
| ---------------- | --------------- | ------------------------------------------------------------------------------------- |
| Git | recent | LFS is **not** required. |
| CMake | 3.20 | Driven by `sdk_v2/cpp/build.py`; do not invoke `cmake --build` directly. |
| vcpkg | recent | Set `VCPKG_ROOT`, or use the copy bundled with Visual Studio (auto-detected). |
| Python | 3.11–3.14, **64-bit** | Required by `build.py` and for the Python SDK. 32-bit Python will not work. |
| .NET SDK | 8.0 | C# tests target `net8.0` (and `net462` on Windows, which comes from .NET Framework Targeting Pack via VS). |
| Node.js | 20 LTS or newer | Brings `npm`. The JS SDK declares `"engines": { "node": ">=20" }`. |
| PowerShell | 7+ (`pwsh`) | The one-shot script and `samples/js/test-v2.ps1` are written for PowerShell 7. |

### Windows-only

| Tool | Version | Notes |
| --------------------------- | ------------------------ | --------------------------------------------------------------------- |
| Visual Studio 2026 (v18) | Enterprise / Professional / Community | Install the **Desktop development with C++** and **.NET desktop development** workloads. Provides MSVC, Windows SDK, and vcpkg. |
| Windows SDK | 10.0.26100 (VS workload) | Needed for the WinML target framework. |
| .NET Framework 4.6.2 Targeting Pack | (VS workload) | One C# test target framework is `net462`. |

Launch your dev shell with **x64** explicitly — `Enter-VsDevShell` defaults
to x86 and silently breaks the Python cffi extension build:

```powershell
pwsh -NoExit -Command "& { Import-Module 'C:\Program Files\Microsoft Visual Studio\18\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll'; Enter-VsDevShell <instance-id> -SkipAutomaticLocation -Arch amd64 -HostArch amd64 }"
```

Verify with `echo $env:VSCMD_ARG_TGT_ARCH` — must be `x64`. (The one-shot
script also forces `VSCMD_ARG_TGT_ARCH=x64` around the Python step as a
safety net, but the C++ step does not, so getting the shell right matters.)

### Linux

* GCC 11+ or Clang 14+ (C++20 with `<format>` support).
* `build-essential`, `ninja-build`, `pkg-config`, `curl`, `zip`, `unzip`, `tar`.
* vcpkg dependencies: `autoconf`, `automake`, `libtool`, `python3-pip`.

### macOS

* Xcode 15+ command-line tools (Clang with C++20).
* Homebrew packages: `cmake`, `ninja`, `pkg-config`, `python@3.11`, `node`, `dotnet-sdk`.

## What gets installed per-SDK

The one-shot script does not pre-install language toolchains, but it does
install per-SDK package dependencies on first run:

| SDK | What runs |
| ------ | -------------------------------------------------------------------------------------- |
| C++ | `python build.py [--config ...] [--use_winml] [--skip_tests]` — configure + build + ctest. vcpkg restores native deps; ORT/GenAI come from NuGet via FetchContent (versions from `sdk_v2/deps_versions.json` or `_winml.json`). |
| C# | `dotnet test Microsoft.AI.Foundry.Local.SDK.sln -c Release [-p:UseWinML=true]` — restores NuGet packages on demand. |
| Python | `python -m pip install -e .[dev]` (compiles the cffi extension; needs MSVC/Clang) → `python -m pytest test/`. |
| JS | `npm install` (runs `node-gyp` against the C++ build output) → `npm run build` → `npm test` (vitest). |

## Common knobs

```powershell
# Full build + test, default config (RelWithDebInfo / Release).
pwsh ./build_and_test_all.ps1

# WinML variant across all SDKs (Windows only).
pwsh ./build_and_test_all.ps1 -UseWinml

# Just rebuild the native and the JS bindings, skip the slow C++ test pass.
pwsh ./build_and_test_all.ps1 -Only cpp,js -SkipCppTests

# Skip one SDK.
pwsh ./build_and_test_all.ps1 -Skip python

# Keep going past failures and print the summary at the end.
pwsh ./build_and_test_all.ps1 -ContinueOnError
```

See `pwsh ./build_and_test_all.ps1 -?` for the full parameter list.

## Troubleshooting

* **`cl.exe ... HostX86\x86\cl.exe` during pip install** — your shell has
`VSCMD_ARG_TGT_ARCH=x86`. Re-launch with `-Arch amd64 -HostArch amd64`
(see above). The one-shot script also guards against this for the Python
step.
* **Python build fails with `__stdcall`/`__cdecl` errors** — same root cause:
cl.exe is targeting x86. Fix the shell.
* **C# tests load the wrong native** — never invoke `cmake --build` directly
or pass `--build_dir` to `build.py`. The C# tests pin an absolute path to
`sdk_v2/cpp/build/<Platform>/<Config>/`; bypassing `build.py` puts the
binary somewhere else.
* **Switching between WinML and non-WinML** — the C++ FetchContent NuGet
packages (`Microsoft.ML.OnnxRuntime.Foundry` vs `.WinML`) are cached and
do not auto-refresh on variant flip. If you hit linker errors after
toggling `-UseWinml`, wipe `sdk_v2/cpp/build/` and rerun.
261 changes: 261 additions & 0 deletions sdk_v2/build_and_test_all.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<#
.SYNOPSIS
Build and test all sdk_v2 SDKs (C++, C#, Python, JS) in one shot.

.DESCRIPTION
The simple developer "build and run all tests" one-shot script for sdk_v2.

Order:
1. C++ — python build.py (configure + build + test)
2. C# — dotnet test (builds via project references)
3. Python — pip install -e . then pytest
4. JS — npm install + npm run build + npm test

Each SDK runs in its own step. The script stops on the first failure
unless -ContinueOnError is supplied, and prints a per-SDK pass/fail
summary at the end.

.PARAMETER UseWinml
Build the WinML variant across all SDKs:
* C++: passes --use_winml to build.py
* C#: passes -p:UseWinML=true to dotnet test
* Python: sets FL_PYTHON_PACKAGE_NAME=foundry-local-sdk-winml before pip install
* JS: rebuilds the native addon against the WinML C++ build
Windows only.

.PARAMETER Config
C++ / C# build configuration. Default: RelWithDebInfo. Maps to dotnet
Configuration=Release when set to RelWithDebInfo or Release.

.PARAMETER Skip
SDKs to skip. Any of: cpp, cs, python, js.

.PARAMETER Only
Run only the named SDKs. Overrides -Skip. Any of: cpp, cs, python, js.

.PARAMETER ContinueOnError
Keep going after a failure instead of aborting on the first one.

.PARAMETER SkipCppTests
Build C++ but skip the integration test run (still builds tests).
Useful when you only want to rebuild the native library for the
downstream SDKs.

.EXAMPLE
pwsh ./build_and_test_all.ps1
# Full build + test, no WinML.

.EXAMPLE
pwsh ./build_and_test_all.ps1 -UseWinml
# Full build + test against the WinML variant.

.EXAMPLE
pwsh ./build_and_test_all.ps1 -Only cpp,js -SkipCppTests
# Rebuild C++ (skip its tests) then build + test the JS SDK.
#>
[CmdletBinding()]
param(
[switch] $UseWinml,
[ValidateSet('Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel')]
[string] $Config = 'RelWithDebInfo',
[ValidateSet('cpp', 'cs', 'python', 'js')]
[string[]] $Skip = @(),
[ValidateSet('cpp', 'cs', 'python', 'js')]
[string[]] $Only,
[switch] $ContinueOnError,
[switch] $SkipCppTests
)

$ErrorActionPreference = 'Stop'

$sdkRoot = $PSScriptRoot
$cppDir = Join-Path $sdkRoot 'cpp'
$csDir = Join-Path $sdkRoot 'cs'
$pythonDir = Join-Path $sdkRoot 'python'
$jsDir = Join-Path $sdkRoot 'js'

if ($UseWinml -and -not $IsWindows -and -not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows)) {
throw "-UseWinml is Windows-only."
}
Comment on lines +77 to +80

# Resolve which SDKs to run.
$all = @('cpp', 'cs', 'python', 'js')
if ($Only) {
$targets = $all | Where-Object { $_ -in $Only }
} else {
$targets = $all | Where-Object { $_ -notin $Skip }
}
Comment on lines +75 to +88
if (-not $targets) {
Write-Host "Nothing to do." -ForegroundColor Yellow
return
}

# .NET Configuration maps RelWithDebInfo/MinSizeRel -> Release.
$dotnetConfig = if ($Config -in @('Debug')) { 'Debug' } else { 'Release' }
Comment on lines +94 to +95

$results = New-Object System.Collections.Generic.List[object]
$overallStart = Get-Date

function Invoke-Step {
param(
[string] $Name,
[scriptblock] $Action
)
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host "==> [$Name] start (UseWinml=$UseWinml, Config=$Config)" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
$start = Get-Date
$ok = $false
$note = ''
try {
& $Action
$ok = $true
} catch {
$note = $_.Exception.Message
Write-Host "[$Name] FAILED: $note" -ForegroundColor Red
if (-not $ContinueOnError) {
$script:results.Add([pscustomobject]@{
Sdk = $Name
Result = 'FAIL'
Duration = ((Get-Date) - $start).ToString('mm\:ss')
Note = $note
})
throw
}
}
$script:results.Add([pscustomobject]@{
Sdk = $Name
Result = if ($ok) { 'OK' } else { 'FAIL' }
Duration = ((Get-Date) - $start).ToString('mm\:ss')
Note = $note
})
}

try {
if ('cpp' -in $targets) {
Invoke-Step 'cpp' {
$args = @('build.py', '--config', $Config)
if ($UseWinml) { $args += '--use_winml' }
if ($SkipCppTests) { $args += '--skip_tests' }
Push-Location $cppDir
try {
# build.py drives configure + build + test by default.
python @args
if ($LASTEXITCODE -ne 0) { throw "C++ build.py exit $LASTEXITCODE" }
} finally {
Pop-Location
}
}
}

if ('cs' -in $targets) {
Invoke-Step 'cs' {
Push-Location $csDir
try {
$dotnetArgs = @(
'test',
'Microsoft.AI.Foundry.Local.SDK.sln',
'-c', $dotnetConfig,
'--nologo'
)
if ($UseWinml) { $dotnetArgs += '-p:UseWinML=true' }
dotnet @dotnetArgs
if ($LASTEXITCODE -ne 0) { throw "dotnet test exit $LASTEXITCODE" }
} finally {
Pop-Location
}
}
}

if ('python' -in $targets) {
Invoke-Step 'python' {
Push-Location $pythonDir
try {
# Sanity check: the cffi extension links against an x64 foundry_local.dll,
# so the Python interpreter MUST be 64-bit. A 32-bit Python here causes
# cl.exe to compile for x86, which produces __stdcall/__cdecl mismatches
# when verifying the function-pointer table in foundry_local_c.h.
$pyInfo = python -c @"
import struct, sys, sysconfig
print(struct.calcsize('P') * 8)
print(sysconfig.get_platform())
print(sys.executable)
"@
if ($LASTEXITCODE -ne 0) { throw "python probe exit $LASTEXITCODE" }
$bits, $plat, $exe = $pyInfo -split "`r?`n" | Where-Object { $_ }
Write-Host "Using Python: $exe ($bits-bit, $plat)" -ForegroundColor DarkGray
if ($bits -ne '64') {
throw "Python at $exe is $bits-bit; sdk_v2/python requires a 64-bit interpreter."
}

# On Windows, force setuptools' MSVC selection to target x64 regardless of
# any inherited VSCMD/Platform state from a previous Developer Prompt.
$restoreTgt = $env:VSCMD_ARG_TGT_ARCH
$restoreHost = $env:VSCMD_ARG_HOST_ARCH
$restorePlat = $env:Platform
if ($IsWindows -or [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows)) {
$env:VSCMD_ARG_TGT_ARCH = 'x64'
$env:VSCMD_ARG_HOST_ARCH = 'x64'
$env:Platform = 'x64'
}

$env:FL_PYTHON_PACKAGE_NAME =
if ($UseWinml) { 'foundry-local-sdk-winml' } else { 'foundry-local-sdk' }
try {
python -m pip install -e '.[dev]'
if ($LASTEXITCODE -ne 0) { throw "pip install exit $LASTEXITCODE" }

python -m pytest test/ -v
if ($LASTEXITCODE -ne 0) { throw "pytest exit $LASTEXITCODE" }
} finally {
Remove-Item Env:FL_PYTHON_PACKAGE_NAME -ErrorAction SilentlyContinue
if ($null -eq $restoreTgt) { Remove-Item Env:VSCMD_ARG_TGT_ARCH -ErrorAction SilentlyContinue } else { $env:VSCMD_ARG_TGT_ARCH = $restoreTgt }
if ($null -eq $restoreHost) { Remove-Item Env:VSCMD_ARG_HOST_ARCH -ErrorAction SilentlyContinue } else { $env:VSCMD_ARG_HOST_ARCH = $restoreHost }
if ($null -eq $restorePlat) { Remove-Item Env:Platform -ErrorAction SilentlyContinue } else { $env:Platform = $restorePlat }
}
} finally {
Pop-Location
}
}
}

if ('js' -in $targets) {
Invoke-Step 'js' {
Push-Location $jsDir
try {
npm install
if ($LASTEXITCODE -ne 0) { throw "npm install exit $LASTEXITCODE" }

# JS picks up the native library copied from the C++ build dir;
# the WinML/non-WinML distinction is whichever C++ build ran above.
npm run build
if ($LASTEXITCODE -ne 0) { throw "npm run build exit $LASTEXITCODE" }

npm test
if ($LASTEXITCODE -ne 0) { throw "npm test exit $LASTEXITCODE" }
} finally {
Pop-Location
}
}
}
} catch {
# Already recorded by Invoke-Step. Fall through to summary.
}

Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host "Summary (total: $(((Get-Date) - $overallStart).ToString('mm\:ss')))" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
$results | Format-Table -AutoSize | Out-String | Write-Host

$failed = $results | Where-Object { $_.Result -ne 'OK' }
if ($failed) {
Write-Host "FAILED: $($failed.Sdk -join ', ')" -ForegroundColor Red
exit 1
} else {
Write-Host "All SDKs passed." -ForegroundColor Green
exit 0
}
3 changes: 3 additions & 0 deletions sdk_v2/cpp/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@ def configure(args: argparse.Namespace) -> None:
command += ["-DFOUNDRY_LOCAL_USE_WINML=ON"]
if args.winml_sdk_version:
command += [f"-DWINML_SDK_VERSION={args.winml_sdk_version}"]
else:
# Pass explicitly so a re-configure without the flag clears any cached ON value.
command += ["-DFOUNDRY_LOCAL_USE_WINML=OFF"]

if args.ort_home:
command += [f"-DORT_HOME={args.ort_home}"]
Expand Down