summary refs log tree commit diff
path: root/gnu/packages/ld-wrapper.in
blob: c9bb235bca5d5914c1a7fce7ca405185a05d2110 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!@BASH@
# -*- mode: scheme; coding: utf-8; -*-

# XXX: We have to go through Bash because there's no Guile command-line switch to
# ignore the various Guile load path related environment variables.

# Unset 'GUILE_LOAD_PATH' to make sure we do not stumble upon
# incompatible Guile files (see
# <https://lists.gnu.org/archive/html/guile-devel/2016-03/msg00000.html>).

unset GUILE_LOAD_PATH
unset GUILE_LOAD_COMPILED_PATH
unset GUILE_SYSTEM_PATH
unset GUILE_SYSTEM_COMPILED_PATH

# Use `load-compiled' because `load' (and `-l') doesn't otherwise load our
# .go file, or causes extraneous `stat' system calls when the "-C" "/" arguments
# are provided.
main="(@ (gnu build-support ld-wrapper) ld-wrapper)"
exec @GUILE@ -c "(load-compiled \"@SELF@.go\") ($main (command-line))" "$@"
!#
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2020 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Marius Bakke <mbakke@fastmail.com>
;;; Copyright © 2024 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu build-support ld-wrapper)
  #:use-module (srfi srfi-1)
  #:use-module (ice-9 match)
  #:autoload   (ice-9 rdelim) (read-delimited)
  #:export (ld-wrapper))

