param(
[Parameter(Mandatory = $true)][string]$ImagePath,
[Parameter(Mandatory = $true)][string]$BootResult,
[Parameter(Mandatory = $true)][string]$HostResult,
[string]$ReportPath,
[string]$Name = ('XFESystem-CSharp-Preview-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
)
$ErrorActionPreference = 'Stop'
if ($Name -notmatch '^XFESystem-CSharp-[A-Za-z0-9.-]+$') { throw 'Use a simple XFESystem-CSharp- package name.' }
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
function Get-WorkspaceFile([string]$Path) {
$full = [IO.Path]::GetFullPath($Path)
if (!$full.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or !(Test-Path -LiteralPath $full -PathType Leaf)) { throw ('Invalid package input: ' + $Path) }
return $full
}
function Confirm-AssetFile([string]$Directory, [string]$Relative, [string]$ExpectedHash) {
$rootPath = [IO.Path]::GetFullPath($Directory)
$full = Get-WorkspaceFile (Join-Path $rootPath $Relative)
if (!$full.StartsWith($rootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ExpectedHash.ToLowerInvariant()) { throw ('Vendored asset changed: ' + $Relative) }
}
$websiteCode = Join-Path $taskRoot 'assets/Browser/WebsiteCode'
$websiteExact = Join-Path $taskRoot 'Source/XFESystem.Documents/Assets/Website/Exact'
$lato = Join-Path $taskRoot 'assets/fonts/lato-2.015'
$webCjk = Join-Path $taskRoot 'assets/fonts/noto-sans-sc-2.004'
$webIcons = Join-Path $taskRoot 'assets/fonts/bootstrap-icons-1.11.3'
$hasWebsiteV6 = (Test-Path -LiteralPath (Join-Path $websiteCode 'Manifest.json')) -and (Test-Path -LiteralPath (Join-Path $websiteExact 'SourceManifest.json'))
if ($hasWebsiteV6) {
$codeManifest = Get-Content -LiteralPath (Join-Path $websiteCode 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $codeManifest.Files) { Confirm-AssetFile $websiteCode $asset.Path $asset.Sha256 }
foreach ($asset in @($codeManifest.DerivedPalette, $codeManifest.Adapter)) { Confirm-AssetFile $websiteCode $asset.Path $asset.Sha256 }
$exactManifest = Get-Content -LiteralPath (Join-Path $websiteExact 'SourceManifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $exactManifest.Entries) {
if ($asset.SourceSha256 -ne $asset.SnapshotSha256) { throw ('Website exact resource was transformed: ' + $asset.SourcePath) }
Confirm-AssetFile $websiteExact $asset.SnapshotPath $asset.SnapshotSha256
}
}
if (Test-Path -LiteralPath (Join-Path $lato 'Manifest.json')) {
$fontManifest = Get-Content -LiteralPath (Join-Path $lato 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $fontManifest.Files) { Confirm-AssetFile $lato $asset.Path $asset.Sha256 }
}
if (Test-Path -LiteralPath (Join-Path $webCjk 'Manifest.json')) {
$cjkManifest = Get-Content -LiteralPath (Join-Path $webCjk 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $cjkManifest.Files) { Confirm-AssetFile $webCjk $asset.Path $asset.Sha256 }
Confirm-AssetFile $taskRoot $cjkManifest.Transformation.Script $cjkManifest.Transformation.ScriptSha256
}
if (Test-Path -LiteralPath (Join-Path $webIcons 'Manifest.json')) {
$iconManifest = Get-Content -LiteralPath (Join-Path $webIcons 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $iconManifest.Files) { Confirm-AssetFile $webIcons $asset.Path $asset.Sha256 }
Confirm-AssetFile $taskRoot $iconManifest.Transformation.Script $iconManifest.Transformation.ScriptSha256
}
$image = (Resolve-Path -LiteralPath $ImagePath).Path
$bootPath = (Resolve-Path -LiteralPath $BootResult).Path
$hostPath = (Resolve-Path -LiteralPath $HostResult).Path
$boot = Get-Content -LiteralPath $bootPath -Raw -Encoding UTF8 | ConvertFrom-Json
$hostChecks = Get-Content -LiteralPath $hostPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($boot.Passed -ne $true -or $boot.ImageUnchanged -ne $true -or $boot.QemuExitCode -ne 0 -or $hostChecks.Passed -ne $true) {
throw 'A passing guest receipt and host test receipt are required.'
}
$hash = (Get-FileHash -LiteralPath $image -Algorithm SHA256).Hash.ToLowerInvariant()
if ($hash -ne $boot.ImageSha256) { throw 'The image does not match its verified boot receipt.' }
$licenses = Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Collected'
$licenseManifest = Get-Content -LiteralPath (Join-Path $licenses 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
if ($licenseManifest.KernelLockSha256 -ne (Get-FileHash -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Kernel/Packages.lock.json') -Algorithm SHA256).Hash.ToLowerInvariant()) { throw 'License collection belongs to another kernel lock file.' }
foreach ($file in $licenseManifest.Files) {
$path = [IO.Path]::GetFullPath((Join-Path $licenses $file.Path))
if (!$path.StartsWith($licenses + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Invalid license path.' }
if ((Get-Item -LiteralPath $path).Length -ne $file.Bytes -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $file.Sha256) { throw ('License file changed: ' + $file.Path) }
}
if (!$boot.SourceSnapshotPath -or $null -eq $hostChecks.SourceHashes) { throw 'Source-bound final guest and host receipts are required.' }
if ((Get-FileHash -LiteralPath $boot.SourceSnapshotPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $boot.SourceSnapshotSha256) { throw 'The guest source snapshot changed.' }
$guestSourceHashes = Get-Content -LiteralPath $boot.SourceSnapshotPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($map in @($guestSourceHashes, $hostChecks.SourceHashes)) {
foreach ($property in $map.PSObject.Properties) {
# A report generator is not part of the guest or its boot test. All
# compiled sources and build/boot inputs must still match exactly.
if ($property.Name -eq 'Tools/CSharpBoot/CreateReport.py') { continue }
$path = [IO.Path]::GetFullPath((Join-Path $taskRoot $property.Name))
if (!$path.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $property.Value) { throw ('Verified sources changed: ' + $property.Name) }
}
}
$samplePath = Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/bin/Release/net10.0/XFESystem.ManagedHello.dll'
if ((Get-FileHash -LiteralPath $samplePath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $boot.ManagedSampleSha256) { throw 'The distributed sample does not match the guest payload.' }
$reportFile = $null
if ($ReportPath) {
$reportFile = (Resolve-Path -LiteralPath $ReportPath).Path
$report = Get-Content -LiteralPath $reportFile -Raw -Encoding UTF8 | ConvertFrom-Json
if ($report.PreviewValidated -ne $true -or $report.ImageSha256 -ne $hash) { throw 'The aggregate report does not validate this image.' }
if (!(Test-Path -LiteralPath ([IO.Path]::ChangeExtension($reportFile, '.md')))) { throw 'The aggregate Markdown report is missing.' }
foreach ($entry in $report.Receipts.PSObject.Properties) {
if ($entry.Name -notmatch '^[A-Za-z0-9.-]+$') { throw 'Invalid receipt name.' }
$receiptSource = [IO.Path]::GetFullPath([string]$entry.Value.Path)
if (!$receiptSource.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $receiptSource -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry.Value.Sha256) { throw ('Aggregate receipt changed: ' + $entry.Name) }
}
if ($report.NativeBuildWarnings.BuildLogBound -eq $true -and
(Get-FileHash -LiteralPath $report.NativeBuildWarnings.Path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $report.NativeBuildWarnings.Sha256) { throw 'Native compiler log changed.' }
}
$dist = Join-Path $taskRoot 'dist'
$package = Join-Path $dist $Name
$zipPath = $package + '.zip'
if ((Test-Path -LiteralPath $package) -or (Test-Path -LiteralPath $zipPath)) { throw 'Existing preview artifacts are never overwritten.' }
[void][IO.Directory]::CreateDirectory($package)
foreach ($directory in @('Tools','Tools/CSharpBoot','Firmware','Validation','Validation/Host','Validation/Guest','Validation/Receipts','Validation/DisplayGuest','Licenses','Samples','DesktopIcons')) { [void][IO.Directory]::CreateDirectory((Join-Path $package $directory)) }
Copy-Item -LiteralPath $image -Destination (Join-Path $package 'System.img')
Copy-Item -LiteralPath $bootPath -Destination (Join-Path $package 'Validation/Boot.json')
Copy-Item -LiteralPath $hostPath -Destination (Join-Path $package 'Validation/Host.json')
Copy-Item -LiteralPath $boot.SourceSnapshotPath -Destination (Join-Path $package 'Validation/SourceSnapshot.json')
Copy-Item -LiteralPath (Join-Path (Split-Path -Parent $bootPath) 'BootShimReceipt.json') -Destination (Join-Path $package 'Validation/BootShimReceipt.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/OpenCSharp.ps1') -Destination (Join-Path $package 'Tools/OpenCSharp.ps1')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/ReadHostDisplay.ps1') -Destination (Join-Path $package 'Tools/CSharpBoot/ReadHostDisplay.ps1')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Dependencies.json') -Destination (Join-Path $package 'Validation/Dependencies.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Kernel/Packages.lock.json') -Destination (Join-Path $package 'Validation/Packages.lock.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'docs/CSharpReconstruction.md') -Destination (Join-Path $package 'Reconstruction.md')
foreach ($document in @('CSharpDesktopPreview2.md','CSharpDesktopPreview3.md','CSharpDesktopPreview4.md','CSharpBrowserPreview5.md','CSharpNativeDisplay.md','ToolboxDesktopDesign.md')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('docs/' + $document)) -Destination (Join-Path $package $document)
}
if ($hasWebsiteV6) { Copy-Item -LiteralPath (Join-Path $taskRoot 'docs/CSharpBrowserPreview6.md') -Destination (Join-Path $package 'CSharpBrowserPreview6.md') }
Get-ChildItem -LiteralPath (Join-Path $taskRoot 'assets/DesktopIcons') | Copy-Item -Destination (Join-Path $package 'DesktopIcons') -Recurse
[void][IO.Directory]::CreateDirectory((Join-Path $package 'BrowserAssets/Monaco/0.52.2'))
Get-ChildItem -LiteralPath (Join-Path $taskRoot 'assets/Browser/Monaco/0.52.2') | Copy-Item -Destination (Join-Path $package 'BrowserAssets/Monaco/0.52.2') -Recurse
if ($hasWebsiteV6) {
foreach ($directory in @('BrowserAssets/WebsiteCode','DocumentAssets/Website/Exact','DocumentAssets/Website/Licenses','Licenses/Preview6Website')) { [void][IO.Directory]::CreateDirectory((Join-Path $package $directory)) }
Get-ChildItem -LiteralPath $websiteCode | Copy-Item -Destination (Join-Path $package 'BrowserAssets/WebsiteCode') -Recurse
Get-ChildItem -LiteralPath $websiteExact | Copy-Item -Destination (Join-Path $package 'DocumentAssets/Website/Exact') -Recurse
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Documents/ThirdPartyNotices.md') -Destination (Join-Path $package 'DocumentAssets/Website/ThirdPartyNotices.md')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Documents/ThirdPartyNotices.md') -Destination (Join-Path $package 'Licenses/Preview6Website/ThirdPartyNotices.md')
Get-ChildItem -LiteralPath (Split-Path -Parent $websiteExact) -File | Where-Object { $_.Name -like '*.License.txt' -or $_.Name -eq 'LicenseManifest.json' } | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $package 'DocumentAssets/Website/Licenses')
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $package 'Licenses/Preview6Website')
}
Copy-Item -LiteralPath (Join-Path (Split-Path -Parent $websiteExact) 'Highlight.License.txt') -Destination (Join-Path $package 'BrowserAssets/WebsiteCode/Highlight.License.txt')
Copy-Item -LiteralPath (Join-Path $websiteCode 'WebsiteLicense.txt') -Destination (Join-Path $package 'Licenses/Preview6Website/WebsiteCodeLicense.txt')
}
if (Test-Path -LiteralPath (Join-Path $lato 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/Lato'))
foreach ($file in @('OFL.txt','Readme.md','Manifest.json')) { Copy-Item -LiteralPath (Join-Path $lato $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/Lato/' + $file)) }
}
if (Test-Path -LiteralPath (Join-Path $webCjk 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/WebCjk'))
foreach ($file in @('OFL.txt','Readme.md','Manifest.json','OfficialContents.json')) { Copy-Item -LiteralPath (Join-Path $webCjk $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/WebCjk/' + $file)) }
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/BrowserCompatibility/BuildWebCjkFont.py') -Destination (Join-Path $package 'Licenses/Preview6Fonts/WebCjk/BuildWebCjkFont.py')
}
if (Test-Path -LiteralPath (Join-Path $webIcons 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/BootstrapIcons'))
foreach ($file in @('License.md','Readme.md','Manifest.json')) { Copy-Item -LiteralPath (Join-Path $webIcons $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/BootstrapIcons/' + $file)) }
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/BrowserCompatibility/BuildWebIconFont.py') -Destination (Join-Path $package 'Licenses/Preview6Fonts/BootstrapIcons/BuildWebIconFont.py')
}
if ($reportFile) {
Copy-Item -LiteralPath $reportFile -Destination (Join-Path $package 'Validation/Report.json')
Copy-Item -LiteralPath ([IO.Path]::ChangeExtension($reportFile, '.md')) -Destination (Join-Path $package 'Validation/Report.md')
$receiptFiles = @()
$evidenceFiles = @()
foreach ($entry in $report.Receipts.PSObject.Properties) {
$relative = 'Validation/Receipts/' + $entry.Name + [IO.Path]::GetExtension([string]$entry.Value.Path)
Copy-Item -LiteralPath $entry.Value.Path -Destination (Join-Path $package $relative)
$evidenceDirectory = 'Validation/Evidence/' + $entry.Name
[void][IO.Directory]::CreateDirectory((Join-Path $package $evidenceDirectory))
$entryEvidence = @()
# Every receipt gets its own namespace. Reference and native captures
# commonly share names such as Dark.png, Serial.log and Result.json.
# Do not recurse into browser profiles, compiler artifacts or disk files.
foreach ($file in Get-ChildItem -LiteralPath (Split-Path -Parent $entry.Value.Path) -File) {
if ($file.Extension.ToLowerInvariant() -notin @('.png','.jpg','.jpeg','.webp','.svg','.html','.css','.js','.json','.log','.md','.txt','.trx')) { continue }
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
$source = Get-WorkspaceFile $file.FullName
$destination = $evidenceDirectory + '/' + $file.Name
$sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
Copy-Item -LiteralPath $source -Destination (Join-Path $package $destination)
if ((Get-FileHash -LiteralPath (Join-Path $package $destination) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sourceHash) { throw ('Evidence copy changed: ' + $source) }
$entryEvidence += $destination
$evidenceFiles += [ordered]@{Receipt=$entry.Name;PackagedPath=$destination;SourcePath=$source;Sha256=$sourceHash;Bytes=$file.Length}
}
$attachmentIndex = 0
foreach ($attachment in $entry.Value.Artifacts) {
$source = Get-WorkspaceFile ([string]$attachment.Path)
$sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
if (!$attachment.Sha256 -or $sourceHash -ne $attachment.Sha256) { throw ('Receipt artifact changed: ' + $source) }
$attachmentIndex++
$attachmentDirectory = $evidenceDirectory + '/Attachments'
[void][IO.Directory]::CreateDirectory((Join-Path $package $attachmentDirectory))
$destination = $attachmentDirectory + '/' + $attachmentIndex.ToString('D4') + '-' + [IO.Path]::GetFileName($source)
Copy-Item -LiteralPath $source -Destination (Join-Path $package $destination)
$entryEvidence += $destination
$evidenceFiles += [ordered]@{Receipt=$entry.Name;PackagedPath=$destination;SourcePath=$source;Sha256=$sourceHash;Bytes=(Get-Item -LiteralPath $source).Length}
}
$receiptFiles += [ordered]@{Name=$entry.Name;PackagedPath=$relative;SourcePath=$entry.Value.Path;Sha256=$entry.Value.Sha256;EvidenceDirectory=$evidenceDirectory;EvidenceFiles=$entryEvidence}
if ($entry.Name -eq 'Display') {
Get-ChildItem -LiteralPath (Split-Path -Parent $entry.Value.Path) -File |
Where-Object { $_.Extension -eq '.png' -or $_.Name -eq 'Serial.log' } |
Copy-Item -Destination (Join-Path $package 'Validation/DisplayGuest')
}
}
$receiptFiles | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $package 'Validation/ReceiptFiles.json') -Encoding UTF8
$evidenceFiles | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $package 'Validation/EvidenceFiles.json') -Encoding UTF8
if ($report.NativeBuildWarnings.BuildLogBound -eq $true) {
Copy-Item -LiteralPath $report.NativeBuildWarnings.Path -Destination (Join-Path $package 'Validation/NativeBuild.log')
}
}
Copy-Item -LiteralPath (Join-Path $taskRoot 'THIRD_PARTY.md') -Destination (Join-Path $package 'ThirdParty.md')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Bflat-8.0.2.txt') -Destination (Join-Path $package 'Licenses/Bflat-8.0.2.txt')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Bflat-8.0.2-ThirdPartyNotices.txt') -Destination (Join-Path $package 'Licenses/Bflat-8.0.2-ThirdPartyNotices.txt')
foreach ($document in @('NativeCudaPort.md', 'CSharpRenderBackends.md')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('docs/' + $document)) -Destination (Join-Path $package $document)
}
Get-ChildItem -LiteralPath $licenses | Copy-Item -Destination (Join-Path $package 'Licenses') -Recurse
foreach ($suite in $hostChecks.Suites) {
Copy-Item -LiteralPath $suite.Log -Destination (Join-Path $package ('Validation/Host/' + $suite.Suite + '.log'))
Copy-Item -LiteralPath $suite.Trx -Destination (Join-Path $package ('Validation/Host/' + $suite.Suite + '.trx'))
}
Get-ChildItem -LiteralPath (Split-Path -Parent $bootPath) -File |
Where-Object { $_.Extension -eq '.png' -or $_.Name -eq 'Serial.log' -or $_.Name -like 'HttpPeer*' } |
Copy-Item -Destination (Join-Path $package 'Validation/Guest')
foreach ($firmware in @('edk2-x86_64-code.fd','edk2-i386-vars.fd')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('.tools/firmware/' + $firmware)) -Destination (Join-Path $package ('Firmware/' + $firmware))
}
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/Program.cs') -Destination (Join-Path $package 'Samples/Program.cs')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/XFESystem.ManagedHello.csproj') -Destination (Join-Path $package 'Samples/XFESystem.ManagedHello.csproj')
Copy-Item -LiteralPath $samplePath -Destination (Join-Path $package 'Samples/XFESystem.ManagedHello.dll')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/Packages.lock.json') -Destination (Join-Path $package 'Samples/Packages.lock.json')
$startScript = @'
[CmdletBinding()]
param([string]$QemuPath, [switch]$Fullscreen, [switch]$FitToWindow, [switch]$NativePixels, [switch]$PlanOnly, [ValidateSet('auto','tcg','whpx')][string]$Acceleration = 'auto', [ValidateSet('gtk','sdl')][string]$DisplayBackend = 'gtk')
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'Tools/OpenCSharp.ps1') -ImagePath (Join-Path $PSScriptRoot 'System.img') -VerifiedBootResult (Join-Path $PSScriptRoot 'Validation/Boot.json') -QemuPath $QemuPath -Fullscreen:$Fullscreen -FitToWindow:$FitToWindow -NativePixels:$NativePixels -PlanOnly:$PlanOnly -Acceleration $Acceleration -DisplayBackend $DisplayBackend
'@
[IO.File]::WriteAllText((Join-Path $package 'Start.ps1'), $startScript, (New-Object Text.UTF8Encoding($true)))
@'
# XFESystem C# 桌面开发预览
这是 C# NativeAOT 裸机内核的已验证开发快照,运行于 x86_64 QEMU q35 + UEFI。
没有使用 Linux 或 Windows 来宾内核。它复用 Cosmos 的运行时、GC 与驱动。
这不是完整 .NET 10/CoreCLR,也不包含 .NET Framework 4.8。
项目自有内核、UEFI 显示初始化和系统应用已改为 C#;旧 Rust 源码不再属于当前构建。
当前仍使用 CPU 软件绘制;NVIDIA PCI 资源发现不等于 CUDA 或 GPU 光栅化。
Markdown 首屏先显示真实正文和原始 CSS,再加载增强脚本;1 秒目标以当前 Validation 记录为准。
增强脚本仍存在不可中断的解释执行阶段,不能承诺脚本加载期间全部交互都流畅。
## 启动
在 Windows 安装 QEMU 后,用 PowerShell 在此目录执行:
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start.ps1 -QemuPath "C:\Program Files\qemu\qemu-system-x86_64.exe"
启动脚本核验 System.img,复制会话系统盘和固件变量,打开可交互 GTK 窗口。
只读检测主显示器最高分辨率与该分辨率的最高刷新声明;来宾仍使用原生分辨率。
默认将完整桌面适配到普通最大化窗口,避免底部任务栏被裁掉;宿主适配会重采样显示画面。
使用 -NativePixels 可关闭视口适配,按原样像素显示,但较小窗口可能裁掉桌面边缘。
-FitToWindow 保留为显式适配选项,与 -NativePixels 互斥;界面倍率与原生分辨率独立。
-Fullscreen 为显式全屏选项,部分 WHPX/GTK 组合可能启动较慢;默认不使用全屏启动。
默认交互模式使用已通过同一镜像验收的 TCG 对照;包会核对其记录哈希、镜像、内核与源快照。
本机 WHPX 无 GUI 验收通过,但 GTK 曾在固件阶段超时;-Acceleration whpx 保留为显式实验选项。
WHPX 需要宿主已有可用的 Windows Hypervisor Platform,脚本不会修改 Windows 功能或重启电脑。
本机可显式使用 `-Acceleration whpx -DisplayBackend sdl` 绕过 WHPX+GTK 的固件启动问题;SDL 使用 gl=off。
WHPX 是 CPU 虚拟化,SDL 是 QEMU 的宿主显示后端;它们均不是来宾 CUDA/GPU 渲染。
使用鼠标与键盘操作;关闭 QEMU 窗口结束。日志保存在 build/CSharp/Interactive-*。
不会打开物理磁盘。文件保存在 RAM,关闭后消失。
## 操作
- 拖动标题栏移动;双击最大化/还原;右下角拖动调整尺寸。右上角仅保留最小化与关闭按钮。
- 窗口圆角和线条使用原生像素抗锯齿;交互入口提供悬停与按下反馈。
- 底部任务栏切换、最小化与恢复;Win 开始菜单,Win+E 文件,Win+D 桌面。
- Alt+Tab 切换,Alt+Shift+Tab 反向,松开 Alt 提交;Alt+F4 关闭。
- Win+左右贴靠,Win+上最大化,Win+下还原/最小化。QEMU 需先捕获键盘焦点。
- Files:打开文件夹和文件;New Folder / New File 创建内容。
- Editor:代码文件使用真实 Monaco 模型编辑,Ctrl+S 保存;Markdown 的 Preview / F5 进入自研浏览器的真实文档 DOM/CSS/JavaScript 管线。
- Browser:Ctrl+L 选择地址,输入后 Enter;about:Start 会实际执行内置 JavaScript,about:Monaco 打开真实 Monaco 编辑器。
- Terminal:Run Demo 运行独立编译的 net10.0 示例 DLL,展示 Console 颜色、ANSI RGB 和真实 XFEConsole。
- 文件管理器进入 X:\Programs\ManagedHello,点击 DLL 也会实际运行此程序集。
- 桌面 Settings 图标或任务栏右侧入口打开外观设置,切换 Light / Dark、预设强调色或输入 #RRGGBB 后按 Enter / Apply。
- 桌面不再显示系统顶部栏;窗口最大化覆盖顶部,保留底部任务栏。
- 标题栏最小化和关闭图标有平滑悬停动画,中途离开会连续反向;拖动和缩放按原生像素更新。
- Dark 延续已有深蓝配色;设置保存在当前 RAM 会话,关闭虚拟机后消失。
浏览器采用自研有限 CSS 布局与绘制、AngleSharp DOM 和 Jint JavaScript,支持外部经典脚本、defer、动态资源和浏览器事件。
Monaco 0.52.2 核心提供真实编辑和 JavaScript 着色;C# 使用本系统显式适配器,将网站原 highlight.js/csharp.js 对完整模型的分类和原 CSS 调色板送入 Monaco TokensProvider。
原网站本身采用 highlight.js,没有 Monaco;共享语法与颜色来源不等于两种编辑界面逐像素相同。来源与转换清单见 BrowserAssets/WebsiteCode。
没有 Worker / TypeScript 语言服务或完整 CSS/Web 平台。操作和范围见 CSharpBrowserPreview6.md;历史核心说明见 CSharpBrowserPreview5.md。
启动内存与验收记录一致;本轮 Monaco 配置为 1 GiB,512 MiB 未通过完整编辑验收。内存耗尽时明确停止整个来宾,尚不能恢复或隔离应用。
原生 HTTPS 尚未移植,明确拒绝连接。实际 HTTP 状态以 Validation/Boot.json 为准。
Markdown 采用指定网站的 Markdig/清理策略、原字节主题/文章 CSS 和原始高亮/代码工具脚本,经自研 Web 布局和绘制进入来宾。参考浏览器截图仅作对照,不替代来宾验收。
系统正文实际回退 Lato,等宽字形回退 Cascadia Mono;Web 缺失的 BMP 中文字形采用 Noto Sans SC 派生静态 TrueType 400/700,最终缺字仍回退 Unifont。
原网站 Bootstrap Icons 1.11.3 的完整字体以 TTF 嵌入,通用 CSS before/after 内容生成布局盒并使用真实字形;来源、MIT 和无轮廓修改转换记录位于 Licenses/Preview6Fonts/BootstrapIcons。
字体按真实glyph存在性选择并使用轮廓抗锯齿;未捆绑 Segoe UI/Consolas/微软雅黑,不能声明与 Windows 字体相同。
CSS、SVG、网页字体、图标、数学和 Mermaid 支持仍须逐项以 Validation/Report.json 与独立截图证据为准,不能把资源导入或宿主通过视为原生完整通过。
Validation/Receipts 与 ReceiptFiles.json 保存原始记录,Validation/Evidence/<记录名>/ 分别保存参考图、来宾图、DOM和日志,避免同名图片互相覆盖。
独立 DLL 只支持 XFESystem.Net10.ConsoleIL.v1 的有限 IL 和 Console/XFEConsole 接口。
自编程序、内存文件与图形窗口仍在同一个内核地址空间,尚无独立进程隔离。
实体 USB 输入、多显示器、完整账号和持久化仍需移植。详细边界见 Reconstruction.md。
EDID 刷新声明、绘制目标、实测提交 FPS 与物理 VSync 分别记录。当前 GOP 没有扫描时钟/VSync 驱动,不能承诺实体 240 Hz 同步;性能证据见 Validation/Report.md。
镜像、依赖与验证信息见 Validation,完整第三方许可见 Licenses。
9 个图标由内置 Imagegen 生成,原图、完整提示词及资产哈希见 DesktopIcons。
'@ | Set-Content -LiteralPath (Join-Path $package 'Readme.md') -Encoding UTF8
if ((Get-FileHash -LiteralPath (Join-Path $package 'System.img') -Algorithm SHA256).Hash.ToLowerInvariant() -ne $hash) { throw 'Packaged image verification failed.' }
$manifest = @()
foreach ($file in Get-ChildItem -LiteralPath $package -File -Recurse) {
$manifest += [ordered]@{Path=$file.FullName.Substring($package.Length + 1).Replace('\','/');Bytes=$file.Length;Sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}
}
[ordered]@{Kind='CSharpBareMetalDevelopmentPreview';CompleteSystemRewrite=$false;FullDotNet10=$false;Framework48=$false;ImageSha256=$hash;BootReceiptSha256=(Get-FileHash -LiteralPath $bootPath -Algorithm SHA256).Hash.ToLowerInvariant();Files=$manifest} |
ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $package 'Manifest.json') -Encoding UTF8
Compress-Archive -Path (Join-Path $package '*') -DestinationPath $zipPath -CompressionLevel Optimal
Write-Host ('Preview package: ' + $zipPath)
param(
[Parameter(Mandatory = $true)][string]$ImagePath,
[Parameter(Mandatory = $true)][string]$BootResult,
[Parameter(Mandatory = $true)][string]$HostResult,
[string]$ReportPath,
[string]$Name = ('XFESystem-CSharp-Preview-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
)
$ErrorActionPreference = 'Stop'
if ($Name -notmatch '^XFESystem-CSharp-[A-Za-z0-9.-]+$') { throw 'Use a simple XFESystem-CSharp- package name.' }
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
function Get-WorkspaceFile([string]$Path) {
$full = [IO.Path]::GetFullPath($Path)
if (!$full.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or !(Test-Path -LiteralPath $full -PathType Leaf)) { throw ('Invalid package input: ' + $Path) }
return $full
}
function Confirm-AssetFile([string]$Directory, [string]$Relative, [string]$ExpectedHash) {
$rootPath = [IO.Path]::GetFullPath($Directory)
$full = Get-WorkspaceFile (Join-Path $rootPath $Relative)
if (!$full.StartsWith($rootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ExpectedHash.ToLowerInvariant()) { throw ('Vendored asset changed: ' + $Relative) }
}
$websiteCode = Join-Path $taskRoot 'assets/Browser/WebsiteCode'
$websiteExact = Join-Path $taskRoot 'Source/XFESystem.Documents/Assets/Website/Exact'
$lato = Join-Path $taskRoot 'assets/fonts/lato-2.015'
$webCjk = Join-Path $taskRoot 'assets/fonts/noto-sans-sc-2.004'
$webIcons = Join-Path $taskRoot 'assets/fonts/bootstrap-icons-1.11.3'
$hasWebsiteV6 = (Test-Path -LiteralPath (Join-Path $websiteCode 'Manifest.json')) -and (Test-Path -LiteralPath (Join-Path $websiteExact 'SourceManifest.json'))
if ($hasWebsiteV6) {
$codeManifest = Get-Content -LiteralPath (Join-Path $websiteCode 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $codeManifest.Files) { Confirm-AssetFile $websiteCode $asset.Path $asset.Sha256 }
foreach ($asset in @($codeManifest.DerivedPalette, $codeManifest.Adapter)) { Confirm-AssetFile $websiteCode $asset.Path $asset.Sha256 }
$exactManifest = Get-Content -LiteralPath (Join-Path $websiteExact 'SourceManifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $exactManifest.Entries) {
if ($asset.SourceSha256 -ne $asset.SnapshotSha256) { throw ('Website exact resource was transformed: ' + $asset.SourcePath) }
Confirm-AssetFile $websiteExact $asset.SnapshotPath $asset.SnapshotSha256
}
}
if (Test-Path -LiteralPath (Join-Path $lato 'Manifest.json')) {
$fontManifest = Get-Content -LiteralPath (Join-Path $lato 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $fontManifest.Files) { Confirm-AssetFile $lato $asset.Path $asset.Sha256 }
}
if (Test-Path -LiteralPath (Join-Path $webCjk 'Manifest.json')) {
$cjkManifest = Get-Content -LiteralPath (Join-Path $webCjk 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $cjkManifest.Files) { Confirm-AssetFile $webCjk $asset.Path $asset.Sha256 }
Confirm-AssetFile $taskRoot $cjkManifest.Transformation.Script $cjkManifest.Transformation.ScriptSha256
}
if (Test-Path -LiteralPath (Join-Path $webIcons 'Manifest.json')) {
$iconManifest = Get-Content -LiteralPath (Join-Path $webIcons 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($asset in $iconManifest.Files) { Confirm-AssetFile $webIcons $asset.Path $asset.Sha256 }
Confirm-AssetFile $taskRoot $iconManifest.Transformation.Script $iconManifest.Transformation.ScriptSha256
}
$image = (Resolve-Path -LiteralPath $ImagePath).Path
$bootPath = (Resolve-Path -LiteralPath $BootResult).Path
$hostPath = (Resolve-Path -LiteralPath $HostResult).Path
$boot = Get-Content -LiteralPath $bootPath -Raw -Encoding UTF8 | ConvertFrom-Json
$hostChecks = Get-Content -LiteralPath $hostPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($boot.Passed -ne $true -or $boot.ImageUnchanged -ne $true -or $boot.QemuExitCode -ne 0 -or $hostChecks.Passed -ne $true) {
throw 'A passing guest receipt and host test receipt are required.'
}
$hash = (Get-FileHash -LiteralPath $image -Algorithm SHA256).Hash.ToLowerInvariant()
if ($hash -ne $boot.ImageSha256) { throw 'The image does not match its verified boot receipt.' }
$licenses = Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Collected'
$licenseManifest = Get-Content -LiteralPath (Join-Path $licenses 'Manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
if ($licenseManifest.KernelLockSha256 -ne (Get-FileHash -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Kernel/Packages.lock.json') -Algorithm SHA256).Hash.ToLowerInvariant()) { throw 'License collection belongs to another kernel lock file.' }
foreach ($file in $licenseManifest.Files) {
$path = [IO.Path]::GetFullPath((Join-Path $licenses $file.Path))
if (!$path.StartsWith($licenses + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Invalid license path.' }
if ((Get-Item -LiteralPath $path).Length -ne $file.Bytes -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $file.Sha256) { throw ('License file changed: ' + $file.Path) }
}
if (!$boot.SourceSnapshotPath -or $null -eq $hostChecks.SourceHashes) { throw 'Source-bound final guest and host receipts are required.' }
if ((Get-FileHash -LiteralPath $boot.SourceSnapshotPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $boot.SourceSnapshotSha256) { throw 'The guest source snapshot changed.' }
$guestSourceHashes = Get-Content -LiteralPath $boot.SourceSnapshotPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($map in @($guestSourceHashes, $hostChecks.SourceHashes)) {
foreach ($property in $map.PSObject.Properties) {
# A report generator is not part of the guest or its boot test. All
# compiled sources and build/boot inputs must still match exactly.
if ($property.Name -eq 'Tools/CSharpBoot/CreateReport.py') { continue }
$path = [IO.Path]::GetFullPath((Join-Path $taskRoot $property.Name))
if (!$path.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $property.Value) { throw ('Verified sources changed: ' + $property.Name) }
}
}
$samplePath = Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/bin/Release/net10.0/XFESystem.ManagedHello.dll'
if ((Get-FileHash -LiteralPath $samplePath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $boot.ManagedSampleSha256) { throw 'The distributed sample does not match the guest payload.' }
$reportFile = $null
if ($ReportPath) {
$reportFile = (Resolve-Path -LiteralPath $ReportPath).Path
$report = Get-Content -LiteralPath $reportFile -Raw -Encoding UTF8 | ConvertFrom-Json
if ($report.PreviewValidated -ne $true -or $report.ImageSha256 -ne $hash) { throw 'The aggregate report does not validate this image.' }
if (!(Test-Path -LiteralPath ([IO.Path]::ChangeExtension($reportFile, '.md')))) { throw 'The aggregate Markdown report is missing.' }
foreach ($entry in $report.Receipts.PSObject.Properties) {
if ($entry.Name -notmatch '^[A-Za-z0-9.-]+$') { throw 'Invalid receipt name.' }
$receiptSource = [IO.Path]::GetFullPath([string]$entry.Value.Path)
if (!$receiptSource.StartsWith($taskRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
(Get-FileHash -LiteralPath $receiptSource -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry.Value.Sha256) { throw ('Aggregate receipt changed: ' + $entry.Name) }
}
if ($report.NativeBuildWarnings.BuildLogBound -eq $true -and
(Get-FileHash -LiteralPath $report.NativeBuildWarnings.Path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $report.NativeBuildWarnings.Sha256) { throw 'Native compiler log changed.' }
}
$dist = Join-Path $taskRoot 'dist'
$package = Join-Path $dist $Name
$zipPath = $package + '.zip'
if ((Test-Path -LiteralPath $package) -or (Test-Path -LiteralPath $zipPath)) { throw 'Existing preview artifacts are never overwritten.' }
[void][IO.Directory]::CreateDirectory($package)
foreach ($directory in @('Tools','Tools/CSharpBoot','Firmware','Validation','Validation/Host','Validation/Guest','Validation/Receipts','Validation/DisplayGuest','Licenses','Samples','DesktopIcons')) { [void][IO.Directory]::CreateDirectory((Join-Path $package $directory)) }
Copy-Item -LiteralPath $image -Destination (Join-Path $package 'System.img')
Copy-Item -LiteralPath $bootPath -Destination (Join-Path $package 'Validation/Boot.json')
Copy-Item -LiteralPath $hostPath -Destination (Join-Path $package 'Validation/Host.json')
Copy-Item -LiteralPath $boot.SourceSnapshotPath -Destination (Join-Path $package 'Validation/SourceSnapshot.json')
Copy-Item -LiteralPath (Join-Path (Split-Path -Parent $bootPath) 'BootShimReceipt.json') -Destination (Join-Path $package 'Validation/BootShimReceipt.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/OpenCSharp.ps1') -Destination (Join-Path $package 'Tools/OpenCSharp.ps1')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/ReadHostDisplay.ps1') -Destination (Join-Path $package 'Tools/CSharpBoot/ReadHostDisplay.ps1')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Dependencies.json') -Destination (Join-Path $package 'Validation/Dependencies.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Kernel/Packages.lock.json') -Destination (Join-Path $package 'Validation/Packages.lock.json')
Copy-Item -LiteralPath (Join-Path $taskRoot 'docs/CSharpReconstruction.md') -Destination (Join-Path $package 'Reconstruction.md')
foreach ($document in @('CSharpDesktopPreview2.md','CSharpDesktopPreview3.md','CSharpDesktopPreview4.md','CSharpBrowserPreview5.md','CSharpNativeDisplay.md','ToolboxDesktopDesign.md')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('docs/' + $document)) -Destination (Join-Path $package $document)
}
if ($hasWebsiteV6) { Copy-Item -LiteralPath (Join-Path $taskRoot 'docs/CSharpBrowserPreview6.md') -Destination (Join-Path $package 'CSharpBrowserPreview6.md') }
Get-ChildItem -LiteralPath (Join-Path $taskRoot 'assets/DesktopIcons') | Copy-Item -Destination (Join-Path $package 'DesktopIcons') -Recurse
[void][IO.Directory]::CreateDirectory((Join-Path $package 'BrowserAssets/Monaco/0.52.2'))
Get-ChildItem -LiteralPath (Join-Path $taskRoot 'assets/Browser/Monaco/0.52.2') | Copy-Item -Destination (Join-Path $package 'BrowserAssets/Monaco/0.52.2') -Recurse
if ($hasWebsiteV6) {
foreach ($directory in @('BrowserAssets/WebsiteCode','DocumentAssets/Website/Exact','DocumentAssets/Website/Licenses','Licenses/Preview6Website')) { [void][IO.Directory]::CreateDirectory((Join-Path $package $directory)) }
Get-ChildItem -LiteralPath $websiteCode | Copy-Item -Destination (Join-Path $package 'BrowserAssets/WebsiteCode') -Recurse
Get-ChildItem -LiteralPath $websiteExact | Copy-Item -Destination (Join-Path $package 'DocumentAssets/Website/Exact') -Recurse
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Documents/ThirdPartyNotices.md') -Destination (Join-Path $package 'DocumentAssets/Website/ThirdPartyNotices.md')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Source/XFESystem.Documents/ThirdPartyNotices.md') -Destination (Join-Path $package 'Licenses/Preview6Website/ThirdPartyNotices.md')
Get-ChildItem -LiteralPath (Split-Path -Parent $websiteExact) -File | Where-Object { $_.Name -like '*.License.txt' -or $_.Name -eq 'LicenseManifest.json' } | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $package 'DocumentAssets/Website/Licenses')
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $package 'Licenses/Preview6Website')
}
Copy-Item -LiteralPath (Join-Path (Split-Path -Parent $websiteExact) 'Highlight.License.txt') -Destination (Join-Path $package 'BrowserAssets/WebsiteCode/Highlight.License.txt')
Copy-Item -LiteralPath (Join-Path $websiteCode 'WebsiteLicense.txt') -Destination (Join-Path $package 'Licenses/Preview6Website/WebsiteCodeLicense.txt')
}
if (Test-Path -LiteralPath (Join-Path $lato 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/Lato'))
foreach ($file in @('OFL.txt','Readme.md','Manifest.json')) { Copy-Item -LiteralPath (Join-Path $lato $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/Lato/' + $file)) }
}
if (Test-Path -LiteralPath (Join-Path $webCjk 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/WebCjk'))
foreach ($file in @('OFL.txt','Readme.md','Manifest.json','OfficialContents.json')) { Copy-Item -LiteralPath (Join-Path $webCjk $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/WebCjk/' + $file)) }
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/BrowserCompatibility/BuildWebCjkFont.py') -Destination (Join-Path $package 'Licenses/Preview6Fonts/WebCjk/BuildWebCjkFont.py')
}
if (Test-Path -LiteralPath (Join-Path $webIcons 'Manifest.json')) {
[void][IO.Directory]::CreateDirectory((Join-Path $package 'Licenses/Preview6Fonts/BootstrapIcons'))
foreach ($file in @('License.md','Readme.md','Manifest.json')) { Copy-Item -LiteralPath (Join-Path $webIcons $file) -Destination (Join-Path $package ('Licenses/Preview6Fonts/BootstrapIcons/' + $file)) }
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/BrowserCompatibility/BuildWebIconFont.py') -Destination (Join-Path $package 'Licenses/Preview6Fonts/BootstrapIcons/BuildWebIconFont.py')
}
if ($reportFile) {
Copy-Item -LiteralPath $reportFile -Destination (Join-Path $package 'Validation/Report.json')
Copy-Item -LiteralPath ([IO.Path]::ChangeExtension($reportFile, '.md')) -Destination (Join-Path $package 'Validation/Report.md')
$receiptFiles = @()
$evidenceFiles = @()
foreach ($entry in $report.Receipts.PSObject.Properties) {
$relative = 'Validation/Receipts/' + $entry.Name + [IO.Path]::GetExtension([string]$entry.Value.Path)
Copy-Item -LiteralPath $entry.Value.Path -Destination (Join-Path $package $relative)
$evidenceDirectory = 'Validation/Evidence/' + $entry.Name
[void][IO.Directory]::CreateDirectory((Join-Path $package $evidenceDirectory))
$entryEvidence = @()
# Every receipt gets its own namespace. Reference and native captures
# commonly share names such as Dark.png, Serial.log and Result.json.
# Do not recurse into browser profiles, compiler artifacts or disk files.
foreach ($file in Get-ChildItem -LiteralPath (Split-Path -Parent $entry.Value.Path) -File) {
if ($file.Extension.ToLowerInvariant() -notin @('.png','.jpg','.jpeg','.webp','.svg','.html','.css','.js','.json','.log','.md','.txt','.trx')) { continue }
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
$source = Get-WorkspaceFile $file.FullName
$destination = $evidenceDirectory + '/' + $file.Name
$sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
Copy-Item -LiteralPath $source -Destination (Join-Path $package $destination)
if ((Get-FileHash -LiteralPath (Join-Path $package $destination) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sourceHash) { throw ('Evidence copy changed: ' + $source) }
$entryEvidence += $destination
$evidenceFiles += [ordered]@{Receipt=$entry.Name;PackagedPath=$destination;SourcePath=$source;Sha256=$sourceHash;Bytes=$file.Length}
}
$attachmentIndex = 0
foreach ($attachment in $entry.Value.Artifacts) {
$source = Get-WorkspaceFile ([string]$attachment.Path)
$sourceHash = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
if (!$attachment.Sha256 -or $sourceHash -ne $attachment.Sha256) { throw ('Receipt artifact changed: ' + $source) }
$attachmentIndex++
$attachmentDirectory = $evidenceDirectory + '/Attachments'
[void][IO.Directory]::CreateDirectory((Join-Path $package $attachmentDirectory))
$destination = $attachmentDirectory + '/' + $attachmentIndex.ToString('D4') + '-' + [IO.Path]::GetFileName($source)
Copy-Item -LiteralPath $source -Destination (Join-Path $package $destination)
$entryEvidence += $destination
$evidenceFiles += [ordered]@{Receipt=$entry.Name;PackagedPath=$destination;SourcePath=$source;Sha256=$sourceHash;Bytes=(Get-Item -LiteralPath $source).Length}
}
$receiptFiles += [ordered]@{Name=$entry.Name;PackagedPath=$relative;SourcePath=$entry.Value.Path;Sha256=$entry.Value.Sha256;EvidenceDirectory=$evidenceDirectory;EvidenceFiles=$entryEvidence}
if ($entry.Name -eq 'Display') {
Get-ChildItem -LiteralPath (Split-Path -Parent $entry.Value.Path) -File |
Where-Object { $_.Extension -eq '.png' -or $_.Name -eq 'Serial.log' } |
Copy-Item -Destination (Join-Path $package 'Validation/DisplayGuest')
}
}
$receiptFiles | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $package 'Validation/ReceiptFiles.json') -Encoding UTF8
$evidenceFiles | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $package 'Validation/EvidenceFiles.json') -Encoding UTF8
if ($report.NativeBuildWarnings.BuildLogBound -eq $true) {
Copy-Item -LiteralPath $report.NativeBuildWarnings.Path -Destination (Join-Path $package 'Validation/NativeBuild.log')
}
}
Copy-Item -LiteralPath (Join-Path $taskRoot 'THIRD_PARTY.md') -Destination (Join-Path $package 'ThirdParty.md')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Bflat-8.0.2.txt') -Destination (Join-Path $package 'Licenses/Bflat-8.0.2.txt')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/CSharpBoot/Licenses/Bflat-8.0.2-ThirdPartyNotices.txt') -Destination (Join-Path $package 'Licenses/Bflat-8.0.2-ThirdPartyNotices.txt')
foreach ($document in @('NativeCudaPort.md', 'CSharpRenderBackends.md')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('docs/' + $document)) -Destination (Join-Path $package $document)
}
Get-ChildItem -LiteralPath $licenses | Copy-Item -Destination (Join-Path $package 'Licenses') -Recurse
foreach ($suite in $hostChecks.Suites) {
Copy-Item -LiteralPath $suite.Log -Destination (Join-Path $package ('Validation/Host/' + $suite.Suite + '.log'))
Copy-Item -LiteralPath $suite.Trx -Destination (Join-Path $package ('Validation/Host/' + $suite.Suite + '.trx'))
}
Get-ChildItem -LiteralPath (Split-Path -Parent $bootPath) -File |
Where-Object { $_.Extension -eq '.png' -or $_.Name -eq 'Serial.log' -or $_.Name -like 'HttpPeer*' } |
Copy-Item -Destination (Join-Path $package 'Validation/Guest')
foreach ($firmware in @('edk2-x86_64-code.fd','edk2-i386-vars.fd')) {
Copy-Item -LiteralPath (Join-Path $taskRoot ('.tools/firmware/' + $firmware)) -Destination (Join-Path $package ('Firmware/' + $firmware))
}
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/Program.cs') -Destination (Join-Path $package 'Samples/Program.cs')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/XFESystem.ManagedHello.csproj') -Destination (Join-Path $package 'Samples/XFESystem.ManagedHello.csproj')
Copy-Item -LiteralPath $samplePath -Destination (Join-Path $package 'Samples/XFESystem.ManagedHello.dll')
Copy-Item -LiteralPath (Join-Path $taskRoot 'Samples/XFESystem.ManagedHello/Packages.lock.json') -Destination (Join-Path $package 'Samples/Packages.lock.json')
$startScript = @'
[CmdletBinding()]
param([string]$QemuPath, [switch]$Fullscreen, [switch]$FitToWindow, [switch]$NativePixels, [switch]$PlanOnly, [ValidateSet('auto','tcg','whpx')][string]$Acceleration = 'auto', [ValidateSet('gtk','sdl')][string]$DisplayBackend = 'gtk')
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'Tools/OpenCSharp.ps1') -ImagePath (Join-Path $PSScriptRoot 'System.img') -VerifiedBootResult (Join-Path $PSScriptRoot 'Validation/Boot.json') -QemuPath $QemuPath -Fullscreen:$Fullscreen -FitToWindow:$FitToWindow -NativePixels:$NativePixels -PlanOnly:$PlanOnly -Acceleration $Acceleration -DisplayBackend $DisplayBackend
'@
[IO.File]::WriteAllText((Join-Path $package 'Start.ps1'), $startScript, (New-Object Text.UTF8Encoding($true)))
@'
# XFESystem C# 桌面开发预览
这是 C# NativeAOT 裸机内核的已验证开发快照,运行于 x86_64 QEMU q35 + UEFI。
没有使用 Linux 或 Windows 来宾内核。它复用 Cosmos 的运行时、GC 与驱动。
这不是完整 .NET 10/CoreCLR,也不包含 .NET Framework 4.8。
项目自有内核、UEFI 显示初始化和系统应用已改为 C#;旧 Rust 源码不再属于当前构建。
当前仍使用 CPU 软件绘制;NVIDIA PCI 资源发现不等于 CUDA 或 GPU 光栅化。
Markdown 首屏先显示真实正文和原始 CSS,再加载增强脚本;1 秒目标以当前 Validation 记录为准。
增强脚本仍存在不可中断的解释执行阶段,不能承诺脚本加载期间全部交互都流畅。
## 启动
在 Windows 安装 QEMU 后,用 PowerShell 在此目录执行:
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start.ps1 -QemuPath "C:\Program Files\qemu\qemu-system-x86_64.exe"
启动脚本核验 System.img,复制会话系统盘和固件变量,打开可交互 GTK 窗口。
只读检测主显示器最高分辨率与该分辨率的最高刷新声明;来宾仍使用原生分辨率。
默认将完整桌面适配到普通最大化窗口,避免底部任务栏被裁掉;宿主适配会重采样显示画面。
使用 -NativePixels 可关闭视口适配,按原样像素显示,但较小窗口可能裁掉桌面边缘。
-FitToWindow 保留为显式适配选项,与 -NativePixels 互斥;界面倍率与原生分辨率独立。
-Fullscreen 为显式全屏选项,部分 WHPX/GTK 组合可能启动较慢;默认不使用全屏启动。
默认交互模式使用已通过同一镜像验收的 TCG 对照;包会核对其记录哈希、镜像、内核与源快照。
本机 WHPX 无 GUI 验收通过,但 GTK 曾在固件阶段超时;-Acceleration whpx 保留为显式实验选项。
WHPX 需要宿主已有可用的 Windows Hypervisor Platform,脚本不会修改 Windows 功能或重启电脑。
本机可显式使用 `-Acceleration whpx -DisplayBackend sdl` 绕过 WHPX+GTK 的固件启动问题;SDL 使用 gl=off。
WHPX 是 CPU 虚拟化,SDL 是 QEMU 的宿主显示后端;它们均不是来宾 CUDA/GPU 渲染。
使用鼠标与键盘操作;关闭 QEMU 窗口结束。日志保存在 build/CSharp/Interactive-*。
不会打开物理磁盘。文件保存在 RAM,关闭后消失。
## 操作
- 拖动标题栏移动;双击最大化/还原;右下角拖动调整尺寸。右上角仅保留最小化与关闭按钮。
- 窗口圆角和线条使用原生像素抗锯齿;交互入口提供悬停与按下反馈。
- 底部任务栏切换、最小化与恢复;Win 开始菜单,Win+E 文件,Win+D 桌面。
- Alt+Tab 切换,Alt+Shift+Tab 反向,松开 Alt 提交;Alt+F4 关闭。
- Win+左右贴靠,Win+上最大化,Win+下还原/最小化。QEMU 需先捕获键盘焦点。
- Files:打开文件夹和文件;New Folder / New File 创建内容。
- Editor:代码文件使用真实 Monaco 模型编辑,Ctrl+S 保存;Markdown 的 Preview / F5 进入自研浏览器的真实文档 DOM/CSS/JavaScript 管线。
- Browser:Ctrl+L 选择地址,输入后 Enter;about:Start 会实际执行内置 JavaScript,about:Monaco 打开真实 Monaco 编辑器。
- Terminal:Run Demo 运行独立编译的 net10.0 示例 DLL,展示 Console 颜色、ANSI RGB 和真实 XFEConsole。
- 文件管理器进入 X:\Programs\ManagedHello,点击 DLL 也会实际运行此程序集。
- 桌面 Settings 图标或任务栏右侧入口打开外观设置,切换 Light / Dark、预设强调色或输入 #RRGGBB 后按 Enter / Apply。
- 桌面不再显示系统顶部栏;窗口最大化覆盖顶部,保留底部任务栏。
- 标题栏最小化和关闭图标有平滑悬停动画,中途离开会连续反向;拖动和缩放按原生像素更新。
- Dark 延续已有深蓝配色;设置保存在当前 RAM 会话,关闭虚拟机后消失。
浏览器采用自研有限 CSS 布局与绘制、AngleSharp DOM 和 Jint JavaScript,支持外部经典脚本、defer、动态资源和浏览器事件。
Monaco 0.52.2 核心提供真实编辑和 JavaScript 着色;C# 使用本系统显式适配器,将网站原 highlight.js/csharp.js 对完整模型的分类和原 CSS 调色板送入 Monaco TokensProvider。
原网站本身采用 highlight.js,没有 Monaco;共享语法与颜色来源不等于两种编辑界面逐像素相同。来源与转换清单见 BrowserAssets/WebsiteCode。
没有 Worker / TypeScript 语言服务或完整 CSS/Web 平台。操作和范围见 CSharpBrowserPreview6.md;历史核心说明见 CSharpBrowserPreview5.md。
启动内存与验收记录一致;本轮 Monaco 配置为 1 GiB,512 MiB 未通过完整编辑验收。内存耗尽时明确停止整个来宾,尚不能恢复或隔离应用。
原生 HTTPS 尚未移植,明确拒绝连接。实际 HTTP 状态以 Validation/Boot.json 为准。
Markdown 采用指定网站的 Markdig/清理策略、原字节主题/文章 CSS 和原始高亮/代码工具脚本,经自研 Web 布局和绘制进入来宾。参考浏览器截图仅作对照,不替代来宾验收。
系统正文实际回退 Lato,等宽字形回退 Cascadia Mono;Web 缺失的 BMP 中文字形采用 Noto Sans SC 派生静态 TrueType 400/700,最终缺字仍回退 Unifont。
原网站 Bootstrap Icons 1.11.3 的完整字体以 TTF 嵌入,通用 CSS before/after 内容生成布局盒并使用真实字形;来源、MIT 和无轮廓修改转换记录位于 Licenses/Preview6Fonts/BootstrapIcons。
字体按真实glyph存在性选择并使用轮廓抗锯齿;未捆绑 Segoe UI/Consolas/微软雅黑,不能声明与 Windows 字体相同。
CSS、SVG、网页字体、图标、数学和 Mermaid 支持仍须逐项以 Validation/Report.json 与独立截图证据为准,不能把资源导入或宿主通过视为原生完整通过。
Validation/Receipts 与 ReceiptFiles.json 保存原始记录,Validation/Evidence/<记录名>/ 分别保存参考图、来宾图、DOM和日志,避免同名图片互相覆盖。
独立 DLL 只支持 XFESystem.Net10.ConsoleIL.v1 的有限 IL 和 Console/XFEConsole 接口。
自编程序、内存文件与图形窗口仍在同一个内核地址空间,尚无独立进程隔离。
实体 USB 输入、多显示器、完整账号和持久化仍需移植。详细边界见 Reconstruction.md。
EDID 刷新声明、绘制目标、实测提交 FPS 与物理 VSync 分别记录。当前 GOP 没有扫描时钟/VSync 驱动,不能承诺实体 240 Hz 同步;性能证据见 Validation/Report.md。
镜像、依赖与验证信息见 Validation,完整第三方许可见 Licenses。
9 个图标由内置 Imagegen 生成,原图、完整提示词及资产哈希见 DesktopIcons。
'@ | Set-Content -LiteralPath (Join-Path $package 'Readme.md') -Encoding UTF8
if ((Get-FileHash -LiteralPath (Join-Path $package 'System.img') -Algorithm SHA256).Hash.ToLowerInvariant() -ne $hash) { throw 'Packaged image verification failed.' }
$manifest = @()
foreach ($file in Get-ChildItem -LiteralPath $package -File -Recurse) {
$manifest += [ordered]@{Path=$file.FullName.Substring($package.Length + 1).Replace('\','/');Bytes=$file.Length;Sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}
}
[ordered]@{Kind='CSharpBareMetalDevelopmentPreview';CompleteSystemRewrite=$false;FullDotNet10=$false;Framework48=$false;ImageSha256=$hash;BootReceiptSha256=(Get-FileHash -LiteralPath $bootPath -Algorithm SHA256).Hash.ToLowerInvariant();Files=$manifest} |
ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $package 'Manifest.json') -Encoding UTF8
Compress-Archive -Path (Join-Path $package '*') -DestinationPath $zipPath -CompressionLevel Optimal
Write-Host ('Preview package: ' + $zipPath)