1# Copyright 2013-2014 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from glob import glob
16from pathlib import Path
17import argparse
18import errno
19import os
20import pickle
21import shlex
22import shutil
23import subprocess
24import sys
25import typing as T
26
27from . import environment
28from .backend.backends import InstallData
29from .coredata import major_versions_differ, MesonVersionMismatchException
30from .coredata import version as coredata_version
31from .mesonlib import Popen_safe, RealPathAction, is_windows
32from .scripts import depfixer, destdir_join
33from .scripts.meson_exe import run_exe
34try:
35    from __main__ import __file__ as main_file
36except ImportError:
37    # Happens when running as meson.exe which is native Windows.
38    # This is only used for pkexec which is not, so this is fine.
39    main_file = None
40
41if T.TYPE_CHECKING:
42    from .mesonlib import FileMode
43
44    try:
45        from typing import Protocol
46    except AttributeError:
47        from typing_extensions import Protocol  # type: ignore
48
49    class ArgumentType(Protocol):
50        """Typing information for the object returned by argparse."""
51        no_rebuild: bool
52        only_changed: bool
53        profile: bool
54        quiet: bool
55        wd: str
56        destdir: str
57        dry_run: bool
58        skip_subprojects: str
59
60
61symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file,
62but this will be changed in a future version of Meson to copy the symlink as is. Please update your
63build definitions so that it will not break when the change happens.'''
64
65selinux_updates: T.List[str] = []
66
67def add_arguments(parser: argparse.ArgumentParser) -> None:
68    parser.add_argument('-C', dest='wd', action=RealPathAction,
69                        help='directory to cd into before running')
70    parser.add_argument('--profile-self', action='store_true', dest='profile',
71                        help=argparse.SUPPRESS)
72    parser.add_argument('--no-rebuild', default=False, action='store_true',
73                        help='Do not rebuild before installing.')
74    parser.add_argument('--only-changed', default=False, action='store_true',
75                        help='Only overwrite files that are older than the copied file.')
76    parser.add_argument('--quiet', default=False, action='store_true',
77                        help='Do not print every file that was installed.')
78    parser.add_argument('--destdir', default=None,
79                        help='Sets or overrides DESTDIR environment. (Since 0.57.0)')
80    parser.add_argument('--dry-run', '-n', action='store_true',
81                        help='Doesn\'t actually install, but print logs. (Since 0.57.0)')
82    parser.add_argument('--skip-subprojects', nargs='?', const='*', default='',
83                        help='Do not install files from given subprojects. (Since 0.58.0)')
84
85class DirMaker:
86    def __init__(self, lf: T.TextIO, makedirs: T.Callable[..., None]):
87        self.lf = lf
88        self.dirs: T.List[str] = []
89        self.makedirs_impl = makedirs
90
91    def makedirs(self, path: str, exist_ok: bool = False) -> None:
92        dirname = os.path.normpath(path)
93        dirs = []
94        while dirname != os.path.dirname(dirname):
95            if dirname in self.dirs:
96                # In dry-run mode the directory does not exist but we would have
97                # created it with all its parents otherwise.
98                break
99            if not os.path.exists(dirname):
100                dirs.append(dirname)
101            dirname = os.path.dirname(dirname)
102        self.makedirs_impl(path, exist_ok=exist_ok)
103
104        # store the directories in creation order, with the parent directory
105        # before the child directories. Future calls of makedir() will not
106        # create the parent directories, so the last element in the list is
107        # the last one to be created. That is the first one to be removed on
108        # __exit__
109        dirs.reverse()
110        self.dirs += dirs
111
112    def __enter__(self) -> 'DirMaker':
113        return self
114
115    def __exit__(self, exception_type: T.Type[Exception], value: T.Any, traceback: T.Any) -> None:
116        self.dirs.reverse()
117        for d in self.dirs:
118            append_to_log(self.lf, d)
119
120
121def is_executable(path: str, follow_symlinks: bool = False) -> bool:
122    '''Checks whether any of the "x" bits are set in the source file mode.'''
123    return bool(os.stat(path, follow_symlinks=follow_symlinks).st_mode & 0o111)
124
125
126def append_to_log(lf: T.TextIO, line: str) -> None:
127    lf.write(line)
128    if not line.endswith('\n'):
129        lf.write('\n')
130    lf.flush()
131
132
133def set_chown(path: str, user: T.Union[str, int, None] = None,
134              group: T.Union[str, int, None] = None,
135              dir_fd: T.Optional[int] = None, follow_symlinks: bool = True) -> None:
136    # shutil.chown will call os.chown without passing all the parameters
137    # and particularly follow_symlinks, thus we replace it temporary
138    # with a lambda with all the parameters so that follow_symlinks will
139    # be actually passed properly.
140    # Not nice, but better than actually rewriting shutil.chown until
141    # this python bug is fixed: https://bugs.python.org/issue18108
142    real_os_chown = os.chown
143
144    def chown(path: T.Union[int, str, 'os.PathLike[str]', bytes, 'os.PathLike[bytes]'],
145              uid: int, gid: int, *, dir_fd: T.Optional[int] = dir_fd,
146              follow_symlinks: bool = follow_symlinks) -> None:
147        """Override the default behavior of os.chown
148
149        Use a real function rather than a lambda to help mypy out. Also real
150        functions are faster.
151        """
152        real_os_chown(path, uid, gid, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
153
154    try:
155        os.chown = chown
156        shutil.chown(path, user, group)
157    finally:
158        os.chown = real_os_chown
159
160
161def set_chmod(path: str, mode: int, dir_fd: T.Optional[int] = None,
162              follow_symlinks: bool = True) -> None:
163    try:
164        os.chmod(path, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
165    except (NotImplementedError, OSError, SystemError):
166        if not os.path.islink(path):
167            os.chmod(path, mode, dir_fd=dir_fd)
168
169
170def sanitize_permissions(path: str, umask: T.Union[str, int]) -> None:
171    # TODO: with python 3.8 or typing_extensions we could replace this with
172    # `umask: T.Union[T.Literal['preserve'], int]`, which would be mroe correct
173    if umask == 'preserve':
174        return
175    assert isinstance(umask, int), 'umask should only be "preserver" or an integer'
176    new_perms = 0o777 if is_executable(path, follow_symlinks=False) else 0o666
177    new_perms &= ~umask
178    try:
179        set_chmod(path, new_perms, follow_symlinks=False)
180    except PermissionError as e:
181        msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
182        print(msg.format(path, new_perms, e.strerror))
183
184
185def set_mode(path: str, mode: T.Optional['FileMode'], default_umask: T.Union[str, int]) -> None:
186    if mode is None or all(m is None for m in [mode.perms_s, mode.owner, mode.group]):
187        # Just sanitize permissions with the default umask
188        sanitize_permissions(path, default_umask)
189        return
190    # No chown() on Windows, and must set one of owner/group
191    if not is_windows() and (mode.owner is not None or mode.group is not None):
192        try:
193            set_chown(path, mode.owner, mode.group, follow_symlinks=False)
194        except PermissionError as e:
195            msg = '{!r}: Unable to set owner {!r} and group {!r}: {}, ignoring...'
196            print(msg.format(path, mode.owner, mode.group, e.strerror))
197        except LookupError:
198            msg = '{!r}: Non-existent owner {!r} or group {!r}: ignoring...'
199            print(msg.format(path, mode.owner, mode.group))
200        except OSError as e:
201            if e.errno == errno.EINVAL:
202                msg = '{!r}: Non-existent numeric owner {!r} or group {!r}: ignoring...'
203                print(msg.format(path, mode.owner, mode.group))
204            else:
205                raise
206    # Must set permissions *after* setting owner/group otherwise the
207    # setuid/setgid bits will get wiped by chmod
208    # NOTE: On Windows you can set read/write perms; the rest are ignored
209    if mode.perms_s is not None:
210        try:
211            set_chmod(path, mode.perms, follow_symlinks=False)
212        except PermissionError as e:
213            msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
214            print(msg.format(path, mode.perms_s, e.strerror))
215    else:
216        sanitize_permissions(path, default_umask)
217
218
219def restore_selinux_contexts() -> None:
220    '''
221    Restores the SELinux context for files in @selinux_updates
222
223    If $DESTDIR is set, do not warn if the call fails.
224    '''
225    try:
226        subprocess.check_call(['selinuxenabled'])
227    except (FileNotFoundError, NotADirectoryError, PermissionError, subprocess.CalledProcessError):
228        # If we don't have selinux or selinuxenabled returned 1, failure
229        # is ignored quietly.
230        return
231
232    if not shutil.which('restorecon'):
233        # If we don't have restorecon, failure is ignored quietly.
234        return
235
236    if not selinux_updates:
237        # If the list of files is empty, do not try to call restorecon.
238        return
239
240    with subprocess.Popen(['restorecon', '-F', '-f-', '-0'],
241                          stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
242        out, err = proc.communicate(input=b'\0'.join(os.fsencode(f) for f in selinux_updates) + b'\0')
243        if proc.returncode != 0 and not os.environ.get('DESTDIR'):
244            print('Failed to restore SELinux context of installed files...',
245                  'Standard output:', out.decode(),
246                  'Standard error:', err.decode(), sep='\n')
247
248
249def get_destdir_path(destdir: str, fullprefix: str, path: str) -> str:
250    if os.path.isabs(path):
251        output = destdir_join(destdir, path)
252    else:
253        output = os.path.join(fullprefix, path)
254    return output
255
256
257def check_for_stampfile(fname: str) -> str:
258    '''Some languages e.g. Rust have output files
259    whose names are not known at configure time.
260    Check if this is the case and return the real
261    file instead.'''
262    if fname.endswith('.so') or fname.endswith('.dll'):
263        if os.stat(fname).st_size == 0:
264            (base, suffix) = os.path.splitext(fname)
265            files = glob(base + '-*' + suffix)
266            if len(files) > 1:
267                print("Stale dynamic library files in build dir. Can't install.")
268                sys.exit(1)
269            if len(files) == 1:
270                return files[0]
271    elif fname.endswith('.a') or fname.endswith('.lib'):
272        if os.stat(fname).st_size == 0:
273            (base, suffix) = os.path.splitext(fname)
274            files = glob(base + '-*' + '.rlib')
275            if len(files) > 1:
276                print("Stale static library files in build dir. Can't install.")
277                sys.exit(1)
278            if len(files) == 1:
279                return files[0]
280    return fname
281
282
283class Installer:
284
285    def __init__(self, options: 'ArgumentType', lf: T.TextIO):
286        self.did_install_something = False
287        self.options = options
288        self.lf = lf
289        self.preserved_file_count = 0
290        self.dry_run = options.dry_run
291        # [''] means skip none,
292        # ['*'] means skip all,
293        # ['sub1', ...] means skip only those.
294        self.skip_subprojects = [i.strip() for i in options.skip_subprojects.split(',')]
295
296    def remove(self, *args: T.Any, **kwargs: T.Any) -> None:
297        if not self.dry_run:
298            os.remove(*args, **kwargs)
299
300    def symlink(self, *args: T.Any, **kwargs: T.Any) -> None:
301        if not self.dry_run:
302            os.symlink(*args, **kwargs)
303
304    def makedirs(self, *args: T.Any, **kwargs: T.Any) -> None:
305        if not self.dry_run:
306            os.makedirs(*args, **kwargs)
307
308    def copy(self, *args: T.Any, **kwargs: T.Any) -> None:
309        if not self.dry_run:
310            shutil.copy(*args, **kwargs)
311
312    def copy2(self, *args: T.Any, **kwargs: T.Any) -> None:
313        if not self.dry_run:
314            shutil.copy2(*args, **kwargs)
315
316    def copyfile(self, *args: T.Any, **kwargs: T.Any) -> None:
317        if not self.dry_run:
318            shutil.copyfile(*args, **kwargs)
319
320    def copystat(self, *args: T.Any, **kwargs: T.Any) -> None:
321        if not self.dry_run:
322            shutil.copystat(*args, **kwargs)
323
324    def fix_rpath(self, *args: T.Any, **kwargs: T.Any) -> None:
325        if not self.dry_run:
326            depfixer.fix_rpath(*args, **kwargs)
327
328    def set_chown(self, *args: T.Any, **kwargs: T.Any) -> None:
329        if not self.dry_run:
330            set_chown(*args, **kwargs)
331
332    def set_chmod(self, *args: T.Any, **kwargs: T.Any) -> None:
333        if not self.dry_run:
334            set_chmod(*args, **kwargs)
335
336    def sanitize_permissions(self, *args: T.Any, **kwargs: T.Any) -> None:
337        if not self.dry_run:
338            sanitize_permissions(*args, **kwargs)
339
340    def set_mode(self, *args: T.Any, **kwargs: T.Any) -> None:
341        if not self.dry_run:
342            set_mode(*args, **kwargs)
343
344    def restore_selinux_contexts(self) -> None:
345        if not self.dry_run:
346            restore_selinux_contexts()
347
348    def Popen_safe(self, *args: T.Any, **kwargs: T.Any) -> T.Tuple[int, str, str]:
349        if not self.dry_run:
350            p, o, e = Popen_safe(*args, **kwargs)
351            return p.returncode, o, e
352        return 0, '', ''
353
354    def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int:
355        if not self.dry_run:
356            return run_exe(*args, **kwargs)
357        return 0
358
359    def install_subproject(self, subproject: str) -> bool:
360        if subproject and (subproject in self.skip_subprojects or '*' in self.skip_subprojects):
361            return False
362        return True
363
364    def log(self, msg: str) -> None:
365        if not self.options.quiet:
366            print(msg)
367
368    def should_preserve_existing_file(self, from_file: str, to_file: str) -> bool:
369        if not self.options.only_changed:
370            return False
371        # Always replace danging symlinks
372        if os.path.islink(from_file) and not os.path.isfile(from_file):
373            return False
374        from_time = os.stat(from_file).st_mtime
375        to_time = os.stat(to_file).st_mtime
376        return from_time <= to_time
377
378    def do_copyfile(self, from_file: str, to_file: str,
379                    makedirs: T.Optional[T.Tuple[T.Any, str]] = None) -> bool:
380        outdir = os.path.split(to_file)[0]
381        if not os.path.isfile(from_file) and not os.path.islink(from_file):
382            raise RuntimeError('Tried to install something that isn\'t a file:'
383                               '{!r}'.format(from_file))
384        # copyfile fails if the target file already exists, so remove it to
385        # allow overwriting a previous install. If the target is not a file, we
386        # want to give a readable error.
387        if os.path.exists(to_file):
388            if not os.path.isfile(to_file):
389                raise RuntimeError('Destination {!r} already exists and is not '
390                                   'a file'.format(to_file))
391            if self.should_preserve_existing_file(from_file, to_file):
392                append_to_log(self.lf, f'# Preserving old file {to_file}\n')
393                self.preserved_file_count += 1
394                return False
395            self.remove(to_file)
396        elif makedirs:
397            # Unpack tuple
398            dirmaker, outdir = makedirs
399            # Create dirs if needed
400            dirmaker.makedirs(outdir, exist_ok=True)
401        self.log(f'Installing {from_file} to {outdir}')
402        if os.path.islink(from_file):
403            if not os.path.exists(from_file):
404                # Dangling symlink. Replicate as is.
405                self.copy(from_file, outdir, follow_symlinks=False)
406            else:
407                # Remove this entire branch when changing the behaviour to duplicate
408                # symlinks rather than copying what they point to.
409                print(symlink_warning)
410                self.copy2(from_file, to_file)
411        else:
412            self.copy2(from_file, to_file)
413        selinux_updates.append(to_file)
414        append_to_log(self.lf, to_file)
415        return True
416
417    def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str,
418                   exclude: T.Optional[T.Tuple[T.Set[str], T.Set[str]]],
419                   install_mode: 'FileMode', dm: DirMaker) -> None:
420        '''
421        Copies the contents of directory @src_dir into @dst_dir.
422
423        For directory
424            /foo/
425              bar/
426                excluded
427                foobar
428              file
429        do_copydir(..., '/foo', '/dst/dir', {'bar/excluded'}) creates
430            /dst/
431              dir/
432                bar/
433                  foobar
434                file
435
436        Args:
437            src_dir: str, absolute path to the source directory
438            dst_dir: str, absolute path to the destination directory
439            exclude: (set(str), set(str)), tuple of (exclude_files, exclude_dirs),
440                     each element of the set is a path relative to src_dir.
441        '''
442        if not os.path.isabs(src_dir):
443            raise ValueError(f'src_dir must be absolute, got {src_dir}')
444        if not os.path.isabs(dst_dir):
445            raise ValueError(f'dst_dir must be absolute, got {dst_dir}')
446        if exclude is not None:
447            exclude_files, exclude_dirs = exclude
448        else:
449            exclude_files = exclude_dirs = set()
450        for root, dirs, files in os.walk(src_dir):
451            assert os.path.isabs(root)
452            for d in dirs[:]:
453                abs_src = os.path.join(root, d)
454                filepart = os.path.relpath(abs_src, start=src_dir)
455                abs_dst = os.path.join(dst_dir, filepart)
456                # Remove these so they aren't visited by os.walk at all.
457                if filepart in exclude_dirs:
458                    dirs.remove(d)
459                    continue
460                if os.path.isdir(abs_dst):
461                    continue
462                if os.path.exists(abs_dst):
463                    print(f'Tried to copy directory {abs_dst} but a file of that name already exists.')
464                    sys.exit(1)
465                dm.makedirs(abs_dst)
466                self.copystat(abs_src, abs_dst)
467                self.sanitize_permissions(abs_dst, data.install_umask)
468            for f in files:
469                abs_src = os.path.join(root, f)
470                filepart = os.path.relpath(abs_src, start=src_dir)
471                if filepart in exclude_files:
472                    continue
473                abs_dst = os.path.join(dst_dir, filepart)
474                if os.path.isdir(abs_dst):
475                    print(f'Tried to copy file {abs_dst} but a directory of that name already exists.')
476                    sys.exit(1)
477                parent_dir = os.path.dirname(abs_dst)
478                if not os.path.isdir(parent_dir):
479                    dm.makedirs(parent_dir)
480                    self.copystat(os.path.dirname(abs_src), parent_dir)
481                # FIXME: what about symlinks?
482                self.do_copyfile(abs_src, abs_dst)
483                self.set_mode(abs_dst, install_mode, data.install_umask)
484
485    @staticmethod
486    def check_installdata(obj: InstallData) -> InstallData:
487        if not isinstance(obj, InstallData) or not hasattr(obj, 'version'):
488            raise MesonVersionMismatchException('<unknown>', coredata_version)
489        if major_versions_differ(obj.version, coredata_version):
490            raise MesonVersionMismatchException(obj.version, coredata_version)
491        return obj
492
493    def do_install(self, datafilename: str) -> None:
494        with open(datafilename, 'rb') as ifile:
495            d = self.check_installdata(pickle.load(ifile))
496
497        # Override in the env because some scripts could be relying on it.
498        if self.options.destdir is not None:
499            os.environ['DESTDIR'] = self.options.destdir
500
501        destdir = os.environ.get('DESTDIR', '')
502        fullprefix = destdir_join(destdir, d.prefix)
503
504        if d.install_umask != 'preserve':
505            assert isinstance(d.install_umask, int)
506            os.umask(d.install_umask)
507
508        self.did_install_something = False
509        try:
510            with DirMaker(self.lf, self.makedirs) as dm:
511                self.install_subdirs(d, dm, destdir, fullprefix) # Must be first, because it needs to delete the old subtree.
512                self.install_targets(d, dm, destdir, fullprefix)
513                self.install_headers(d, dm, destdir, fullprefix)
514                self.install_man(d, dm, destdir, fullprefix)
515                self.install_data(d, dm, destdir, fullprefix)
516                self.restore_selinux_contexts()
517                self.run_install_script(d, destdir, fullprefix)
518                if not self.did_install_something:
519                    self.log('Nothing to install.')
520                if not self.options.quiet and self.preserved_file_count > 0:
521                    self.log('Preserved {} unchanged files, see {} for the full list'
522                             .format(self.preserved_file_count, os.path.normpath(self.lf.name)))
523        except PermissionError:
524            if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ and destdir == '':
525                print('Installation failed due to insufficient permissions.')
526                print('Attempting to use polkit to gain elevated privileges...')
527                os.execlp('pkexec', 'pkexec', sys.executable, main_file, *sys.argv[1:],
528                          '-C', os.getcwd())
529            else:
530                raise
531
532    def install_subdirs(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
533        for i in d.install_subdirs:
534            if not self.install_subproject(i.subproject):
535                continue
536            self.did_install_something = True
537            full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path)
538            self.log(f'Installing subdir {i.path} to {full_dst_dir}')
539            dm.makedirs(full_dst_dir, exist_ok=True)
540            self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm)
541
542    def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
543        for i in d.data:
544            if not self.install_subproject(i.subproject):
545                continue
546            fullfilename = i.path
547            outfilename = get_destdir_path(destdir, fullprefix, i.install_path)
548            outdir = os.path.dirname(outfilename)
549            if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
550                self.did_install_something = True
551            self.set_mode(outfilename, i.install_mode, d.install_umask)
552
553    def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
554        for m in d.man:
555            if not self.install_subproject(m.subproject):
556                continue
557            full_source_filename = m.path
558            outfilename = get_destdir_path(destdir, fullprefix, m.install_path)
559            outdir = os.path.dirname(outfilename)
560            if self.do_copyfile(full_source_filename, outfilename, makedirs=(dm, outdir)):
561                self.did_install_something = True
562            self.set_mode(outfilename, m.install_mode, d.install_umask)
563
564    def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
565        for t in d.headers:
566            if not self.install_subproject(t.subproject):
567                continue
568            fullfilename = t.path
569            fname = os.path.basename(fullfilename)
570            outdir = get_destdir_path(destdir, fullprefix, t.install_path)
571            outfilename = os.path.join(outdir, fname)
572            if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
573                self.did_install_something = True
574            self.set_mode(outfilename, t.install_mode, d.install_umask)
575
576    def run_install_script(self, d: InstallData, destdir: str, fullprefix: str) -> None:
577        env = {'MESON_SOURCE_ROOT': d.source_dir,
578               'MESON_BUILD_ROOT': d.build_dir,
579               'MESON_INSTALL_PREFIX': d.prefix,
580               'MESON_INSTALL_DESTDIR_PREFIX': fullprefix,
581               'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in d.mesonintrospect]),
582               }
583        if self.options.quiet:
584            env['MESON_INSTALL_QUIET'] = '1'
585
586        for i in d.install_scripts:
587            if not self.install_subproject(i.subproject):
588                continue
589            name = ' '.join(i.cmd_args)
590            if i.skip_if_destdir and destdir:
591                self.log(f'Skipping custom install script because DESTDIR is set {name!r}')
592                continue
593            self.did_install_something = True  # Custom script must report itself if it does nothing.
594            self.log(f'Running custom install script {name!r}')
595            try:
596                rc = self.run_exe(i, env)
597            except OSError:
598                print(f'FAILED: install script \'{name}\' could not be run, stopped')
599                # POSIX shells return 127 when a command could not be found
600                sys.exit(127)
601            if rc != 0:
602                print(f'FAILED: install script \'{name}\' exit code {rc}, stopped')
603                sys.exit(rc)
604
605    def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
606        for t in d.targets:
607            if not self.install_subproject(t.subproject):
608                continue
609            if not os.path.exists(t.fname):
610                # For example, import libraries of shared modules are optional
611                if t.optional:
612                    self.log(f'File {t.fname!r} not found, skipping')
613                    continue
614                else:
615                    raise RuntimeError(f'File {t.fname!r} could not be found')
616            file_copied = False # not set when a directory is copied
617            fname = check_for_stampfile(t.fname)
618            outdir = get_destdir_path(destdir, fullprefix, t.outdir)
619            outname = os.path.join(outdir, os.path.basename(fname))
620            final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname))
621            aliases = t.aliases
622            should_strip = t.strip
623            install_rpath = t.install_rpath
624            install_name_mappings = t.install_name_mappings
625            install_mode = t.install_mode
626            if not os.path.exists(fname):
627                raise RuntimeError(f'File {fname!r} could not be found')
628            elif os.path.isfile(fname):
629                file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir))
630                self.set_mode(outname, install_mode, d.install_umask)
631                if should_strip and d.strip_bin is not None:
632                    if fname.endswith('.jar'):
633                        self.log('Not stripping jar target: {}'.format(os.path.basename(fname)))
634                        continue
635                    self.log('Stripping target {!r} using {}.'.format(fname, d.strip_bin[0]))
636                    returncode, stdo, stde = self.Popen_safe(d.strip_bin + [outname])
637                    if returncode != 0:
638                        print('Could not strip file.\n')
639                        print(f'Stdout:\n{stdo}\n')
640                        print(f'Stderr:\n{stde}\n')
641                        sys.exit(1)
642                if fname.endswith('.js'):
643                    # Emscripten outputs js files and optionally a wasm file.
644                    # If one was generated, install it as well.
645                    wasm_source = os.path.splitext(fname)[0] + '.wasm'
646                    if os.path.exists(wasm_source):
647                        wasm_output = os.path.splitext(outname)[0] + '.wasm'
648                        file_copied = self.do_copyfile(wasm_source, wasm_output)
649            elif os.path.isdir(fname):
650                fname = os.path.join(d.build_dir, fname.rstrip('/'))
651                outname = os.path.join(outdir, os.path.basename(fname))
652                dm.makedirs(outdir, exist_ok=True)
653                self.do_copydir(d, fname, outname, None, install_mode, dm)
654            else:
655                raise RuntimeError(f'Unknown file type for {fname!r}')
656            printed_symlink_error = False
657            for alias, to in aliases.items():
658                try:
659                    symlinkfilename = os.path.join(outdir, alias)
660                    try:
661                        self.remove(symlinkfilename)
662                    except FileNotFoundError:
663                        pass
664                    self.symlink(to, symlinkfilename)
665                    append_to_log(self.lf, symlinkfilename)
666                except (NotImplementedError, OSError):
667                    if not printed_symlink_error:
668                        print("Symlink creation does not work on this platform. "
669                              "Skipping all symlinking.")
670                        printed_symlink_error = True
671            if file_copied:
672                self.did_install_something = True
673                try:
674                    self.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path,
675                                         install_name_mappings, verbose=False)
676                except SystemExit as e:
677                    if isinstance(e.code, int) and e.code == 0:
678                        pass
679                    else:
680                        raise
681
682
683def rebuild_all(wd: str) -> bool:
684    if not (Path(wd) / 'build.ninja').is_file():
685        print('Only ninja backend is supported to rebuild the project before installation.')
686        return True
687
688    ninja = environment.detect_ninja()
689    if not ninja:
690        print("Can't find ninja, can't rebuild test.")
691        return False
692
693    ret = subprocess.run(ninja + ['-C', wd]).returncode
694    if ret != 0:
695        print(f'Could not rebuild {wd}')
696        return False
697
698    return True
699
700
701def run(opts: 'ArgumentType') -> int:
702    datafilename = 'meson-private/install.dat'
703    private_dir = os.path.dirname(datafilename)
704    log_dir = os.path.join(private_dir, '../meson-logs')
705    if not os.path.exists(os.path.join(opts.wd, datafilename)):
706        sys.exit('Install data not found. Run this command in build directory root.')
707    if not opts.no_rebuild:
708        if not rebuild_all(opts.wd):
709            sys.exit(-1)
710    os.chdir(opts.wd)
711    with open(os.path.join(log_dir, 'install-log.txt'), 'w', encoding='utf-8') as lf:
712        installer = Installer(opts, lf)
713        append_to_log(lf, '# List of files installed by Meson')
714        append_to_log(lf, '# Does not contain files installed by custom scripts.')
715        if opts.profile:
716            import cProfile as profile
717            fname = os.path.join(private_dir, 'profile-installer.log')
718            profile.runctx('installer.do_install(datafilename)', globals(), locals(), filename=fname)
719        else:
720            installer.do_install(datafilename)
721    return 0
722