mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-26 01:35:22 +02:00
275 lines
11 KiB
Python
Executable File
275 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
#
|
|
# Copyright (C) 2021 Signal Messenger, LLC.
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
#
|
|
|
|
import hashlib
|
|
import mmap
|
|
import optparse
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from typing import List, Optional
|
|
|
|
sys.path.append(os.path.join(
|
|
os.path.dirname(os.path.abspath(os.path.dirname(__file__))),
|
|
'bin'))
|
|
import build_helpers # noqa: E402 (we need to modify sys.path before this import)
|
|
|
|
|
|
def check_for_debug_level_logs(src_path: str) -> None:
|
|
with open(src_path, 'rb') as f:
|
|
with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_READ) as m:
|
|
# See libsignal-node's logging.rs.
|
|
if m.find(b'-LEVEL LOGS ENABLED') != -1:
|
|
raise AssertionError('debug-level logs found in build that should not have them!')
|
|
|
|
|
|
def maybe_dump_debug_symbols(*, src_path: str, src_checksum_path: str, dst_path: str, dst_checksum_path: str) -> None:
|
|
dump_syms = shutil.which('dump_syms')
|
|
if not dump_syms:
|
|
print('note: dump_syms not installed; skipping debug info processing')
|
|
return
|
|
|
|
with open(src_checksum_path, 'rb') as f:
|
|
digest = hashlib.sha256()
|
|
# Use read1 to use the file object's buffering.
|
|
# We don't want to load the entire input in memory if we can help it.
|
|
while next := f.read1():
|
|
digest.update(next)
|
|
checksum = digest.hexdigest()
|
|
|
|
if os.path.exists(dst_checksum_path):
|
|
with open(dst_checksum_path, 'r') as f:
|
|
if f.read() == checksum:
|
|
print('Debug info did not change')
|
|
return
|
|
|
|
with open(dst_checksum_path, 'w') as f:
|
|
f.write(checksum)
|
|
|
|
print('Dumping debug symbols to %s' % dst_path)
|
|
subprocess.check_call([dump_syms, src_path, '-o', dst_path])
|
|
|
|
|
|
def main(args: Optional[List[str]] = None) -> int:
|
|
if args is None:
|
|
args = sys.argv
|
|
|
|
if sys.platform == 'win32':
|
|
args = shlex.split(' '.join(args), posix=0)
|
|
|
|
print("Invoked with '%s'" % (' '.join(args)))
|
|
|
|
parser = optparse.OptionParser()
|
|
parser.add_option('--out-dir', '-o', default=None, metavar='DIR',
|
|
help='specify destination dir (default build/$CONFIGURATION_NAME)')
|
|
parser.add_option('--configuration', default='Release', metavar='C',
|
|
help='specify build configuration (Release or Debug)')
|
|
# Luckily, Python's sys.platform matches Node's OS names for our supported targets.
|
|
parser.add_option('--os-name', default=sys.platform, metavar='OS',
|
|
help='specify Node OS name')
|
|
parser.add_option('--cargo-build-dir', default=os.path.join('..', 'target'), metavar='PATH',
|
|
help='specify cargo build dir (default %default)')
|
|
parser.add_option('--cargo-target', default=None,
|
|
help='specify cargo target')
|
|
parser.add_option('--node-arch', '--arch', default=None,
|
|
help='specify node arch (x64, ia32, arm64)')
|
|
parser.add_option('--copy-to-prebuilds', action='store_true',
|
|
help='copy library to prebuilds/$OS-$ARCH when finished')
|
|
parser.add_option('--debug-level-logs', action='store_true',
|
|
help='include log levels below INFO (default for Debug builds)')
|
|
|
|
(options, args) = parser.parse_args(args)
|
|
|
|
configuration_name = options.configuration.strip('"')
|
|
if configuration_name is None:
|
|
print('ERROR: --configuration is required')
|
|
return 1
|
|
elif configuration_name not in ['Release', 'Debug']:
|
|
print("ERROR: Unknown value for --configuration '%s'" % (configuration_name))
|
|
return 1
|
|
|
|
node_os_name = options.os_name
|
|
|
|
node_arch = options.node_arch
|
|
if node_arch is None:
|
|
# Python doesn't provide many guarantees about the format of platform.machine(),
|
|
# so we turn to Node instead.
|
|
node_arch = subprocess.check_output(
|
|
['node', '-e', "console.log(require('node:process').arch)"],
|
|
encoding='utf8').strip()
|
|
|
|
cargo_target = options.cargo_target
|
|
if cargo_target is None:
|
|
cargo_arch = {
|
|
'ia32': 'i686',
|
|
'x64': 'x86_64',
|
|
'arm64': 'aarch64',
|
|
}.get(node_arch, node_arch)
|
|
cargo_target_suffix = {
|
|
'darwin': 'apple-darwin',
|
|
'win32': 'pc-windows-msvc',
|
|
'linux': 'unknown-linux-gnu',
|
|
}.get(node_os_name, node_os_name)
|
|
cargo_target = f'{cargo_arch}-{cargo_target_suffix}'
|
|
|
|
out_dir = (options.out_dir or os.path.join('build', configuration_name)).strip('"')
|
|
|
|
# Fetch all dependencies first, so we can check information about them in constructing our
|
|
# command lines.
|
|
subprocess.check_call(['cargo', 'fetch'])
|
|
|
|
features = []
|
|
allow_debug_level_logs = False
|
|
if options.debug_level_logs:
|
|
allow_debug_level_logs = True
|
|
else:
|
|
features.append('log/release_max_level_info')
|
|
|
|
cmdline = ['cargo', 'build', '--target', cargo_target, '-p', 'libsignal-node', '--features', ','.join(features)]
|
|
if configuration_name == 'Release':
|
|
cmdline.append('--release')
|
|
else:
|
|
allow_debug_level_logs = True
|
|
print("Running '%s'" % (' '.join(cmdline)))
|
|
|
|
cargo_env = os.environ.copy()
|
|
cargo_env['RUSTFLAGS'] = cargo_env.get('RUSTFLAGS') or ''
|
|
cargo_env['CARGO_BUILD_TARGET_DIR'] = options.cargo_build_dir
|
|
cargo_env['MACOSX_DEPLOYMENT_TARGET'] = '12'
|
|
# Build with debug line tables, but not full debug info.
|
|
cargo_env['CARGO_PROFILE_RELEASE_DEBUG'] = '1'
|
|
# On Linux, cdylibs don't include public symbols from their dependencies,
|
|
# even if those symbols have been re-exported in the Rust source.
|
|
# Using LTO works around this at the cost of a slightly slower build.
|
|
# https://github.com/rust-lang/rfcs/issues/2771
|
|
cargo_env['CARGO_PROFILE_RELEASE_LTO'] = 'thin'
|
|
# Enable ARMv8 cryptography acceleration when available
|
|
cargo_env['RUSTFLAGS'] += ' --cfg aes_armv8'
|
|
# Access tokio's unstable metrics
|
|
cargo_env['RUSTFLAGS'] += ' --cfg tokio_unstable'
|
|
# Work around CMake bug introduced in cmake-rs v1.49.0
|
|
# https://github.com/rust-lang/cmake-rs/pull/158#issuecomment-1544782070
|
|
cargo_env['CMAKE_ARGS'] = '-DCMAKE_SYSTEM_NAME='
|
|
# Strip absolute paths
|
|
for path in build_helpers.rust_paths_to_remap():
|
|
cargo_env['RUSTFLAGS'] += f' --remap-path-prefix {path}='
|
|
|
|
# If set (below), will post-process the build library using this instead of just `cp`-ing it.
|
|
objcopy = None
|
|
|
|
if node_os_name == 'win32':
|
|
# By default, Rust on Windows depends on an MSVC component for the C runtime.
|
|
# Link it statically to avoid propagating that dependency.
|
|
cargo_env['RUSTFLAGS'] += ' -C target-feature=+crt-static'
|
|
|
|
# Hint to the Rust compiler that we're cross-compiling. This shouldn't be necessary
|
|
# since the invoking build script (if any) should be doing that but it's needed
|
|
# since Rust nightly-2024-10-03.
|
|
cargo_env['VSCMD_ARG_TGT_ARCH'] = node_arch
|
|
|
|
# Save the debug info in PDB format...
|
|
cargo_env['CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO'] = 'packed'
|
|
# ...and DLLs don't have anything to strip.
|
|
# (If you turn on stripping the PDB doesn't get generated at all.)
|
|
lib_format = '{}.dll'
|
|
debug_format = '{}.pdb'
|
|
debug_format_for_checksum = debug_format
|
|
|
|
abs_build_dir = os.path.abspath(options.cargo_build_dir)
|
|
if 'GITHUB_WORKSPACE' in cargo_env:
|
|
# Avoid long build directory paths on GitHub Actions,
|
|
# breaking the old Win32 limit of 260 characters.
|
|
# (https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation)
|
|
# We don't do this everywhere because it breaks cleaning.
|
|
#
|
|
# In the long run, Visual Studio's CLI tools will become long-path aware and this should
|
|
# become unnecessary.
|
|
# It would be nice if using extended-length path syntax `\\?\` was sufficient,
|
|
# but that also isn't accepted by all of Visual Studio's CLI tools.
|
|
tmpdir = cargo_env['RUNNER_TEMP']
|
|
if len(tmpdir) < len(abs_build_dir):
|
|
cargo_env['CARGO_BUILD_TARGET_DIR'] = os.path.join(tmpdir, 'libsignal')
|
|
|
|
elif node_os_name == 'darwin':
|
|
# Save the debug info in dSYM format...
|
|
cargo_env['CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO'] = 'packed'
|
|
# ...then have Rust strip the library.
|
|
cargo_env['CARGO_PROFILE_RELEASE_STRIP'] = 'symbols'
|
|
lib_format = 'lib{0}.dylib'
|
|
debug_format = 'lib{0}.dylib.dSYM'
|
|
# The dSYM format is a directory, not a single file.
|
|
# We use the single file that contains the DWARF information for our checksum,
|
|
# since our primary purpose for debug info is symbolicating crashdumps,
|
|
# which uses the line tables stored as DWARF.
|
|
debug_format_for_checksum = os.path.join(debug_format, 'Contents', 'Resources', 'DWARF', lib_format)
|
|
|
|
# macOS has a nice place for us to stash our version number.
|
|
if 'npm_package_version' in cargo_env:
|
|
cargo_env['RUSTFLAGS'] += ' -Clink-arg=-Wl,-current_version,%s' % cargo_env['npm_package_version']
|
|
|
|
else:
|
|
# Assume Linux-like.
|
|
# DWP files don't seem ready for everyday use.
|
|
# We'll just save the whole unstripped binary.
|
|
lib_format = 'lib{}.so'
|
|
debug_format = 'lib{}.so'
|
|
debug_format_for_checksum = debug_format
|
|
|
|
objcopy = shutil.which('%s-linux-gnu-objcopy' % cargo_target.split('-')[0]) or 'objcopy'
|
|
|
|
print('with environment:')
|
|
for (k, v) in cargo_env.items():
|
|
print('%s=%s' % (k, v))
|
|
print('', flush=True)
|
|
|
|
subprocess.check_call(cmdline, env=cargo_env)
|
|
|
|
libs_in = os.path.join(cargo_env['CARGO_BUILD_TARGET_DIR'],
|
|
cargo_target,
|
|
configuration_name.lower())
|
|
|
|
src_path = os.path.join(libs_in, lib_format.format('signal_node'))
|
|
if os.access(src_path, os.R_OK):
|
|
if not allow_debug_level_logs:
|
|
check_for_debug_level_logs(src_path)
|
|
|
|
dst_base = 'libsignal_client_%s_%s' % (node_os_name, node_arch)
|
|
|
|
dst_path = os.path.join(out_dir, dst_base + '.node')
|
|
print('Copying %s to %s' % (src_path, dst_path))
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
if objcopy:
|
|
subprocess.check_call([objcopy, '-S', src_path, dst_path])
|
|
else:
|
|
shutil.copyfile(src_path, dst_path)
|
|
|
|
maybe_dump_debug_symbols(
|
|
src_path=os.path.join(libs_in, debug_format.format('signal_node')),
|
|
src_checksum_path=os.path.join(libs_in, debug_format_for_checksum.format('signal_node')),
|
|
dst_path=os.path.join(out_dir, dst_base + '-debuginfo.sym'),
|
|
dst_checksum_path=os.path.join(out_dir, dst_base + '-debuginfo.sha256'),
|
|
)
|
|
|
|
if options.copy_to_prebuilds:
|
|
prebuild_dir = os.path.join('prebuilds', f'{node_os_name}-{node_arch}')
|
|
prebuild_path = os.path.join(prebuild_dir, '@signalapp+libsignal-client.node')
|
|
print('Copying %s to %s' % (dst_path, prebuild_path))
|
|
os.makedirs(prebuild_dir, exist_ok=True)
|
|
# We copy from dst_path in the build directory because it's already gone through an objcopy pass
|
|
shutil.copyfile(dst_path, prebuild_path)
|
|
else:
|
|
print('ERROR: did not find generated library')
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|