Skip to content

Commit 759ab5b

Browse files
committed
Isolate V8 from inherited Visual Studio state
1 parent 41d8adf commit 759ab5b

6 files changed

Lines changed: 320 additions & 69 deletions

File tree

‎.github/workflows/CI.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ jobs:
7474
run: python tests/GraphicsCompatibility/prepare-kestrel.py
7575
- name: Graphics evidence boundary checks
7676
run: python -m unittest discover -s eng/graphics/tests -v
77+
- name: V8 Windows child environment checks
78+
shell: pwsh
79+
run: ./scripts/tests/test_v8_windows_environment.ps1
7780
- name: Graphics reference workload and trace checks
7881
run: node --test tests/GraphicsCompatibility/reference-tests.mjs
7982
- name: Build Release

‎.github/workflows/native-runtime-packages.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ on:
2626
- 'scripts/build-native-engine-runtime.sh'
2727
- 'scripts/build-native-engine-runtime-linux-container.sh'
2828
- 'scripts/build-native-engine-runtime.ps1'
29+
- 'scripts/V8WindowsEnvironment.psm1'
2930
- 'scripts/pack-packages.sh'
3031
- 'scripts/verify-cross-rid-compatibility.py'
3132
- 'scripts/tests/test_native_runtime_workflow_policy.py'
3233
- 'scripts/tests/test_verify_cross_rid_compatibility.py'
34+
- 'scripts/tests/test_v8_windows_environment.ps1'
3335
- 'scripts/verify-release-packages.py'
3436
- 'src/WebScene.Backend.Abstractions/**'
3537
- 'src/WebScene.Backend.Avalonia/**'
@@ -61,6 +63,9 @@ jobs:
6163
python3 -m unittest \
6264
scripts/tests/test_native_runtime_workflow_policy.py \
6365
scripts/tests/test_verify_cross_rid_compatibility.py
66+
- name: Test V8 Windows child environment
67+
shell: pwsh
68+
run: ./scripts/tests/test_v8_windows_environment.ps1
6469
- name: Setup .NET
6570
uses: actions/setup-dotnet@v5
6671
env:

