Files
BrowserOS/packages/browseros/build/modules/package/universalizer_patched.py
Nikhil 24e9cfd8f2 chromium 142 upgrade, new cli (#214)
* refactor 1: new typer based cli and browseros cli module

* refactor 2: fixes to context.py

* refactor 3: common/ and notify

* new sign and package module

* update .gitignore

* refactor 5: dev.py and modules for each

* refactor 6: clean-up old files

* refactor 7: organise modules fruther

* refactor 8: renaming nxtscape to browseros

* refactor 9: dev.py remove cli load

* fix: pyproject.toml

* fix: typer pretty exception disable

* refactor 10: cli/build.py set to primary

* refactor 10: cli/build.py set to primary, move OS detection

* refactor: context split, env and module dataclass

* reactor: clean and git moved ot new module type

* refactor: compile and configure

* reactor: sign and package module update

* refactor: new build.py cli

* 'refactor: remove reducant OS checks

* refactor: rename BuildContext to Context

* refactor: rename BuildModule to CommandModule

* refactor: dev.py to use the new modules

* build.py: improve help output

* remove old patching way

* clean-up: remove old build.py stuff

* refactor: move to proper yaml parsing

* clean-up: remove legacy args gating

* fix: patches issues

* fix: clean-up build.py and ars resolver

* minor: gitignore

* fix: patches.py issue

* support universal build

* fix: ENV variable and YAMLs

* fix: move compile to folder to avoid compflics

* fixes: more env fixes

* fix: build_type override in CLI fix

* fix: universal clean all archs before starting

* fix: universal build type constants

* fix: linter, extract options

* fix: linter

* fix: remove chromium_src as a not a conflicting flag

* fix: support chromium_src from cli in config mode

* fix: notify with better messages

* feat: new apply patch with --reset-to feature

* feat: refactor apply and extract into separate sub modules

* 142 patches working (#211)

* updates to build.py apply/patch

* removed all old patches

* 142 build update

* fix: get updated patches from main to 142

* fix: correct patches dir

* fix: import path

* add pyright

* fix: setup pyright

* fix: new updated patches from 137 rebased on 142

* feat: new extract_patch command

* fix: add mising side_panel build patch

* fix: extension uninstall for browseros

* fix: prefs fix

* fix: ota extension updater patch fix

* fix: llm hub and chat

* feat: unvisersal module also package individual archs

* fix: add browseros-server binaries

* fix: attach color for notify

* fix: attachment for slack

* fix: update chromium version to 142.0.7444.175

* feat: add new icons needed

* fix: disable settings in menu

* fix: uv add build-backend

* minor: chromium version bump

* clean-up: removed old files of extnesion and sidepanel

* fix: product logo generate and assets.car and appicon.icns

* feat: few chromium UI fixes

* fix: update features.yaml

* fix: features.yaml path in context

* refactor: rename to get_patches_dir()

* feat: show browserOS version in about page

* fix: copy browseros_version on the build time and rename other to offset

* bump offset

* fix: update features.yaml

* feat: load env from .env files too

* fix: enable split view

* clean-up: removed old prefs

* fix: minor import issue

* fix: linux flag update
2025-12-03 13:09:23 -08:00

425 lines
16 KiB
Python
Generated

#!/usr/bin/env python
# coding: utf-8
# Copyright Nxtscape Authors
# Patch of src/chrome/installer/mac/universalizer.py to handle merging of two archs
# for MacOS into unverisal build when third_party tools already are in universal format
import argparse
import errno
import filecmp
import os
import plistlib
import shutil
import stat
import subprocess
import sys
import time
def _stat_or_none(path, root):
"""Calls os.stat or os.lstat to obtain information about a path.
This program traverses parallel directory trees, which may have subtle
differences such as directory entries that are present in fewer than all
trees. It also operates on symbolic links directly, instead of on their
targets.
Args:
path: The path to call os.stat or os.lstat on.
root: True if called on the root of a tree to be merged, False
otherwise. See the discussion below.
Returns:
The return value of os.stat or os.lstat, or possibly None if the path
does not exist.
When root is True, indicating that path is at the root of one of these
trees, this permissiveness is disabled, as all roots are required to be
present. If one is absent, an exception will be raised. When root is True,
os.stat will be used, as this is the one case when it is desirable to
operate on a symbolic link's target.
When root is False, os.lstat will be used to operate on symbolic links
directly, and a missing path will cause None to be returned.
"""
if root:
return os.stat(path)
try:
return os.lstat(path)
except OSError as e:
if e.errno == errno.ENOENT:
return None
raise
def _file_type_for_stat(st):
"""Returns a string indicating the type of directory entry in st.
Args:
st: The return value of os.stat or os.lstat.
Returns:
'symbolic link', 'file', or 'directory'.
"""
if stat.S_ISLNK(st.st_mode):
return "symbolic_link"
if stat.S_ISREG(st.st_mode):
return "file"
if stat.S_ISDIR(st.st_mode):
return "directory"
raise Exception("unknown file type for mode 0o%o" % mode)
def _sole_list_element(l, exception_message):
"""Assures that every element in a list is identical.
Args:
l: The list to consider.
exception_message: A message used to convey failure if every element in
l is not identical.
Returns:
The value of each identical element in the list.
"""
s = set(l)
if len(s) != 1:
raise Exception(exception_message)
return l[0]
def _read_plist(path):
"""Reads a macOS property list, API compatibility adapter."""
with open(path, "rb") as file:
try:
# New API, available since Python 3.4.
return plistlib.load(file)
except AttributeError:
# Old API, available (but deprecated) until Python 3.9.
return plistlib.readPlist(file)
def _write_plist(value, path):
"""Writes a macOS property list, API compatibility adapter."""
with open(path, "wb") as file:
try:
# New API, available since Python 3.4.
plistlib.dump(value, file)
except AttributeError:
# Old API, available (but deprecated) until Python 3.9.
plistlib.writePlist(value, file)
class CantMergeException(Exception):
"""Raised when differences exist between input files such that they cannot
be merged successfully.
"""
pass
def _merge_info_plists(input_paths, output_path):
"""Merges multiple macOS Info.plist files.
Args:
input_plists: A list of paths containing Info.plist files to be merged.
output_plist: The path of the merged Info.plist to create.
Raises:
CantMergeException if all input_paths could not successfully be merged
into output_path.
A small number of differences are tolerated in the input Info.plists. If a
key identifying the build environment (OS or toolchain) is different in any
of the inputs, it will be removed from the output. There are valid reasons
to produce builds for different architectures using different toolchains or
SDKs, and there is no way to rationalize these differences into a single
value.
If present, the Chrome KSChannelID family of keys are rationalized by using
"universal" to identify the architecture (compared to, for example,
"arm64".)
"""
input_plists = [_read_plist(x) for x in input_paths]
output_plist = input_plists[0]
for index in range(1, len(input_plists)):
input_plist = input_plists[index]
for key in set(input_plist.keys()) | set(output_plist.keys()):
if input_plist.get(key, None) == output_plist.get(key, None):
continue
if key in (
"BuildMachineOSBuild",
"DTCompiler",
"DTPlatformBuild",
"DTPlatformName",
"DTPlatformVersion",
"DTSDKBuild",
"DTSDKName",
"DTXcode",
"DTXcodeBuild",
):
if key in input_plist:
del input_plist[key]
if key in output_plist:
del output_plist[key]
elif key == "KSChannelID" or key.startswith("KSChannelID-"):
# These keys are Chrome-specific, where it's only present in the
# outer browser .app's Info.plist.
#
# Ensure that the values match the expected format as a
# prerequisite to what follows.
key_tail = key[len("KSChannelID") :]
input_value = input_plist.get(key, "")
output_value = output_plist.get(key, "")
assert input_value.endswith(key_tail)
assert output_value.endswith(key_tail)
# Find the longest common trailing sequence of hyphen-separated
# elements, and use that as the trailing sequence of the new
# value.
input_parts = reversed(input_value.split("-"))
output_parts = output_value.split("-")
output_parts.reverse()
new_parts = []
for input_part, output_part in zip(input_parts, output_parts):
if input_part == output_part:
new_parts.append(output_part)
else:
break
# Prepend "universal" to the entire value if it's not already
# there.
if len(new_parts) == 0 or new_parts[-1] != "universal":
new_parts.append("universal")
output_plist[key] = "-".join(reversed(new_parts))
assert output_plist[key] != ""
else:
raise CantMergeException(input_paths[index], output_path)
_write_plist(output_plist, output_path)
def _is_macho_file(path):
"""Check if a file is a Mach-O binary."""
try:
# Try to get architectures - if it works, it's a Mach-O file
result = subprocess.run(
["file", "-b", path], capture_output=True, text=True, check=True
)
return "Mach-O" in result.stdout
except subprocess.CalledProcessError:
return False
def _get_architectures(path):
"""Get architectures of a Mach-O file using lipo."""
if not _is_macho_file(path):
return set()
try:
result = subprocess.run(
["lipo", "-archs", path], capture_output=True, text=True, check=True
)
return set(result.stdout.strip().split())
except subprocess.CalledProcessError:
# Shouldn't happen if _is_macho_file worked correctly
return set()
def _universalize(input_paths, output_path, root):
"""Merges multiple trees into a "universal" tree.
This function provides the recursive internal implementation for
universalize.
Args:
input_paths: The input directory trees to be merged.
output_path: The merged tree to produce.
root: True if operating at the root of the input and output trees.
"""
input_stats = [_stat_or_none(x, root) for x in input_paths]
for index in range(len(input_paths) - 1, -1, -1):
if input_stats[index] is None:
del input_paths[index]
del input_stats[index]
input_types = [_file_type_for_stat(x) for x in input_stats]
type = _sole_list_element(
input_types, "varying types %r for input paths %r" % (input_types, input_paths)
)
if type == "file":
identical = True
for index in range(1, len(input_paths)):
if not filecmp.cmp(input_paths[0], input_paths[index]):
identical = False
if os.path.basename(output_path) == "Info.plist" or os.path.basename(
output_path
).endswith("-Info.plist"):
_merge_info_plists(input_paths, output_path)
else:
# Check if this is a Mach-O file that can be merged
is_macho = _is_macho_file(input_paths[0])
if not is_macho:
# Not a Mach-O file, handle as a regular file
# For code signing resources, they should be identical
if not identical:
# If files differ but aren't Mach-O, this is an error
# unless it's a known special case
if os.path.basename(output_path) == "CodeResources":
# CodeResources files can differ, just copy the first one
shutil.copyfile(input_paths[0], output_path)
else:
raise CantMergeException(
"non-Mach-O files differ: %r" % input_paths
)
else:
# Check if files are already universal with same architectures
all_archs = []
for path in input_paths:
archs = _get_architectures(path)
if archs:
all_archs.append(archs)
# If all files have the same non-empty architectures, they're likely the same universal binary
if (
all_archs
and all(archs == all_archs[0] for archs in all_archs)
and len(all_archs[0]) > 1
):
# All files are universal with same architectures, just copy the first one
shutil.copyfile(input_paths[0], output_path)
else:
# Normal lipo merge
command = ["lipo", "-create", "-output", output_path]
# Force 16kB alignment for both x86_64 and arm64 slices. The
# inherent alignment requirement for x86_64 (absent Rosetta
# x86_64-on-arm64 concerns) is 4kB, and that is what lipo
# traditionally aligned x86_64 slices to. Since
# cctools-959.0.1 (Xcode 11.4), lipo attempts to guess the
# desired alignment of each slice, with the sometimes
# comical result being a slice over-aligned for its
# architecture. Over-alignment is normally benign, but
# https://crbug.com/1281111 documents a bug caused by "slice
# mobility" in the the main executable across updates, when
# the x86_64 slice moved from its traditional offset of 4kB
# to 16kB as a result of over-aligning. Until a code change
# lifts that restriction, the main executable's physical
# layout across the installed base is frozen. In order to
# ensure that this temporary requirement can be met,
# artificially inflate the x86_64 slice's alignment
# requirement to 16kB to keep its location stable. The arm64
# slice's alignment requirement is also frozen at 16kB,
# although this is the correct value for that architecture.
#
# TODO(mark): Implement "Change 3" from
# https://crbug.com/1281111#c33 by reducing the x86_64
# alignment requirement to 4kB and truncating this comment,
# or if appropriate, implement "Change 3A" instead, updating
# this comment with a revised rationale.
command.extend(["-segalign", "x86_64", "0x4000"])
command.extend(["-segalign", "arm64", "0x4000"])
command.extend(input_paths)
subprocess.check_call(command)
if identical:
shutil.copyfile(input_paths[0], output_path)
elif type == "directory":
os.mkdir(output_path)
entries = set()
for input in input_paths:
entries.update(os.listdir(input))
for entry in entries:
input_entry_paths = [os.path.join(x, entry) for x in input_paths]
output_entry_path = os.path.join(output_path, entry)
_universalize(input_entry_paths, output_entry_path, False)
elif type == "symbolic_link":
targets = [os.readlink(x) for x in input_paths]
target = _sole_list_element(
targets,
"varying symbolic link targets %r for input paths %r"
% (targets, input_paths),
)
os.symlink(target, output_path)
input_permissions = [stat.S_IMODE(x.st_mode) for x in input_stats]
permission = _sole_list_element(
input_permissions,
"varying permissions %r for input paths %r"
% (["0o%o" % x for x in input_permissions], input_paths),
)
os.lchmod(output_path, permission)
if type != "file" or identical:
input_mtimes = [x.st_mtime for x in input_stats]
if len(set(input_mtimes)) == 1:
times = (time.time(), input_mtimes[0])
try:
# follow_symlinks is only available since Python 3.3.
os.utime(output_path, times, follow_symlinks=False)
except TypeError:
# If it's a symbolic link and this version of Python isn't able
# to set its timestamp, just leave it alone.
if type != "symbolic_link":
os.utime(output_path, times)
elif type == "directory":
# Always touch directories, in case a directory is a bundle, as a
# cue to LaunchServices to invalidate anything it may have cached
# about the bundle as it was being built.
os.utime(output_path, None)
def universalize(input_paths, output_path):
"""Merges multiple trees into a "universal" tree.
Args:
input_paths: The input directory trees to be merged.
output_path: The merged tree to produce.
input_paths are expected to be parallel directory trees. Each directory
entry at a given subpath in the input_paths, if present, must be identical
to all others when present, with these exceptions:
- Mach-O files that are not identical are merged using lipo.
- Info.plist files that are not identical are merged by _merge_info_plists.
"""
rmtree_on_error = not os.path.exists(output_path)
try:
return _universalize(input_paths, output_path, True)
except:
if rmtree_on_error and os.path.exists(output_path):
shutil.rmtree(output_path)
raise
def main(args):
parser = argparse.ArgumentParser(
description="Merge multiple single-architecture directory trees into a "
"single universal tree."
)
parser.add_argument(
"inputs",
nargs="+",
metavar="input",
help="An input directory tree to be merged. At least two inputs must "
"be provided.",
)
parser.add_argument("output", help="The merged directory tree to produce.")
parsed = parser.parse_args(args)
if len(parsed.inputs) < 2:
raise Exception("too few inputs")
universalize(parsed.inputs, parsed.output)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))