]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - library-bundler
bundle: bundle macos binaries (no .app file done yet)
[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')
11                         gsed "${@}"
12                         ;;
13                 *)
14                         sed "${@}"
15                         ;;
16         esac
17 }
18
19 _cpr () {
20         case "${system_name}" in
21                 'macos')
22                         cp -R ${@}
23                         ;;
24                 *)
25                         cp -R --preserve=timestamps ${@}
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')
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                                 else
107                                         echo "${ldd_line}"
108                                 fi
109                         done
110                         ;;
111                 'windows')
112                         egrep -i '\.dll => [A-Z]:\\msys64\\'
113                         ;;
114                 'macos')
115                         egrep -v '^\t/System/|^\t/usr/lib/'
116                         ;;
117         esac
118 }
119
120 Multi::filterLib () {
121         Common::grepLdd \
122         | Multi::excludeLdd \
123         | Common::stripLdd \
124         | Common::getPath -
125 }
126
127 Multi::printLdd () {
128         local exe_file="${1}"
129
130         case "${system_name}" in
131                 'linux')
132                         ldd "${exe_file}"
133                         ;;
134                 'windows')
135                         ntldd --recursive "${exe_file}"
136                         ;;
137                 'macos')
138                         otool -L "${exe_file}"
139         esac
140 }
141
142 Multi::getGtkThemeName () {
143         case "${system_name}" in
144                 'linux')
145                         echo 'Adwaita'
146                         ;;
147                 'windows')
148                         echo 'MS-Windows'
149                         ;;
150                 *)
151                         echo 'Default'
152                         ;;
153         esac
154 }
155
156 Multi::getGtkLibName () {
157         case "${system_name}" in
158                 'linux')
159                         echo 'libgtk-x11-2.0.so.0'
160                         ;;
161                 'windows')
162                         echo 'libgtk-win32-2.0-0.dll'
163                         ;;
164                 'macos')
165                         echo 'libgtk-quartz-2.0.0.dylib'
166                         ;;
167         esac
168 }
169
170 Multi::getRootPrefix () {
171         local lib_file="${1}"
172
173         case "${system_name}" in
174                 'linux')
175                         echo "${lib_file}" \
176                         | cut -f2 -d'/'
177                         ;;
178                 'windows')
179                         basename "${lib_file}" \
180                         | xargs -n1 -P1 which \
181                         | cut -f2 -d'/'
182                         ;;
183                 'macos')
184                         echo 'usr/local'
185         esac
186 }
187
188 Multi::getLibPrefix () {
189         local lib_file="${1}"
190
191         case "${system_name}" in
192                 'linux')
193                         dirname "${lib_file}" \
194                         | cut -f3- -d'/'
195                         ;;
196                 'windows')
197                         echo 'lib'
198                         ;;
199                 'macos')
200                         echo 'lib'
201                         ;;
202         esac
203 }
204
205 Multi::getGtkDeps () {
206         local lib_prefix="${1}"
207         local gtk_theme_name="${2}"
208
209         case "${system_name}" in
210                 'linux'|'windows')
211                         cat <<-EOF
212                         share/themes/${gtk_theme_name}/gtk-2.0
213                         share/icons/hicolor
214                         ${lib_prefix}/gdk-pixbuf-2.0
215                         ${lib_prefix}/gtk-2.0
216                         EOF
217                         ;;
218                 'macos')
219                         cat <<-EOF
220                         share/icons/hicolor
221                         share/locale
222                         ${lib_prefix}/gdk-pixbuf-2.0
223                         ${lib_prefix}/gtk-2.0
224                         EOF
225                         ;;
226         esac
227
228         case "${system_name}" in
229                 'linux')
230                         cat <<-EOF
231                         ${lib_prefix}/libatk-bridge-2.0.so.0
232                         ${lib_prefix}/libcanberra-0.30
233                         ${lib_prefix}/libcanberra.so.0
234                         ${lib_prefix}/libcanberra-gtk.so.0
235                         EOF
236                         ;;
237         esac
238 }
239
240 Multi::rewriteLoadersCache () {
241         local bundle_component_path="${1}"
242         local cache_file
243
244         find "${bundle_component_path}" \
245                 -type f \
246                 \( \
247                 -name 'loaders.cache' \
248                 -o -name 'immodules.cache' \
249                 \) \
250         | while read cache_file
251         do
252                 _sed \
253                         -e 's|^"/[^"]*/lib/|"lib/|;s| "/[^"]*/share/| "share/|;/^# ModulesPath = /d;/^# Created by /d;/^#$/d' \
254                         -i "${cache_file}"
255         done
256 }
257
258 Multi::bundleGtkDepsFromFile () {
259         local lib_file="${1}"
260         local component_dir
261         local real_component_dir
262         local bundle_component_dir
263
264         lib_basename="$(basename "${lib_file}")"
265
266         gtk_lib_name="$(Multi::getGtkLibName)"
267         if [ "${lib_basename}" = "${gtk_lib_name}" ]
268         then
269                 root_prefix="$(Multi::getRootPrefix "${lib_file}")"
270                 lib_prefix="$(Multi::getLibPrefix "${lib_file}")"
271                 gtk_theme_name="$(Multi::getGtkThemeName)"
272
273                 for component_dir in $(Multi::getGtkDeps "${lib_prefix}" "${gtk_theme_name}")
274                 do
275                         bundle_component_dir="$(echo "${component_dir}" | _sed -e 's|^'"${lib_prefix}"'|lib|')"
276                         if ! [ -e "${bundle_dir}/${bundle_component_dir}" ]
277                         then
278                                 real_component_dir="$(realpath "/${root_prefix}/${component_dir}")"
279
280                                 mkdir -p "${bundle_dir}/$(dirname "${bundle_component_dir}")"
281
282                                 _cpr -H -L \
283                                         "${real_component_dir}" \
284                                         "${bundle_dir}/${bundle_component_dir}"
285
286                                 touch -r \
287                                         "/${real_component_dir}" \
288                                         "${bundle_dir}/${bundle_component_dir}"
289
290                                 Multi::rewriteLoadersCache "${bundle_dir}/${bundle_component_dir}"
291                         fi
292                 done
293         fi
294 }
295
296 Multi::bundleLibFromFile () {
297         local exe_file="${1}"
298         local lib_file
299
300         Multi::printLdd "${exe_file}" \
301         | Multi::filterLib \
302         | while read lib_file
303         do
304                 if [ "${lib_file}" = 'not found' ]
305                 then
306                         printf 'ERROR: library not found while bundling %s (but link worked)\n' "${exe_file}" >&2
307                         Multi::printLdd "${exe_file}" | grep 'not found'
308                         exit 1
309                 fi
310                 lib_basename="$(basename "${lib_file}")"
311
312                 if [ -f "${lib_dir}/${lib_basename}" ]
313                 then
314                         continue
315                 fi
316
317                 cp -H \
318                         "${lib_file}" \
319                         "${lib_dir}/${lib_basename}"
320
321                 touch -r \
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         find "${bundle_dir}/lib" \
337                 -type f \
338                 -name '*.a' \
339                 -exec rm -f {} \;
340
341         find "${bundle_dir}/lib" \
342                 -type f \
343                 -name '*.h' \
344                 -exec rm -f {} \;
345
346         find "${bundle_dir}/lib" \
347                 -depth \
348                 -type d \
349                 -exec rmdir {} \; \
350         || true
351 }
352
353 Linux::getRpath () {
354         local exe_file="${1}"
355
356         local exe_dir="$(dirname "${exe_file}")"
357         local path_start="$(printf '%s' "${bundle_dir}" | wc -c)"
358         path_start="$((${path_start} + 1))"
359
360         local exe_subdir="$(echo "${exe_dir}" | cut -c "${path_start}-" | _sed -e 's|//*|/|;s|^/||')"
361
362         local rpath_origin='$ORIGIN'
363
364         if [ "${exe_subdir}" = '' ]
365         then
366                 printf '%s/lib\n' "${rpath_origin}"
367         else
368                 if [ "${exe_subdir}" = 'lib' ]
369                 then
370                         printf '%s\n' "${rpath_origin}"
371                 else
372                         local num_parent_dir="$(echo "${exe_subdir}" | tr '/' '\n' | wc -l)"
373                         local rpath_subdir
374                         local i=0
375                         while [ "${i}" -lt "${num_parent_dir}" ]
376                         do
377                                 rpath_subdir="${rpath_subdir}/.."
378                                 i="$((${i} + 1))"
379                         done
380                         printf '%s%s/lib\n' "${rpath_origin}" "${rpath_subdir}"
381                 fi
382         fi
383 }
384
385 Linux::patchExe () {
386         local exe_file="${1}"
387
388         local linux_rpath_string=$"$(Linux::getRpath "${exe_file}")"
389         patchelf --set-rpath "${linux_rpath_string}" "${exe_file}"
390 }
391
392 Linux::patchLib () {
393         local lib_dir="${1}"
394         local exe_file
395
396         find "${lib_dir}" \
397                 -type f \
398                 -name '*.so*' \
399         | while read exe_file
400         do
401                 Linux::patchExe "${exe_file}"
402         done
403 }
404
405 Darwin::patchExe () {
406         local exe_file="${1}"
407
408         Multi::printLdd "${exe_file}" \
409         | Multi::filterLib \
410         | while read lib_file
411         do
412                 new_path="$(echo "${lib_file}" | _sed -e 's|^/.*/lib/|@executable_path/lib/|')"
413                 id_name="$(echo "${lib_file}" | _sed -e 's|.*/||g')"
414                 chmod u+w,go-w "${exe_file}"
415                 install_name_tool -change "${lib_file}" "${new_path}" "${exe_file}"
416                 install_name_tool -id "${id_name}" "${exe_file}"
417         done
418 }
419
420 Darwin::patchLib () {
421         local lib_dir="${1}"
422         local exe_file
423
424         find "${lib_dir}" \
425                 -type f \
426                 \( \
427                 -name '*.dylib' \
428                 -o -name '*.so' \
429                 \) \
430         | while read exe_file
431         do
432                 Darwin::patchExe "${exe_file}"
433                 chmod ugo-x "${exe_file}"
434         done
435 }
436
437 Windows::listLibForManifest () {
438         local lib_dir="${1}"
439
440         find "${lib_dir}" \
441                 -maxdepth 1 \
442                 -type f \
443                 -name '*.dll' \
444                 -exec basename {} \; \
445         | tr '\n' '\0' \
446         | xargs -0 -n1 -P1 -I{} \
447                 printf '  <file name="{}"/>\n'
448 }
449
450 Windows::writeManifest () {
451         local lib_dir="${1}"
452
453         cat > "${manifest_file}" <<-EOF
454         <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
455           <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
456         $(Windows::listLibForManifest "${lib_dir}")
457         </assembly>
458         EOF
459 }
460
461 system_name="${1}"; shift
462 bundle_dir="${1}"; shift
463
464 if ! [ -z "${1}" ]
465 then
466         exe_file="${1}"; shift
467 fi
468
469 bundle_dir="$(Common::getPath "${bundle_dir}")"
470 registry_dir="${bundle_dir}/registry"
471 lib_dir="${bundle_dir}/lib"
472
473 manifest_file="${lib_dir}/lib.manifest"
474
475 exe_action='Common::noOp'
476 lib_action='Common::noOp'
477
478 case "${system_name}" in
479         'register')
480                 mkdir -p "${registry_dir}"
481                 Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)"
482                 exit
483                 ;;
484         'linux')
485                 exe_action='Linux::patchExe'
486                 lib_action='Linux::patchLib'
487                 ;;
488         'windows')
489                 lib_action='Windows::writeManifest'
490                 ;;
491         'macos')
492                 exe_action='Darwin::patchExe'
493                 lib_action='Darwin::patchLib'
494                 ;;
495         *)
496                 printf 'ERROR: unsupported system: %s\n' "${system_name}" >&2
497                 exit 1
498                 ;;
499 esac
500
501 mkdir -p "${lib_dir}"
502
503 if [ -d "${registry_dir}" ]
504 then
505         for registry_entry in "${registry_dir}"/*
506         do
507                 exe_file="$(cat "${registry_entry}")"
508
509                 Multi::bundleLibFromFile "${exe_file}"
510
511                 "${exe_action}" "${exe_file}"
512
513                 rm "${registry_entry}"
514
515                 "${exe_action}" "${exe_file}"
516         done
517
518         rmdir "${registry_dir}"
519 fi
520
521 "${lib_action}" "${lib_dir}"
522
523 Multi::cleanUp