;;; Commentary:
;;;
;;; This is a wrapper for the linker.  Its purpose is to inspect the -L and
;;; -l switches passed to the linker, add corresponding -rpath arguments, and
;;; invoke the actual linker with this new set of arguments.
;;;
;;; The alternatives to this hack would be:
;;;
;;;   1. Using $LD_RUN_PATH.  However, that would tend to include more than
;;;      needed in the RPATH; for instance, given a package with `libfoo' as
;;;      an input, all its binaries would have libfoo in their RPATH,
;;;      regardless of whether they actually NEED it.
;;;
;;;   2. Use a GCC "lib" spec string such as `%{L*:-rpath %*}', which adds a
;;;      `-rpath LIBDIR' argument for each occurrence of `-L LIBDIR'.
;;;      However, this doesn't work when $LIBRARY_PATH is used, because the
;;;      additional `-L' switches are not matched by the above rule, because
;;;      the rule only matches explicit user-provided switches.  See
;;;      <http://gcc.gnu.org/ml/gcc-help/2012-09/msg00110.html> for details.
;;;
;;; As a bonus, this wrapper checks for "impurities"--i.e., references to
;;; libraries outside the store.
;;;
;;; Code:

(define %real-ld
  ;; Name of the linker that we wrap.
  "@LD@")

(define %store-directory
  ;; File name of the store.
  (or (getenv "NIX_STORE") "/gnu/store"))

(define %temporary-directory
  ;; Temporary directory.
  (or (getenv "TMPDIR") "/tmp"))

(define %build-directory
  ;; Top build directory when run from a builder.
  (getenv "NIX_BUILD_TOP"))

(define %allow-impurities?
  ;; Whether to allow references to libraries outside the store.
  ;; Allow them by default for convenience.
  (let ((value (getenv "GUIX_LD_WRAPPER_ALLOW_IMPURITIES")))
    (or (not value)
        (let ((value (string-downcase value)))
          (cond ((member value '("yes" "y" "t" "true" "1"))
                 #t)
                ((member value '("no" "n" "f" "false" "0"))
                 #f)
                (else
                 (format (current-error-port)
                         "ld-wrapper: ~s: invalid value for \
'GUIX_LD_WRAPPER_ALLOW_IMPURITIES'~%"
                         value)))))))

(define %debug?
  ;; Whether to emit debugging output.
  (getenv "GUIX_LD_WRAPPER_DEBUG"))

(define %disable-rpath?
  ;; Whether to disable automatic '-rpath' addition.
  (getenv "GUIX_LD_WRAPPER_DISABLE_RPATH"))

(define (readlink* file)
  ;; Call 'readlink' until the result is not a symlink.
  (define %max-symlink-depth 50)

  (let loop ((file  file)
             (depth 0))
    (define (absolute target)
      (if (absolute-file-name? target)
          target
          (string-append (dirname file) "/" target)))

    (if (>= depth %max-symlink-depth)
        file
        (call-with-values
            (lambda ()
              (catch 'system-error
                (lambda ()
                  (values #t (readlink file)))
                (lambda args
                  (let ((errno (system-error-errno args)))
                    (if (or (= errno EINVAL) (= errno ENOENT))
                        (values #f file)
                        (apply throw args))))))
          (lambda (success? target)
            (if success?
                (loop (absolute target) (+ depth 1))
                file))))))

(define (pure-file-name? file)
  ;; Return #t when FILE is the name of a file either within the store
  ;; (possibly via a symlink) or within the build directory.
  (let ((file (readlink* file)))
    (or (not (string-prefix? "/" file))
        (string-prefix? %store-directory file)
        (string-prefix? %temporary-directory file)
        (and %build-directory
             (string-prefix? %build-directory file)))))

(define (store-file-name? file)
  ;; Return #t when FILE is a store file, possibly indirectly.
  (string-prefix? %store-directory (readlink* file)))

(define (shared-library? file)
  ;; Return #t when FILE denotes a shared library.
  (or (string-suffix? ".so" file)
      (let ((index (string-contains file ".so.")))
        ;; Since we cannot use regexps during bootstrap, roll our own.
        (and index
             (string-every (char-set-union (char-set #\.) char-set:digit)
                           (string-drop file (+ index 3)))))))

(define (library-search-path args)
  ;; Return the library search path as a list of directory names.  The GNU ld
  ;; manual notes that "[a]ll `-L' options apply to all `-l' options,
  ;; regardless of the order in which the options appear", so we must compute
  ;; the search path independently of the -l options.
  (let loop ((args args)
             (path '()))
    (match args
      (()
       (reverse path))
      (("-L" directory . rest)
       (loop rest (cons directory path)))
      ((argument . rest)
       (if (string-prefix? "-L" argument)         ;augment the search path
           (loop rest
                 (cons (string-drop argument 2) path))
           (loop rest path))))))

(define (library-files-linked args library-path)
  ;; Return the absolute file names of shared libraries explicitly linked
  ;; against via `-l' or with an absolute file name in ARGS, looking them up
  ;; in LIBRARY-PATH.
  (define files+args
    (fold (lambda (argument result)
            (match result
              ((library-files ((and flag
                                    (or "-dynamic-linker" "-plugin"))
                               . rest))
               ;; When passed '-dynamic-linker ld.so', ignore 'ld.so'; when
               ;; passed '-plugin liblto_plugin.so', ignore
               ;; 'liblto_plugin.so'.  See <http://bugs.gnu.org/20102>.
               (list library-files
                     (cons* argument flag rest)))
              ((library-files previous-args)
               (cond ((string-prefix? "-l" argument) ;add library
                      (let* ((lib  (string-append "lib"
                                                  (string-drop argument 2)
                                                  ".so"))
                             (full (search-path library-path lib)))
                        (list (if full
                                  (cons full library-files)
                                  library-files)
                              (cons argument previous-args))))
                     ((and (string-prefix? %store-directory argument)
                           (shared-library? argument)) ;add library
                      (list (cons argument library-files)
                            (cons argument previous-args)))
                     (else
                      (list library-files
                            (cons argument previous-args)))))))
          (list '() '())
          args))

  (match files+args
    ((files arguments)
     (reverse files))))

(define (rpath-arguments library-files)
  ;; Return the `-rpath' argument list for each of LIBRARY-FILES, a list of
  ;; absolute file names.
  (fold-right (lambda (file args)
                ;; Add '-rpath' if and only if FILE is in the store; we don't
                ;; want to add '-rpath' for files under %BUILD-DIRECTORY or
                ;; %TEMPORARY-DIRECTORY because that could leak to installed
                ;; files.
                (cond ((and (not %disable-rpath?)
                            (store-file-name? file))
                       (cons* "-rpath" (dirname file) args))
                      ((or %allow-impurities?
                           (pure-file-name? file))
                       args)
                      (else
                       (begin
                         (format (current-error-port)
                                 "ld-wrapper: error: attempt to use \
library outside of ~a: ~s~%"
                                 %store-directory file)
                         (exit 1)))))
              '()
              library-files))

(define (expand-arguments args)
  ;; Expand ARGS such that "response file" arguments, such as "@args.txt", are
  ;; expanded (info "(gcc) Overall Options").
  (define (response-file-arguments file)
    (define (tokenize port)
      ;; Return a list of all strings found in PORT.  Quote characters are
      ;; removed, but whitespaces within quoted strings are preserved.
      (let loop ((tokens '()))
        (let* ((token+delimiter (read-delimited " '\"\n" port 'split))
               (token (car token+delimiter))
               (delim (cdr token+delimiter)))
          (if (eof-object? token)
              (reverse tokens)
              (case delim
                ((#\") (loop (cons (read-delimited "\"" port) tokens)))
                ((#\') (loop (cons (read-delimited "'" port) tokens)))
                (else (if (> (string-length token) 0)
                          (loop (cons token tokens))
                          (loop tokens))))))))

    (when %debug?
      (format (current-error-port)
              "ld-wrapper: attempting to read arguments from '~a'~%" file))

    (call-with-input-file file tokenize))

  (define result
    (fold-right (lambda (arg result)
                  (if (string-prefix? "@" arg)
                      (let ((file (string-drop arg 1)))
                        (append (catch 'system-error
                                  (lambda ()
                                    (response-file-arguments file))
                                  (lambda args
                                    ;; FILE doesn't exist or cannot be read so
                                    ;; leave ARG as is.
                                    (list arg)))
                                result))
                      (cons arg result)))
                '()
                args))

  ;; If there are "@" arguments in RESULT *and* we can expand them (they don't
  ;; refer to nonexistent files), then recurse.
  (if (equal? result args)
      result
      (expand-arguments result)))

(define (ld-wrapper args)
  ;; Invoke the real `ld' with ARGS, augmented with `-rpath' switches.
  (let* ((args (expand-arguments (cdr args))) ;strip arg0
         (path (library-search-path args))
         (libs (library-files-linked args path))
         (args (append args (rpath-arguments libs))))
    (when %debug?
      (format (current-error-port)
              "ld-wrapper: library search path: ~s~%" path)
      (format (current-error-port)
              "ld-wrapper: libraries linked: ~s~%" libs)
      (format (current-error-port)
              "ld-wrapper: invoking `~a' with ~s~%"
              %real-ld args)
      (force-output (current-error-port)))
    (apply execl %real-ld (basename %real-ld) args)))

;;; ld-wrapper.scm ends here