#! /usr/bin/env bash set -e export LANG='C.UTF-8' export LANGUAGE="${LANG}" _sed () { case "${system_name}" in 'macos') gsed "${@}" ;; *) sed "${@}" ;; esac } _cp () { case "${system_name}" in 'macos') gcp -R --preserve=timestamps -H -L "${@}" ;; *) cp -R --preserve=timestamps -H -L "${@}" ;; esac } Common::noOp () { true } Common::getPath () { local file_path="${1}" if command -v cygpath >/dev/null then if [ "${file_path}" = '-' ] then tr '\n' '\0' \ | xargs -0 -n1 -P1 -I{} \ cygpath --unix '{}' else cygpath --unix "${file_path}" fi else if [ "${file_path}" = '-' ] then cat else printf '%s\n' "${file_path}" fi fi \ | _sed -e 's|/*$||' } Common::grepLdd () { case "${system_name}" in 'macos') egrep '^\t/' ;; *) egrep ' => ' ;; esac } Common::stripLdd () { case "${system_name}" in 'macos') _sed -e 's/^\t\(.*\) (compatibility version .*/\1/' ;; *) _sed -e 's/ (0x[0-9a-f]*)$//;s/^.* => //' ;; esac } Multi::excludeLdd () { case "${system_name}" in 'linux') # - always bundle built-in libraries # - always rely on up-to-date x11 and gl libraries, bundling them will break on future distros # - gtk is not easily bundlable on linux because it looks for harcoded system path to optional # shared libraries like image codecs, theme engines, sound notification system, etc. # so expect user to install gtk first # - since we ask user to instal gtk, we can also ask them to install gtkglext, # which is likely to pull gtk itself, x11 and gl dependencies # - old fontconfig does not work correctly if newer fontconfig configuration is installed # - if gtk and fontconfig is installed, pango and freetype are local ldd_line while read ldd_line do if echo "${ldd_line}" | egrep '/builtins/' then echo "${ldd_line}" elif echo "${ldd_line}" \ | egrep -q '/libc\.|/libstdc\+\+\.|/libdl\.|/libm\.|/libX|/libxcb|/libGL|/libICE\.|/libSM\.|/libpthread\.' then Common::noOp elif echo "${ldd_line}" \ | egrep -q '/libatk|/libgdk|/libgtk|/libgio|/libglib|/libgmodule|/libgobject|/libcairo|/libpango|/libfontconfig|/libfreetype' then Common::noOp else echo "${ldd_line}" fi done ;; 'windows') egrep -i '\.dll => [A-Z]:\\msys64\\' ;; 'macos') egrep -v '^\t/System/|^\t/usr/lib/' ;; esac } Multi::filterLib () { Common::grepLdd \ | Multi::excludeLdd \ | Common::stripLdd \ | Common::getPath - } Multi::printLdd () { local exe_file="${1}" case "${system_name}" in 'linux') ldd "${exe_file}" ;; 'windows') ntldd --recursive "${exe_file}" ;; 'macos') otool -L "${exe_file}" esac } Multi::getGtkThemeName () { case "${system_name}" in 'linux') echo 'Adwaita' ;; 'windows') echo 'MS-Windows' ;; *) echo 'Raleigh' ;; esac } Multi::getGtkLibName () { case "${system_name}" in 'linux') echo 'libgtk-x11-2.0.so.0' ;; 'windows') echo 'libgtk-win32-2.0-0.dll' ;; 'macos') echo 'libgtk-quartz-2.0.0.dylib' ;; esac } Multi::getRootPrefix () { local lib_file="${1}" case "${system_name}" in 'linux') echo "${lib_file}" \ | cut -f2 -d'/' ;; 'windows') basename "${lib_file}" \ | xargs -n1 -P1 which \ | cut -f2 -d'/' ;; 'macos') echo 'usr/local' esac } Multi::getLibPrefix () { local lib_file="${1}" case "${system_name}" in 'linux') dirname "${lib_file}" \ | cut -f3- -d'/' ;; 'windows') echo 'lib' ;; 'macos') echo 'lib' ;; esac } Multi::getGtkDeps () { local lib_prefix="${1}" local gtk_theme_name="${2}" case "${system_name}" in 'linux'|'windows') cat <<-EOF share/themes/${gtk_theme_name}/gtk-2.0 share/icons/hicolor ${lib_prefix}/gdk-pixbuf-2.0 ${lib_prefix}/gtk-2.0 EOF ;; 'macos') cat <<-EOF etc/fonts share/themes/${gtk_theme_name}/gtk-2.0 share/fontconfig share/icons/hicolor share/locale ${lib_prefix}/gdk-pixbuf-2.0 ${lib_prefix}/gtk-2.0 EOF ;; esac case "${system_name}" in 'linux') cat <<-EOF ${lib_prefix}/libatk-bridge-2.0.so.0 ${lib_prefix}/libcanberra-0.30 ${lib_prefix}/libcanberra.so.0 ${lib_prefix}/libcanberra-gtk.so.0 EOF ;; esac } Multi::rewriteLoadersCache () { local bundle_component_path="${1}" local cache_file find "${bundle_component_path}" \ -type f \ \( \ -name 'loaders.cache' \ -o -name 'immodules.cache' \ \) \ | while read cache_file do _sed \ -e 's|^"/[^"]*/lib/|"lib/|;s| "/[^"]*/share/| "share/|;/^# ModulesPath = /d;/^# Created by /d;/^#$/d' \ -i "${cache_file}" done } Multi::bundleGtkDepsFromFile () { local lib_file="${1}" local component_dir local real_component_dir local bundle_component_dir lib_basename="$(basename "${lib_file}")" gtk_lib_name="$(Multi::getGtkLibName)" if [ "${lib_basename}" = "${gtk_lib_name}" ] then root_prefix="$(Multi::getRootPrefix "${lib_file}")" lib_prefix="$(Multi::getLibPrefix "${lib_file}")" gtk_theme_name="$(Multi::getGtkThemeName)" for component_dir in $(Multi::getGtkDeps "${lib_prefix}" "${gtk_theme_name}") do bundle_component_dir="$(echo "${component_dir}" | _sed -e 's|^'"${lib_prefix}"'|lib|')" if ! [ -e "${bundle_dir}/${bundle_component_dir}" ] then real_component_dir="$(realpath "/${root_prefix}/${component_dir}")" mkdir -p "${bundle_dir}/$(dirname "${bundle_component_dir}")" _cp \ "${real_component_dir}" \ "${bundle_dir}/${bundle_component_dir}" Multi::rewriteLoadersCache "${bundle_dir}/${bundle_component_dir}" fi done fi } Multi::bundleLibFromFile () { local exe_file="${1}" local lib_file Multi::printLdd "${exe_file}" \ | Multi::filterLib \ | while read lib_file do if [ "${lib_file}" = 'not found' ] then printf 'ERROR: library not found while bundling %s (but link worked)\n' "${exe_file}" >&2 Multi::printLdd "${exe_file}" | grep 'not found' exit 1 fi lib_basename="$(basename "${lib_file}")" if [ -f "${lib_dir}/${lib_basename}" ] then continue fi _cp \ "${lib_file}" \ "${lib_dir}/${lib_basename}" Multi::bundleGtkDepsFromFile "${lib_file}" case "${system_name}" in 'macos') Multi::bundleLibFromFile "${lib_file}" ;; esac done } Multi::cleanUp () { find "${bundle_dir}/lib" \ -type f \ -name '*.a' \ -exec rm -f {} \; find "${bundle_dir}/lib" \ -type f \ -name '*.h' \ -exec rm -f {} \; find "${bundle_dir}/lib" \ -depth \ -type d \ -exec rmdir {} \; \ || true } Linux::getRpath () { local exe_file="${1}" local exe_dir="$(dirname "${exe_file}")" local path_start="$(printf '%s' "${bundle_dir}" | wc -c)" path_start="$((${path_start} + 1))" local exe_subdir="$(echo "${exe_dir}" | cut -c "${path_start}-" | _sed -e 's|//*|/|;s|^/||')" local rpath_origin='$ORIGIN' if [ "${exe_subdir}" = '' ] then printf '%s/lib\n' "${rpath_origin}" else if [ "${exe_subdir}" = 'lib' ] then printf '%s\n' "${rpath_origin}" else local num_parent_dir="$(echo "${exe_subdir}" | tr '/' '\n' | wc -l)" local rpath_subdir local i=0 while [ "${i}" -lt "${num_parent_dir}" ] do rpath_subdir="${rpath_subdir}/.." i="$((${i} + 1))" done printf '%s%s/lib\n' "${rpath_origin}" "${rpath_subdir}" fi fi } Linux::patchExe () { local exe_file="${1}" local linux_rpath_string=$"$(Linux::getRpath "${exe_file}")" patchelf --set-rpath "${linux_rpath_string}" "${exe_file}" } Linux::patchLib () { local lib_dir="${1}" local exe_file find "${lib_dir}" \ -type f \ -name '*.so*' \ | while read exe_file do Linux::patchExe "${exe_file}" done } Darwin::patchExe () { local exe_file="${1}" Multi::printLdd "${exe_file}" \ | Multi::filterLib \ | while read lib_file do new_path="$(echo "${lib_file}" | _sed -e 's|^/.*/lib/|@executable_path/lib/|')" id_name="$(echo "${lib_file}" | _sed -e 's|.*/||g')" chmod u+w,go-w "${exe_file}" install_name_tool -change "${lib_file}" "${new_path}" "${exe_file}" install_name_tool -id "${id_name}" "${exe_file}" done } Darwin::patchLib () { local lib_dir="${1}" local exe_file find "${lib_dir}" \ -type f \ \( \ -name '*.dylib' \ -o -name '*.so' \ \) \ | while read exe_file do Darwin::patchExe "${exe_file}" chmod ugo-x "${exe_file}" done } Windows::listLibForManifest () { local lib_dir="${1}" find "${lib_dir}" \ -maxdepth 1 \ -type f \ -name '*.dll' \ -exec basename {} \; \ | tr '\n' '\0' \ | xargs -0 -n1 -P1 -I{} \ printf ' \n' } Windows::writeManifest () { local lib_dir="${1}" cat > "${manifest_file}" <<-EOF $(Windows::listLibForManifest "${lib_dir}") EOF } system_name="${1}"; shift bundle_dir="${1}"; shift if ! [ -z "${1}" ] then exe_file="${1}"; shift fi bundle_dir="$(Common::getPath "${bundle_dir}")" registry_dir="${bundle_dir}/registry" lib_dir="${bundle_dir}/lib" manifest_file="${lib_dir}/lib.manifest" exe_action='Common::noOp' lib_action='Common::noOp' case "${system_name}" in 'register') mkdir -p "${registry_dir}" Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)" exit ;; 'linux') exe_action='Linux::patchExe' lib_action='Linux::patchLib' ;; 'windows') lib_action='Windows::writeManifest' ;; 'macos') exe_action='Darwin::patchExe' lib_action='Darwin::patchLib' ;; *) printf 'ERROR: unsupported system: %s\n' "${system_name}" >&2 exit 1 ;; esac mkdir -p "${lib_dir}" if [ -d "${registry_dir}" ] then for registry_entry in "${registry_dir}"/* do exe_file="$(cat "${registry_entry}")" Multi::bundleLibFromFile "${exe_file}" "${exe_action}" "${exe_file}" rm "${registry_entry}" "${exe_action}" "${exe_file}" done rmdir "${registry_dir}" fi "${lib_action}" "${lib_dir}" Multi::cleanUp