Compare commits
14 Commits
8f61d5fb61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dfae1f136b | |||
|
|
74e48aa1e5 | ||
| a6ac6ce355 | |||
|
|
9832121dbb | ||
| d0a5925652 | |||
|
|
e91c4de7ed | ||
| 51ab88b1f8 | |||
|
|
709744d533 | ||
|
|
ddd8784b56 | ||
| 226a823c68 | |||
|
|
67befa56df | ||
|
|
13df66d137 | ||
| 541a17c792 | |||
|
|
9fa613b8c1 |
@@ -184,6 +184,15 @@ jobs:
|
||||
# RUNNER_TEMP is per-job/ephemeral. Keep the latest validated build at a
|
||||
# stable path so it can be retrieved (e.g. for VM boot-testing) out of band.
|
||||
New-Item -ItemType Directory -Force 'C:\silvermetal\out' | Out-Null
|
||||
# The ISO is already built + validated; free the build working set (extracted ISO
|
||||
# tree + the mounted/expanded install.wim + the 5GB base ISO) BEFORE the ~5GB persist
|
||||
# copy, or the single-volume runner runs out of space mid-copy. The ISO itself lives
|
||||
# in RUNNER_TEMP\out (untouched) and the SBOM/SHA uploads read from there too.
|
||||
$before = [math]::Round((Get-PSDrive C).Free/1GB,1)
|
||||
Remove-Item "$env:RUNNER_TEMP\smbuild" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "$env:RUNNER_TEMP\base.iso" -Force -ErrorAction SilentlyContinue
|
||||
$after = [math]::Round((Get-PSDrive C).Free/1GB,1)
|
||||
Write-Host " freed build working set: C: ${before}GB -> ${after}GB before persist"
|
||||
Copy-Item "$env:RUNNER_TEMP\out\*" 'C:\silvermetal\out\' -Force
|
||||
Get-ChildItem 'C:\silvermetal\out' | ForEach-Object { Write-Host $_.Name }
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -68,3 +68,7 @@ coverage/
|
||||
|
||||
# SBOM intermediates (final SBOMs are committed; intermediates are not)
|
||||
sbom/work/
|
||||
|
||||
# Driver binaries (e.g. virtio NetKVM netkvmp.exe) must be tracked despite the global
|
||||
# *.exe / *.msi ignores above -- they are referenced by the .inf and DISM needs them.
|
||||
!windows/drivers/**
|
||||
|
||||
@@ -1,5 +1,68 @@
|
||||
#Requires -Version 5.1
|
||||
$ErrorActionPreference='SilentlyContinue'
|
||||
# Register the inbox App Installer if present, else nothing to do (offline image w/o it).
|
||||
# Provision winget (the App Installer) when absent. Windows IoT Enterprise LTSC
|
||||
# ships WITHOUT the inbox Microsoft.DesktopAppInstaller package, so re-registering
|
||||
# it is not enough - we download and install it (plus dependencies) online at apply
|
||||
# time. Best-effort and idempotent: exit 0 if winget ends up available, else 1.
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
function Test-Winget {
|
||||
return [bool](Get-Command winget -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
# Fast path 1: winget already on PATH.
|
||||
if (Test-Winget) { exit 0 }
|
||||
|
||||
# Fast path 2: an inbox App Installer package is present - just re-register it.
|
||||
Get-AppxPackage -AllUsers Microsoft.DesktopAppInstaller |
|
||||
ForEach-Object { Add-AppxPackage -DisableDevelopmentMode -Register "$($_.InstallLocation)\AppXManifest.xml" }
|
||||
ForEach-Object { Add-AppxPackage -DisableDevelopmentMode -Register "$($_.InstallLocation)\AppXManifest.xml" }
|
||||
if (Test-Winget) { exit 0 }
|
||||
|
||||
# Slow path: download + install the App Installer and its dependencies online.
|
||||
$temp = $env:TEMP
|
||||
$bundlePath = Join-Path $temp 'Microsoft.DesktopAppInstaller.msixbundle'
|
||||
$vclibsPath = Join-Path $temp 'Microsoft.VCLibs.x64.14.00.Desktop.appx'
|
||||
$uixamlNupkg = Join-Path $temp 'microsoft.ui.xaml.2.8.6.nupkg'
|
||||
$uixamlExtract = Join-Path $temp 'uixaml.2.8.6'
|
||||
$uixamlAppx = Join-Path $uixamlExtract 'tools\AppX\x64\Release\Microsoft.UI.Xaml.2.8.appx'
|
||||
|
||||
$bundleUrl = 'https://aka.ms/getwinget'
|
||||
$vclibsUrl = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx'
|
||||
$uixamlUrl = 'https://globalcdn.nuget.org/packages/microsoft.ui.xaml.2.8.6.nupkg'
|
||||
|
||||
function Get-File {
|
||||
param([string]$Url, [string]$Destination)
|
||||
try {
|
||||
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing
|
||||
return (Test-Path $Destination)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Download the App Installer bundle (required).
|
||||
if (-not (Get-File -Url $bundleUrl -Destination $bundlePath)) { exit 1 }
|
||||
|
||||
# Download the VCLibs desktop dependency (required).
|
||||
if (-not (Get-File -Url $vclibsUrl -Destination $vclibsPath)) { exit 1 }
|
||||
|
||||
# Download the UI.Xaml 2.8 nuget package (a .zip) and extract the appx from it.
|
||||
if (-not (Get-File -Url $uixamlUrl -Destination $uixamlNupkg)) { exit 1 }
|
||||
try {
|
||||
if (Test-Path $uixamlExtract) { Remove-Item -Path $uixamlExtract -Recurse -Force }
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($uixamlNupkg, $uixamlExtract)
|
||||
} catch {
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Path $uixamlAppx)) { exit 1 }
|
||||
|
||||
# Install order: VCLibs dependency, then UI.Xaml dependency, then the bundle with
|
||||
# both supplied as DependencyPath. Per-user Add-AppxPackage (toolbox runs as the
|
||||
# real admin user at first logon).
|
||||
Add-AppxPackage -Path $vclibsPath
|
||||
Add-AppxPackage -Path $uixamlAppx
|
||||
Add-AppxPackage -Path $bundlePath -DependencyPath $vclibsPath, $uixamlAppx
|
||||
|
||||
# Final re-check.
|
||||
if (Test-Winget) { exit 0 }
|
||||
exit 1
|
||||
|
||||
BIN
windows/drivers/netkvm/netkvmco.exe
Normal file
BIN
windows/drivers/netkvm/netkvmco.exe
Normal file
Binary file not shown.
BIN
windows/drivers/netkvm/netkvmp.exe
Normal file
BIN
windows/drivers/netkvm/netkvmp.exe
Normal file
Binary file not shown.
@@ -160,6 +160,12 @@ function Invoke-PublishWelcome {
|
||||
Write-Stage 'Stage 3b: publish SilverOS Welcome app (win-x64 self-contained)'
|
||||
$proj = Join-Path $WindowsDir 'welcome\src\SilverOS.Welcome.App'
|
||||
$out = Join-Path $WorkDir 'welcome-publish'
|
||||
# Force a CLEAN compile. The CI runner reuses build artifacts across runs, and dotnet's
|
||||
# incremental build has shipped a STALE SilverOS.Welcome.Core.dll (old code despite fixed
|
||||
# source) -- so wipe every bin/obj under welcome/ before publishing (a clean tree forces a
|
||||
# full recompile; note `dotnet publish` does NOT accept --no-incremental).
|
||||
Get-ChildItem (Join-Path $WindowsDir 'welcome') -Recurse -Directory -EA SilentlyContinue |
|
||||
Where-Object { $_.Name -in 'bin', 'obj' } | Remove-Item -Recurse -Force -EA SilentlyContinue
|
||||
& dotnet publish $proj -c Release -f net9.0-windows10.0.19041.0 -r win-x64 --self-contained true -o $out
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Welcome app dotnet publish failed' }
|
||||
Write-Host " Published to: $out"
|
||||
|
||||
@@ -4,17 +4,38 @@ namespace SilverOS.Welcome.Core.Apps;
|
||||
|
||||
public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppInstaller
|
||||
{
|
||||
// Best-effort diagnostic log (winget resolution, bootstrap output, per-app results).
|
||||
// Lives under ProgramData so it survives + is readable post-install. Never throws.
|
||||
private static readonly string LogPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "SilverMetal", "appinstall.log");
|
||||
|
||||
private static void Log(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
|
||||
File.AppendAllText(LogPath, $"{DateTime.Now:HH:mm:ss.fff} {msg}{Environment.NewLine}");
|
||||
}
|
||||
catch { /* logging is best-effort */ }
|
||||
}
|
||||
|
||||
private static string Snip(string? s) =>
|
||||
string.IsNullOrWhiteSpace(s) ? "" : s.Trim().Replace("\r", " ").Replace("\n", " ") is var t && t.Length > 300 ? t[..300] : t;
|
||||
|
||||
public async Task<IReadOnlyList<AppInstallResult>> InstallAsync(
|
||||
IReadOnlyList<AppCatalogEntry> apps, IProgress<ApplyProgress> progress, CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<AppInstallResult>();
|
||||
if (apps.Count == 0) return results;
|
||||
|
||||
Log($"InstallAsync: {apps.Count} app(s) requested: {string.Join(", ", apps.Select(a => a.Id))}");
|
||||
|
||||
// App installs are non-critical: a missing/broken winget (e.g. offline IoT LTSC) must
|
||||
// NEVER fail onboarding. Resolve winget defensively; if it can't be found, skip installs.
|
||||
var winget = await ResolveWingetAsync(progress, ct);
|
||||
if (winget is null)
|
||||
{
|
||||
Log($"winget UNAVAILABLE -> skipping all {apps.Count} app(s)");
|
||||
progress.Report(new($"App installer unavailable - skipping {apps.Count} app(s)", 80));
|
||||
foreach (var app in apps) results.Add(new AppInstallResult(app.Id, false));
|
||||
return results;
|
||||
@@ -33,6 +54,7 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
|
||||
$"install --id {id} --silent --accept-package-agreements --accept-source-agreements --disable-interactivity",
|
||||
ct);
|
||||
ok = r.ExitCode == 0;
|
||||
Log($"install {id}: exit={r.ExitCode} ok={ok} err={Snip(r.StdErr)}");
|
||||
if (ok && !string.IsNullOrWhiteSpace(app.Configure))
|
||||
{
|
||||
var script = Path.Combine(appsDir, "configure", app.Configure);
|
||||
@@ -43,6 +65,7 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
|
||||
}
|
||||
results.Add(new AppInstallResult(app.Id, ok));
|
||||
}
|
||||
Log($"InstallAsync done: {results.Count(r => r.Installed)}/{results.Count} installed");
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -52,22 +75,34 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
|
||||
private async Task<string?> ResolveWingetAsync(IProgress<ApplyProgress> progress, CancellationToken ct)
|
||||
{
|
||||
// 1) Already launchable by name (on PATH for this process)?
|
||||
if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";
|
||||
var p1 = await TryRunAsync("winget", "--version", ct);
|
||||
Log($"winget probe (PATH): exit={p1.ExitCode} out={Snip(p1.StdOut)}");
|
||||
if (p1.ExitCode == 0) return "winget";
|
||||
|
||||
// 2) Provision App Installer via the bundled bootstrap (or registered package), then re-probe.
|
||||
// 2) Provision App Installer, then re-probe. Run the bootstrap SCRIPT FILE directly
|
||||
// (it checks for winget and installs it online if absent). Invoking the .ps1 file
|
||||
// avoids an inline -Command (a prior inline if/else had an unbalanced-brace parse bug
|
||||
// from a non-interpolated string, so the bootstrap never actually ran).
|
||||
progress.Report(new("Preparing app installer", 68));
|
||||
var bootstrap = Path.Combine(appsDir, "bootstrap-winget.ps1");
|
||||
await TryRunAsync("powershell.exe",
|
||||
$"-NoProfile -ExecutionPolicy Bypass -Command \"if (Test-Path '{bootstrap}') {{ & '{bootstrap}' }} else {{ " +
|
||||
"Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue }}\"",
|
||||
ct);
|
||||
if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";
|
||||
var b = File.Exists(bootstrap)
|
||||
? await TryRunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -File \"{bootstrap}\"", ct)
|
||||
: await TryRunAsync("powershell.exe",
|
||||
"-NoProfile -ExecutionPolicy Bypass -Command \"Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue\"",
|
||||
ct);
|
||||
Log($"bootstrap-winget: exit={b.ExitCode} out={Snip(b.StdOut)} err={Snip(b.StdErr)}");
|
||||
var p2 = await TryRunAsync("winget", "--version", ct);
|
||||
Log($"winget probe (post-bootstrap): exit={p2.ExitCode} out={Snip(p2.StdOut)}");
|
||||
if (p2.ExitCode == 0) return "winget";
|
||||
|
||||
// 3) Fall back to the WindowsApps execution-alias path (bare-name launch can fail under
|
||||
// UseShellExecute=false even when winget is installed).
|
||||
var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var aliased = Path.Combine(local, "Microsoft", "WindowsApps", "winget.exe");
|
||||
if (File.Exists(aliased) && (await TryRunAsync(aliased, "--version", ct)).ExitCode == 0) return aliased;
|
||||
var aliasExists = File.Exists(aliased);
|
||||
var p3Exit = aliasExists ? (await TryRunAsync(aliased, "--version", ct)).ExitCode : -1;
|
||||
Log($"winget alias path '{aliased}': exists={aliasExists} probe={p3Exit}");
|
||||
if (aliasExists && p3Exit == 0) return aliased;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@inject IAppCatalog AppCatalog
|
||||
@inject IPreconfigStore PreconfigStore
|
||||
@inject WizardState State
|
||||
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
|
||||
|
||||
@if (_toolboxHome)
|
||||
{
|
||||
@@ -83,6 +84,10 @@ else
|
||||
@(_currentStep == _stepTitles.Length - 2 ? "Apply" : "Next")
|
||||
</button>
|
||||
}
|
||||
else if (_currentStep == _stepTitles.Length - 1)
|
||||
{
|
||||
<button class="btn-primary" @onclick="RestartNow">Restart now</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -108,6 +113,11 @@ else
|
||||
private string? _error;
|
||||
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
|
||||
|
||||
private async Task RestartNow()
|
||||
{
|
||||
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", default);
|
||||
}
|
||||
|
||||
private bool CanGoNext => _currentStep switch
|
||||
{
|
||||
1 => State.Flavour is not null,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using QRCoder
|
||||
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
|
||||
|
||||
<div class="step done-step">
|
||||
<h1>All Done!</h1>
|
||||
@@ -27,8 +26,6 @@
|
||||
</small></p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn-primary btn-restart" @onclick="RestartNow">Restart Now</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
@@ -56,9 +53,4 @@
|
||||
catch { /* QR is best-effort; the key text still shows */ }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestartNow()
|
||||
{
|
||||
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user