mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
build: read the macOS resource allowlist from the linked framework (#54233)
* build: read the macOS resource allowlist from the linked framework grit's --allowlist-support markers are __attribute__((used)), so on macOS every ui::AllowlistedResource<id> the framework references survives the link as a symbol (ICF folds the bodies, not the names). Read them back from the linked Electron Framework with llvm-nm - from the dSYM when the framework itself is stripped - instead of pseudo-linking its inputs, and hand the framework's resources to the bundle through bundle_deps so the locale repack can depend on the link without a cycle. That drops the linker_driver.py --collect-inputs-only mode and the tools/resources/ar.py archive handling from the Chromium patch; what is left is the one-line gcc_toolchain.gni assert for Linux, which is https://chromium-review.googlesource.com/c/chromium/src/+/8360559. * build: keep the resource allowlist arg permitted on macOS gcc_toolchain.gni declares enable_resource_allowlist_generation and grit keys --allowlist-support off it, so the assert there still has to admit mac for the markers to exist; only the linker_driver.py and ar.py changes go. * build: keep bundle-only deps off the Electron Framework link //chrome:packed_resources forwards to :packed_resources, which now reaches the framework link through :electron_resource_allowlist, so bundle_data left on the link (the crashpad helper, xibs, dylib copies) leaked into //chrome:chrome_framework and collided with its own Helpers entries at gn gen. Give the link only electron_lib and libnode and route everything that exists to populate the bundle through bundle_deps. * build: keep the allowlist check out of dist.zip's runtime deps GN's runtime-deps walk skips a shared library an action depends on directly but follows one reached through a group, so ordering electron_dist_zip after :electron_resource_allowlist_check via the runtime-deps group now pulled the framework link's closure (Squirrel.framework, libffmpeg.dylib, the unbundled framework binary) into dist.zip on macOS. Give dist_zip a build_deps that orders the zip action without joining the group whose runtime deps are zipped.
This commit is contained in:
1 parent
826ffcf375
commit
88833ddc7b
3 files changed
+107
-204
No files matched your search
@@ -1118,15 +1118,18 @@ if (electron_use_resource_allowlist) {
|
||||
electron_resource_allowlist_file =
|
||||
"$target_gen_dir/electron_resource_allowlist.txt"
|
||||
|
||||
# TODO: on Windows this should extract from electron.exe.pdb instead of the
|
||||
# inputs pseudo-link (and the executable must then stop depending on
|
||||
# :packed_resources to avoid a dependency cycle).
|
||||
generate_resource_allowlist("electron_resource_allowlist") {
|
||||
deps = [ ":electron_resource_allowlist_inputs" ]
|
||||
inputs = [
|
||||
"$root_out_dir/libelectron_resource_allowlist_inputs$shlib_extension",
|
||||
]
|
||||
output = electron_resource_allowlist_file
|
||||
# On macOS this target is defined with the framework below.
|
||||
if (!is_mac) {
|
||||
# TODO: on Windows this should extract from electron.exe.pdb instead of the
|
||||
# inputs pseudo-link (and the executable must then stop depending on
|
||||
# :packed_resources to avoid a dependency cycle).
|
||||
generate_resource_allowlist("electron_resource_allowlist") {
|
||||
deps = [ ":electron_resource_allowlist_inputs" ]
|
||||
inputs = [
|
||||
"$root_out_dir/libelectron_resource_allowlist_inputs$shlib_extension",
|
||||
]
|
||||
output = electron_resource_allowlist_file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,27 +1274,39 @@ if (is_mac) {
|
||||
}
|
||||
}
|
||||
|
||||
# Deps that determine the native code linked into the Electron Framework
|
||||
# (all of Electron's native code lives in the framework on macOS, so its
|
||||
# link inputs, not the app shim's, determine the reachable resource set).
|
||||
# Shared with :electron_resource_allowlist_inputs below so the resource
|
||||
# allowlist is computed from exactly the same link inputs as the shipped
|
||||
# framework.
|
||||
electron_framework_link_deps = [
|
||||
":electron_lib",
|
||||
"//third_party/electron_node:libnode",
|
||||
]
|
||||
|
||||
if (electron_use_resource_allowlist) {
|
||||
# Pseudo-link sharing the Electron Framework's link inputs:
|
||||
# --collect-inputs-only makes linker_driver.py skip the real link and
|
||||
# write the list of link inputs instead. generate_resource_allowlist
|
||||
# greps those inputs for ui::AllowlistedResource<id> markers.
|
||||
shared_library("electron_resource_allowlist_inputs") {
|
||||
ldflags = [ "--collect-inputs-only" ]
|
||||
include_dirs = [ "." ]
|
||||
sources = filenames.framework_sources
|
||||
deps = electron_framework_link_deps
|
||||
# Read the markers back from the linked framework's symbol table (the
|
||||
# dSYM's copy when the framework itself is stripped). The locale paks that
|
||||
# consume this reach the framework through bundle_deps, not the link, so
|
||||
# there is no cycle.
|
||||
if (enable_dsyms) {
|
||||
_allowlist_binary = "$root_out_dir/$electron_framework_name.dSYM/Contents/Resources/DWARF/$electron_framework_name"
|
||||
} else if (save_unstripped_output) {
|
||||
_allowlist_binary = "$root_out_dir/$electron_framework_name.unstripped"
|
||||
} else {
|
||||
_allowlist_binary =
|
||||
get_label_info(":electron_framework_shared_library",
|
||||
"target_out_dir") +
|
||||
"/electron_framework_shared_library/$electron_framework_name"
|
||||
}
|
||||
|
||||
action("electron_resource_allowlist") {
|
||||
script = "build/mac_resource_allowlist.py"
|
||||
deps = [ ":electron_framework+link_nested" ]
|
||||
_nm = "//third_party/llvm-build/Release+Asserts/bin/llvm-nm"
|
||||
inputs = [
|
||||
_allowlist_binary,
|
||||
_nm,
|
||||
]
|
||||
outputs = [ electron_resource_allowlist_file ]
|
||||
args = [
|
||||
"--nm",
|
||||
rebase_path(_nm, root_build_dir),
|
||||
"--input",
|
||||
rebase_path(_allowlist_binary, root_build_dir),
|
||||
"--output",
|
||||
rebase_path(electron_resource_allowlist_file, root_build_dir),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,16 +1345,20 @@ if (is_mac) {
|
||||
if (!is_mas_build) {
|
||||
framework_contents += [ "Helpers" ]
|
||||
}
|
||||
public_deps =
|
||||
[ ":electron_framework_libraries" ] + electron_framework_link_deps
|
||||
deps = [
|
||||
public_deps = [ ":electron_lib" ]
|
||||
deps = [ "//third_party/electron_node:libnode" ]
|
||||
|
||||
# bundle_deps reach the bundle but not the framework's link, which
|
||||
# :electron_resource_allowlist (and through it :packed_resources) depends
|
||||
# on; bundle_data on the link would leak to every consumer of the paks.
|
||||
bundle_deps = [
|
||||
":electron_framework_libraries",
|
||||
":electron_framework_resources",
|
||||
":electron_swiftshader_library",
|
||||
":electron_xibs",
|
||||
]
|
||||
if (!is_mas_build) {
|
||||
deps += [ ":electron_crashpad_helper" ]
|
||||
bundle_deps += [ ":electron_crashpad_helper" ]
|
||||
}
|
||||
info_plist = "shell/common/resources/mac/Info.plist"
|
||||
|
||||
@@ -1842,6 +1861,10 @@ template("dist_zip") {
|
||||
action(target_name) {
|
||||
script = "//electron/build/zip.py"
|
||||
deps = [ ":$_runtime_deps_target" ]
|
||||
if (defined(invoker.build_deps)) {
|
||||
# Ordered before the zip without contributing to its runtime deps.
|
||||
deps += invoker.build_deps
|
||||
}
|
||||
forward_variables_from(invoker,
|
||||
[
|
||||
"outputs",
|
||||
@@ -1903,10 +1926,7 @@ dist_zip("electron_dist_zip") {
|
||||
}
|
||||
deps = data_deps
|
||||
if (electron_use_resource_allowlist) {
|
||||
# Build-order dep only (not data_deps): the check must pass before the
|
||||
# dist zip is created, but its stamp output must not be packed into the
|
||||
# zip's runtime deps.
|
||||
deps += [ ":electron_resource_allowlist_check" ]
|
||||
build_deps = [ ":electron_resource_allowlist_check" ]
|
||||
}
|
||||
outputs = [ "$root_build_dir/dist.zip" ]
|
||||
}
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2026 Anthropic, PBC.
|
||||
# Use of this source code is governed by the MIT license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Writes the grit resource allowlist for a linked Mach-O binary.
|
||||
|
||||
grit's --allowlist-support makes every resource-macro use instantiate
|
||||
ui::AllowlistedResource<id>(), which is __attribute__((used)) and so keeps a
|
||||
symbol in the linked image (ICF folds the bodies, not the names). This lists
|
||||
those symbols with llvm-nm and writes the ids in the format
|
||||
//tools/resources/generate_resource_allowlist.py produces, one per line.
|
||||
Point it at the dSYM's DWARF companion when the binary itself is stripped.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
_MARKER = re.compile(rb'__ZN2ui19AllowlistedResourceILi(\d+)EEEvv')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--nm', required=True)
|
||||
parser.add_argument('--input', required=True)
|
||||
parser.add_argument('--output', required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
symbols = subprocess.run(
|
||||
[args.nm, '--defined-only', '--format=just-symbols', args.input],
|
||||
check=True, stdout=subprocess.PIPE).stdout
|
||||
ids = sorted({int(m.group(1)) for m in _MARKER.finditer(symbols)})
|
||||
if not ids:
|
||||
print(f'No ui::AllowlistedResource<> symbols in {args.input}; is it '
|
||||
'stripped?', file=sys.stderr)
|
||||
return 1
|
||||
with open(args.output, 'w') as out:
|
||||
out.writelines(f'{i}\n' for i in ids)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+5
-167
@@ -5,157 +5,13 @@ Subject: build: allow resource allowlist generation for Linux and macOS
|
||||
|
||||
The resource allowlist machinery (grit --allowlist-support plus
|
||||
tools/resources/generate_resource_allowlist.py) works on desktop Linux via
|
||||
the linker-input scanning path, but the assert in gcc_toolchain.gni only
|
||||
permits android and win. Electron uses the allowlist on Linux and macOS to
|
||||
repack its locale .pak files with only the strings its binary references.
|
||||
the same linker-input scanning path Android uses, and on macOS Electron reads
|
||||
the ui::AllowlistedResource<id> markers back from the linked framework's
|
||||
symbol table, but the assert in gcc_toolchain.gni (which also declares the
|
||||
arg that turns grit's markers on) only permits android and win.
|
||||
|
||||
On macOS the same linker-input scanning path works (Mach-O object files
|
||||
carry the same AllowlistedResource marker symbols in their symbol tables),
|
||||
with two gaps:
|
||||
The Linux half is https://chromium-review.googlesource.com/c/chromium/src/+/8360559.
|
||||
|
||||
* the Apple solink tool runs through linker_driver.py, which had no
|
||||
equivalent of gcc_solink_wrapper.py's --collect-inputs-only pseudo-link
|
||||
mode. Add one: when --collect-inputs-only is passed via ldflags, skip
|
||||
the real link, write the list of link inputs (object files and static
|
||||
archives) to the output file, and stub out the other declared outputs
|
||||
(TOC, dSYM bundle, unstripped copy).
|
||||
* ar.ExpandThinArchives dropped regular archives from the scan list (and
|
||||
raised on Mach-O universal archives like libclang_rt.osx.a). That is
|
||||
fine on Linux/Android where the build uses thin archives everywhere,
|
||||
but on macOS static libraries are regular archives, so their objects
|
||||
were never scanned. Keep non-thin archives in the list instead; they
|
||||
embed their members' contents, so the raw scan finds the markers.
|
||||
|
||||
diff --git a/build/toolchain/apple/linker_driver.py b/build/toolchain/apple/linker_driver.py
|
||||
index 5b2edf92a47be38a327678db3a6968eedb971e63..a2c82fed08562d9fffeb4f0da5986d53a6426f80 100755
|
||||
--- a/build/toolchain/apple/linker_driver.py
|
||||
+++ b/build/toolchain/apple/linker_driver.py
|
||||
@@ -7,6 +7,7 @@
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
+import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -84,6 +85,14 @@ LINKER_DRIVER_COMPILER_ARG_PREFIX = '-Wcrl,driver,'
|
||||
# Output TOC for solink.
|
||||
# It would be processed both before the linker (to check reexport
|
||||
# in old module) and after the linker (to produce TOC if needed).
|
||||
+#
|
||||
+# --collect-inputs-only
|
||||
+# Instead of linking, writes the linker command's input files (object
|
||||
+# files and static archives, including those in @response-files) to the
|
||||
+# output file, one per line, and stubs out the other outputs (TOC, dSYM,
|
||||
+# unstripped copy). Used by enable_resource_allowlist_generation to
|
||||
+# extract the resource allowlist from the link inputs without paying for
|
||||
+# a link. Mirrors the flag of the same name in gcc_solink_wrapper.py.
|
||||
|
||||
|
||||
class LinkerDriver(object):
|
||||
@@ -179,6 +188,14 @@ class LinkerDriver(object):
|
||||
'Could not find path to linker output (-o or --output)'
|
||||
)
|
||||
|
||||
+ # Instead of linking, record all inputs to the output file. This is
|
||||
+ # used by enable_resource_allowlist_generation in order to avoid
|
||||
+ # needing to link (which is slow) to build the resource allowlist.
|
||||
+ # Mirrors --collect-inputs-only in gcc_solink_wrapper.py.
|
||||
+ if '--collect-inputs-only' in self._compiler_driver_args:
|
||||
+ self._collect_inputs_only()
|
||||
+ return
|
||||
+
|
||||
# We want to link rlibs as --whole-archive if they are part of a unit
|
||||
# test target. This is determined by switch
|
||||
# `-LinkWrapper,add-whole-archive`.
|
||||
@@ -275,6 +292,59 @@ class LinkerDriver(object):
|
||||
if not found:
|
||||
raise ValueError('Unknown linker driver argument: %s' % (arg,))
|
||||
|
||||
+ def _collect_inputs_only(self):
|
||||
+ """Records the linker command's input files (object files and static
|
||||
+ archives, including those listed in @response-files) in the linker
|
||||
+ output file instead of linking, and stubs out the declared outputs of
|
||||
+ the requested linker driver actions (TOC, dSYM bundle, unstripped
|
||||
+ copy), since there is no Mach-O output to derive them from."""
|
||||
+ with open(self._get_linker_output(), 'w') as f:
|
||||
+ _write_linker_inputs(f, self._compiler_driver_args)
|
||||
+ for arg in self._args[1:]:
|
||||
+ if not arg.startswith(LINKER_DRIVER_ARG_PREFIX):
|
||||
+ continue
|
||||
+ sub_arg = arg[len(LINKER_DRIVER_ARG_PREFIX):]
|
||||
+ if sub_arg.startswith('tocname,'):
|
||||
+ _stub_output(sub_arg[len('tocname,'):])
|
||||
+ elif sub_arg.startswith('unstripped,'):
|
||||
+ base = os.path.basename(self._get_linker_output())
|
||||
+ _stub_output(
|
||||
+ os.path.join(sub_arg[len('unstripped,'):],
|
||||
+ base + '.unstripped'))
|
||||
+ elif sub_arg.startswith('dsym,'):
|
||||
+ self._stub_dsym(sub_arg[len('dsym,'):])
|
||||
+
|
||||
+ def _stub_dsym(self, dsym_path_prefix):
|
||||
+ """Creates an empty dSYM bundle matching the outputs declared by the
|
||||
+ build for -Wcrl,dsym,<dsym-path-prefix> (see the dsym_output list in
|
||||
+ build/toolchain/apple/toolchain.gni)."""
|
||||
+ base = os.path.basename(self._get_linker_output())
|
||||
+ dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM')
|
||||
+ _remove_path(dsym_out)
|
||||
+ contents = os.path.join(dsym_out, 'Contents')
|
||||
+ _stub_output(os.path.join(contents, 'Info.plist'))
|
||||
+ _stub_output(os.path.join(contents, 'Resources', 'DWARF', base))
|
||||
+ _stub_output(
|
||||
+ os.path.join(contents, 'Resources', 'Relocations',
|
||||
+ self._get_dsym_arch(), base + '.yml'))
|
||||
+
|
||||
+ def _get_dsym_arch(self):
|
||||
+ """Returns the dsymutil name of the architecture being linked for.
|
||||
+ The dSYM outputs are declared using dsym_arch from the archs_mapping
|
||||
+ in build/config/apple/arch.gni, which differs from the compiler
|
||||
+ target's name for arm64."""
|
||||
+ args = self._compiler_driver_args
|
||||
+ for index, arg in enumerate(args):
|
||||
+ if arg == '-arch' and index + 1 < len(args):
|
||||
+ arch = args[index + 1]
|
||||
+ break
|
||||
+ if arg.startswith('--target='):
|
||||
+ arch = arg[len('--target='):].split('-')[0]
|
||||
+ break
|
||||
+ else:
|
||||
+ raise ValueError('Could not determine the target architecture')
|
||||
+ return {'arm64': 'aarch64'}.get(arch, arch)
|
||||
+
|
||||
def prepare_object_path_lto(self, arg):
|
||||
"""Linker driver pre-action for -Wcrl,object_path_lto.
|
||||
|
||||
@@ -565,6 +635,27 @@ def _remove_path(path):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
+def _write_linker_inputs(out, args):
|
||||
+ """Writes the file arguments of a linker command (object files and static
|
||||
+ archives, including those listed in @response-files) to |out|."""
|
||||
+ for arg in args:
|
||||
+ if arg.startswith('@'):
|
||||
+ with open(arg[1:]) as rsp:
|
||||
+ _write_linker_inputs(out, shlex.split(rsp.read()))
|
||||
+ elif not arg.startswith('-') and arg.endswith(('.o', '.a')):
|
||||
+ out.write(arg + '\n')
|
||||
+
|
||||
+
|
||||
+def _stub_output(path):
|
||||
+ """Creates an empty file at |path|, making parent directories as
|
||||
+ needed."""
|
||||
+ dirname = os.path.dirname(path)
|
||||
+ if dirname:
|
||||
+ os.makedirs(dirname, exist_ok=True)
|
||||
+ with open(path, 'w'):
|
||||
+ pass
|
||||
+
|
||||
+
|
||||
if __name__ == '__main__':
|
||||
LinkerDriver(sys.argv).run()
|
||||
sys.exit(0)
|
||||
diff --git a/build/toolchain/gcc_toolchain.gni b/build/toolchain/gcc_toolchain.gni
|
||||
index ac7a5b84e93fd0bdf9eed224b2dd391621259636..78c1d0fcfd80b137d770a0064c4ffdf241f7a950 100644
|
||||
--- a/build/toolchain/gcc_toolchain.gni
|
||||
@@ -170,21 +26,3 @@ index ac7a5b84e93fd0bdf9eed224b2dd391621259636..78c1d0fcfd80b137d770a0064c4ffdf2
|
||||
"enable_resource_allowlist_generation=true does not work for target_os=$target_os")
|
||||
}
|
||||
|
||||
diff --git a/tools/resources/ar.py b/tools/resources/ar.py
|
||||
index fe62384148b5ab93d0eb5275b04a974ee0a08af2..492af3458b9a63210f4b3427d7752529100def52 100755
|
||||
--- a/tools/resources/ar.py
|
||||
+++ b/tools/resources/ar.py
|
||||
@@ -90,7 +90,12 @@ def ExpandThinArchives(paths):
|
||||
if is_thin:
|
||||
for subpath in _IterThinPaths(path):
|
||||
expanded_paths.append(_ResolveThinObjectPath(path, subpath))
|
||||
- elif header != b'!<arch>\n':
|
||||
+ elif (header == b'!<arch>\n'
|
||||
+ or header[:4] in (b'\xca\xfe\xba\xbe', b'\xca\xfe\xba\xbf')):
|
||||
+ # Regular archives (and Mach-O universal archives on Apple platforms)
|
||||
+ # embed their members' contents, so keep them to be scanned directly.
|
||||
+ expanded_paths.append(path)
|
||||
+ else:
|
||||
raise Exception('Invalid .a: ' + path)
|
||||
|
||||
return expanded_paths
|
||||
Reference in new issue
Block a user