#! /usr/bin/env bash
-Windows::listLibForManifest () {
- local lib_dir="${1}"
+set -e
- find "${lib_dir}" \
- -maxdepth 1 \
- -type f \
- -name '*.dll' \
- -exec basename {} \; \
- | xargs -I {} \
- printf ' <file name="{}"/>\n'
+export LANG='C.UTF-8'
+export LANGUAGE="${LANG}"
+
+_sed () {
+ case "${system_name}" in
+ 'macos'|'freebsd')
+ gsed "${@}"
+ ;;
+ *)
+ sed "${@}"
+ ;;
+ esac
}
-Windows::printManifest () {
- local lib_dir="${1}"
+_cp () {
+ case "${system_name}" in
+ 'macos'|'freebsd')
+ gcp -R --preserve=timestamps -H -L "${1}" "${2}"
+ ;;
+ *)
+ cp -R --preserve=timestamps -H -L "${1}" "${2}"
+ ;;
+ esac
+}
- cat <<-EOF
- <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
- <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
- $(Windows::listLibForManifest "${lib_dir}")
- </assembly>
- EOF
+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 -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 -
}
-Windows::bundleLibFromFile () {
+Multi::printLdd () {
local exe_file="${1}"
- exe_file="$(cygpath --unix "${exe_file}")"
+ 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
- ntldd --recursive "${exe_file}" \
- | egrep -i '\.dll => [A-Z]:\\msys64\\' \
- | sed -e 's/ (0x[0-9a-f]*)$//;s/^.* => //' \
- | cygpath --unix --file - \
- | while read dll_file
+ 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
- dll_basename="$(basename "${dll_file}")"
+ 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 "${bundle_dir}/${dll_basename}" ]
+ if [ -f "${lib_dir}/${lib_basename}" ]
then
continue
fi
- cp --preserve=timestamps "${dll_file}" "${lib_dir}/${dll_basename}"
+ _cp \
+ "${lib_file}" \
+ "${lib_dir}/${lib_basename}"
- if [ "${dll_basename}" = 'libgtk-win32-2.0-0.dll' ]
- then
- mingw="$(which 'libgtk-win32-2.0-0.dll' | cut -f2 -d'/')"
+ 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|^/||')"
- for component_dir in \
- 'share/themes/MS-Windows' \
- 'share/icons/hicolor' \
- 'lib/gdk-pixbuf-2.0' \
- 'lib/gtk-2.0'
+ 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
- if ! [ -d "${bundle_dir}/${component_dir}" ]
- then
- mkdir --parents "${bundle_dir}/$(dirname "${component_dir}")"
- cp -r --preserve=timestamps "/${mingw}/${component_dir}" \
- "${bundle_dir}/${component_dir}"
- fi
+ rpath_subdir="${rpath_subdir}/.."
+ i="$((${i} + 1))"
done
-
- find "${bundle_dir}/lib" -type f -name '*.a' -exec rm {} \;
- find "${bundle_dir}/lib" -type f -name '*.h' -exec rm {} \;
- find "${bundle_dir}/lib" -type d -exec rmdir --ignore-fail-on-non-empty {} \;
+ 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 -P1 -I{} \
+ printf ' <file name="{}"/>\n'
+}
+
+Windows::writeManifest () {
+ local lib_dir="${1}"
+
+ cat > "${manifest_file}" <<-EOF
+ <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
+ $(Windows::listLibForManifest "${lib_dir}")
+ </assembly>
+ EOF
+}
+
system_name="${1}"; shift
bundle_dir="${1}"; shift
-exe_file="${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 --parents "${registry_dir}"
- printf '%s\n' "${exe_file}" > "${registry_dir}/$(uuidgen)"
+ mkdir -p "${registry_dir}"
+ Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)"
+ exit
+ ;;
+ 'linux'|'freebsd')
+ exe_action='Linux::patchExe'
+ lib_action='Linux::patchLib'
;;
'windows')
- bundle_dir="$(cygpath --unix "${bundle_dir}")"
-
- lib_dir="${bundle_dir}/lib"
- mkdir --parents "${lib_dir}"
-
- if [ -d "${registry_dir}" ]
- then
- for registry_entry in "${registry_dir}"/*
- do
- exe_file="$(cat "${registry_entry}")"
-
- Windows::bundleLibFromFile "${exe_file}"
-
- rm "${registry_entry}"
- rmdir --ignore-fail-on-non-empty "${registry_dir}"
- done
-
- manifest_file="${lib_dir}/lib.manifest"
- Windows::printManifest "${lib_dir}" > "${manifest_file}"
- fi
+ 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