mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
build: update v8_embedder_string from net patch effects (#54618)
* fix: derive V8 cache compatibility from net patch effects Replace the custom V8 cache salt and Wasm embedded blob guard from #54234 with embedder string version hashing. Generate -electron.<16-hex fingerprint> during GN configuration from the net committed changes between Chromium's pinned V8 revision and patched HEAD. This requires no release time bookkeeping and preserves cache identities across packaging only changes to the patch series. * build: fix metadata generation and restoration * build: address review follow-ups - Keep code cache hash checks free of process global mutation - Limit the helper to DCHECK enabled builds - Make the fingerprint algorithm self contained and include it in the source cache key - Allow an explicit packager identity - Skip Wasm tests without generated assets - Correct the documentation structure * build: simplify V8 fingerprint regeneration tracking Track Git HEAD and the exported V8 patch series instead of symbolic branch refs, packed-refs, or Git directories. Detached checkout updates and patch exports trigger regeneration, while equivalent re-exports preserve the committed source fingerprint. Reject reftable storage explicitly and provide actionable diagnostics when the checkout does not contain Chromium's pinned V8 revision. * fix: gn check for release builds * chore: convert to vitest imports * chore: cleanup testing binding dependency * chore: update patches * chore: remove redundant dep declaration
This commit is contained in:
21 files changed
+777
-302
No files matched your search
@@ -162,6 +162,10 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
node src/electron/script/patches-stats.mjs --upload-stats || true
|
||||
- name: Record V8 patch fingerprint for the source cache
|
||||
if: ${{ steps.check-cache.outputs.cache_exists == 'false' && inputs.use-cache == 'true' }}
|
||||
shell: bash
|
||||
run: python3 src/electron/script/v8-patch-fingerprint.py --write-ci-cache
|
||||
# delete all .git directories under src/ except for
|
||||
# third_party/angle/ and third_party/dawn/ because of build time generation of files
|
||||
# gen/angle/commit.h depends on third_party/angle/.git/HEAD
|
||||
|
||||
@@ -107,6 +107,11 @@ jobs:
|
||||
cd src/electron
|
||||
node script/yarn.js install --immutable
|
||||
node script/yarn.js lint
|
||||
- name: Run V8 Fingerprint Tests
|
||||
shell: bash
|
||||
run: |
|
||||
cd src/electron
|
||||
python3 script/v8-patch-fingerprint-test.py
|
||||
- name: Run Script Typechecker
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -648,6 +648,10 @@ source_set("electron_lib") {
|
||||
if (!is_mac) {
|
||||
sources += filenames.lib_sources_views
|
||||
}
|
||||
if (is_debug || dcheck_always_on) {
|
||||
sources += [ "shell/common/api/electron_api_testing.cc" ]
|
||||
deps += [ "shell/common:v8_code_cache_test_helpers" ]
|
||||
}
|
||||
|
||||
if (electron_generate_js2c_code_cache) {
|
||||
sources += [
|
||||
@@ -2049,8 +2053,22 @@ action("libcxx_objects_zip") {
|
||||
args = rebase_path(outputs)
|
||||
}
|
||||
|
||||
if (is_debug || dcheck_always_on) {
|
||||
# Test runners receive these assets rather than a full V8 checkout.
|
||||
copy("v8_code_cache_spec_assets") {
|
||||
sources = [
|
||||
"//v8/LICENSE",
|
||||
"//v8/test/mjsunit/wasm/wasm-module-builder.js",
|
||||
]
|
||||
outputs = [ "$root_gen_dir/electron/v8-code-cache/{{source_file_part}}" ]
|
||||
}
|
||||
}
|
||||
|
||||
group("electron") {
|
||||
public_deps = [ ":electron_app" ]
|
||||
if (is_debug || dcheck_always_on) {
|
||||
data_deps = [ ":v8_code_cache_spec_assets" ]
|
||||
}
|
||||
}
|
||||
|
||||
##### node_headers
|
||||
@@ -2122,6 +2140,9 @@ group("testing_build") {
|
||||
":electron_mksnapshot_zip",
|
||||
":node_headers",
|
||||
]
|
||||
if (is_debug || dcheck_always_on) {
|
||||
data_deps = [ ":v8_code_cache_spec_assets" ]
|
||||
}
|
||||
if (!is_mas_build) {
|
||||
public_deps += [ ":electron_xcache_zip" ]
|
||||
}
|
||||
|
||||
+22
-12
@@ -8,20 +8,30 @@ node_module_version = 150
|
||||
node_openssl_path = "//third_party/boringssl"
|
||||
|
||||
v8_promise_internal_field_count = 1
|
||||
v8_embedder_string = "-electron.0"
|
||||
|
||||
# Salt the hash V8 uses to key code caches and serialized wasm modules with a
|
||||
# fingerprint of the patches we apply on top of the V8 release. Release
|
||||
# branches cherry-pick V8 fixes without changing V8's own version number, so
|
||||
# without this a build with a different patch set would still accept (and
|
||||
# mis-link) serialized code written by the previous release. V8 hashes these
|
||||
# files at `gn gen` time and re-runs gn whenever one of them changes.
|
||||
_v8_patch_dir = "//electron/patches/v8"
|
||||
v8_version_hash_salt_files = [ "$_v8_patch_dir/.patches" ]
|
||||
foreach(_patch, read_file("$_v8_patch_dir/.patches", "list lines")) {
|
||||
if (_patch != "") {
|
||||
v8_version_hash_salt_files += [ "$_v8_patch_dir/$_patch" ]
|
||||
# Args are evaluated before GN installs its script interpreter. Invoke the
|
||||
# hermetic Python executable directly to generate the ordinary V8 argument.
|
||||
_v8_embedder_override = getenv("ELECTRON_V8_EMBEDDER_STRING")
|
||||
if (_v8_embedder_override != "") {
|
||||
v8_embedder_string = _v8_embedder_override
|
||||
} else {
|
||||
_v8_fingerprint_python = "//third_party/cpython3/host/bin/python3"
|
||||
if (getenv("OS") == "Windows_NT") {
|
||||
_v8_fingerprint_python += ".exe"
|
||||
}
|
||||
_v8_fingerprint_script = "//electron/script/v8-patch-fingerprint.py"
|
||||
_v8_fingerprint_inputs = exec_script(_v8_fingerprint_python,
|
||||
[
|
||||
rebase_path(_v8_fingerprint_script),
|
||||
"--inputs",
|
||||
],
|
||||
"json",
|
||||
[ _v8_fingerprint_script ])
|
||||
v8_embedder_string =
|
||||
exec_script(_v8_fingerprint_python,
|
||||
[ rebase_path(_v8_fingerprint_script) ],
|
||||
"trim string",
|
||||
_v8_fingerprint_inputs + [ _v8_fingerprint_script ])
|
||||
}
|
||||
|
||||
# TODO: this breaks mksnapshot
|
||||
|
||||
@@ -94,3 +94,42 @@ $ ../../electron/script/git-import-patches -3 ../../electron/patches/node
|
||||
```
|
||||
|
||||
If `git-import-patches -3` encounters a merge conflict that it can't resolve automatically, it will pause and allow you to resolve the conflict manually. Once you have resolved the conflict, `git add` the resolved files and continue to apply the rest of the patches by running `git am --continue`.
|
||||
|
||||
### V8 serialized bytecode compatibility
|
||||
|
||||
V8 includes its embedder string in the version hash used to validate JavaScript
|
||||
code caches and serialized WebAssembly modules. Electron's common GN arguments
|
||||
automatically set `v8_embedder_string` to `-electron.<fingerprint>`. No release time bump is required.
|
||||
|
||||
The fingerprint identifies the net committed source changes between the patched
|
||||
V8 `HEAD` produced by sync and the revision pinned by Chromium's `DEPS`. It
|
||||
includes changed paths, Git normalized content
|
||||
identities and file modes, including additions and deletions. Patch filenames,
|
||||
descriptions, authors and commit history are not inputs. Reordering, splitting or
|
||||
re-exporting patches preserves the fingerprint when the resulting source is
|
||||
identical. Actual source changes invalidate caches conservatively, even when
|
||||
they are ABI-compatible.
|
||||
|
||||
Uncommitted local edits do not change the fingerprint. Compatibility between
|
||||
cached code from differently modified development builds is outside this
|
||||
mechanism's scope, clear local caches when testing such changes.
|
||||
|
||||
Local builds derive the fingerprint directly from the synced V8 Git checkout
|
||||
without writing sidecar files. CI source caches deliberately omit V8 Git
|
||||
metadata, so cache creation records the same Git derived fingerprint in
|
||||
`v8/.electron-patch-fingerprint.json` before stripping it. Restored caches verify
|
||||
the pinned V8 revision, patch series provenance and fingerprint schema before
|
||||
using that identity. Missing or stale metadata fails generation rather than
|
||||
substituting a weaker cache key.
|
||||
|
||||
The source cache key in `script/generate-deps-hash.js` hashes
|
||||
`script/v8-patch-fingerprint.py`, which contains the fingerprint algorithm and
|
||||
Git input handling. Changes to the algorithm or metadata schema invalidate
|
||||
existing source caches automatically.
|
||||
|
||||
Packagers building source trees without V8 Git metadata or CI fingerprint
|
||||
metadata can explicitly set `ELECTRON_V8_EMBEDDER_STRING` before GN generation
|
||||
to supply their own compatibility identity. This bypasses automatic fingerprint
|
||||
generation and validation; the packager must change the identity whenever V8
|
||||
cache compatibility changes. GN does not track environment variable changes,
|
||||
so changing or removing this override requires rerunning `gn gen` explicitly.
|
||||
@@ -619,7 +619,6 @@ filenames = {
|
||||
"shell/common/api/electron_api_net.cc",
|
||||
"shell/common/api/electron_api_shared_texture.cc",
|
||||
"shell/common/api/electron_api_shell.cc",
|
||||
"shell/common/api/electron_api_testing.cc",
|
||||
"shell/common/api/electron_api_url_loader.cc",
|
||||
"shell/common/api/electron_api_url_loader.h",
|
||||
"shell/common/api/electron_api_v8_util.cc",
|
||||
|
||||
@@ -5,11 +5,8 @@ Subject: build: allow electron to use exec_script
|
||||
|
||||
This is similar to the //build usecase so we're OK adding ourselves here.
|
||||
|
||||
//v8/gni/version_hash_salt.gni comes from our V8 patch that derives the
|
||||
code-cache salt from patches/v8 at gen time.
|
||||
|
||||
diff --git a/.gn b/.gn
|
||||
index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..e35f9ed26b0c9451c3f8f010c1940f3a19727f74 100644
|
||||
index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..f2ab0a1711b94dd03ec5474e6d383161d0032a88 100644
|
||||
--- a/.gn
|
||||
+++ b/.gn
|
||||
@@ -462,6 +462,31 @@ exec_script_allowlist =
|
||||
@@ -18,7 +15,7 @@ index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..e35f9ed26b0c9451c3f8f010c1940f3a
|
||||
"//third_party/dawn/third_party/meson/meson_target.gni",
|
||||
+
|
||||
+ "//electron/BUILD.gn",
|
||||
+ "//v8/gni/version_hash_salt.gni",
|
||||
+ "//electron/build/args/all.gn",
|
||||
+ "//third_party/electron_node/deps/ada/unofficial.gni",
|
||||
+ "//third_party/electron_node/deps/base64/BUILD.gn",
|
||||
+ "//third_party/electron_node/deps/base64/unofficial.gni",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
|
||||
src_use_legacy_trace_macros_in_perfetto_to_support_all_phases.patch
|
||||
feat_key_code_caches_to_the_patch_set_and_the_embedded_blob.patch
|
||||
fix_allow_embedder_slots_of_non-api_objects_in_snapshot.patch
|
||||
@@ -1,230 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Wed, 23 Sep 2026 17:52:51 +0000
|
||||
Subject: feat: key code caches to the patch set and the embedded blob
|
||||
|
||||
V8 keys serialized code (JS code caches and serialized wasm modules) on
|
||||
Version::Hash(), i.e. on major.minor.build.patch only. Electron applies
|
||||
V8 fixes on release branches as patches without changing that version
|
||||
number, so a build with a different patch set still accepts serialized
|
||||
code written by the previous release. Serialized wasm code stores calls
|
||||
to builtins as raw Builtin ids, so when a patch adds, removes or reorders
|
||||
a builtin the deserialized code is relinked against the wrong builtins
|
||||
and crashes or traps at runtime.
|
||||
|
||||
This adds two things:
|
||||
|
||||
* a v8_version_hash_salt GN arg that is mixed into Version::Hash(), plus
|
||||
v8_version_hash_salt_files to derive the salt at gn gen time from a
|
||||
list of files. Electron points the latter at patches/v8 (see
|
||||
build/args/all.gn), so any change to the patch set invalidates every
|
||||
code cache and every serialized wasm module. The salt reaches
|
||||
version.cc through a header written at gn gen time so that changing it
|
||||
recompiles one file.
|
||||
* the embedded blob's data and code hashes in the serialized wasm module
|
||||
header, so that a module is only ever deserialized into a build whose
|
||||
builtins are identical to the one that produced it, whatever the
|
||||
version number says.
|
||||
|
||||
Both parts are suitable for upstreaming.
|
||||
|
||||
diff --git a/BUILD.gn b/BUILD.gn
|
||||
index cbab33240a3e8a9007ebacc6e7d0f954a8c22b1b..bfe1248266f9e5c90ad0e77a99923b99af7cce4f 100644
|
||||
--- a/BUILD.gn
|
||||
+++ b/BUILD.gn
|
||||
@@ -21,6 +21,7 @@ import("//third_party/icu/config.gni")
|
||||
import("gni/cluster_build.gni")
|
||||
import("gni/snapshot_toolchain.gni")
|
||||
import("gni/v8.gni")
|
||||
+import("gni/version_hash_salt.gni")
|
||||
|
||||
if (is_clang) {
|
||||
import("//build/config/clang/clang.gni")
|
||||
@@ -1330,6 +1331,14 @@ enabled_external_defines =
|
||||
disabled_external_defines =
|
||||
disabled_external_v8_defines + disabled_external_cppgc_defines
|
||||
|
||||
+# The salt is written to a generated header that only src/utils/version.cc
|
||||
+# includes, so that changing it recompiles a single file rather than all of
|
||||
+# V8. write_file() only touches the file when its contents change.
|
||||
+assert(v8_version_hash_salt_files == [] || v8_version_hash_salt != "",
|
||||
+ "v8_version_hash_salt_files is set but produced an empty salt")
|
||||
+write_file("$target_gen_dir/src/utils/version-hash-salt.inc",
|
||||
+ [ "#define V8_VERSION_HASH_SALT \"$v8_version_hash_salt\"" ])
|
||||
+
|
||||
# Put defines here that are only used in our internal files and NEVER in
|
||||
# external headers that embedders (such as chromium and node) might include.
|
||||
config("features") {
|
||||
diff --git a/gni/version_hash_salt.gni b/gni/version_hash_salt.gni
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..80031afb917abf21a175198537a4d2b784a66ee6
|
||||
--- /dev/null
|
||||
+++ b/gni/version_hash_salt.gni
|
||||
@@ -0,0 +1,27 @@
|
||||
+# Copyright 2026 the V8 project authors. All rights reserved.
|
||||
+# Use of this source code is governed by a BSD-style license that can be
|
||||
+# found in the LICENSE file.
|
||||
+
|
||||
+declare_args() {
|
||||
+ # An arbitrary string that is mixed into the hash that keys code caches and
|
||||
+ # serialized wasm modules (see Version::Hash). Embedders that carry patches
|
||||
+ # on top of a V8 release should derive this from their patch set so that
|
||||
+ # serialized code produced by a differently-patched build of the same V8
|
||||
+ # version is rejected instead of deserialized.
|
||||
+ v8_version_hash_salt = ""
|
||||
+
|
||||
+ # Alternatively, a list of files (for example the embedder's patch series)
|
||||
+ # whose contents are fingerprinted at gn gen time to produce the salt.
|
||||
+ # Changing any of them re-runs gn gen.
|
||||
+ v8_version_hash_salt_files = []
|
||||
+}
|
||||
+
|
||||
+if (v8_version_hash_salt_files != []) {
|
||||
+ assert(v8_version_hash_salt == "",
|
||||
+ "Set either v8_version_hash_salt or v8_version_hash_salt_files")
|
||||
+ v8_version_hash_salt =
|
||||
+ exec_script("../tools/gen-version-hash-salt.py",
|
||||
+ rebase_path(v8_version_hash_salt_files, root_build_dir),
|
||||
+ "trim string",
|
||||
+ v8_version_hash_salt_files)
|
||||
+}
|
||||
diff --git a/src/utils/version.cc b/src/utils/version.cc
|
||||
index 550db661144947dbc31b5eb62c0da7ea2e6e1139..c643c809f7f33d9644dea148ee6e69ad4a1b8f2f 100644
|
||||
--- a/src/utils/version.cc
|
||||
+++ b/src/utils/version.cc
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "src/base/hashing.h"
|
||||
#include "src/base/strings.h"
|
||||
#include "src/base/vector.h"
|
||||
+// Generated at gn gen time from the v8_version_hash_salt GN arg; defines
|
||||
+// V8_VERSION_HASH_SALT.
|
||||
+#include "src/utils/version-hash-salt.inc"
|
||||
|
||||
// Define SONAME to have the build system put a specific SONAME into the
|
||||
// shared library instead the generic SONAME generated from the V8 version
|
||||
@@ -31,6 +34,7 @@ uint32_t Version::Hash() {
|
||||
base::Hasher hasher;
|
||||
hasher.Add(major_).Add(minor_).Add(build_).Add(patch_);
|
||||
hasher.AddRange(base::OneByteVector(embedder_));
|
||||
+ hasher.AddRange(base::OneByteVector(V8_VERSION_HASH_SALT));
|
||||
return static_cast<uint32_t>(hasher.hash());
|
||||
}
|
||||
|
||||
diff --git a/src/utils/version.h b/src/utils/version.h
|
||||
index 0a0134e043722757bb85db55cf555974420e9b57..918d4bacf66e45bbd6ffc63d01de755f2ef179b9 100644
|
||||
--- a/src/utils/version.h
|
||||
+++ b/src/utils/version.h
|
||||
@@ -27,6 +27,12 @@ class V8_EXPORT Version {
|
||||
static int GetPatch() { return patch_; }
|
||||
static const char* GetEmbedder() { return embedder_; }
|
||||
static bool IsCandidate() { return candidate_; }
|
||||
+
|
||||
+ // Keys serialized code (code caches, serialized wasm modules) to the V8
|
||||
+ // build that produced it. This covers the version number and embedder
|
||||
+ // string plus the embedder-provided v8_version_hash_salt GN arg, so that
|
||||
+ // embedders which apply patches without changing the version number still
|
||||
+ // invalidate serialized code whenever their patch set changes.
|
||||
static uint32_t Hash();
|
||||
|
||||
// Calculate the V8 version string.
|
||||
diff --git a/src/wasm/wasm-serialization.cc b/src/wasm/wasm-serialization.cc
|
||||
index a955b8f3a3d942d381c06b366928e8aadb9e4c9d..efb5aa20ae1f1ea87837ac029f82669440538d4f 100644
|
||||
--- a/src/wasm/wasm-serialization.cc
|
||||
+++ b/src/wasm/wasm-serialization.cc
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "src/codegen/assembler-inl.h"
|
||||
#include "src/debug/debug.h"
|
||||
#include "src/runtime/runtime.h"
|
||||
+#include "src/snapshot/embedded/embedded-data.h"
|
||||
#include "src/snapshot/snapshot-data.h"
|
||||
#include "src/utils/ostreams.h"
|
||||
#include "src/utils/version.h"
|
||||
@@ -129,9 +130,20 @@ class Reader {
|
||||
const uint8_t* pos_;
|
||||
};
|
||||
|
||||
+// Serialized code refers to builtins by id and embeds root-register-relative
|
||||
+// offsets, so it is only valid for the exact embedded blob it was compiled
|
||||
+// against. The version hash alone does not capture that for builds which
|
||||
+// patch V8 without changing the version number.
|
||||
+uint32_t EmbeddedBlobHash() {
|
||||
+ EmbeddedData embedded_data = EmbeddedData::FromBlob();
|
||||
+ return static_cast<uint32_t>(
|
||||
+ base::hash_combine(embedded_data.EmbeddedBlobDataHash(),
|
||||
+ embedded_data.EmbeddedBlobCodeHash()));
|
||||
+}
|
||||
+
|
||||
size_t MeasureHeader(const CompileTimeImports& compile_imports) {
|
||||
- return 5 * kUInt32Size + // magic number, version hash, cpu features, flags
|
||||
- // hash, enabled features
|
||||
+ return 6 * kUInt32Size + // magic number, version hash, embedded blob hash,
|
||||
+ // cpu features, flags hash, enabled features
|
||||
sizeof(typename CompileTimeImportFlags::StorageType) +
|
||||
sizeof(uint32_t) + // length of constants_module.
|
||||
compile_imports.constants_module().size();
|
||||
@@ -142,6 +154,7 @@ void WriteHeader(Writer* writer, WasmEnabledFeatures enabled_features,
|
||||
DCHECK_EQ(0, writer->bytes_written());
|
||||
writer->Write(SerializedData::kMagicNumber);
|
||||
writer->Write(Version::Hash());
|
||||
+ writer->Write(EmbeddedBlobHash());
|
||||
writer->Write(CpuFeatures::SupportedFeatures().ToIntegral());
|
||||
writer->Write(FlagList::Hash());
|
||||
writer->Write(enabled_features.ToIntegral());
|
||||
diff --git a/src/wasm/wasm-serialization.h b/src/wasm/wasm-serialization.h
|
||||
index 3e570ad246e065c12b12d10a8766e79599362298..0806e312e76d7eec05a7d586c3b32247ae977f34 100644
|
||||
--- a/src/wasm/wasm-serialization.h
|
||||
+++ b/src/wasm/wasm-serialization.h
|
||||
@@ -31,10 +31,11 @@ class V8_EXPORT_PRIVATE WasmSerializer {
|
||||
// The data header consists of (see {WriteHeader}):
|
||||
// [0] magic number (uint32_t)
|
||||
// [1] version hash (uint32_t)
|
||||
- // [2] supported CPU features (uint32_t)
|
||||
- // [3] flag hash (uint32_t)
|
||||
- // [4] enabled features (via flags and OT) (uint32_t)
|
||||
- // [5] compile time imports (variable size)
|
||||
+ // [2] embedded blob hash (uint32_t)
|
||||
+ // [3] supported CPU features (uint32_t)
|
||||
+ // [4] flag hash (uint32_t)
|
||||
+ // [5] enabled features (via flags and OT) (uint32_t)
|
||||
+ // [6] compile time imports (variable size)
|
||||
static constexpr size_t kVersionHashOffset = kUInt32Size;
|
||||
|
||||
private:
|
||||
diff --git a/tools/gen-version-hash-salt.py b/tools/gen-version-hash-salt.py
|
||||
new file mode 100755
|
||||
index 0000000000000000000000000000000000000000..bd443f28d11f130ef419d7f04b5b86800bd01fc0
|
||||
--- /dev/null
|
||||
+++ b/tools/gen-version-hash-salt.py
|
||||
@@ -0,0 +1,32 @@
|
||||
+#!/usr/bin/env python3
|
||||
+
|
||||
+# Copyright 2026 the V8 project authors. All rights reserved.
|
||||
+# Use of this source code is governed by a BSD-style license that can be
|
||||
+# found in the LICENSE file.
|
||||
+"""Prints a short, stable fingerprint of the given files.
|
||||
+
|
||||
+Called by GN at gen time (see gni/version_hash_salt.gni) to derive
|
||||
+v8_version_hash_salt from v8_version_hash_salt_files.
|
||||
+
|
||||
+Usage:
|
||||
+ gen-version-hash-salt.py <file1> [file2] ...
|
||||
+"""
|
||||
+
|
||||
+import hashlib
|
||||
+import os
|
||||
+import sys
|
||||
+
|
||||
+
|
||||
+def main(paths):
|
||||
+ digest = hashlib.sha256()
|
||||
+ for path in paths:
|
||||
+ digest.update(os.path.basename(path).encode('utf-8') + b'\n')
|
||||
+ with open(path, 'rb') as f:
|
||||
+ # Normalise line endings so that a CRLF checkout yields the same salt.
|
||||
+ digest.update(f.read().replace(b'\r\n', b'\n'))
|
||||
+ print(digest.hexdigest()[:16])
|
||||
+ return 0
|
||||
+
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ sys.exit(main(sys.argv[1:]))
|
||||
@@ -49,6 +49,7 @@ move_src_dirs_if_exist() {
|
||||
mkdir src_artifacts
|
||||
|
||||
dirs=("src/out/Default/gen/node_headers" \
|
||||
"src/out/Default/gen/electron/v8-code-cache" \
|
||||
"src/out/Default/overlapped-checker" \
|
||||
"src/out/Default/ffmpeg" \
|
||||
"src/out/Default/hunspell_dictionaries" \
|
||||
|
||||
@@ -14,7 +14,8 @@ const filesToHash = [
|
||||
path.resolve(__dirname, '../DEPS'),
|
||||
path.resolve(__dirname, '../yarn.lock'),
|
||||
path.resolve(__dirname, '../script/sysroots.json'),
|
||||
path.resolve(__dirname, '../.github/actions/checkout/action.yml')
|
||||
path.resolve(__dirname, '../.github/actions/checkout/action.yml'),
|
||||
path.resolve(__dirname, '../script/v8-patch-fingerprint.py')
|
||||
];
|
||||
|
||||
const addAllFiles = (dir) => {
|
||||
|
||||
+8
-4
@@ -134,13 +134,17 @@ def update_ref(repo, ref, newvalue):
|
||||
return subprocess.check_call(args)
|
||||
|
||||
|
||||
def _run(repo, *args):
|
||||
return subprocess.check_output(
|
||||
['git', '--no-optional-locks', '-C', str(repo), *args])
|
||||
|
||||
|
||||
def get_commit_for_ref(repo, ref):
|
||||
args = ['git', '-C', repo, 'rev-parse', '--verify', ref]
|
||||
return subprocess.check_output(args).decode('utf-8').strip()
|
||||
return _run(repo, 'rev-parse', '--verify', ref).decode('utf-8').strip()
|
||||
|
||||
def get_commit_count(repo, commit_range):
|
||||
args = ['git', '-C', repo, 'rev-list', '--count', commit_range]
|
||||
return int(subprocess.check_output(args).decode('utf-8').strip())
|
||||
return int(_run(repo, 'rev-list', '--count', commit_range))
|
||||
|
||||
|
||||
def guess_base_commit(repo, ref):
|
||||
"""Guess which commit the patches might be based on"""
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Focused tests for V8 patch fingerprints; requires Python and Git only."""
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest import mock
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
'v8_patch_fingerprint', Path(__file__).with_name('v8-patch-fingerprint.py'))
|
||||
fingerprint = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(fingerprint)
|
||||
|
||||
|
||||
def run_main(*args):
|
||||
output = io.StringIO()
|
||||
with mock.patch('sys.argv', ['v8-patch-fingerprint.py', *args]), redirect_stdout(output):
|
||||
fingerprint.main()
|
||||
return output.getvalue().strip()
|
||||
|
||||
|
||||
class RepositoryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Cleanup must run after the test, not after setUp.
|
||||
temporary = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
|
||||
prefix='electron-v8-fingerprint-')
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.repo = Path(temporary.name).resolve()
|
||||
self.git('-c', 'init.defaultRefFormat=files',
|
||||
'init', '--quiet', '--initial-branch=main')
|
||||
self.git('config', 'user.name', 'Fingerprint test')
|
||||
self.git('config', 'user.email', 'fingerprint@example.invalid')
|
||||
self.git('config', 'commit.gpgsign', 'false')
|
||||
self.git('config', 'core.autocrlf', 'false')
|
||||
self.git('config', 'core.filemode', 'true')
|
||||
(self.repo / 'first.txt').write_text('before\n', encoding='utf-8')
|
||||
(self.repo / 'second.txt').write_text('before\n', encoding='utf-8')
|
||||
self.commit('base')
|
||||
self.base = self.git('rev-parse', 'HEAD')
|
||||
|
||||
def git(self, *args):
|
||||
return subprocess.check_output(
|
||||
['git', '-C', str(self.repo), *args], stderr=subprocess.PIPE
|
||||
).decode().strip()
|
||||
|
||||
def commit(self, message):
|
||||
self.git('add', '--all')
|
||||
self.git('commit', '--quiet', '--message', message)
|
||||
|
||||
def identity(self):
|
||||
return fingerprint.repository_fingerprint(self.repo, self.base)
|
||||
|
||||
def test_equivalent_commit_histories(self):
|
||||
original = self.identity()
|
||||
for name in ('first.txt', 'second.txt'):
|
||||
(self.repo / name).write_text('after\n', encoding='utf-8')
|
||||
self.commit('combined patch')
|
||||
expected = self.identity()
|
||||
self.assertNotEqual(original, expected)
|
||||
self.assertRegex(expected, r'^-electron\.[0-9a-f]{16}$')
|
||||
for index, order in enumerate((('first.txt', 'second.txt'),
|
||||
('second.txt', 'first.txt'))):
|
||||
self.git('checkout', '--quiet', '-b', f'split-{index}', self.base)
|
||||
for name in order:
|
||||
(self.repo / name).write_text('after\n', encoding='utf-8')
|
||||
self.commit(f'change {name}')
|
||||
self.assertEqual(expected, self.identity())
|
||||
self.git('commit', '--amend', '--quiet', '--message', 're-exported description')
|
||||
self.assertEqual(expected, self.identity())
|
||||
(self.repo / 'first.txt').write_text('uncommitted\n', encoding='utf-8')
|
||||
self.assertEqual(expected, self.identity())
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'Requires POSIX filenames and executable modes')
|
||||
def test_raw_diff_paths_modes_and_objects(self):
|
||||
unusual = 'space tab\tnewline\n.txt'
|
||||
(self.repo / unusual).write_text('added\n', encoding='utf-8')
|
||||
(self.repo / 'first.txt').write_text('changed\n', encoding='utf-8')
|
||||
(self.repo / 'first.txt').chmod(0o755)
|
||||
(self.repo / 'second.txt').unlink()
|
||||
self.commit('content, mode, addition and deletion')
|
||||
changes = {name: (before, after) for name, before, after in
|
||||
fingerprint.get_tree_diff(self.repo, self.base, 'HEAD')}
|
||||
self.assertEqual(set(changes), {'first.txt', 'second.txt', unusual})
|
||||
self.assertEqual(changes['first.txt'], (
|
||||
('100644', self.git('rev-parse', f'{self.base}:first.txt')),
|
||||
('100755', self.git('rev-parse', 'HEAD:first.txt'))))
|
||||
self.assertEqual(changes['second.txt'], (
|
||||
('100644', self.git('rev-parse', f'{self.base}:second.txt')),
|
||||
('000000', '0' * 40)))
|
||||
self.assertEqual(changes[unusual], (
|
||||
('000000', '0' * 40),
|
||||
('100644', self.git('rev-parse', f'HEAD:{unusual}'))))
|
||||
|
||||
def test_gn_inputs_track_head_and_exported_series(self):
|
||||
patches = self.repo / 'exported-patches'
|
||||
patches.mkdir()
|
||||
manifest = patches / '.patches'
|
||||
patch = patches / 'example.patch'
|
||||
manifest.write_text('example.patch\n', encoding='utf-8')
|
||||
patch.write_text('exported patch\n', encoding='utf-8')
|
||||
with mock.patch.object(fingerprint, 'V8_ROOT', self.repo), \
|
||||
mock.patch.object(fingerprint, 'PATCH_ROOT', patches), \
|
||||
mock.patch.object(fingerprint, 'pinned_revision', return_value=self.base):
|
||||
dependencies = {Path(name) for name in json.loads(run_main('--inputs'))}
|
||||
self.assertEqual(dependencies, {
|
||||
self.repo / '.git' / 'HEAD', self.repo.parent / 'DEPS', manifest, patch})
|
||||
self.git('checkout', '--quiet', '--detach')
|
||||
self.assertEqual(dependencies, {Path(name) for name in json.loads(run_main('--inputs'))})
|
||||
additional = patches / 'additional.patch'
|
||||
additional.write_text('additional exported patch\n', encoding='utf-8')
|
||||
manifest.write_text('example.patch\nadditional.patch\n', encoding='utf-8')
|
||||
self.assertIn(str(additional), json.loads(run_main('--inputs')))
|
||||
|
||||
def test_non_ancestor_has_actionable_error(self):
|
||||
self.git('checkout', '--quiet', '--orphan', 'unrelated')
|
||||
self.commit('unrelated root')
|
||||
with self.assertRaisesRegex(ValueError, 're-sync.*v8_revision') as context:
|
||||
self.identity()
|
||||
self.assertIn(self.base, str(context.exception))
|
||||
self.assertIn(str(self.repo), str(context.exception))
|
||||
self.assertIsInstance(context.exception.__cause__, subprocess.CalledProcessError)
|
||||
|
||||
class MetadataTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Cleanup must run after the test, not after setUp.
|
||||
temporary = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
|
||||
prefix='electron-v8-metadata-')
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.metadata_path = self.root / 'metadata.json'
|
||||
self.patch = self.root / 'example.patch'
|
||||
self.patch.write_bytes(b'patch contents\n')
|
||||
patcher = mock.patch.object(fingerprint, 'CACHE_METADATA', self.metadata_path)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.base = 'a' * 40
|
||||
self.metadata = {
|
||||
'schema': fingerprint.SCHEMA,
|
||||
'base_revision': self.base,
|
||||
'patch_provenance': fingerprint.patch_provenance([self.patch]),
|
||||
'fingerprint': '-electron.0123456789abcdef',
|
||||
}
|
||||
|
||||
def write_metadata(self, metadata):
|
||||
self.metadata_path.write_text(json.dumps(metadata), encoding='utf-8')
|
||||
|
||||
def test_cached_metadata_validation(self):
|
||||
with self.assertRaisesRegex(ValueError, 'regenerate the source cache'):
|
||||
fingerprint.cached_fingerprint(self.base, [self.patch])
|
||||
|
||||
self.write_metadata(self.metadata)
|
||||
self.assertEqual(fingerprint.cached_fingerprint(self.base, [self.patch]),
|
||||
self.metadata['fingerprint'])
|
||||
|
||||
cases = [
|
||||
('schema', 999, 'unsupported.*schema'),
|
||||
('base_revision', 'b' * 40, 'does not match Chromium DEPS'),
|
||||
('patch_provenance', 'wrong', 'does not match the patch series'),
|
||||
('fingerprint', '-electron.not-a-hash', 'invalid V8 patch fingerprint'),
|
||||
]
|
||||
for field, value, message in cases:
|
||||
with self.subTest(field=field, value=value):
|
||||
self.write_metadata({**self.metadata, field: value})
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
fingerprint.cached_fingerprint(self.base, [self.patch])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Identify net committed source changes on top of Chromium's pinned V8."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
ELECTRON_ROOT = Path(__file__).resolve().parent.parent
|
||||
V8_ROOT = ELECTRON_ROOT.parent / 'v8'
|
||||
PATCH_ROOT = ELECTRON_ROOT / 'patches' / 'v8'
|
||||
CACHE_METADATA = V8_ROOT / '.electron-patch-fingerprint.json'
|
||||
SCHEMA = 1
|
||||
|
||||
|
||||
def run_git(repo, *args):
|
||||
return subprocess.check_output(
|
||||
['git', '--no-optional-locks', '-C', str(repo), *args])
|
||||
|
||||
|
||||
def get_tree_diff(repo, before, after):
|
||||
"""Return changed paths and their before/after (mode, object ID) pairs."""
|
||||
records = run_git(repo, 'diff', '--raw', '-z', '--no-renames', '--abbrev=64',
|
||||
before, after).split(b'\0')
|
||||
changes = []
|
||||
for index in range(0, len(records) - 1, 2):
|
||||
before_mode, after_mode, before_id, after_id, _status = (
|
||||
records[index].decode('ascii').split())
|
||||
changes.append((os.fsdecode(records[index + 1]),
|
||||
(before_mode[1:], before_id),
|
||||
(after_mode, after_id)))
|
||||
return changes
|
||||
|
||||
|
||||
def get_ref_inputs(repo):
|
||||
"""Track HEAD; commits on a branch are tracked through exported patches."""
|
||||
repo = Path(repo).resolve()
|
||||
ref_format = run_git(repo, 'rev-parse', '--show-ref-format').decode().strip()
|
||||
# Git before 2.45 echoes this unknown option and predates reftable support.
|
||||
if ref_format not in ('files', '--show-ref-format'):
|
||||
raise ValueError(
|
||||
f'Unsupported V8 Git ref storage {ref_format!r} in {repo}; '
|
||||
'use a checkout with files-based refs for automatic GN regeneration')
|
||||
path = Path(run_git(repo, 'rev-parse', '--git-path', 'HEAD').decode().strip())
|
||||
if not path.is_absolute():
|
||||
path = repo / path
|
||||
return {path.resolve()}
|
||||
|
||||
|
||||
def pinned_revision(deps):
|
||||
match = re.search(r"""['"]v8_revision['"]:\s*['"]([0-9a-f]{40,64})['"]""",
|
||||
deps.read_text(encoding='utf-8'))
|
||||
if not match:
|
||||
raise ValueError(f'Cannot find the pinned V8 revision in {deps}')
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def hash_changes(object_format, changes):
|
||||
digest = hashlib.sha256()
|
||||
fields = [f'electron-v8-patch-effect-v{SCHEMA}', object_format]
|
||||
for name, before, after in sorted(changes):
|
||||
fields.extend([name, *before, *after])
|
||||
for field in fields:
|
||||
encoded = os.fsencode(field)
|
||||
digest.update(len(encoded).to_bytes(8, 'big'))
|
||||
digest.update(encoded)
|
||||
return '-electron.' + digest.hexdigest()[:16]
|
||||
|
||||
|
||||
def patch_inputs():
|
||||
manifest = PATCH_ROOT / '.patches'
|
||||
return [manifest] + [
|
||||
PATCH_ROOT / name for name in manifest.read_text(encoding='utf-8').splitlines()
|
||||
if name]
|
||||
|
||||
|
||||
def patch_provenance(inputs):
|
||||
digest = hashlib.sha256()
|
||||
for path in inputs:
|
||||
for field in (path.name.encode('utf-8'),
|
||||
path.read_bytes().replace(b'\r\n', b'\n')):
|
||||
digest.update(len(field).to_bytes(8, 'big'))
|
||||
digest.update(field)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def repository_fingerprint(repo, base):
|
||||
try:
|
||||
run_git(repo, 'merge-base', '--is-ancestor', base, 'HEAD')
|
||||
except subprocess.CalledProcessError as error:
|
||||
detail = ('does not contain' if error.returncode == 1 else
|
||||
'could not be checked against')
|
||||
raise ValueError(
|
||||
f'V8 HEAD in {repo} {detail} Chromium\'s pinned revision {base}; '
|
||||
're-sync the checkout and check any v8_revision custom override') from error
|
||||
head_tree = run_git(repo, 'rev-parse', '--verify', 'HEAD^{tree}').decode().strip()
|
||||
changes = get_tree_diff(repo, base, head_tree)
|
||||
object_format = run_git(repo, 'rev-parse', '--show-object-format').decode().strip()
|
||||
return hash_changes(object_format, changes)
|
||||
|
||||
|
||||
def cached_fingerprint(base, inputs):
|
||||
if not CACHE_METADATA.is_file():
|
||||
raise ValueError(
|
||||
'V8 Git metadata is unavailable and the source cache has no V8 patch '
|
||||
'fingerprint metadata; regenerate the source cache from a synced checkout')
|
||||
metadata = json.loads(CACHE_METADATA.read_text(encoding='utf-8'))
|
||||
if not isinstance(metadata, dict):
|
||||
raise ValueError('The source cache contains invalid V8 fingerprint metadata')
|
||||
if metadata.get('schema') != SCHEMA:
|
||||
raise ValueError('The source cache uses an unsupported V8 fingerprint schema')
|
||||
if metadata.get('base_revision') != base:
|
||||
raise ValueError('The source cache V8 fingerprint does not match Chromium DEPS')
|
||||
if metadata.get('patch_provenance') != patch_provenance(inputs):
|
||||
raise ValueError('The source cache V8 fingerprint does not match the patch series')
|
||||
fingerprint = metadata.get('fingerprint')
|
||||
if not isinstance(fingerprint, str) or not re.fullmatch(
|
||||
r'-electron\.[0-9a-f]{16}', fingerprint):
|
||||
raise ValueError('The source cache contains an invalid V8 patch fingerprint')
|
||||
return fingerprint
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group()
|
||||
mode.add_argument('--inputs', action='store_true')
|
||||
mode.add_argument('--write-ci-cache', action='store_true',
|
||||
help='Record the Git derived identity before CI removes V8 Git metadata')
|
||||
args = parser.parse_args()
|
||||
base = pinned_revision(V8_ROOT.parent / 'DEPS')
|
||||
has_git = (V8_ROOT / '.git').exists()
|
||||
if args.write_ci_cache:
|
||||
if not has_git:
|
||||
parser.error('--write-ci-cache requires a synced V8 Git checkout')
|
||||
fingerprint = repository_fingerprint(V8_ROOT, base)
|
||||
metadata = {
|
||||
'schema': SCHEMA,
|
||||
'base_revision': base,
|
||||
'patch_provenance': patch_provenance(patch_inputs()),
|
||||
'fingerprint': fingerprint,
|
||||
}
|
||||
with CACHE_METADATA.open('w', encoding='utf-8', newline='\n') as output:
|
||||
json.dump(metadata, output, sort_keys=True)
|
||||
output.write('\n')
|
||||
print(fingerprint)
|
||||
return
|
||||
|
||||
series_inputs = patch_inputs()
|
||||
fingerprint = None if has_git else cached_fingerprint(base, series_inputs)
|
||||
if args.inputs:
|
||||
inputs = get_ref_inputs(V8_ROOT) if has_git else {CACHE_METADATA}
|
||||
inputs.update([V8_ROOT.parent / 'DEPS', *series_inputs])
|
||||
print(json.dumps(sorted(str(path).replace('\\', '/') for path in inputs)))
|
||||
else:
|
||||
print(repository_fingerprint(V8_ROOT, base) if has_git else fingerprint)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,5 +1,24 @@
|
||||
import("//mojo/public/tools/bindings/mojom.gni")
|
||||
|
||||
source_set("v8_code_cache_test_helpers") {
|
||||
sources = [
|
||||
"v8_code_cache_test_helpers.cc",
|
||||
"v8_code_cache_test_helpers.h",
|
||||
]
|
||||
include_dirs = [
|
||||
"//electron",
|
||||
"//v8",
|
||||
]
|
||||
configs += [ "//v8:external_config" ]
|
||||
deps = [
|
||||
"//base:dcheck_is_on",
|
||||
"//base:debugging_buildflags",
|
||||
"//v8",
|
||||
"//v8:v8_libbase",
|
||||
"//v8:v8_version",
|
||||
]
|
||||
}
|
||||
|
||||
source_set("serialized_value") {
|
||||
sources = [
|
||||
"serialized_value.cc",
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "base/dcheck_is_on.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/logging.h"
|
||||
@@ -25,6 +26,7 @@
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "content/public/browser/network_service_instance.h"
|
||||
#include "content/public/common/content_switches.h"
|
||||
#include "gin/array_buffer.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/webauthn/electron_authenticator_request_client_delegate.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
@@ -36,9 +38,11 @@
|
||||
#include "shell/common/gin_helper/function_template_cache.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/v8_code_cache_test_helpers.h" // nogncheck
|
||||
#include "ui/accessibility/platform/ax_platform.h"
|
||||
#include "v8/include/cppgc/allocation.h"
|
||||
#include "v8/include/v8-cppgc.h"
|
||||
#include "v8/include/v8-wasm.h"
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
@@ -49,7 +53,6 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#if DCHECK_IS_ON()
|
||||
namespace {
|
||||
|
||||
std::atomic<int> live_callback_holder_probes{0};
|
||||
@@ -96,6 +99,64 @@ int GetLiveCallbackHolderProbeCountForTesting() {
|
||||
return live_callback_holder_probes.load();
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> SerializeWasmModuleForTesting(
|
||||
v8::Isolate* isolate,
|
||||
gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> module) {
|
||||
if (!module->IsWasmModuleObject()) {
|
||||
thrower.ThrowTypeError("Expected a WebAssembly.Module");
|
||||
return {};
|
||||
}
|
||||
auto serialized =
|
||||
module.As<v8::WasmModuleObject>()->GetCompiledModule().Serialize();
|
||||
return node::Buffer::Copy(
|
||||
isolate, reinterpret_cast<const char*>(serialized.buffer.get()),
|
||||
serialized.size)
|
||||
.ToLocalChecked();
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> CompileWasmModuleWithCacheForTesting(
|
||||
v8::Isolate* isolate,
|
||||
const gin::ArrayBufferView& wire_bytes,
|
||||
const gin::ArrayBufferView& cached_bytes) {
|
||||
auto promise =
|
||||
std::make_unique<gin_helper::Promise<v8::Local<v8::Value>>>(isolate);
|
||||
auto handle = promise->GetHandle();
|
||||
using ModuleResult =
|
||||
std::variant<v8::Local<v8::WasmModuleObject>, v8::Local<v8::Value>>;
|
||||
auto resolve = electron::AdaptCallbackForRepeating(base::BindOnce(
|
||||
[](std::unique_ptr<gin_helper::Promise<v8::Local<v8::Value>>> promise,
|
||||
ModuleResult result) {
|
||||
if (auto* module =
|
||||
std::get_if<v8::Local<v8::WasmModuleObject>>(&result)) {
|
||||
v8::Local<v8::Value> value = *module;
|
||||
promise->Resolve(value);
|
||||
} else {
|
||||
promise->Reject(std::get<v8::Local<v8::Value>>(result));
|
||||
}
|
||||
},
|
||||
std::move(promise)));
|
||||
v8::WasmModuleCompilation compilation;
|
||||
compilation.SetHasCompiledModuleBytes();
|
||||
compilation.OnBytesReceived(wire_bytes.span().data(),
|
||||
wire_bytes.span().size());
|
||||
bool accepted = false;
|
||||
compilation.Finish(
|
||||
isolate,
|
||||
[&accepted,
|
||||
&cached_bytes](v8::WasmStreaming::ModuleCachingInterface& cache) {
|
||||
accepted = cache.SetCachedCompiledModuleBytes(
|
||||
std::span(cached_bytes.span().data(), cached_bytes.span().size()));
|
||||
},
|
||||
[resolve = std::move(resolve)](ModuleResult result) {
|
||||
resolve.Run(std::move(result));
|
||||
});
|
||||
auto result = gin_helper::Dictionary::CreateEmpty(isolate);
|
||||
result.Set("accepted", accepted);
|
||||
result.Set("module", handle);
|
||||
return result.GetHandle();
|
||||
}
|
||||
|
||||
class CallbackTestingHelper final {
|
||||
public:
|
||||
void HoldRepeatingCallback(const base::RepeatingClosure& callback) {
|
||||
@@ -442,6 +503,12 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
"getCachedCallbackTemplateForTesting");
|
||||
dict.SetMethod<&GetLiveCallbackHolderProbeCountForTesting>(
|
||||
"getLiveCallbackHolderProbeCountForTesting");
|
||||
dict.SetMethod<&electron::testing::ComputeV8VersionHash>(
|
||||
"computeV8VersionHashForTesting");
|
||||
dict.SetMethod<&SerializeWasmModuleForTesting>(
|
||||
"serializeWasmModuleForTesting");
|
||||
dict.SetMethod<&CompileWasmModuleWithCacheForTesting>(
|
||||
"compileWasmModuleWithCacheForTesting");
|
||||
dict.SetMethod<&Log>("log");
|
||||
dict.SetMethod<&GetLoggingDestination>("getLoggingDestination");
|
||||
dict.SetMethod<&IsPlatformCaretBrowsingEnabled>(
|
||||
@@ -479,4 +546,3 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
} // namespace
|
||||
|
||||
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_common_testing, Initialize)
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2026 Microsoft Corporation.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/common/v8_code_cache_test_helpers.h"
|
||||
|
||||
#include "base/dcheck_is_on.h"
|
||||
|
||||
#if DCHECK_IS_ON()
|
||||
#include "v8/include/v8-version.h"
|
||||
#include "v8/src/base/hashing.h" // nogncheck
|
||||
#include "v8/src/base/vector.h" // nogncheck
|
||||
|
||||
namespace electron::testing {
|
||||
|
||||
uint32_t ComputeV8VersionHash(const std::string& embedder) {
|
||||
// Mirror Version::Hash without changing the process-wide embedder string.
|
||||
v8::base::Hasher hasher;
|
||||
hasher.Add(V8_MAJOR_VERSION)
|
||||
.Add(V8_MINOR_VERSION)
|
||||
.Add(V8_BUILD_NUMBER)
|
||||
.Add(V8_PATCH_LEVEL);
|
||||
hasher.AddRange(v8::base::OneByteVector(embedder.c_str()));
|
||||
return static_cast<uint32_t>(hasher.hash());
|
||||
}
|
||||
|
||||
} // namespace electron::testing
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2026 Microsoft Corporation.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
|
||||
#define ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace electron::testing {
|
||||
|
||||
[[nodiscard]] uint32_t ComputeV8VersionHash(const std::string& embedder);
|
||||
|
||||
} // namespace electron::testing
|
||||
|
||||
#endif // ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifdescribe, ifit, isTestingBindingAvailable, startRemoteControlApp } from './lib/spec-helpers.ts';
|
||||
|
||||
interface CacheResult {
|
||||
matchingAccepted: boolean;
|
||||
matchingValue: number;
|
||||
foreignAccepted: boolean;
|
||||
foreignValue: number;
|
||||
rebuiltAccepted: boolean;
|
||||
rebuiltValue: number;
|
||||
}
|
||||
|
||||
const fixture = path.resolve(import.meta.dirname, 'fixtures/api/v8-code-cache.cjs');
|
||||
const buildDir =
|
||||
process.platform === 'darwin'
|
||||
? path.resolve(path.dirname(process.execPath), '../../..')
|
||||
: path.dirname(process.execPath);
|
||||
const builder = path.join(buildDir, 'gen', 'electron', 'v8-code-cache', 'wasm-module-builder.js');
|
||||
|
||||
describe('V8 patch code cache compatibility', () => {
|
||||
it('exposes a nonempty V8 version with a 16-hex Electron patch fingerprint', () => {
|
||||
expect(process.versions.v8).toBeTypeOf('string');
|
||||
expect(process.versions.v8).not.toBe('');
|
||||
expect(process.versions.v8).toMatch(/^\d+\.\d+\.\d+(?:\.\d+)?-electron\.[0-9a-f]{16}(?: \(candidate\))?$/);
|
||||
});
|
||||
|
||||
ifdescribe(isTestingBindingAvailable())('serialized caches', () => {
|
||||
it('rejects JavaScript caches from a different patch fingerprint and falls back to source', async () => {
|
||||
const { remotely } = await startRemoteControlApp();
|
||||
const result: CacheResult = await remotely((file: string) => require(file).javascript(), fixture);
|
||||
expect(result).toEqual({
|
||||
matchingAccepted: true,
|
||||
matchingValue: 42,
|
||||
foreignAccepted: false,
|
||||
foreignValue: 42,
|
||||
rebuiltAccepted: true,
|
||||
rebuiltValue: 42
|
||||
});
|
||||
});
|
||||
|
||||
ifit(fs.existsSync(builder))(
|
||||
'rejects serialized WebAssembly from a different patch fingerprint and falls back safely',
|
||||
async () => {
|
||||
const { remotely } = await startRemoteControlApp(['--js-flags=--no-liftoff --no-wasm-lazy-compilation']);
|
||||
const result: CacheResult = await remotely(
|
||||
(file: string, helper: string) => require(file).webassembly(helper),
|
||||
fixture,
|
||||
builder
|
||||
);
|
||||
expect(result).toEqual({
|
||||
matchingAccepted: true,
|
||||
matchingValue: 1,
|
||||
foreignAccepted: false,
|
||||
foreignValue: 1,
|
||||
rebuiltAccepted: true,
|
||||
rebuiltValue: 1
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2733,9 +2733,6 @@ describe('webContents module', () => {
|
||||
describe('caretBrowsingEnabled', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
const platformCaretBrowsing = () =>
|
||||
process._linkedBinding('electron_common_testing').isPlatformCaretBrowsingEnabled();
|
||||
|
||||
it('defaults to false', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
expect(w.webContents.caretBrowsingEnabled).toBe(false);
|
||||
@@ -2828,6 +2825,9 @@ describe('webContents module', () => {
|
||||
|
||||
// These depend on a binding that is only available when DCHECK_IS_ON.
|
||||
ifdescribe(isTestingBindingAvailable())('process-wide accessibility notification', () => {
|
||||
const platformCaretBrowsing = () =>
|
||||
process._linkedBinding('electron_common_testing').isPlatformCaretBrowsingEnabled();
|
||||
|
||||
it('stays on while another WebContents still has caret browsing enabled', () => {
|
||||
const w1 = new BrowserWindow({ show: false });
|
||||
const w2 = new BrowserWindow({ show: false });
|
||||
@@ -2845,6 +2845,47 @@ describe('webContents module', () => {
|
||||
expect(platformCaretBrowsing()).toBe(false);
|
||||
});
|
||||
|
||||
describe('guest preference inheritance', () => {
|
||||
it('releases exactly one reference when an inherited preference is disabled', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
w.webContents.caretBrowsingEnabled = true;
|
||||
|
||||
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
|
||||
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
|
||||
const [, guest] = await created;
|
||||
|
||||
expect(guest.caretBrowsingEnabled).toBe(true);
|
||||
|
||||
// An inherited preference has to come with its own reference, otherwise
|
||||
// the guest keeps caret browsing on with nothing holding the count up.
|
||||
w.webContents.caretBrowsingEnabled = false;
|
||||
expect(platformCaretBrowsing()).toBe(true);
|
||||
|
||||
guest.caretBrowsingEnabled = false;
|
||||
expect(platformCaretBrowsing()).toBe(false);
|
||||
});
|
||||
|
||||
it('releases an inherited reference when the guest is destroyed', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
w.webContents.caretBrowsingEnabled = true;
|
||||
|
||||
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
|
||||
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
|
||||
const [, guest] = await created;
|
||||
|
||||
expect(guest.caretBrowsingEnabled).toBe(true);
|
||||
expect(platformCaretBrowsing()).toBe(true);
|
||||
|
||||
// An attached guest's WebContents is owned by its embedder frame, so the
|
||||
// wrapper holding the reference is only deleted at garbage collection.
|
||||
const destroyed = once(guest, 'destroyed');
|
||||
w.close();
|
||||
await destroyed;
|
||||
|
||||
expect(platformCaretBrowsing()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('takes at most one reference per WebContents however often it is set', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
|
||||
@@ -2952,48 +2993,6 @@ describe('webContents module', () => {
|
||||
expect(guest.getType()).toBe('webview');
|
||||
expect(guest.caretBrowsingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
ifit(isTestingBindingAvailable())(
|
||||
'releases exactly one reference when an inherited preference is disabled',
|
||||
async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
w.webContents.caretBrowsingEnabled = true;
|
||||
|
||||
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
|
||||
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
|
||||
const [, guest] = await created;
|
||||
|
||||
expect(guest.caretBrowsingEnabled).toBe(true);
|
||||
|
||||
// An inherited preference has to come with its own reference, otherwise
|
||||
// the guest keeps caret browsing on with nothing holding the count up.
|
||||
w.webContents.caretBrowsingEnabled = false;
|
||||
expect(platformCaretBrowsing()).toBe(true);
|
||||
|
||||
guest.caretBrowsingEnabled = false;
|
||||
expect(platformCaretBrowsing()).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
ifit(isTestingBindingAvailable())('releases an inherited reference when the guest is destroyed', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
w.webContents.caretBrowsingEnabled = true;
|
||||
|
||||
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
|
||||
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
|
||||
const [, guest] = await created;
|
||||
|
||||
expect(guest.caretBrowsingEnabled).toBe(true);
|
||||
expect(platformCaretBrowsing()).toBe(true);
|
||||
|
||||
// An attached guest's WebContents is owned by its embedder frame, so the
|
||||
// wrapper holding the reference is only deleted at garbage collection.
|
||||
const destroyed = once(guest, 'destroyed');
|
||||
w.close();
|
||||
await destroyed;
|
||||
|
||||
expect(platformCaretBrowsing()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const testing = process._linkedBinding('electron_common_testing');
|
||||
const embedder = process.versions.v8.match(
|
||||
/^\d+\.\d+\.\d+(?:\.\d+)?(-electron\.[0-9a-f]{16})(?: \(candidate\))?$/
|
||||
)?.[1];
|
||||
assert.ok(embedder, `V8 is missing its patch fingerprint: ${process.versions.v8}`);
|
||||
const foreignEmbedder =
|
||||
embedder === '-electron.0000000000000000' ? '-electron.1111111111111111' : '-electron.0000000000000000';
|
||||
const versionHash = testing.computeV8VersionHashForTesting(embedder);
|
||||
const foreignVersionHash = testing.computeV8VersionHashForTesting(foreignEmbedder);
|
||||
assert.notEqual(versionHash, foreignVersionHash);
|
||||
|
||||
const readVersionHash = (cache) => (os.endianness() === 'LE' ? cache.readUInt32LE(4) : cache.readUInt32BE(4));
|
||||
const foreignCache = (cache) => {
|
||||
const copy = Buffer.from(cache);
|
||||
if (os.endianness() === 'LE') copy.writeUInt32LE(foreignVersionHash, 4);
|
||||
else copy.writeUInt32BE(foreignVersionHash, 4);
|
||||
return copy;
|
||||
};
|
||||
|
||||
const report = (matching, foreign, rebuilt) => ({
|
||||
matchingAccepted: matching.accepted,
|
||||
matchingValue: matching.value,
|
||||
foreignAccepted: foreign.accepted,
|
||||
foreignValue: foreign.value,
|
||||
rebuiltAccepted: rebuilt.accepted,
|
||||
rebuiltValue: rebuilt.value
|
||||
});
|
||||
|
||||
exports.javascript = () => {
|
||||
const source = '(() => 42)()';
|
||||
const producer = new vm.Script(source, { filename: 'producer.js' });
|
||||
const cache = producer.createCachedData();
|
||||
assert.equal(readVersionHash(cache), versionHash);
|
||||
|
||||
const consume = (cachedData, filename) => {
|
||||
const script = new vm.Script(source, { filename, cachedData });
|
||||
return {
|
||||
accepted: !script.cachedDataRejected,
|
||||
value: script.runInNewContext(),
|
||||
cache: script.createCachedData()
|
||||
};
|
||||
};
|
||||
const matching = consume(cache, 'matching.js');
|
||||
const foreign = consume(foreignCache(cache), 'foreign.js');
|
||||
assert.equal(readVersionHash(foreign.cache), versionHash);
|
||||
const rebuilt = consume(foreign.cache, 'rebuilt.js');
|
||||
return report(matching, foreign, rebuilt);
|
||||
};
|
||||
|
||||
exports.webassembly = async (builderPath) => {
|
||||
const builderSource = fs.readFileSync(builderPath, 'utf8');
|
||||
const wireBytes = vm.runInNewContext(
|
||||
`${builderSource}
|
||||
;(() => {
|
||||
const builder = new WasmModuleBuilder();
|
||||
builder.addMemory(1);
|
||||
builder.addFunction('grow', makeSig([kWasmI32], [kWasmI32]))
|
||||
.addBody([kExprLocalGet, 0, kExprMemoryGrow, 0])
|
||||
.exportFunc();
|
||||
return builder.toBuffer();
|
||||
})()`,
|
||||
{},
|
||||
{ filename: builderPath }
|
||||
);
|
||||
const module = new WebAssembly.Module(wireBytes);
|
||||
const cache = testing.serializeWasmModuleForTesting(module);
|
||||
assert.ok(cache.length > 32, 'Expected serialized optimized Wasm code');
|
||||
assert.equal(readVersionHash(cache), versionHash);
|
||||
|
||||
const consume = async (cachedData) => {
|
||||
const result = testing.compileWasmModuleWithCacheForTesting(wireBytes, cachedData);
|
||||
const module = await result.module;
|
||||
const instance = new WebAssembly.Instance(module);
|
||||
const value = instance.exports.grow(1);
|
||||
assert.equal(instance.exports.grow(0), 2);
|
||||
return {
|
||||
accepted: result.accepted,
|
||||
value,
|
||||
cache: testing.serializeWasmModuleForTesting(module)
|
||||
};
|
||||
};
|
||||
const matching = await consume(cache);
|
||||
const foreign = await consume(foreignCache(cache));
|
||||
assert.equal(readVersionHash(foreign.cache), versionHash);
|
||||
const rebuilt = await consume(foreign.cache);
|
||||
return report(matching, foreign, rebuilt);
|
||||
};
|
||||
Reference in new issue
Block a user