]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - library-bundler
bundler: detect missing libraries
[xonotic/netradiant.git] / library-bundler
1 #! /usr/bin/env bash
2
3 set -e
4
5 export LANG='C.UTF-8'
6 export LANGUAGE="${LANG}"
7
8 Common::noOp () {
9         true
10 }
11
12 Common::getPath () {
13         local file_path="${1}"
14
15         if command -v cygpath >/dev/null
16         then
17                 if [ "${file_path}" = '-' ]
18                 then
19                         tr '\n' '\0' \
20                         | xargs -0 -n1 -P1 -I{} \
21                                 cygpath --unix '{}'
22                 else
23                         cygpath --unix "${file_path}"
24                 fi
25         else
26                 if [ "${file_path}" = '-' ]
27                 then
28                         cat
29                 else
30                         printf '%s\n' "${file_path}"
31                 fi
32         fi \
33         | sed -e 's|/*$||'
34 }
35
36 Common::grepLdd () {
37         egrep ' => '
38 }
39
40 Common::stripLdd () {
41         sed -e 's/ (0x[0-9a-f]*)$//;s/^.* => //'
42 }
43
44 Multi::excludeLdd () {
45         case "${system_name}" in
46                 'linux')
47                         # - always rely on up-to-date x11 and gl libraries, bundling them will break on future distros
48                         # - gtk is not easily bundlable on linux because it looks for harcoded system path to optional
49                         #   shared libraries like image codecs, theme engines, sound notification system, etc.
50                         #   so expect user to install gtk first
51                         # - since we ask user to instal gtk, we can also ask them to install gtkglext,
52                         #   which is likely to pull gtk itself, x11 and gl dependencies
53                         # - old fontconfig does not work correctly if newer fontconfig configuration is installed
54                         # - if gtk and fontconfig is installed, pango and freetype are
55                         egrep -v '/libc\.|/libstdc\+\+\.|/libdl\.|/libm\.|/libX|/libxcb|/libGL|/libICE\.|/libSM\.|/libpthread\.' \
56                         | egrep -v '/libatk|/libgdk|/libgtk|/libgio|/libglib|/libgmodule|/libgobject|/libcairo|/libpango|/libfontconfig|/libfreetype'
57                         ;;
58                 'windows')
59                         egrep -i '\.dll => [A-Z]:\\msys64\\'
60                         ;;
61         esac
62 }
63
64 Multi::filterLib () {
65         Common::grepLdd \
66         | Multi::excludeLdd \
67         | Common::stripLdd \
68         | Common::getPath -
69 }
70
71 Multi::printLdd () {
72         local exe_file="${1}"
73
74         case "${system_name}" in
75                 'linux')
76                         ldd "${exe_file}"
77                         ;;
78                 'windows')
79                         ntldd --recursive "${exe_file}"
80                         ;;
81         esac
82 }
83
84 Multi::getGtkThemeName () {
85         case "${system_name}" in
86                 'linux')
87                         echo 'Adwaita'
88                         ;;
89                 'windows')
90                         echo 'MS-Windows'
91                         ;;
92         esac
93 }
94
95 Multi::getGtkLibName () {
96         case "${system_name}" in
97                 'linux')
98                         echo 'libgtk-x11-2.0.so.0'
99                         ;;
100                 'windows')
101                         echo 'libgtk-win32-2.0-0.dll'
102                         ;;
103         esac
104 }
105
106 Multi::getRootPrefix () {
107         local lib_file="${1}"
108
109         case "${system_name}" in
110                 'linux')
111                         echo "${lib_file}" \
112                         | cut -f2 -d'/'
113                         ;;
114                 'windows')
115                         basename "${lib_file}" \
116                         | xargs -n1 -P1 which \
117                         | cut -f2 -d'/'
118                         ;;
119         esac
120 }
121
122 Multi::getLibPrefix () {
123         local lib_file="${1}"
124
125         case "${system_name}" in
126                 'linux')
127                         dirname "${lib_file}" \
128                         | cut -f3- -d'/'
129                         ;;
130                 'windows')
131                         echo 'lib'
132                         ;;
133         esac
134 }
135
136 Multi::getGtkDeps () {
137         local lib_prefix="${1}"
138         local gtk_theme_name="${2}"
139
140         cat <<-EOF
141         share/themes/${gtk_theme_name}/gtk-2.0
142         share/icons/hicolor
143         ${lib_prefix}/gdk-pixbuf-2.0
144         ${lib_prefix}/gtk-2.0
145         EOF
146
147         case "${system_name}" in
148                 'linux')
149                         cat <<-EOF
150                         ${lib_prefix}/libatk-bridge-2.0.so.0
151                         ${lib_prefix}/libcanberra-0.30
152                         ${lib_prefix}/libcanberra.so.0
153                         ${lib_prefix}/libcanberra-gtk.so.0
154                         EOF
155                         ;;
156         esac
157 }
158
159 Multi::bundleGtkDepsFromFile () {
160         local lib_file="${1}"
161
162         lib_basename="$(basename "${lib_file}")"
163
164         gtk_lib_name="$(Multi::getGtkLibName)"
165         if [ "${lib_basename}" = "${gtk_lib_name}" ]
166         then
167                 root_prefix="$(Multi::getRootPrefix "${lib_file}")"
168                 lib_prefix="$(Multi::getLibPrefix "${lib_file}")"
169                 gtk_theme_name="$(Multi::getGtkThemeName)"
170
171                 for component_dir in $(Multi::getGtkDeps "${lib_prefix}" "${gtk_theme_name}")
172                 do
173                         bundle_component_dir="$(echo "${component_dir}" | sed -e 's|^'"${lib_prefix}"'|lib|')"
174                         if ! [ -e "${bundle_dir}/${bundle_component_dir}" ]
175                         then
176                                 mkdir --parents "${bundle_dir}/$(dirname "${bundle_component_dir}")"
177
178                                 cp -H -r --preserve=timestamps \
179                                         "/${root_prefix}/${component_dir}" \
180                                         "${bundle_dir}/${bundle_component_dir}"
181                         fi
182                 done
183         fi
184 }
185
186 Multi::bundleLibFromFile () {
187         local exe_file="${1}"
188         local lib_file
189
190         Multi::printLdd "${exe_file}" \
191         | Multi::filterLib \
192         | while read lib_file
193         do
194                 if [ "${lib_file}" = 'not found' ]
195                 then
196                         printf 'ERROR: library not found while bundling %s (but link worked)\n' "${exe_file}" >&2
197                         Multi::printLdd "${exe_file}" | grep 'not found'
198                         exit 1
199                 fi
200                 lib_basename="$(basename "${lib_file}")"
201
202                 if [ -f "${lib_dir}/${lib_basename}" ]
203                 then
204                         continue
205                 fi
206
207                 cp --preserve=timestamps \
208                         "${lib_file}" \
209                         "${lib_dir}/${lib_basename}"
210
211                 Multi::bundleGtkDepsFromFile "${lib_file}"
212         done
213 }
214
215 Multi::cleanUp () {
216         find "${bundle_dir}/lib" \
217                 -type f \
218                 -name '*.a' \
219                 -exec rm {} \;
220
221         find "${bundle_dir}/lib" \
222                 -type f \
223                 -name '*.h' \
224                 -exec rm {} \;
225
226         find "${bundle_dir}/lib" \
227                 -depth \
228                 -type d \
229                 -exec rmdir --ignore-fail-on-non-empty {} \;
230 }
231
232 Linux::getRpath () {
233         local exe_file="${1}"
234
235         local exe_dir="$(dirname "${exe_file}")"
236         local path_start="$(printf '%s' "${bundle_dir}" | wc -c)"
237         path_start="$((${path_start} + 1))"
238
239         local exe_subdir="$(echo "${exe_dir}" | cut -c "${path_start}-" | sed -e 's|//*|/|;s|^/||')"
240
241         local rpath_origin='$ORIGIN'
242
243         if [ "${exe_subdir}" = '' ]
244         then
245                 printf '%s/lib\n' "${rpath_origin}"
246         else
247                 if [ "${exe_subdir}" = 'lib' ]
248                 then
249                         printf '%s\n' "${rpath_origin}"
250                 else
251                         local num_parent_dir="$(echo "${exe_subdir}" | tr '/' '\n' | wc -l)"
252                         local rpath_subdir
253                         local i=0
254                         while [ "${i}" -lt "${num_parent_dir}" ]
255                         do
256                                 rpath_subdir="${rpath_subdir}/.."
257                                 i="$((${i} + 1))"
258                         done
259                         printf '%s%s/lib\n' "${rpath_origin}" "${rpath_subdir}"
260                 fi
261         fi
262 }
263
264 Linux::patchExe () {
265         local exe_file="${1}"
266
267         local linux_rpath_string=$"$(Linux::getRpath "${exe_file}")"
268         patchelf --set-rpath "${linux_rpath_string}" "${exe_file}"
269 }
270
271 Linux::patchLib () {
272         local lib_dir="${1}"
273         local exe_file
274
275         find "${lib_dir}" \
276                 -type f \
277                 -name '*.so*' \
278         | while read exe_file
279         do
280                 Linux::patchExe "${exe_file}"
281         done
282 }
283
284 Windows::listLibForManifest () {
285         local lib_dir="${1}"
286
287         find "${lib_dir}" \
288                 -maxdepth 1 \
289                 -type f \
290                 -name '*.dll' \
291                 -exec basename {} \; \
292         | tr '\n' '\0' \
293         | xargs -0 -n1 -P1 -I{} \
294                 printf '  <file name="{}"/>\n'
295 }
296
297 Windows::writeManifest () {
298         local lib_dir="${1}"
299
300         cat > "${manifest_file}" <<-EOF
301         <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
302           <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
303         $(Windows::listLibForManifest "${lib_dir}")
304         </assembly>
305         EOF
306 }
307
308 system_name="${1}"; shift
309 bundle_dir="${1}"; shift
310
311 if ! [ -z "${1}" ]
312 then
313         exe_file="${1}"; shift
314 fi
315
316 bundle_dir="$(Common::getPath "${bundle_dir}")"
317 registry_dir="${bundle_dir}/registry"
318 lib_dir="${bundle_dir}/lib"
319
320 manifest_file="${lib_dir}/lib.manifest"
321
322 exe_action='Common::noOp'
323 lib_action='Common::noOp'
324
325 case "${system_name}" in
326         'register')
327                 mkdir --parents "${registry_dir}"
328                 Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)"
329                 exit
330                 ;;
331         'linux')
332                 exe_action='Linux::patchExe'
333                 lib_action='Linux::patchLib'
334                 ;;
335         'windows')
336                 lib_action='Windows::writeManifest'
337                 ;;
338         *)
339                 printf 'ERROR: unsupported system: %s\n' "${system_name}" >&2
340                 exit 1
341                 ;;
342 esac
343
344 mkdir --parents "${lib_dir}"
345
346 if [ -d "${registry_dir}" ]
347 then
348         for registry_entry in "${registry_dir}"/*
349         do
350                 exe_file="$(cat "${registry_entry}")"
351
352                 Multi::bundleLibFromFile "${exe_file}"
353
354                 "${exe_action}" "${exe_file}"
355
356                 rm "${registry_entry}"
357
358                 "${exe_action}" "${exe_file}"
359         done
360
361         rmdir "${registry_dir}"
362 fi
363
364 "${lib_action}" "${lib_dir}"
365
366 Multi::cleanUp