blob: 2d98fb328ff2d4a5df8f0e264ed46e56c44de4b3 [file] [log] [blame]
# Copyright (C) 2018, 2020, 2021, 2024 Igalia S.L.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import os
import shutil
import subprocess
_log = logging.getLogger(__name__)
WRAPPER_CSOURCE_TEMPLATE = """\
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
char* resolve_abs_path(char *arg) {
char *abs_path = realpath(arg, NULL);
if (abs_path)
return abs_path;
return arg;
}
void args_update_relpaths_to_abs(int argc, char **argv) {
for (int i = 0; i < argc; i++)
if (argv[i][0] != '/' && !access(argv[i], F_OK))
argv[i] = resolve_abs_path(argv[i]);
}
int file_grep(const char *filename, const char *search_str) {
if (access(filename, F_OK))
return 0;
FILE *file = fopen(filename, "r");
if (!file) {
perror("fopen");
return 0;
}
char *line = NULL;
size_t len = 0;
ssize_t read;
int found = 0;
while ((read = getline(&line, &len, file)) != -1) {
if (strstr(line, search_str)) {
found = 1;
break;
}
}
free(line);
fclose(file);
return found;
}
void set_env_with_mydir(const char *env_var, const char *relative_path, const char *mydir) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%%s/%%s", mydir, relative_path);
setenv(env_var, path, 1);
}
void set_ca_file(const char *mydir) {
const char *system_ca_files[] = {
"/etc/ssl/certs/ca-certificates.crt",
"/etc/ssl/cert.pem",
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
};
for (int i = 0; i < sizeof(system_ca_files) / sizeof(system_ca_files[0]); i++) {
if (file_grep(system_ca_files[i], "BEGIN CERTIFICATE")) {
setenv("WEBKIT_TLS_CAFILE_PEM", system_ca_files[i], 1);
return;
}
}
// if we can't find a valid system cert use the one we bundle.
set_env_with_mydir("WEBKIT_TLS_CAFILE_PEM", "sys/share/certs/bundle-ca-certificates.pem", mydir);
}
int exec_and_wait(const char *program, char *const argv[]) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) { //child
execvp(program, argv);
perror("execv");
exit(EXIT_FAILURE);
} else { //parent
int status;
pid_t wpid = waitpid(pid, &status, 0);
if (wpid == -1) {
perror("waitpid");
return -1;
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
}
int maybe_update_gdx_pixbuf_cache(const char *mydir) {
char gdk_pixbuf_module_file[PATH_MAX];
char gdk_pixbuf_module_dir[PATH_MAX];
snprintf(gdk_pixbuf_module_file, sizeof(gdk_pixbuf_module_file), "%%s/sys/lib/gdk-pixbuf/loaders.cache", mydir);
snprintf(gdk_pixbuf_module_dir, sizeof(gdk_pixbuf_module_dir), "%%s/sys/lib/gdk-pixbuf/loaders", mydir);
if (file_grep(gdk_pixbuf_module_file, gdk_pixbuf_module_dir))
return 0;
char *gdk_pixbuf_args[] = { "gdk-pixbuf-query-loaders", "--update-cache", NULL };
return exec_and_wait("bin/gdk-pixbuf-query-loaders", gdk_pixbuf_args);
}
int main(int argc, char *argv[]) {
// options
int should_set_ca_file = %(should_set_ca_file_value)s;
int should_xkb_bundle = %(should_xkb_bundle_value)s;
int should_update_gdx_pixbuf_cache = %(should_update_gdx_pixbuf_cache_value)s;
// Get the directory of the executable
char mydir[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", mydir, sizeof(mydir) - 1);
if (len == -1) {
perror("readlink");
exit(EXIT_FAILURE);
}
mydir[len] = '\\0';
strcpy(mydir, dirname(mydir));
// Update args with full paths
args_update_relpaths_to_abs(argc, argv);
// Change to the directory where the script resides
if (chdir(mydir) == -1) {
perror("chdir");
exit(EXIT_FAILURE);
}
if (should_set_ca_file)
set_ca_file(mydir);
%(export_env_variables)s
if (should_xkb_bundle && !access("/usr/share/X11/xkb", F_OK))
set_env_with_mydir("XKB_CONFIG_ROOT", "sys/share/xkb", mydir);
char ldpath[PATH_MAX];
%(set_ld_path_value)s
setenv("LD_LIBRARY_PATH", ldpath, 1);
if (should_update_gdx_pixbuf_cache) {
int retcode = maybe_update_gdx_pixbuf_cache(mydir);
if (retcode)
fprintf(stderr, "gdk-pixbuf cache update command returned non-zero: %%d\\n", retcode);
}
char realProgram[] = "%(relative_path_binary)s";
execv(realProgram, argv);
// execv failed
perror("execv");
exit(EXIT_FAILURE);
}
"""
class BinaryBundler():
VAR_MYDIR = 'MYDIR'
def __init__(self, destination_dir, build_path=None):
self._destination_dir = destination_dir
self._has_patched_interpreter_relpath = False
self._build_path = build_path
self._should_use_sys_lib_directory = False
def _run_cmd_and_get_output(self, command):
_log.debug("EXEC %s" % command)
command_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8')
stdout, stderr = command_process.communicate()
return command_process.returncode, stdout, stderr
def _run_cmd_or_log_fail(self, command, failure_fatal=True):
_log.debug("EXEC %s" % command)
command_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8')
stdout, stderr = command_process.communicate()
if command_process.returncode != 0:
failure_msg = "The command \"%s\" returned non-zero status: %d.\n\tstdout: %s\n\tstderr: %s" % (" ".join(command), command_process.returncode, stdout, stderr)
if failure_fatal:
raise RuntimeError(failure_msg)
_log.error(failure_msg)
def _is_system_dep(self, path):
if self._build_path is not None:
# We don't consider libwpe and libWPEBackend-fdo syslibs (even when those are not built directly). Neither dlopenwrap
if any(p in path.lower() for p in ['libwpe', 'dlopenwrap']):
return False
return not path.startswith(self._build_path)
return False
def set_use_sys_lib_directory(self, should_use_sys_lib_directory):
self._should_use_sys_lib_directory = should_use_sys_lib_directory
def copy(self, orig_file, subdir=None):
destination_dir = self._destination_dir
if subdir is not None:
assert(not os.path.isabs(subdir))
destination_dir = os.path.join(destination_dir, subdir)
if not os.path.isdir(destination_dir):
os.makedirs(destination_dir)
shutil.copy(orig_file, destination_dir)
def copy_and_maybe_strip_patchelf(self, orig_file, type='bin', strip=True, patchelf_removerpath=True, patchelf_nodefaultlib=False, patchelf_setinterpreter_relativepath=None, object_final_destination_dir=None):
""" This does the following:
1. Copies the binary/lib (object)
2. Optionally runs patchelf with different parameters
3. Optionally strips the binary
If the file is a symlink pointing another file then the symlink is preserved (copying the file pointed)
"""
dir_suffix = 'lib' if type == 'interpreter' else type
if object_final_destination_dir is None:
object_final_destination_dir = self._destination_dir
# If set_use_sys_lib_directory() is enabled then system libraries are shipped in $ROOT/sys/lib to allow to redistribute them separately.
# However, the interpreter is always shipped in $ROOT/lib and all the binaries are always shipped in $ROOT/bin
# Even when they are system_deps because of patchelf_setinterpreter_relativepath
if self._should_use_sys_lib_directory and self._is_system_dep(orig_file) and type not in ['interpreter', 'bin']:
object_final_destination_dir = os.path.join(object_final_destination_dir, 'sys')
object_final_destination_dir = os.path.join(object_final_destination_dir, dir_suffix)
if not os.path.isdir(object_final_destination_dir):
os.makedirs(object_final_destination_dir)
if not os.path.isfile(orig_file):
raise ValueError('Can not find file %s' % orig_file)
_log.info('Add to bundle [%s]: %s' % (type, orig_file))
# Preserve symlinks and copy the targets
if os.path.islink(orig_file):
symlink_src_file = os.path.realpath(orig_file)
symlink_src_basename = os.path.basename(symlink_src_file)
symlink_dst_basename = os.path.basename(orig_file)
symlink_dst_fullpath = os.path.join(object_final_destination_dir, symlink_dst_basename)
if symlink_src_basename != symlink_dst_basename:
try:
# symlink_dst_fullpath -> symlink_src_basename
os.symlink(symlink_src_basename, symlink_dst_fullpath)
except FileExistsError:
previous_symlink_src_path = os.path.realpath(symlink_dst_fullpath)
previous_symlink_src_basename = os.path.basename(previous_symlink_src_path)
if previous_symlink_src_basename != symlink_src_basename:
raise RuntimeError('Not overwriting previous symlink %s pointing to %s with a symlink pointing to %s' % (symlink_dst_fullpath, previous_symlink_src_basename, symlink_src_basename))
return self.copy_and_maybe_strip_patchelf(symlink_src_file, type, strip, patchelf_removerpath, patchelf_nodefaultlib, patchelf_setinterpreter_relativepath, object_final_destination_dir)
try:
shutil.copy(orig_file, object_final_destination_dir)
except shutil.SameFileError:
# May reasonably happen if the caller tries to bundle the files 'in place'.
pass
if type == 'interpreter':
return # no strip/patchelf over it
# Running 'strip' after 'patchelf' may corrupt binaries, so run first 'strip'.
# See: https://github.com/NixOS/patchelf/issues/371
if strip:
if not shutil.which('strip'):
_log.warning('Unable to find the strip command in the system. Please install it.')
raise Exception('Missing required program "strip"')
strip_command = ['strip', '--strip-unneeded', os.path.join(object_final_destination_dir, os.path.basename(orig_file))]
self._run_cmd_or_log_fail(strip_command, failure_fatal=False)
patchelf_arguments = []
if patchelf_removerpath:
patchelf_arguments.append("--remove-rpath")
if type == 'bin':
if patchelf_nodefaultlib:
patchelf_arguments.append("--no-default-lib")
if patchelf_setinterpreter_relativepath:
previous_cwd = os.getcwd()
os.chdir(self.destination_dir())
# We only want the basename of the interpreter because we are updating it
# to use a relative path inside lib/ (which is were we copied it previously)
new_interpreter_relpath = os.path.join('lib', os.path.basename(patchelf_setinterpreter_relativepath))
if not os.path.isfile(new_interpreter_relpath):
raise ValueError('Unable to find interpreter %s at directory %s' % (new_interpreter_relpath, self.destination_dir()))
patchelf_arguments.extend(['--set-interpreter', new_interpreter_relpath])
self._has_patched_interpreter_relpath = True
if patchelf_arguments:
# If we have resolved patchelf_arguments then we should run patchelf
patchelf_arguments.append(os.path.join(object_final_destination_dir, os.path.basename(orig_file)))
if not shutil.which('patchelf'):
raise RuntimeError('Unable to find the "patchelf" command in the system. Please install it.')
self._run_cmd_or_log_fail(['patchelf'] + patchelf_arguments)
if patchelf_setinterpreter_relativepath:
os.chdir(previous_cwd)
def destination_dir(self):
return self._destination_dir
def is_xkb_bundled(self, sys_lib_dir, sys_share_dir):
xkb_bundled = False
if not os.path.isdir(sys_lib_dir):
return False
for entry in os.listdir(sys_lib_dir):
if entry.startswith('libxkbcommon.so'):
xkb_bundled = True
break
if xkb_bundled:
# FIXME: the xkb directory is only copied when --syslibs=bundle-all but not with --syslibs=generate-script and we may be including libxkbcommon.so in that case (jhbuild for example)
if os.path.isdir(os.path.join(sys_share_dir, 'xkb')):
return True
return False
def generate_wrapper_script(self, interpreter, binary_to_wrap, extra_environment_variables={}):
if not os.path.isfile(os.path.join(self._destination_dir, 'bin', binary_to_wrap)):
raise RuntimeError('Cannot find binary to wrap for %s' % binary_to_wrap)
_log.info('Generate wrapper script %s' % binary_to_wrap)
script_file = os.path.join(self._destination_dir, binary_to_wrap)
lib_dir = os.path.join(self._destination_dir, 'lib')
sys_lib_dir = os.path.join(self._destination_dir, 'sys/lib')
sys_share_dir = os.path.join(self._destination_dir, 'sys/share')
with open(script_file, 'w') as script_handle:
script_handle.write('#!/bin/sh\n')
script_handle.write('%s="$(dirname $(readlink -f $0))"\n' % self.VAR_MYDIR)
if self._has_patched_interpreter_relpath:
# The interprerter (section PT_INTERP in ELF binary) needs to be a pre-defined path
# So the only way of using our interpreter in an unknown destination dir is to use relative paths
# For relative paths to work, we need to CD into the main basedir, but doing that may break
# any parameter the user has passed as a relative path to where she is working (like a relpath to some html file)
# So we try to update the arguments resolving relative paths to absolute before executing the program
# Trick to update the arguments inspired from https://unix.stackexchange.com/a/421160
script_handle.write('# Shipped binaries have a relpath to the interpreter, so update passed args with fullpaths and cd to ${MYDIR}\n')
script_handle.write('args_update_relpaths_to_abs() {\n')
script_handle.write(' for arg in "$@"; do\n')
script_handle.write(' [ "${arg}" = "${arg#/}" ] && [ -e "${arg}" ] && arg="$(readlink -f -- "${arg}")"\n')
script_handle.write(' printf %s "${arg}" | sed "s/\'/\'\\\\\\\\\'\'/g;1s/^/\'/;\\$s/\\$/\' /"\n')
script_handle.write(' done\n')
script_handle.write('echo " "\n')
script_handle.write('}\n')
script_handle.write('eval "set -- $(args_update_relpaths_to_abs "$@")"\n')
script_handle.write('cd "${MYDIR}"\n')
if os.path.isfile(os.path.join(sys_share_dir, 'certs/bundle-ca-certificates.pem')):
script_handle.write('# Try to use the system CA file if possible, otherwise use the bundled one\n')
script_handle.write('for WEBKIT_TLS_CAFILE_PEM in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem "${MYDIR}/sys/share/certs/bundle-ca-certificates.pem"; do\n')
script_handle.write(' [ -f "${WEBKIT_TLS_CAFILE_PEM}" ] && grep -q "BEGIN CERTIFICATE" "${WEBKIT_TLS_CAFILE_PEM}" && export WEBKIT_TLS_CAFILE_PEM="${WEBKIT_TLS_CAFILE_PEM}" && break\n')
script_handle.write('done\n')
for var, value in extra_environment_variables.items():
script_handle.write('export %s="%s"\n' % (var, value))
if self.is_xkb_bundled(sys_lib_dir, sys_share_dir):
script_handle.write('[ -d /usr/share/X11/xkb ] || export XKB_CONFIG_ROOT="${%s}/sys/share/xkb"\n' % self.VAR_MYDIR)
ld_library_path = '${%s}/lib' % self.VAR_MYDIR
if os.path.isdir(sys_lib_dir):
ld_library_path += ':${%s}/sys/lib' % self.VAR_MYDIR
# Update cache of gdk-pixbuf loaders if needed (first run or when the absolute paths on disk have changed).
if os.path.isdir(os.path.join(sys_lib_dir, 'gdk-pixbuf/loaders')):
script_handle.write('export GDK_PIXBUF_MODULEDIR="${%s}/sys/lib/gdk-pixbuf/loaders"\n' % self.VAR_MYDIR)
script_handle.write('export GDK_PIXBUF_MODULE_FILE="${%s}/sys/lib/gdk-pixbuf/loaders.cache"\n' % self.VAR_MYDIR)
script_handle.write('grep -sq "\\"${GDK_PIXBUF_MODULEDIR}/" "${GDK_PIXBUF_MODULE_FILE}" || LD_LIBRARY_PATH="%s" "${%s}/bin/gdk-pixbuf-query-loaders" --update-cache\n' % (ld_library_path, self.VAR_MYDIR))
# LD_LIBRARY_PATH is set at the end, just before the exec() call and before calling programs bundled, otherwise it may break commands from the system like script's usage of sed/readlink/grep
script_handle.write('export LD_LIBRARY_PATH="%s"\n' % ld_library_path)
dlopenwrap_libname = 'dlopenwrap.so'
if os.path.isfile(os.path.join(lib_dir, dlopenwrap_libname)):
script_handle.write('export LD_PRELOAD="${%s}/lib/%s"\n' % (self.VAR_MYDIR, dlopenwrap_libname))
# Prefix the program with the interpreter when we are bundling all (interpreter is copied) and the path to the interpreter isn't patched.
# Otherwise prefer to not prefix it, because that allow the process to use a more meaningful progname.
interpreter_basename = os.path.basename(interpreter)
if os.path.isfile(os.path.join(lib_dir, interpreter_basename)) and not self._has_patched_interpreter_relpath:
script_handle.write('INTERPRETER="${%s}/lib/%s"\n' % (self.VAR_MYDIR, interpreter_basename))
script_handle.write('exec "${INTERPRETER}" "${%s}/bin/%s" "$@"\n' % (self.VAR_MYDIR, binary_to_wrap))
else:
script_handle.write('exec "${%s}/bin/%s" "$@"\n' % (self.VAR_MYDIR, binary_to_wrap))
os.chmod(script_file, 0o755)
# A C wrapper that we build statically works much better when bundling everything as we don't need to care
# about breaking the system bash interpreter because of the LD_LIBRARY_PATH
def generate_and_build_static_cwrapper(self, interpreter, binary_to_wrap, extra_environment_variables={}):
if not os.path.isfile(os.path.join(self._destination_dir, 'bin', binary_to_wrap)):
raise RuntimeError('Cannot find binary to wrap for %s' % binary_to_wrap)
_log.info('Generate and build static C wrapper %s' % binary_to_wrap)
wrapper_program = os.path.join(self._destination_dir, binary_to_wrap)
lib_dir = os.path.join(self._destination_dir, 'lib')
sys_lib_dir = os.path.join(self._destination_dir, 'sys/lib')
sys_share_dir = os.path.join(self._destination_dir, 'sys/share')
wrapper_source = wrapper_program + ".c"
export_env_variables = '// export required environment variables\n'
if os.path.isfile(os.path.join(lib_dir, 'dlopenwrap.so')):
export_env_variables += ' set_env_with_mydir("LD_PRELOAD", "lib/dlopenwrap.so", mydir);\n'
for var, value in extra_environment_variables.items():
if value.startswith("${MYDIR}/"):
# remove shell var mydir at start of string
value = value.removeprefix("${MYDIR}/")
export_env_variables += ' set_env_with_mydir("%s", "%s", mydir);\n' % (var, value)
else:
export_env_variables += ' setenv("%s", "%s", 1);\n' % (var, value)
# double check we are not passing unexpected shell vars on the value
assert("$" not in value)
should_set_ca_file_value = 1 if os.path.isfile(os.path.join(sys_share_dir, 'certs/bundle-ca-certificates.pem')) else 0
should_xkb_bundle_value = 1 if self.is_xkb_bundled(sys_lib_dir, sys_share_dir) else 0
should_update_gdx_pixbuf_cache_value = 0
if os.path.isdir(os.path.join(sys_lib_dir, 'gdk-pixbuf/loaders')):
should_update_gdx_pixbuf_cache_value = 1
export_env_variables += ' set_env_with_mydir("GDK_PIXBUF_MODULEDIR", "sys/lib/gdk-pixbuf/loaders", mydir);\n'
export_env_variables += ' set_env_with_mydir("GDK_PIXBUF_MODULE_FILE", "sys/lib/gdk-pixbuf/loaders.cache", mydir);\n'
set_ld_path_value = 'snprintf(ldpath, sizeof(ldpath), "%s/lib", mydir);'
if os.path.isdir(sys_lib_dir):
set_ld_path_value = 'snprintf(ldpath, sizeof(ldpath), "%s/lib:%s/sys/lib", mydir, mydir);'
with open(wrapper_source, 'w') as wrapper_source_handle:
wrapper_source_handle.write(WRAPPER_CSOURCE_TEMPLATE % {
'relative_path_binary': os.path.join('bin', binary_to_wrap),
'should_set_ca_file_value': should_set_ca_file_value,
'should_xkb_bundle_value': should_xkb_bundle_value,
'should_update_gdx_pixbuf_cache_value': should_update_gdx_pixbuf_cache_value,
'set_ld_path_value': set_ld_path_value,
'export_env_variables': export_env_variables
})
compiler_program = os.getenv('CC', default='gcc')
self._run_cmd_or_log_fail([compiler_program, '-static', '-o', wrapper_program, wrapper_source])