#! /usr/bin/env bash
set -e
export LANG='C.UTF-8'
export LANGUAGE="${LANG}"
_sed () {
case "${system_name}" in
'macos'|'freebsd')
gsed "${@}"
;;
*)
sed "${@}"
;;
esac
}
_cp () {
case "${system_name}" in
'macos'|'freebsd')
gcp -R --preserve=timestamps -H -L "${1}" "${2}"
;;
*)
cp -R --preserve=timestamps -H -L "${1}" "${2}"
;;
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'|'freebsd')
# - 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
# FreeBSD specific
elif echo "${ldd_line}" \
| egrep -q '/libc++|/libgxxrt'
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'|'freebsd')
ldd "${exe_file}"
;;
'windows')
ntldd --recursive "${exe_file}"
;;
'macos')
otool -L "${exe_file}"
esac
}
Multi::getGtkThemeName () {
case "${system_name}" in
'linux'|'freebsd')
echo 'Adwaita'
;;
'windows')
echo 'MS-Windows'
;;
*)
echo 'Raleigh'
;;
esac
}
Multi::getGtkLibName () {
case "${system_name}" in
'linux'|'freebsd')
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'|'freebsd')
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'|'freebsd')
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'|'freebsd'|'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'|'freebsd')
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 () {
# Remove from bundle things that useless to be distributed,
# like headers or static libraries, also remove
# empty directories.
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 \
-empty \
-exec rmdir {} \;
}
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}")"
chmod u+w,go-w "${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}"
chmod ugo-x "${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'|'freebsd')
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