‎scripts/V8WindowsEnvironment.psm1‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
Set-StrictMode -Version Latest
2+
3+
$script:WindowsDeveloperEnvironmentKeys = [System.Collections.Generic.HashSet[string]]::new(
4+
[System.StringComparer]::OrdinalIgnoreCase)
5+
@(
6+
"CommandPromptType", "DevEnvDir", "ExtensionSdkDir", "EXTERNAL_INCLUDE",
7+
"Framework40Version", "FrameworkDir", "FrameworkDir64", "FrameworkVersion",
8+
"FrameworkVersion64", "IFCPATH", "INCLUDE", "LIB", "LIBPATH", "NETFXSDKDir",
9+
"Platform", "UCRTVersion", "UniversalCRTSdkDir", "VCIDEInstallDir",
10+
"VCINSTALLDIR", "VCToolsInstallDir", "VCToolsRedistDir", "VCToolsVersion",
11+
"VisualStudioVersion", "VS170COMNTOOLS", "VSINSTALLDIR", "VSSDK150INSTALL",
12+
"VSSDKINSTALL", "WindowsLibPath", "WindowsSDK_ExecutablePath_x64",
13+
"WindowsSDK_ExecutablePath_x86", "WindowsSdkBinPath", "WindowsSdkDir",
14+
"WindowsSDKLibVersion", "WindowsSdkVerBinPath", "WindowsSDKVersion"
15+
) | ForEach-Object { [void] $script:WindowsDeveloperEnvironmentKeys.Add($_) }
16+
17+
function New-WebSceneV8ChildEnvironment {
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory = $true)]
21+
[System.Collections.IDictionary] $Environment
22+
)
23+
24+
$child = [System.Collections.Generic.Dictionary[string,string]]::new(
25+
[System.StringComparer]::OrdinalIgnoreCase)
26+
foreach ($entry in $Environment.GetEnumerator()) {
27+
if ($null -ne $entry.Value) {
28+
$child[[string] $entry.Key] = [string] $entry.Value
29+
}
30+
}
31+
32+
$initialized = $child.ContainsKey("__VSCMD_PREINIT_PATH") `
33+
-or $child.ContainsKey("VSCMD_VER") `
34+
-or $child.ContainsKey("VCINSTALLDIR")
35+
if (-not $initialized) {
36+
return $child
37+
}
38+
if (-not $child.ContainsKey("__VSCMD_PREINIT_PATH") `
39+
-or [string]::IsNullOrWhiteSpace($child["__VSCMD_PREINIT_PATH"])) {
40+
throw "An initialized Visual Studio environment must expose __VSCMD_PREINIT_PATH before V8 toolchain discovery."
41+
}
42+
43+
$originalPath = $child["__VSCMD_PREINIT_PATH"]
44+
foreach ($key in @($child.Keys)) {
45+
if ($script:WindowsDeveloperEnvironmentKeys.Contains($key) `
46+
-or $key.StartsWith("VSCMD_", [System.StringComparison]::OrdinalIgnoreCase) `
47+
-or $key.StartsWith("__VSCMD_", [System.StringComparison]::OrdinalIgnoreCase)) {
48+
[void] $child.Remove($key)
49+
}
50+
}
51+
$child["Path"] = $originalPath
52+
return $child
53+
}
54+
55+
function Invoke-WebSceneV8ChildPowerShell {
56+
[CmdletBinding()]
57+
param(
58+
[Parameter(Mandatory = $true)]
59+
[string] $ScriptPath,
60+
61+
[string[]] $ArgumentList = @(),
62+
63+
[System.Collections.IDictionary] $Environment
64+
)
65+
66+
if ($null -eq $Environment) {
67+
$Environment = [System.Collections.Generic.Dictionary[string,string]]::new(
68+
[System.StringComparer]::OrdinalIgnoreCase)
69+
foreach ($entry in Get-ChildItem Env:) {
70+
$Environment[$entry.Name] = $entry.Value
71+
}
72+
}
73+
$childEnvironment = New-WebSceneV8ChildEnvironment -Environment $Environment
74+
75+
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
76+
$startInfo.FileName = (Get-Process -Id $PID).Path
77+
$startInfo.UseShellExecute = $false
78+
[void] $startInfo.ArgumentList.Add("-NoLogo")
79+
[void] $startInfo.ArgumentList.Add("-NoProfile")
80+
[void] $startInfo.ArgumentList.Add("-File")
81+
[void] $startInfo.ArgumentList.Add((Resolve-Path $ScriptPath).Path)
82+
foreach ($argument in $ArgumentList) {
83+
[void] $startInfo.ArgumentList.Add($argument)
84+
}
85+
$startInfo.Environment.Clear()
86+
foreach ($entry in $childEnvironment.GetEnumerator()) {
87+
$startInfo.Environment[$entry.Key] = $entry.Value
88+
}
89+
90+
$process = [System.Diagnostics.Process]::Start($startInfo)
91+
$process.WaitForExit()
92+
if ($process.ExitCode -ne 0) {
93+
throw "The clean V8 child process exited with code $($process.ExitCode)."
94+
}
95+
}
96+
97+
Export-ModuleMember -Function New-WebSceneV8ChildEnvironment, Invoke-WebSceneV8ChildPowerShell

‎scripts/build-native-engine-runtime.ps1‎

Lines changed: 95 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ param(
2424
[string] $V8Snapshot = "bootstrap",
2525
[switch] $UpstreamV8,
2626
[switch] $ThinLto,
27-
[switch] $PartitionAlloc
27+
[switch] $PartitionAlloc,
28+
[switch] $V8ChildBuild
2829
)
2930

3031
$ErrorActionPreference = "Stop"
@@ -70,85 +71,110 @@ if ([string]::IsNullOrWhiteSpace($V8Root)) {
7071
if ([string]::IsNullOrWhiteSpace($V8Workspace)) {
7172
$V8Workspace = Join-Path $repoRoot "artifacts/native-engine-v8/$Rid"
7273
}
73-
$depotTools = Join-Path $V8Workspace "depot_tools"
7474
$V8Root = Join-Path $V8Workspace "v8"
75-
New-Item -ItemType Directory -Force -Path $V8Workspace | Out-Null
76-
# depot_tools may run without a persistent global Git configuration on
77-
# hosted Windows runners. Force LF checkouts for the pinned V8 sources so
78-
# the pinned native V8 patches apply identically on every platform.
79-
$env:GIT_CONFIG_COUNT = "2"
80-
$env:GIT_CONFIG_KEY_0 = "core.autocrlf"
81-
$env:GIT_CONFIG_VALUE_0 = "false"
82-
$env:GIT_CONFIG_KEY_1 = "core.eol"
83-
$env:GIT_CONFIG_VALUE_1 = "lf"
84-
if (-not (Test-Path (Join-Path $depotTools ".git"))) {
85-
& git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git $depotTools
86-
if ($LASTEXITCODE -ne 0) { throw "Failed to clone depot_tools." }
75+
$v8PreparedByChild = $false
76+
if ($IsWindows -and -not $V8ChildBuild) {
77+
Import-Module (Join-Path $PSScriptRoot "V8WindowsEnvironment.psm1") -Force
78+
$childArguments = @(
79+
"-Rid", $Rid,
80+
"-PackageVersion", $PackageVersion,
81+
"-V8Workspace", $V8Workspace,
82+
"-V8Revision", $v8Revision,
83+
"-V8ChildBuild"
84+
)
85+
if ($UpstreamV8) { $childArguments += "-UpstreamV8" }
86+
if ($ThinLto) { $childArguments += "-ThinLto" }
87+
if ($PartitionAlloc) { $childArguments += "-PartitionAlloc" }
88+
Invoke-WebSceneV8ChildPowerShell `
89+
-ScriptPath $PSCommandPath `
90+
-ArgumentList $childArguments
91+
$v8PreparedByChild = $true
8792
}
88-
$env:Path = "$depotTools;$env:Path"
89-
$depotToolsGit = Join-Path $depotTools "git.bat"
90-
if (-not (Test-Path $depotToolsGit)) {
91-
& (Join-Path $depotTools "bootstrap/win_tools.bat")
92-
if ($LASTEXITCODE -ne 0) { throw "Failed to bootstrap depot_tools for Windows." }
93-
}
94-
$env:DEPOT_TOOLS_UPDATE = "0"
95-
$env:DEPOT_TOOLS_WIN_TOOLCHAIN = "0"
9693

97-
if (-not (Test-Path (Join-Path $V8Workspace ".gclient"))) {
94+
if (-not $v8PreparedByChild) {
95+
$depotTools = Join-Path $V8Workspace "depot_tools"
96+
New-Item -ItemType Directory -Force -Path $V8Workspace | Out-Null
97+
# depot_tools may run without a persistent global Git configuration on
98+
# hosted Windows runners. Force LF checkouts for the pinned V8 sources so
99+
# the pinned native V8 patches apply identically on every platform.
100+
$env:GIT_CONFIG_COUNT = "2"
101+
$env:GIT_CONFIG_KEY_0 = "core.autocrlf"
102+
$env:GIT_CONFIG_VALUE_0 = "false"
103+
$env:GIT_CONFIG_KEY_1 = "core.eol"
104+
$env:GIT_CONFIG_VALUE_1 = "lf"
105+
if (-not (Test-Path (Join-Path $depotTools ".git"))) {
106+
& git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git $depotTools
107+
if ($LASTEXITCODE -ne 0) { throw "Failed to clone depot_tools." }
108+
}
109+
$env:Path = "$depotTools;$env:Path"
110+
$depotToolsGit = Join-Path $depotTools "git.bat"
111+
if (-not (Test-Path $depotToolsGit)) {
112+
& (Join-Path $depotTools "bootstrap/win_tools.bat")
113+
if ($LASTEXITCODE -ne 0) { throw "Failed to bootstrap depot_tools for Windows." }
114+
}
115+
$env:DEPOT_TOOLS_UPDATE = "0"
116+
$env:DEPOT_TOOLS_WIN_TOOLCHAIN = "0"
117+
118+
if (-not (Test-Path (Join-Path $V8Workspace ".gclient"))) {
119+
Push-Location $V8Workspace
120+
try { & gclient.bat config https://chromium.googlesource.com/v8/v8 }
121+
finally { Pop-Location }
122+
if ($LASTEXITCODE -ne 0) { throw "Failed to configure the V8 checkout." }
123+
}
98124
Push-Location $V8Workspace
99-
try { & gclient.bat config https://chromium.googlesource.com/v8/v8 }
125+
try { & gclient.bat sync --no-history -r $v8Revision }
100126
finally { Pop-Location }
101-
if ($LASTEXITCODE -ne 0) { throw "Failed to configure the V8 checkout." }
102-
}
103-
Push-Location $V8Workspace
104-
try { & gclient.bat sync --no-history -r $v8Revision }
105-
finally { Pop-Location }
106-
if ($LASTEXITCODE -ne 0) { throw "Failed to synchronize V8 $v8Revision." }
127+
if ($LASTEXITCODE -ne 0) { throw "Failed to synchronize V8 $v8Revision." }
107128

108-
function Apply-PatchOnce([string] $Checkout, [string] $PatchPath) {
109-
& git -C $Checkout apply --check --ignore-space-change $PatchPath
110-
if ($LASTEXITCODE -eq 0) {
111-
& git -C $Checkout apply --ignore-space-change $PatchPath
112-
if ($LASTEXITCODE -ne 0) { throw "Failed to apply V8 patch '$PatchPath'." }
113-
return
129+
function Apply-PatchOnce([string] $Checkout, [string] $PatchPath) {
130+
& git -C $Checkout apply --check --ignore-space-change $PatchPath
131+
if ($LASTEXITCODE -eq 0) {
132+
& git -C $Checkout apply --ignore-space-change $PatchPath
133+
if ($LASTEXITCODE -ne 0) { throw "Failed to apply V8 patch '$PatchPath'." }
134+
return
135+
}
136+
& git -C $Checkout apply --reverse --check --ignore-space-change $PatchPath
137+
if ($LASTEXITCODE -ne 0) {
138+
throw "Cannot apply or recognize V8 patch '$PatchPath' in '$Checkout'."
139+
}
114140
}
115-
& git -C $Checkout apply --reverse --check --ignore-space-change $PatchPath
116-
if ($LASTEXITCODE -ne 0) {
117-
throw "Cannot apply or recognize V8 patch '$PatchPath' in '$Checkout'."
141+
# WebScene owns the JavaScript console bindings. The inspector bridge keeps
142+
# the original V8 values so CDP clients receive object ids and previews.
143+
Apply-PatchOnce $V8Root (Join-Path $repoRoot "third-party/v8-patches/V8InspectorConsolePatch.txt")
144+
if (-not $UpstreamV8 -and $v8Revision -ne "15.3.10") {
145+
Apply-PatchOnce $V8Root (Join-Path $repoRoot "third-party/v8-patches/V8Patch.txt")
146+
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8ToolchainPatch.txt")
118147
}
119-
}
120-
# WebScene owns the JavaScript console bindings. The inspector bridge keeps
121-
# the original V8 values so CDP clients receive object ids and previews.
122-
Apply-PatchOnce $V8Root (Join-Path $repoRoot "third-party/v8-patches/V8InspectorConsolePatch.txt")
123-
if (-not $UpstreamV8 -and $v8Revision -ne "15.3.10") {
124-
Apply-PatchOnce $V8Root (Join-Path $repoRoot "third-party/v8-patches/V8Patch.txt")
125-
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8ToolchainPatch.txt")
126-
}
127-
if ($ThinLto) {
128-
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8ThinLtoPatch.txt")
129-
}
130-
if ($v8Revision -eq "15.3.10") {
131-
# V8 15.3 needs two clang-cl/MSVC compatibility fixes: model the
132-
# ExtendedMap ABI padding in Torque and use V8's FunctionRef directly
133-
# in the backing-store allocation retry path.
134-
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8WindowsCompatibilityPatch.txt")
135-
}
136-
if (-not $UpstreamV8 -and $v8Revision -ne "15.3.10") {
137-
Apply-PatchOnce (Join-Path $V8Root "build") (Join-Path $repoRoot "third-party/v8-patches/BuildPatch.txt")
138-
Apply-PatchOnce (Join-Path $V8Root "third_party/icu") (Join-Path $repoRoot "third-party/v8-patches/ICUPatch.txt")
148+
if ($ThinLto) {
149+
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8ThinLtoPatch.txt")
150+
}
151+
if ($v8Revision -eq "15.3.10") {
152+
# V8 15.3 needs two clang-cl/MSVC compatibility fixes: model the
153+
# ExtendedMap ABI padding in Torque and use V8's FunctionRef directly
154+
# in the backing-store allocation retry path.
155+
Apply-PatchOnce $V8Root (Join-Path $repoRoot "packaging/WebScene.NativeEngine.Runtime/patches/V8WindowsCompatibilityPatch.txt")
156+
}
157+
if (-not $UpstreamV8 -and $v8Revision -ne "15.3.10") {
158+
Apply-PatchOnce (Join-Path $V8Root "build") (Join-Path $repoRoot "third-party/v8-patches/BuildPatch.txt")
159+
Apply-PatchOnce (Join-Path $V8Root "third_party/icu") (Join-Path $repoRoot "third-party/v8-patches/ICUPatch.txt")
160+
}
161+
162+
# Backslash-escaped quotes survive PowerShell's native argument marshalling
163+
# and reach GN as string delimiters.
164+
$gnArgs = 'chrome_pgo_phase=0 fatal_linker_warnings=false is_cfi=false is_component_build=false is_debug=false symbol_level=0 target_cpu=\"{0}\" treat_warnings_as_errors=false use_clang_modules=false use_custom_libcxx=false use_thin_lto={1} v8_embedder_string=\"-WebScene\" v8_enable_fuzztest=false v8_enable_partition_alloc={2} v8_enable_pointer_compression=true v8_enable_pointer_compression_shared_cage=true v8_enable_sandbox=false v8_enable_static_roots=false v8_enable_31bit_smis_on_64bit_arch=false v8_enable_temporal_support=false v8_monolithic=true v8_use_external_startup_data=false v8_target_cpu=\"{0}\"' -f $cpu, $thinLtoValue, $partitionAllocValue
165+
Push-Location $V8Root
166+
try {
167+
& gn.bat gen "out/$cpu/$v8Configuration" "--args=$gnArgs"
168+
if ($LASTEXITCODE -ne 0) { throw "Failed to generate the V8 build." }
169+
& ninja.exe -C "out/$cpu/$v8Configuration" "obj/v8_monolith.lib"
170+
if ($LASTEXITCODE -ne 0) { throw "Failed to build the V8 monolith." }
171+
}
172+
finally { Pop-Location }
139173
}
140174

141-
# Backslash-escaped quotes survive PowerShell's native argument marshalling
142-
# and reach GN as string delimiters.
143-
$gnArgs = 'chrome_pgo_phase=0 fatal_linker_warnings=false is_cfi=false is_component_build=false is_debug=false symbol_level=0 target_cpu=\"{0}\" treat_warnings_as_errors=false use_clang_modules=false use_custom_libcxx=false use_thin_lto={1} v8_embedder_string=\"-WebScene\" v8_enable_fuzztest=false v8_enable_partition_alloc={2} v8_enable_pointer_compression=true v8_enable_pointer_compression_shared_cage=true v8_enable_sandbox=false v8_enable_static_roots=false v8_enable_31bit_smis_on_64bit_arch=false v8_enable_temporal_support=false v8_monolithic=true v8_use_external_startup_data=false v8_target_cpu=\"{0}\"' -f $cpu, $thinLtoValue, $partitionAllocValue
144-
Push-Location $V8Root
145-
try {
146-
& gn.bat gen "out/$cpu/$v8Configuration" "--args=$gnArgs"
147-
if ($LASTEXITCODE -ne 0) { throw "Failed to generate the V8 build." }
148-
& ninja.exe -C "out/$cpu/$v8Configuration" "obj/v8_monolith.lib"
149-
if ($LASTEXITCODE -ne 0) { throw "Failed to build the V8 monolith." }
175+
if ($V8ChildBuild) {
176+
return
150177
}
151-
finally { Pop-Location }
152178
}
153179

154180
$v8OutputRoot = Join-Path $V8Root "out/$cpu/$v8Configuration"

‎scripts/tests/test_native_runtime_workflow_policy.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ def test_candidate_gaps_are_advisory_without_masking_job_failures(self) -> None:
7373
self.assertNotIn("\n continue-on-error: true", candidate)
7474
self.assertIn("--advisory-test-failures", candidate)
7575

76+
def test_v8_windows_environment_boundary_is_wired_into_build_and_ci(self) -> None:
77+
package_workflow = self.workflows[
78+
ROOT / ".github/workflows/native-runtime-packages.yml"
79+
]
80+
ci_workflow = self.workflows[ROOT / ".github/workflows/CI.yml"]
81+
runtime_build = (
82+
ROOT / "scripts/build-native-engine-runtime.ps1"
83+
).read_text(encoding="utf-8")
84+
test_path = "scripts/tests/test_v8_windows_environment.ps1"
85+
86+
self.assertIn(f"- '{test_path}'", package_workflow)
87+
self.assertIn(f"./{test_path}", package_workflow)
88+
self.assertIn(f"./{test_path}", ci_workflow)
89+
self.assertIn("Invoke-WebSceneV8ChildPowerShell", runtime_build)
90+
self.assertIn("-V8ChildBuild", runtime_build)
91+
7692

7793
if __name__ == "__main__":
7894
unittest.main()

0 commit comments

Comments
 (0)