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