fix: apply direct runtime paths and respect taskbar

This commit is contained in:
2026-07-28 00:35:47 +09:00
parent e3b174eec7
commit 347900701b
16 changed files with 522 additions and 401 deletions

View File

@@ -584,8 +584,18 @@ function Assert-LegacyRuntimeRoot {
throw "Legacy runtime source root was not found: $Root"
}
if ($Root.Contains('$') -or
$Root.Contains('%') -or
$Root.Contains('@') -or
$Root.Contains(';') -or
$Root.Contains("'")) {
throw 'Legacy runtime source root contains characters reserved by MSBuild.'
}
Assert-FixedLocalPath -Path $Root -Description 'Legacy runtime source root'
Assert-NoReparsePointInTree -Root $Root -Description 'Legacy runtime source tree'
Assert-NoReparsePointInExistingAncestry `
-Path $Root `
-Description 'Legacy runtime source root'
if ((Test-SamePath -Left $Root -Right $RepositoryRoot) -or
(Test-PathIsWithin -Root $RepositoryRoot -Candidate $Root) -or
@@ -601,12 +611,17 @@ function Assert-LegacyRuntimeRoot {
-not (Test-SamePath -Left ([IO.Path]::GetDirectoryName($resRoot)) -Right $Root)) {
throw 'The selected folder must be the single common parent of Cuts and Res.'
}
Assert-NoReparsePointInExistingAncestry `
-Path $cutsRoot `
-Description 'Cuts source directory'
Assert-NoReparsePointInExistingAncestry `
-Path $resRoot `
-Description 'Res source directory'
}
function Get-ExistingRuntimeBinding {
param(
[Parameter(Mandatory = $true)][string] $RepositoryRoot,
[Parameter(Mandatory = $true)][string] $RuntimeStorageRoot
[Parameter(Mandatory = $true)][string] $RepositoryRoot
)
$propsPath = Join-Path $RepositoryRoot 'Directory.Build.local.props'
@@ -636,22 +651,10 @@ function Get-ExistingRuntimeBinding {
throw 'Existing Directory.Build.local.props is not the exact generated Required runtime binding.'
}
$installedRoot = Get-NormalizedFullPath -Path ([string] $roots[0].InnerText)
if (-not (Test-Path -LiteralPath $installedRoot -PathType Container) -or
-not (Test-PathIsWithin -Root $RuntimeStorageRoot -Candidate $installedRoot)) {
throw 'Existing runtime binding is not an installed LocalAppData runtime bundle.'
}
Assert-NoReparsePointInTree -Root $installedRoot -Description 'Installed runtime bundle'
$manifestPath = Join-Path $installedRoot $manifestFileName
[void](Assert-RegularFile -Path $manifestPath -Description 'Installed runtime manifest')
$boundRoot = Get-NormalizedFullPath -Path ([string] $roots[0].InnerText)
return [pscustomobject]@{
PropsPath = $propsPath
InstalledRoot = $installedRoot
ManifestPath = $manifestPath
ManifestSha256 = (
Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256
).Hash.ToUpperInvariant()
BoundRoot = $boundRoot
}
}
@@ -1273,6 +1276,41 @@ function Set-RuntimeBindingAtomically {
}
}
function New-DirectRuntimeBindingFile {
param(
[Parameter(Mandatory = $true)][string] $RuntimeRoot,
[Parameter(Mandatory = $true)][string] $OutputDirectory
)
$path = Join-Path $OutputDirectory 'Directory.Build.local.props'
$settings = New-Object Xml.XmlWriterSettings
$settings.Encoding = New-Object Text.UTF8Encoding($false)
$settings.Indent = $true
$settings.NewLineChars = [Environment]::NewLine
$settings.NewLineHandling = [Xml.NewLineHandling]::Replace
$writer = [Xml.XmlWriter]::Create($path, $settings)
try {
$writer.WriteStartDocument()
$writer.WriteStartElement('Project')
$writer.WriteStartElement('PropertyGroup')
$writer.WriteElementString(
'LegacyRuntimeSourceRoot',
(Get-NormalizedFullPath -Path $RuntimeRoot))
$writer.WriteElementString('LegacyRuntimeAssetsMode', 'Required')
$writer.WriteEndElement()
$writer.WriteEndElement()
$writer.WriteEndDocument()
}
finally {
$writer.Dispose()
}
[void](Assert-RegularFile `
-Path $path `
-Description 'Generated direct runtime binding')
return $path
}
function Get-CanonicalJsonText {
param(
[Parameter(Mandatory = $true)][object] $Value,
@@ -1282,6 +1320,44 @@ function Get-CanonicalJsonText {
return ($Value | ConvertTo-Json -Depth $Depth)
}
function Get-RuntimeFoldersPreflight {
param(
[Parameter(Mandatory = $true)][string] $LocalAppDataRoot,
[Parameter(Mandatory = $true)][string] $CutsDirectory,
[Parameter(Mandatory = $true)][string] $ResourceDirectory,
[switch] $AllowReplacement
)
$configurationRoot = Join-Path $LocalAppDataRoot 'MBN_STOCK_WEBVIEW\Config'
$path = Join-Path $configurationRoot 'runtime-folders.local.json'
$contract = [ordered]@{
schemaVersion = 2
sceneDirectory = $CutsDirectory
resourceDirectory = $ResourceDirectory
backgroundDirectory = $null
navigationExpanded = $true
colorTheme = 'system'
viewMode = 'automatic'
startWorkspace = 'stockCut'
}
$json = Get-CanonicalJsonText -Value $contract -Depth 3
$exists = Test-Path -LiteralPath $path
$matches = $exists -and
(Test-ExistingJsonContract -Path $path -ExpectedJson $json)
if ($exists -and -not $matches -and -not $AllowReplacement) {
throw (
'The stored Cuts/Res paths differ from the selected folders. ' +
'Re-run with -ReplaceRuntimeBinding.')
}
return [pscustomobject]@{
NeedsWrite = [bool] (-not $matches)
NeedsReplace = [bool] $exists
Path = $path
Json = $json
}
}
function Test-ExistingJsonContract {
param(
[Parameter(Mandatory = $true)][string] $Path,
@@ -1768,14 +1844,13 @@ function Invoke-DebugBuildAndVerify {
param(
[Parameter(Mandatory = $true)][string] $DotNetPath,
[Parameter(Mandatory = $true)][string] $ProjectPath,
[Parameter(Mandatory = $true)][string] $ExpectedRuntimeRoot,
[Parameter(Mandatory = $true)][string] $ExpectedManifestPath,
[Parameter(Mandatory = $true)][string] $ExpectedManifestSha256
[Parameter(Mandatory = $true)][string] $ExpectedRuntimeRoot
)
$runtimeRootProperty = "-p:LegacyRuntimeSourceRoot=$ExpectedRuntimeRoot"
& $DotNetPath build $ProjectPath `
-c Debug `
'--no-restore' `
'-p:Platform=x64' `
'-p:LegacyRuntimeAssetsMode=Required' `
$runtimeRootProperty |
@@ -1841,10 +1916,9 @@ function Invoke-DebugBuildAndVerify {
throw 'Debug x64 target directory is missing or is a SourceOnly output.'
}
$payload = Assert-DebugRuntimePayloadMatchesManifest `
-TargetDirectory $targetDir `
-ManifestPath $ExpectedManifestPath `
-ExpectedManifestSha256 $ExpectedManifestSha256
if (Test-Path -LiteralPath (Join-Path $targetDir 'Cuts')) {
throw 'Debug x64 copied Cuts instead of using the selected external path.'
}
if (@(Get-ChildItem -LiteralPath $targetDir -File -Recurse -Force |
Where-Object {
$_.Name.Equals('MmoneyCoder.ini', [StringComparison]::OrdinalIgnoreCase)
@@ -1854,8 +1928,6 @@ function Invoke-DebugBuildAndVerify {
return [pscustomobject]@{
TargetDirectory = $targetDir
CutsFileCount = [int] $payload.CutsFileCount
ResFileCount = [int] $payload.ResFileCount
SourceOnly = $false
}
}
@@ -1880,14 +1952,10 @@ if (-not $setupMutexOwned) {
$repositoryRoot = Get-NormalizedFullPath -Path (Join-Path $PSScriptRoot '..')
$projectPath = Join-Path $repositoryRoot (
'src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj')
$bundleCreatorPath = Join-Path $PSScriptRoot 'New-LegacyRuntimeBundle.ps1'
$bundleInitializerPath = Join-Path $PSScriptRoot 'Initialize-LegacyRuntimeBundle.ps1'
$registrationInspectorPath = Join-Path $PSScriptRoot 'Inspect-K3DRegistration.ps1'
$liveInitializerPath = Join-Path $PSScriptRoot 'Initialize-DevelopmentLiveConfig.ps1'
foreach ($requiredScript in @(
$bundleCreatorPath,
$bundleInitializerPath,
$registrationInspectorPath,
$liveInitializerPath)) {
[void](Assert-RegularFile -Path $requiredScript -Description 'Required setup script')
@@ -1936,6 +2004,22 @@ $runtimeSourceRoot = Select-LegacyRuntimeRoot `
-RequestedRoot $LegacyRuntimeSourceRoot `
-DisablePicker:$NoFolderPicker
Assert-LegacyRuntimeRoot -Root $runtimeSourceRoot -RepositoryRoot $repositoryRoot
$cutsSourceRoot = Get-NormalizedFullPath -Path (
Join-Path $runtimeSourceRoot 'Cuts')
$resourceSourceRoot = Get-NormalizedFullPath -Path (
Join-Path $runtimeSourceRoot 'Res')
$existingRuntime = Get-ExistingRuntimeBinding -RepositoryRoot $repositoryRoot
$runtimeBindingReused = $null -ne $existingRuntime -and
(Test-SamePath `
-Left ([string] $existingRuntime.BoundRoot) `
-Right $runtimeSourceRoot)
if ($null -ne $existingRuntime -and
-not $runtimeBindingReused -and
-not $ReplaceRuntimeBinding) {
throw (
'The selected Cuts/Res path differs from the existing runtime binding. ' +
'Re-run with -ReplaceRuntimeBinding.')
}
# This is a read-only registry/file inspection. It never activates a COM class.
$registrationReports = @(& $registrationInspectorPath)
@@ -1980,6 +2064,11 @@ Assert-FixedLocalPath -Path $localAppDataRoot -Description 'LocalAppData'
Assert-NoReparsePointInExistingAncestry `
-Path $localAppDataRoot `
-Description 'LocalAppData'
$runtimeFoldersPreflight = Get-RuntimeFoldersPreflight `
-LocalAppDataRoot $localAppDataRoot `
-CutsDirectory $cutsSourceRoot `
-ResourceDirectory $resourceSourceRoot `
-AllowReplacement:$ReplaceRuntimeBinding
# Preflight a requested Live replacement before any setup write. The caller
# either supplies both independent approval hashes or explicitly requests
@@ -2162,12 +2251,6 @@ elseif (-not [string]::IsNullOrWhiteSpace($DatabaseIniPath) -or
throw '-DatabaseIniPath/-ReplaceDatabaseProfile cannot be used with -SkipDatabaseProfile.'
}
$runtimeStorageRoot = Join-Path $localAppDataRoot (
'MBN_STOCK_WEBVIEW\RuntimeBundles')
$existingRuntime = Get-ExistingRuntimeBinding `
-RepositoryRoot $repositoryRoot `
-RuntimeStorageRoot $runtimeStorageRoot
$setupParent = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\SetupTemp'
$rollbackParent = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\SetupRollback'
Assert-NoReparsePointInExistingAncestry `
@@ -2179,14 +2262,16 @@ Assert-NoReparsePointInExistingAncestry `
$setupRoot = Join-Path $setupParent ([Guid]::NewGuid().ToString('N'))
$rollbackRoot = Join-Path $rollbackParent ([Guid]::NewGuid().ToString('N'))
$runtimeResult = $null
$pendingRuntimeBinding = $null
$buildResult = $null
$databaseInstalled = $false
$runtimeFoldersWritten = $false
$liveConfigured = $false
$runtimeBindingMutationStarted = $false
$runtimeFoldersMutationStarted = $false
$databaseMutationStarted = $false
$propsSnapshot = $null
$runtimeFoldersSnapshot = $null
$databaseSnapshot = $null
$playoutSnapshot = $null
$playoutMutationStarted = $false
@@ -2216,6 +2301,10 @@ try {
-Path (Join-Path $repositoryRoot 'Directory.Build.local.props') `
-BackupDirectory $rollbackRoot `
-Description 'Local build props'
$runtimeFoldersSnapshot = New-FileRollbackSnapshot `
-Path ([string] $runtimeFoldersPreflight.Path) `
-BackupDirectory $rollbackRoot `
-Description 'Runtime folder configuration'
if (-not $SkipDatabaseProfile) {
$databaseSnapshot = New-FileRollbackSnapshot `
-Path $databaseDestination `
@@ -2229,90 +2318,12 @@ try {
-Description 'Development Live base configuration'
}
$bundleOutput = Join-Path $setupRoot 'bundle'
$bundleResults = @(& $bundleCreatorPath `
-LegacyRuntimeSourceRoot $runtimeSourceRoot `
-OutputDirectory $bundleOutput)
if ($bundleResults.Count -ne 1) {
throw 'Runtime bundle creation did not return one result.'
}
$verifiedBundle = Get-VerifiedBundleCreatorResult `
-Result $bundleResults[0] `
-BundleOutput $bundleOutput
$bundleResult = $verifiedBundle.Result
$newManifestPath = [string] $verifiedBundle.Manifest.Path
$newManifestHash = [string] $verifiedBundle.Manifest.Sha256
$sameRuntime = $null -ne $existingRuntime -and
$existingRuntime.ManifestSha256.Equals(
$newManifestHash,
[StringComparison]::Ordinal)
if ($sameRuntime) {
Assert-InstalledRuntimeMatchesManifest `
-InstalledRoot $existingRuntime.InstalledRoot `
-ManifestPath $newManifestPath
$runtimeResult = [pscustomobject]@{
InstalledRoot = $existingRuntime.InstalledRoot
ManifestSha256 = $newManifestHash
ManifestPath = $existingRuntime.ManifestPath
CutsFileCount = [int] $verifiedBundle.Manifest.CutsFileCount
ResFileCount = [int] $verifiedBundle.Manifest.ResFileCount
Reused = $true
}
}
else {
if ($null -ne $existingRuntime -and -not $ReplaceRuntimeBinding) {
throw (
'The selected Cuts/Res manifest differs from the installed runtime ' +
'binding. Re-run with -ReplaceRuntimeBinding.')
}
# Install and fully verify the new immutable bundle while the current
# repository binding remains untouched. A minimal shadow repository lets
# the existing hardened initializer generate its exact Required props.
$shadowRepositoryRoot = Join-Path $setupRoot 'repository'
$shadowProjectDirectory = Join-Path $shadowRepositoryRoot (
'src\MBN_STOCK_WEBVIEW.LegacyParityApp')
[IO.Directory]::CreateDirectory($shadowProjectDirectory) | Out-Null
[IO.File]::Copy(
(Join-Path $repositoryRoot '.gitignore'),
(Join-Path $shadowRepositoryRoot '.gitignore'),
$false)
[IO.File]::Copy(
(Join-Path $repositoryRoot 'Directory.Build.props'),
(Join-Path $shadowRepositoryRoot 'Directory.Build.props'),
$false)
[IO.File]::Copy(
$projectPath,
(Join-Path $shadowProjectDirectory (
'MBN_STOCK_WEBVIEW.LegacyParityApp.csproj')),
$false)
$initializerResults = @(& $bundleInitializerPath `
-ZipPath ([string] $bundleResult.ArchivePath) `
-ExpectedSha256 ([string] $bundleResult.ArchiveSha256) `
-RepositoryRoot $shadowRepositoryRoot)
if ($initializerResults.Count -ne 1) {
throw 'Runtime bundle initialization did not return one result.'
}
$initialized = Get-VerifiedBundleInitializerResult `
-Result $initializerResults[0] `
-VerifiedBundle $verifiedBundle `
-ShadowRepositoryRoot $shadowRepositoryRoot `
-RuntimeStorageRoot $runtimeStorageRoot
$runtimeResult = [pscustomobject]@{
InstalledRoot = [string] $initialized.InstalledRoot
ManifestSha256 = [string] $initialized.ManifestSha256
ManifestPath = Join-Path `
([string] $initialized.InstalledRoot) `
$manifestFileName
CutsFileCount = [int] $initialized.CutsFileCount
ResFileCount = [int] $initialized.ResFileCount
Reused = $false
}
if (-not $runtimeBindingReused) {
$generatedPropsPath = New-DirectRuntimeBindingFile `
-RuntimeRoot $runtimeSourceRoot `
-OutputDirectory $setupRoot
$pendingRuntimeBinding = [pscustomobject]@{
GeneratedPropsPath = [string] $initialized.LocalBuildPropsPath
GeneratedPropsPath = $generatedPropsPath
Replace = [bool] ($null -ne $existingRuntime)
}
}
@@ -2321,15 +2332,7 @@ try {
$buildResult = Invoke-DebugBuildAndVerify `
-DotNetPath $dotnetPath `
-ProjectPath $projectPath `
-ExpectedRuntimeRoot $runtimeResult.InstalledRoot `
-ExpectedManifestPath $runtimeResult.ManifestPath `
-ExpectedManifestSha256 $runtimeResult.ManifestSha256
if ([int] $buildResult.CutsFileCount -ne
[int] $runtimeResult.CutsFileCount -or
[int] $buildResult.ResFileCount -ne
[int] $runtimeResult.ResFileCount) {
throw 'Debug x64 payload count mismatch after exact manifest verification.'
}
-ExpectedRuntimeRoot $runtimeSourceRoot
}
if ($databaseNeedsWrite) {
@@ -2342,6 +2345,20 @@ try {
$databaseInstalled = $true
}
if ($runtimeFoldersPreflight.NeedsWrite) {
$runtimeFoldersMutationStarted = $true
Write-ProtectedUtf8TextAtomically `
-Path ([string] $runtimeFoldersPreflight.Path) `
-ExpectedText ([string] $runtimeFoldersPreflight.Json) `
-Description 'runtime folder configuration' `
-Replace:$runtimeFoldersPreflight.NeedsReplace
Assert-ExistingJsonContract `
-Path ([string] $runtimeFoldersPreflight.Path) `
-ExpectedJson ([string] $runtimeFoldersPreflight.Json) `
-Description 'Runtime folder configuration'
$runtimeFoldersWritten = $true
}
if ($ConfigureDevelopmentLive) {
[void](Assert-RegularFile `
-Path $nativePath `
@@ -2410,20 +2427,13 @@ try {
-RepositoryRoot $repositoryRoot `
-Replace:([bool] $pendingRuntimeBinding.Replace)
$verifiedBinding = Get-ExistingRuntimeBinding `
-RepositoryRoot $repositoryRoot `
-RuntimeStorageRoot $runtimeStorageRoot
-RepositoryRoot $repositoryRoot
if ($null -eq $verifiedBinding -or
-not (Test-SamePath `
-Left $verifiedBinding.InstalledRoot `
-Right ([string] $runtimeResult.InstalledRoot)) -or
-not $verifiedBinding.ManifestSha256.Equals(
[string] $runtimeResult.ManifestSha256,
[StringComparison]::Ordinal)) {
-Left ([string] $verifiedBinding.BoundRoot) `
-Right $runtimeSourceRoot)) {
throw 'The atomically installed runtime binding failed final verification.'
}
Assert-InstalledRuntimeMatchesManifest `
-InstalledRoot $verifiedBinding.InstalledRoot `
-ManifestPath $runtimeResult.ManifestPath
}
# A cleanup failure is still a transaction failure. Rollback snapshots are
@@ -2469,6 +2479,19 @@ if (-not $transactionCommitted) {
$_.Exception.Message)
}
}
if ($runtimeFoldersMutationStarted -and
$null -ne $runtimeFoldersSnapshot) {
try {
Restore-FileRollbackSnapshot `
-Snapshot $runtimeFoldersSnapshot `
-Description 'Runtime folder configuration'
}
catch {
$secondaryFailures.Add(
'Runtime folder configuration rollback failed: ' +
$_.Exception.Message)
}
}
if ($databaseMutationStarted -and $null -ne $databaseSnapshot) {
try {
Restore-FileRollbackSnapshot `
@@ -2538,9 +2561,10 @@ if (-not $transactionCommitted) {
[pscustomobject]@{
RuntimeSourceRoot = $runtimeSourceRoot
InstalledRuntimeRoot = $runtimeResult.InstalledRoot
RuntimeManifestSha256 = $runtimeResult.ManifestSha256
RuntimeBindingReused = [bool] $runtimeResult.Reused
BoundRuntimeRoot = $runtimeSourceRoot
RuntimeBindingReused = [bool] $runtimeBindingReused
RuntimeFolderConfigurationPath = [string] $runtimeFoldersPreflight.Path
RuntimeFolderConfigurationWritten = $runtimeFoldersWritten
DatabaseProfilePath = if ($SkipDatabaseProfile) { $null } else {
$databaseDestination
}