radiant/layouts: add a single-window layout
[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 _sed () {
9         case "${system_name}" in
10                 'macos'|'freebsd')
11                         gsed "${@}"
12                         ;;
13                 *)
14                         sed "${@}"
15                         ;;
16         esac
17 }
18
19 _cp () {
20         case "${system_name}" in
21                 'macos'|'freebsd')
22                         gcp -R --preserve=timestamps -H -L "${1}" "${2}"
23                         ;;
24                 *)
25                         cp -R --preserve=timestamps -H -L "${1}" "${2}"
26                         ;;
27         esac
28 }
29
30 Common::noOp () {
31         true
32 }
33
34 Common::getPath () {
35         local file_path="${1}"
36
37         if command -v cygpath >/dev/null
38         then
39                 if [ "${file_path}" = '-' ]
40                 then
41                         tr '\n' '\0' \
42                         | xargs -0 -n1 -P1 -I{} \
43                                 cygpath --unix '{}'
44                 else
45                         cygpath --unix "${file_path}"
46                 fi
47         else
48                 if [ "${file_path}" = '-' ]
49                 then
50                         cat
51                 else
52                         printf '%s\n' "${file_path}"
53                 fi
54         fi \
55         | _sed -e 's|/*$||'
56 }
57
58 Common::grepLdd () {
59         case "${system_name}" in
60                 'macos')
61                         egrep '^\t/'
62                         ;;
63                 *)
64                         egrep ' => '
65                         ;;
66         esac
67 }
68
69 Common::stripLdd () {
70         case "${system_name}" in
71                 'macos')
72                         _sed -e 's/^\t\(.*\) (compatibility version .*/\1/'
73                         ;;
74                 *)
75                         _sed -e 's/ (0x[0-9a-f]*)$//;s/^.* => //'
76                         ;;
77         esac
78 }
79
80 Multi::excludeLdd () {
81         case "${system_name}" in
82                 'linux'|'freebsd')
83                         # - always bundle built-in libraries
84                         # - always rely on up-to-date x11 and gl libraries, bundling them will break on future distros
85                         # - gtk is not easily bundlable on linux because it looks for harcoded system path to optional
86                         #   shared libraries like image codecs, theme engines, sound notification system, etc.
87                         #   so expect user to install gtk first
88                         # - since we ask user to instal gtk, we can also ask them to install gtkglext,
89                         #   which is likely to pull gtk itself, x11 and gl dependencies
90                         # - old fontconfig does not work correctly if newer fontconfig configuration is installed
91                         # - if gtk and fontconfig is installed, pango and freetype are
92                         local ldd_line
93                         while read ldd_line
94                         do
95                                 if echo "${ldd_line}" | egrep '/builtins/'
96                                 then
97                                         echo "${ldd_line}"
98                                 elif echo "${ldd_line}" \
99                                         | egrep -q '/libc\.|/libstdc\+\+\.|/libdl\.|/libm\.|/libX|/libxcb|/libGL|/libICE\.|/libSM\.|/libpthread\.'
100                                 then
101                                         Common::noOp
102                                 elif echo "${ldd_line}" \
103                                         | egrep -q '/libatk|/libgdk|/libgtk|/libgio|/libglib|/libgmodule|/libgobject|/libcairo|/libpango|/libfontconfig|/libfreetype'
104                                 then
105                                         Common::noOp
106                                 # FreeBSD specific
107                                 elif echo "${ldd_line}" \
108                                         | egrep -q '/libc++|/libgxxrt'
109                                 then
110                                         Common::noOp
111                                 else
112                                         echo "${ldd_line}"
113                                 fi
114                         done
115                         ;;
116                 'windows')
117                         egrep -i '\.dll => [A-Z]:\\msys64\\'
118                         ;;
119                 'macos')
120                         egrep -v '^\t/System/|^\t/usr/lib/'
121                         ;;
122         esac
123 }
124
125 Multi::filterLib () {
126         Common::grepLdd \
127         | Multi::excludeLdd \
128         | Common::stripLdd \
129         | Common::getPath -
130 }
131
132 Multi::printLdd () {
133         local exe_file="${1}"
134
135         case "${system_name}" in
136                 'linux'|'freebsd')
137                         ldd "${exe_file}"
138                         ;;
139                 'windows')
140                         ntldd --recursive "${exe_file}"
141                         ;;
142                 'macos')
143                         otool -L "${exe_file}"
144         esac
145 }
146
147 Multi::getGtkThemeName () {
148         case "${system_name}" in
149                 'linux'|'freebsd')
150                         echo 'Adwaita'
151                         ;;
152                 'windows')
153                         echo 'MS-Windows'
154                         ;;
155                 *)
156                         echo 'Raleigh'
157                         ;;
158         esac
159 }
160
161 Multi::getGtkLibName () {
162         case "${system_name}" in
163                 'linux'|'freebsd')
164                         echo 'libgtk-x11-2.0.so.0'
165                         ;;
166                 'windows')
167                         echo 'libgtk-win32-2.0-0.dll'
168                         ;;
169                 'macos')
170                         echo 'libgtk-quartz-2.0.0.dylib'
171                         ;;
172         esac
173 }
174
175 Multi::getRootPrefix () {
176         local lib_file="${1}"
177
178         case "${system_name}" in
179                 'linux'|'freebsd')
180                         echo "${lib_file}" \
181                         | cut -f2 -d'/'
182                         ;;
183                 'windows')
184                         basename "${lib_file}" \
185                         | xargs -n1 -P1 which \
186                         | cut -f2 -d'/'
187                         ;;
188                 'macos')
189                         echo 'usr/local'
190         esac
191 }
192
193 Multi::getLibPrefix () {
194         local lib_file="${1}"
195
196         case "${system_name}" in
197                 'linux'|'freebsd')
198                         dirname "${lib_file}" \
199                         | cut -f3- -d'/'
200                         ;;
201                 'windows')
202                         echo 'lib'
203                         ;;
204                 'macos')
205                         echo 'lib'
206                         ;;
207         esac
208 }
209
210 Multi::getGtkDeps () {
211         local lib_prefix="${1}"
212         local gtk_theme_name="${2}"
213
214         case "${system_name}" in
215                 'linux'|'freebsd'|'windows')
216                         cat <<-EOF
217                         share/themes/${gtk_theme_name}/gtk-2.0
218                         share/icons/hicolor
219                         ${lib_prefix}/gdk-pixbuf-2.0
220                         ${lib_prefix}/gtk-2.0
221                         EOF
222                         ;;
223                 'macos')
224                         cat <<-EOF
225                         etc/fonts
226                         share/themes/${gtk_theme_name}/gtk-2.0
227                         share/fontconfig
228                         share/icons/hicolor
229                         share/locale
230                         ${lib_prefix}/gdk-pixbuf-2.0
231                         ${lib_prefix}/gtk-2.0
232                         EOF
233                         ;;
234         esac
235
236         case "${system_name}" in
237                 'linux'|'freebsd')
238                         cat <<-EOF
239                         ${lib_prefix}/libatk-bridge-2.0.so.0
240                         ${lib_prefix}/libcanberra-0.30
241                         ${lib_prefix}/libcanberra.so.0
242                         ${lib_prefix}/libcanberra-gtk.so.0
243                         EOF
244                         ;;
245         esac
246 }
247
248 Multi::rewriteLoadersCache () {
249         local bundle_component_path="${1}"
250         local cache_file
251
252         find "${bundle_component_path}" \
253                 -type f \
254                 \( \
255                 -name 'loaders.cache' \
256                 -o -name 'immodules.cache' \
257                 \) \
258         | while read cache_file
259         do
260                 _sed \
261                         -e 's|^"/[^"]*/lib/|"lib/|;s| "/[^"]*/share/| "share/|;/^# ModulesPath = /d;/^# Created by /d;/^#$/d' \
262                         -i "${cache_file}"
263         done
264 }
265
266 Multi::bundleGtkDepsFromFile () {
267         local lib_file="${1}"
268         local component_dir
269         local real_component_dir
270         local bundle_component_dir
271
272         lib_basename="$(basename "${lib_file}")"
273
274         gtk_lib_name="$(Multi::getGtkLibName)"
275         if [ "${lib_basename}" = "${gtk_lib_name}" ]
276         then
277                 root_prefix="$(Multi::getRootPrefix "${lib_file}")"
278                 lib_prefix="$(Multi::getLibPrefix "${lib_file}")"
279                 gtk_theme_name="$(Multi::getGtkThemeName)"
280
281                 for component_dir in $(Multi::getGtkDeps "${lib_prefix}" "${gtk_theme_name}")
282                 do
283                         bundle_component_dir="$(echo "${component_dir}" | _sed -e 's|^'"${lib_prefix}"'|lib|')"
284                         if ! [ -e "${bundle_dir}/${bundle_component_dir}" ]
285                         then
286                                 real_component_dir="$(realpath "/${root_prefix}/${component_dir}")"
287
288                                 mkdir -p "${bundle_dir}/$(dirname "${bundle_component_dir}")"
289
290                                 _cp \
291                                         "${real_component_dir}" \
292                                         "${bundle_dir}/${bundle_component_dir}"
293
294                                 Multi::rewriteLoadersCache "${bundle_dir}/${bundle_component_dir}"
295                         fi
296                 done
297         fi
298 }
299
300 Multi::bundleLibFromFile () {
301         local exe_file="${1}"
302         local lib_file
303
304         Multi::printLdd "${exe_file}" \
305         | Multi::filterLib \
306         | while read lib_file
307         do
308                 if [ "${lib_file}" = 'not found' ]
309                 then
310                         printf 'ERROR: library not found while bundling %s (but link worked)\n' "${exe_file}" >&2
311                         Multi::printLdd "${exe_file}" | grep 'not found'
312                         exit 1
313                 fi
314                 lib_basename="$(basename "${lib_file}")"
315
316                 if [ -f "${lib_dir}/${lib_basename}" ]
317                 then
318                         continue
319                 fi
320
321                 _cp \
322                         "${lib_file}" \
323                         "${lib_dir}/${lib_basename}"
324
325                 Multi::bundleGtkDepsFromFile "${lib_file}"
326
327                 case "${system_name}" in
328                         'macos')
329                                 Multi::bundleLibFromFile "${lib_file}"
330                                 ;;
331                 esac
332         done
333 }
334
335 Multi::cleanUp () {
336         # Remove from bundle things that useless to be distributed,
337         # like headers or static libraries, also remove
338         # empty directories.
339         find "${bundle_dir}/lib" \
340                 -type f \
341                 -name '*.a' \
342                 -exec rm -f {} \;
343
344         find "${bundle_dir}/lib" \
345                 -type f \
346                 -name '*.h' \
347                 -exec rm -f {} \;
348
349         find "${bundle_dir}/lib" \
350                 -depth \
351                 -type d \
352                 -empty \
353                 -exec rmdir {} \;
354 }
355
356 Linux::getRpath () {
357         local exe_file="${1}"
358
359         local exe_dir="$(dirname "${exe_file}")"
360         local path_start="$(printf '%s' "${bundle_dir}" | wc -c)"
361         path_start="$((${path_start} + 1))"
362
363         local exe_subdir="$(echo "${exe_dir}" | cut -c "${path_start}-" | _sed -e 's|//*|/|;s|^/||')"
364
365         local rpath_origin='$ORIGIN'
366
367         if [ "${exe_subdir}" = '' ]
368         then
369                 printf '%s/lib\n' "${rpath_origin}"
370         else
371                 if [ "${exe_subdir}" = 'lib' ]
372                 then
373                         printf '%s\n' "${rpath_origin}"
374                 else
375                         local num_parent_dir="$(echo "${exe_subdir}" | tr '/' '\n' | wc -l)"
376                         local rpath_subdir
377                         local i=0
378                         while [ "${i}" -lt "${num_parent_dir}" ]
379                         do
380                                 rpath_subdir="${rpath_subdir}/.."
381                                 i="$((${i} + 1))"
382                         done
383                         printf '%s%s/lib\n' "${rpath_origin}" "${rpath_subdir}"
384                 fi
385         fi
386 }
387
388 Linux::patchExe () {
389         local exe_file="${1}"
390
391         local linux_rpath_string=$"$(Linux::getRpath "${exe_file}")"
392         chmod u+w,go-w "${exe_file}"
393         patchelf --set-rpath "${linux_rpath_string}" "${exe_file}"
394 }
395
396 Linux::patchLib () {
397         local lib_dir="${1}"
398         local exe_file
399
400         find "${lib_dir}" \
401                 -type f \
402                 -name '*.so*' \
403         | while read exe_file
404         do
405                 Linux::patchExe "${exe_file}"
406                 chmod ugo-x "${exe_file}"
407         done
408 }
409
410 Darwin::patchExe () {
411         local exe_file="${1}"
412
413         Multi::printLdd "${exe_file}" \
414         | Multi::filterLib \
415         | while read lib_file
416         do
417                 new_path="$(echo "${lib_file}" | _sed -e 's|^/.*/lib/|@executable_path/lib/|')"
418                 id_name="$(echo "${lib_file}" | _sed -e 's|.*/||g')"
419                 chmod u+w,go-w "${exe_file}"
420                 install_name_tool -change "${lib_file}" "${new_path}" "${exe_file}"
421                 install_name_tool -id "${id_name}" "${exe_file}"
422         done
423 }
424
425 Darwin::patchLib () {
426         local lib_dir="${1}"
427         local exe_file
428
429         find "${lib_dir}" \
430                 -type f \
431                 \( \
432                 -name '*.dylib' \
433                 -o -name '*.so' \
434                 \) \
435         | while read exe_file
436         do
437                 Darwin::patchExe "${exe_file}"
438                 chmod ugo-x "${exe_file}"
439         done
440 }
441
442 Windows::listLibForManifest () {
443         local lib_dir="${1}"
444
445         find "${lib_dir}" \
446                 -maxdepth 1 \
447                 -type f \
448                 -name '*.dll' \
449                 -exec basename {} \; \
450         | tr '\n' '\0' \
451         | xargs -0 -n1 -P1 -I{} \
452                 printf '  <file name="{}"/>\n'
453 }
454
455 Windows::writeManifest () {
456         local lib_dir="${1}"
457
458         cat > "${manifest_file}" <<-EOF
459         <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
460           <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
461         $(Windows::listLibForManifest "${lib_dir}")
462         </assembly>
463         EOF
464 }
465
466 system_name="${1}"; shift
467 bundle_dir="${1}"; shift
468
469 if ! [ -z "${1}" ]
470 then
471         exe_file="${1}"; shift
472 fi
473
474 bundle_dir="$(Common::getPath "${bundle_dir}")"
475 registry_dir="${bundle_dir}/registry"
476 lib_dir="${bundle_dir}/lib"
477
478 manifest_file="${lib_dir}/lib.manifest"
479
480 exe_action='Common::noOp'
481 lib_action='Common::noOp'
482
483 case "${system_name}" in
484         'register')
485                 mkdir -p "${registry_dir}"
486                 Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)"
487                 exit
488                 ;;
489         'linux'|'freebsd')
490                 exe_action='Linux::patchExe'
491                 lib_action='Linux::patchLib'
492                 ;;
493         'windows')
494                 lib_action='Windows::writeManifest'
495                 ;;
496         'macos')
497                 exe_action='Darwin::patchExe'
498                 lib_action='Darwin::patchLib'
499                 ;;
500         *)
501                 printf 'ERROR: unsupported system: %s\n' "${system_name}" >&2
502                 exit 1
503                 ;;
504 esac
505
506 mkdir -p "${lib_dir}"
507
508 if [ -d "${registry_dir}" ]
509 then
510         for registry_entry in "${registry_dir}"/*
511         do
512                 exe_file="$(cat "${registry_entry}")"
513
514                 Multi::bundleLibFromFile "${exe_file}"
515
516                 "${exe_action}" "${exe_file}"
517
518                 rm "${registry_entry}"
519
520                 "${exe_action}" "${exe_file}"
521         done
522
523         rmdir "${registry_dir}"
524 fi
525
526 "${lib_action}" "${lib_dir}"
527
528 Multi::cleanUp