diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..0f07bb74f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,37 @@ +# GitHub Copilot Instructions (WindowsAppSDK-Samples) + +Read scripts first; improve their headers instead of duplicating detail here. + +## Build +Use only `build.ps1` / `build.cmd`. Read `build.ps1` before invoking. It auto: detects platform, picks solutions (current dir > selected sample > all), initializes VS DevShell, restores via local nuget, emits `.binlog` per solution. Do NOT hand-roll msbuild/nuget restore logic. + +## Versions +Run `UpdateVersions.ps1` only after reading it. Get the WinAppSDK version string from: https://www.nuget.org/packages/Microsoft.WindowsAppSdk/ (stable/preview/servicing) and pass as `-WinAppSDKVersion`. If script params unclear, fix that script. + +## Coding Guidelines +- Small, focused diffs; no mass reformatting; ensure SOLID principles. +- Preserve APIs, encoding, line endings, relative paths. +- Support arm64 & x64 paths. +- PowerShell: approved verbs; each new script has Synopsis/Description/Parameters/Examples header. +- C#/C++: follow existing style; lean headers; forward declare where practical; keep samples illustrative. +- Minimal necessary comments; avoid noise. Centralize user strings for localization. +- Never log secrets or absolute external paths. + +## PR Guidance +- One intent per PR. Update script README/header if behavior changes. +- Provide summary: what / why / validation. +- Run a targeted build (e.g. `pwsh -File build.ps1 -Sample AppLifecycle`). +- For version bumps: inspect at least one changed project file. +- No new warnings/errors or large cosmetic churn. + +## Design Docs (Large / Cross-Sample) +Before broad edits create `DESIGN-.md`: +- Single sample: `Samples//DESIGN-.md` +- Multi-sample/shared: repo root. +Include: Problem, Goals/Non-Goals, Affected Areas, Approach, Risks, Validation Plan. Reference doc in PR. + +## When Unsure +Draft a design doc or WIP PR summarizing assumptions—don't guess. + +--- +Keep this file lean; source-of-truth for behavior lives in script headers. \ No newline at end of file diff --git a/UpdateVersions.ps1 b/UpdateVersions.ps1 index 32e9744e4..4a8eff594 100644 --- a/UpdateVersions.ps1 +++ b/UpdateVersions.ps1 @@ -1,8 +1,50 @@ Param( - [string]$WinAppSDKVersion = "", + [Parameter(Mandatory=$true)] + [string]$WinAppSDKVersion, [string]$NuGetPackagesFolder = "" ) +# Prerequisites +# If the NuGetPackagesFolder parameter wasn't provided, +# Install the version of WinAppSDK in 'packages' folder first, +if ($NuGetPackagesFolder -eq "") { + $NuGetPackagesFolder = Join-Path $PSScriptRoot "packages" + Write-Host "NuGetPackagesFolder not supplied. Using default: $NuGetPackagesFolder" + + if (!(Test-Path $NuGetPackagesFolder)) { + Write-Host "Packages folder not found. Will perform a minimal restore to populate it." + } + + $nugetToolDir = Join-Path $PSScriptRoot ".nuget" + $nugetExe = Join-Path $nugetToolDir "nuget.exe" + if (!(Test-Path $nugetExe)) { + if (!(Test-Path $nugetToolDir)) { New-Item -ItemType Directory -Path $nugetToolDir | Out-Null } + Write-Host "Downloading nuget.exe..." + Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nugetExe + } + + # If the WinAppSDK package (any variant) isn't already present, install just that + # package (and its dependencies) at the requested version directly. + $hasWinAppSdk = Get-ChildItem -Path $NuGetPackagesFolder -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "Microsoft.WindowsAppSDK.*" } | Select-Object -First 1 + if (-not $hasWinAppSdk) { + if ([string]::IsNullOrWhiteSpace($WinAppSDKVersion)) { + Write-Warning "WinAppSDKVersion not supplied; cannot install Microsoft.WindowsAppSDK package automatically." + } else { + if (!(Test-Path $NuGetPackagesFolder)) { New-Item -ItemType Directory -Path $NuGetPackagesFolder | Out-Null } + Write-Host "Installing Microsoft.WindowsAppSDK $WinAppSDKVersion into $NuGetPackagesFolder (running inside folder)" + Push-Location $NuGetPackagesFolder + try { + & $nugetExe install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -OutputDirectory . -Prerelease -DependencyVersion Highest | Write-Host + } + finally { + Pop-Location + } + } + } else { + Write-Host "Detected existing Microsoft.WindowsAppSDK package; skipping install." + } +} + # First, add the metapackage $nugetPackageToVersionTable = @{"Microsoft.WindowsAppSDK" = $WinAppSDKVersion} diff --git a/build.cmd b/build.cmd index 146a74ea0..6622e01f3 100644 --- a/build.cmd +++ b/build.cmd @@ -1,79 +1,21 @@ @echo off +:: Thin wrapper calling PowerShell implementation +set SCRIPT_DIR=%~dp0 +set PS_SCRIPT=%SCRIPT_DIR%build.ps1 + if "%1"=="/?" goto :usage if "%1"=="-?" goto :usage -if "%VSINSTALLDIR%" == "" goto :usage - -setlocal enabledelayedexpansion enableextensions - -set BUILDCMDSTARTTIME=%time% - -set platform=%1 -set configuration=%2 -set sample_filter=%3\ - -if "%platform%"=="" set platform=x64 -if "%configuration%"=="" set configuration=Release - -if not exist ".\.nuget" mkdir ".\.nuget" -if not exist ".\.nuget\nuget.exe" powershell -Command "Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile .\.nuget\nuget.exe" - -set NUGET_RESTORE_MSBUILD_ARGS=/p:PublishReadyToRun=true -for /f "delims=" %%D in ('dir /s/b samples\%sample_filter%*.sln') do ( - call .nuget\nuget.exe restore "%%D" -configfile Samples\nuget.config -PackagesDirectory %~dp0packages - call msbuild /warnaserror /p:platform=%platform% /p:configuration=%configuration% /p:NugetPackageDirectory=%~dp0packages /bl:"%%~nD.binlog" "%%D" - - if ERRORLEVEL 1 goto :eof -) - -:showDurationAndExit -set BUILDCMDENDTIME=%time% -:: Note: The '1's in this line are to convert a value like "08" to "108", since numbers which -:: begin with '0' are interpreted as octal, which makes "08" and "09" invalid. Adding the -:: '1's effectively adds 100 to both sides of the subtraction, avoiding this issue. -:: Hours has a leading space instead of 0, so the '1's trick isn't used on that one. -set /a BUILDDURATION_HRS= %BUILDCMDENDTIME:~0,2%- %BUILDCMDSTARTTIME:~0,2% -set /a BUILDDURATION_MIN=1%BUILDCMDENDTIME:~3,2%-1%BUILDCMDSTARTTIME:~3,2% -set /a BUILDDURATION_SEC=1%BUILDCMDENDTIME:~6,2%-1%BUILDCMDSTARTTIME:~6,2% -set /a BUILDDURATION_HSC=1%BUILDCMDENDTIME:~9,2%-1%BUILDCMDSTARTTIME:~9,2% -if %BUILDDURATION_HSC% lss 0 ( - set /a BUILDDURATION_HSC=!BUILDDURATION_HSC!+100 - set /a BUILDDURATION_SEC=!BUILDDURATION_SEC!-1 -) -if %BUILDDURATION_SEC% lss 0 ( - set /a BUILDDURATION_SEC=!BUILDDURATION_SEC!+60 - set /a BUILDDURATION_MIN=!BUILDDURATION_MIN!-1 -) -if %BUILDDURATION_MIN% lss 0 ( - set /a BUILDDURATION_MIN=!BUILDDURATION_MIN!+60 - set /a BUILDDURATION_HRS=!BUILDDURATION_HRS!-1 -) -if %BUILDDURATION_HRS% lss 0 ( - set /a BUILDDURATION_HRS=!BUILDDURATION_HRS!+24 -) -:: Add a '0' at the start to ensure at least two digits. The output will then just -:: show the last two digits for each. -set BUILDDURATION_HRS=0%BUILDDURATION_HRS% -set BUILDDURATION_MIN=0%BUILDDURATION_MIN% -set BUILDDURATION_SEC=0%BUILDDURATION_SEC% -set BUILDDURATION_HSC=0%BUILDDURATION_HSC% -echo --- -echo Start time: %BUILDCMDSTARTTIME%. End time: %BUILDCMDENDTIME% -echo Elapsed: %BUILDDURATION_HRS:~-2%:%BUILDDURATION_MIN:~-2%:%BUILDDURATION_SEC:~-2%.%BUILDDURATION_HSC:~-2% -endlocal - -goto :eof +:: Forward all arguments directly; build.ps1 handles defaults/validation +powershell -NoProfile -ExecutionPolicy Bypass -File "%PS_SCRIPT%" %* +exit /b %ERRORLEVEL% :usage echo Usage: -echo This script should be run under a Visual Studio Developer Command Prompt. -echo. echo build.cmd [Platform] [Configuration] [Sample] echo. -echo [Platform] Either x86, x64, or arm64. Default is x64. -echo [Configuration] Either Debug or Release. Default is Release. -echo [Sample] The sample folder under Samples to build. If none specified, all samples are built. -echo. -echo If no parameters are specified, all samples are built for x64 Release. - -exit /b /1 \ No newline at end of file +echo (Wrapper over build.ps1) +echo Platform: x86|x64|arm64 (default x64) +echo Configuration: Debug|Release (default Release) +echo Sample: Optional sample folder name under Samples +exit /b 1 \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..805902ea3 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,163 @@ +<#! +.SYNOPSIS + Builds WindowsAppSDK Samples solutions (or local solution(s)). +.DESCRIPTION + PowerShell port of the original build.cmd logic. + * Ensures Visual Studio Developer environment (DevShell) is initialized. + * Downloads a local nuget.exe if not present and restores packages. + * Builds one or more .sln files with MSBuild producing a .binlog per solution. + + Solution discovery order: + 1. If -Sample is specified: build all .sln files under Samples/. + 2. Else if the current working directory (where you invoked the script) contains one or more .sln files: build those only (no recursion). + 3. Else build all .sln files under Samples (recursive). + +.PARAMETER Platform + Target platform (x86, x64, arm64, auto). Default: auto (arm64 on ARM64 OS, else x64). +.PARAMETER Configuration + Build configuration (Debug or Release). Default: Release. +.PARAMETER Sample + Optional sample folder name (child of Samples) to restrict the build scope. +.PARAMETER Help + Show usage information. + +.EXAMPLE + ./build.ps1 + (Auto-detect platform, build local solutions or all samples.) + +.EXAMPLE + ./build.ps1 -Platform arm64 -Configuration Debug -Sample AppLifecycle + (Build only the AppLifecycle sample solutions for arm64 Debug.) +#> +#[CmdletBinding()] parameters +[CmdletBinding()] param( + [Parameter(Position=0)] [ValidateSet('x86','x64','arm64','auto')] [string]$Platform = 'auto', + [Parameter(Position=1)] [ValidateSet('Debug','Release')] [string]$Configuration = 'Release', + [Parameter(Position=2)] [string]$Sample = '', + [switch]$Help +) + +# --- Functions (grouped at top for clarity) --- + +# Show-Usage: Display help/usage information. +function Show-Usage { + Write-Host 'Usage:' + Write-Host ' build.ps1 [-Platform x86|x64|arm64|auto] [-Configuration Debug|Release] [-Sample ]' + Write-Host '' + Write-Host 'Platform:' + Write-Host ' auto (default) Detects host OS arch: arm64 on ARM64, else x64.' + Write-Host '' + Write-Host 'Solution selection:' + Write-Host ' -Sample Build solutions under Samples/' + Write-Host ' (no Sample) If current directory has .sln file(s), build only those; otherwise build all Samples solutions.' + Write-Host '' + Write-Host 'Examples:' + Write-Host ' ./build.ps1' + Write-Host ' ./build.ps1 -Platform arm64 -Configuration Debug' + Write-Host ' ./build.ps1 -Sample AppLifecycle' +} + +# Initialize-VSEnvironment: Ensure VS dev environment & MSBuild are available. +function Initialize-VSEnvironment { + if (Get-Command msbuild -ErrorAction SilentlyContinue) { Write-Host 'MSBuild already on PATH; assuming VS env initialized.'; return } + if ($env:VSINSTALLDIR) { Write-Host "VS environment already initialized: $env:VSINSTALLDIR"; return } + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + if (-not (Test-Path $vswhere)) { Write-Error 'vswhere.exe not found. Install Visual Studio 2022 with MSBuild.'; exit 1 } + $installationPath = & $vswhere -latest -prerelease -products * -requires Microsoft.Component.MSBuild -property installationPath | Select-Object -First 1 + if (-not $installationPath) { Write-Error 'Unable to locate a suitable Visual Studio installation.'; exit 1 } + $devShellModule = Join-Path $installationPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll' + if (Test-Path $devShellModule) { + Write-Host "Initializing VS DevShell from: $devShellModule" -ForegroundColor Yellow + try { Import-Module $devShellModule -ErrorAction Stop; Enter-VsDevShell -VsInstallPath $installationPath -SkipAutomaticLocation | Out-Null } + catch { Write-Warning "DevShell initialization failed: $($_.Exception.Message)" } + } +} + +# Resolve-Platform: Turn 'auto' into a concrete platform value. +function Resolve-Platform { param([string]$PlatformValue) + if ($PlatformValue -ne 'auto') { return $PlatformValue } + $detected = 'x64' + try { + $osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() + switch ($osArch) { 'arm64' { $detected = 'arm64' } 'x86' { $detected = 'x86' } default { $detected = 'x64' } } + if ($detected -eq 'x86' -and $env:PROCESSOR_ARCHITEW6432) { $detected = 'x64' } + } catch {} + Write-Host "Auto-detected platform: $detected" -ForegroundColor Yellow + return $detected +} + +# Initialize-NuGetEnvironment: Create folders/download nuget.exe if needed. +function Initialize-NuGetEnvironment { + if (-not (Test-Path $nugetDir)) { New-Item -ItemType Directory -Path $nugetDir | Out-Null } + if (-not (Test-Path $nugetExe)) { Write-Host 'Downloading nuget.exe...'; Invoke-WebRequest -UseBasicParsing https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nugetExe } + if (-not (Test-Path $packagesDir)) { New-Item -ItemType Directory -Path $packagesDir | Out-Null } +} + +# Get-Solutions: Discover which solutions to build. +function Get-Solutions { param([string]$SampleFilter) + $targetRoot = $null; $solutions = @() + if (-not [string]::IsNullOrWhiteSpace($SampleFilter)) { + $targetRoot = Join-Path $samplesRoot $SampleFilter + if (-not (Test-Path $targetRoot)) { Write-Error "Sample path not found: $targetRoot"; exit 1 } + $solutions = Get-ChildItem -Path $targetRoot -Filter *.sln -Recurse | Sort-Object FullName + } else { + $currentDir = Get-Location + $localSolutions = Get-ChildItem -Path $currentDir -Filter *.sln -File -ErrorAction SilentlyContinue | Sort-Object FullName + if ($localSolutions) { Write-Host "Detected $($localSolutions.Count) solution(s) in current directory: $currentDir" -ForegroundColor Yellow; $solutions = $localSolutions } + else { $targetRoot = $samplesRoot; $solutions = Get-ChildItem -Path $targetRoot -Filter *.sln -Recurse | Sort-Object FullName } + } + return $solutions +} + + # Restore-Solution: Perform NuGet restore for a solution (.sln) using repo nuget.exe. + function Restore-Solution { param([string]$SolutionPath) + Write-Host "Restoring: $SolutionPath" -ForegroundColor Cyan + & $nugetExe restore $SolutionPath -ConfigFile (Join-Path $samplesRoot 'nuget.config') -PackagesDirectory $packagesDir + if ($LASTEXITCODE -ne 0) { Write-Error 'NuGet restore failed.'; exit $LASTEXITCODE } + } + + # Build-Solution: Invoke MSBuild on a solution with platform/config and emit binlog. + function Build-Solution { param([string]$SolutionPath,[string]$Platform,[string]$Configuration) + $binlog = Join-Path (Split-Path $SolutionPath -Parent) ("{0}.binlog" -f ([IO.Path]::GetFileNameWithoutExtension($SolutionPath))) + Write-Host "Building: $SolutionPath" -ForegroundColor Cyan + & msbuild /warnaserror /p:Platform=$Platform /p:Configuration=$Configuration /p:NugetPackageDirectory=$packagesDir /bl:"$binlog" "$SolutionPath" + if ($LASTEXITCODE -ne 0) { Write-Error 'MSBuild failed.'; exit $LASTEXITCODE } + } + +# --- Main Execution Flow --- +if ($Help -or $PSBoundParameters.ContainsKey('?')) { Show-Usage; return } +Initialize-VSEnvironment + +if (-not $env:VSINSTALLDIR) { Write-Warning 'VSINSTALLDIR environment variable not found. Run this from a VS Developer PowerShell prompt.'; Show-Usage; exit 1 } + +$Platform = Resolve-Platform -PlatformValue $Platform +$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$nugetDir = Join-Path $repoRoot '.nuget' +$nugetExe = Join-Path $nugetDir 'nuget.exe' +$packagesDir = Join-Path $repoRoot 'packages' +$samplesRoot = Join-Path $repoRoot 'Samples' + +Initialize-NuGetEnvironment +$solutions = Get-Solutions -SampleFilter $Sample + +if (-not $solutions -or $solutions.Count -eq 0) { + Write-Warning 'No solution files found to build.' + exit 0 +} + +$stopwatch = [System.Diagnostics.Stopwatch]::StartNew() +$startTime = Get-Date + +Write-Host "Building $($solutions.Count) solution(s) for Platform=$Platform Configuration=$Configuration" -ForegroundColor Green +foreach ($sln in $solutions) { + Write-Host '---' -ForegroundColor Cyan + Restore-Solution -SolutionPath $sln.FullName + Build-Solution -SolutionPath $sln.FullName -Platform $Platform -Configuration $Configuration +} + +$stopwatch.Stop() +$endTime = Get-Date + +Write-Host '---' +Write-Host ("Start time: {0}. End time: {1}" -f $startTime.ToLongTimeString(), $endTime.ToLongTimeString()) +Write-Host (" Elapsed: {0}" -f [System.String]::Format('{0:hh\\:mm\\:ss\.ff}', $stopwatch.Elapsed))