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:
Shelley Vohr authored and GitHub committed 2026-09-23 16:17:22 -07:00
1 parent 826ffcf375
commit 88833ddc7b
3 files changed
+107 -204

No files matched your search

+57 -37
View File
@@ -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" ]
}
+45
View File
@@ -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,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