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 15import sys, pickle, os, shutil, subprocess, errno 16import argparse 17import shlex 18from glob import glob 19from .scripts import depfixer 20from .scripts import destdir_join 21from .mesonlib import is_windows, Popen_safe 22from .mtest import rebuild_all 23try: 24 from __main__ import __file__ as main_file 25except ImportError: 26 # Happens when running as meson.exe which is native Windows. 27 # This is only used for pkexec which is not, so this is fine. 28 main_file = None 29 30symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, 31but this will be changed in a future version of Meson to copy the symlink as is. Please update your 32build definitions so that it will not break when the change happens.''' 33 34selinux_updates = [] 35 36def add_arguments(parser): 37 parser.add_argument('-C', default='.', dest='wd', 38 help='directory to cd into before running') 39 parser.add_argument('--profile-self', action='store_true', dest='profile', 40 help=argparse.SUPPRESS) 41 parser.add_argument('--no-rebuild', default=False, action='store_true', 42 help='Do not rebuild before installing.') 43 parser.add_argument('--only-changed', default=False, action='store_true', 44 help='Only overwrite files that are older than the copied file.') 45 parser.add_argument('--quiet', default=False, action='store_true', 46 help='Do not print every file that was installed.') 47 48class DirMaker: 49 def __init__(self, lf): 50 self.lf = lf 51 self.dirs = [] 52 53 def makedirs(self, path, exist_ok=False): 54 dirname = os.path.normpath(path) 55 dirs = [] 56 while dirname != os.path.dirname(dirname): 57 if not os.path.exists(dirname): 58 dirs.append(dirname) 59 dirname = os.path.dirname(dirname) 60 os.makedirs(path, exist_ok=exist_ok) 61 62 # store the directories in creation order, with the parent directory 63 # before the child directories. Future calls of makedir() will not 64 # create the parent directories, so the last element in the list is 65 # the last one to be created. That is the first one to be removed on 66 # __exit__ 67 dirs.reverse() 68 self.dirs += dirs 69 70 def __enter__(self): 71 return self 72 73 def __exit__(self, exception_type, value, traceback): 74 self.dirs.reverse() 75 for d in self.dirs: 76 append_to_log(self.lf, d) 77 78def is_executable(path, follow_symlinks=False): 79 '''Checks whether any of the "x" bits are set in the source file mode.''' 80 return bool(os.stat(path, follow_symlinks=follow_symlinks).st_mode & 0o111) 81 82def append_to_log(lf, line): 83 lf.write(line) 84 if not line.endswith('\n'): 85 lf.write('\n') 86 lf.flush() 87 88def set_chown(path, user=None, group=None, dir_fd=None, follow_symlinks=True): 89 # shutil.chown will call os.chown without passing all the parameters 90 # and particularly follow_symlinks, thus we replace it temporary 91 # with a lambda with all the parameters so that follow_symlinks will 92 # be actually passed properly. 93 # Not nice, but better than actually rewriting shutil.chown until 94 # this python bug is fixed: https://bugs.python.org/issue18108 95 real_os_chown = os.chown 96 try: 97 os.chown = lambda p, u, g: real_os_chown(p, u, g, 98 dir_fd=dir_fd, 99 follow_symlinks=follow_symlinks) 100 shutil.chown(path, user, group) 101 except Exception: 102 raise 103 finally: 104 os.chown = real_os_chown 105 106def set_chmod(path, mode, dir_fd=None, follow_symlinks=True): 107 try: 108 os.chmod(path, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks) 109 except (NotImplementedError, OSError, SystemError): 110 if not os.path.islink(path): 111 os.chmod(path, mode, dir_fd=dir_fd) 112 113def sanitize_permissions(path, umask): 114 if umask == 'preserve': 115 return 116 new_perms = 0o777 if is_executable(path, follow_symlinks=False) else 0o666 117 new_perms &= ~umask 118 try: 119 set_chmod(path, new_perms, follow_symlinks=False) 120 except PermissionError as e: 121 msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...' 122 print(msg.format(path, new_perms, e.strerror)) 123 124def set_mode(path, mode, default_umask): 125 if mode is None or (mode.perms_s or mode.owner or mode.group) is None: 126 # Just sanitize permissions with the default umask 127 sanitize_permissions(path, default_umask) 128 return 129 # No chown() on Windows, and must set one of owner/group 130 if not is_windows() and (mode.owner or mode.group) is not None: 131 try: 132 set_chown(path, mode.owner, mode.group, follow_symlinks=False) 133 except PermissionError as e: 134 msg = '{!r}: Unable to set owner {!r} and group {!r}: {}, ignoring...' 135 print(msg.format(path, mode.owner, mode.group, e.strerror)) 136 except LookupError: 137 msg = '{!r}: Non-existent owner {!r} or group {!r}: ignoring...' 138 print(msg.format(path, mode.owner, mode.group)) 139 except OSError as e: 140 if e.errno == errno.EINVAL: 141 msg = '{!r}: Non-existent numeric owner {!r} or group {!r}: ignoring...' 142 print(msg.format(path, mode.owner, mode.group)) 143 else: 144 raise 145 # Must set permissions *after* setting owner/group otherwise the 146 # setuid/setgid bits will get wiped by chmod 147 # NOTE: On Windows you can set read/write perms; the rest are ignored 148 if mode.perms_s is not None: 149 try: 150 set_chmod(path, mode.perms, follow_symlinks=False) 151 except PermissionError as e: 152 msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...' 153 print(msg.format(path, mode.perms_s, e.strerror)) 154 else: 155 sanitize_permissions(path, default_umask) 156 157def restore_selinux_contexts(): 158 ''' 159 Restores the SELinux context for files in @selinux_updates 160 161 If $DESTDIR is set, do not warn if the call fails. 162 ''' 163 try: 164 subprocess.check_call(['selinuxenabled']) 165 except (FileNotFoundError, NotADirectoryError, PermissionError, subprocess.CalledProcessError): 166 # If we don't have selinux or selinuxenabled returned 1, failure 167 # is ignored quietly. 168 return 169 170 if not shutil.which('restorecon'): 171 # If we don't have restorecon, failure is ignored quietly. 172 return 173 174 if not selinux_updates: 175 # If the list of files is empty, do not try to call restorecon. 176 return 177 178 with subprocess.Popen(['restorecon', '-F', '-f-', '-0'], 179 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: 180 out, err = proc.communicate(input=b'\0'.join(os.fsencode(f) for f in selinux_updates) + b'\0') 181 if proc.returncode != 0 and not os.environ.get('DESTDIR'): 182 print('Failed to restore SELinux context of installed files...', 183 'Standard output:', out.decode(), 184 'Standard error:', err.decode(), sep='\n') 185 186 187def get_destdir_path(d, path): 188 if os.path.isabs(path): 189 output = destdir_join(d.destdir, path) 190 else: 191 output = os.path.join(d.fullprefix, path) 192 return output 193 194 195def check_for_stampfile(fname): 196 '''Some languages e.g. Rust have output files 197 whose names are not known at configure time. 198 Check if this is the case and return the real 199 file instead.''' 200 if fname.endswith('.so') or fname.endswith('.dll'): 201 if os.stat(fname).st_size == 0: 202 (base, suffix) = os.path.splitext(fname) 203 files = glob(base + '-*' + suffix) 204 if len(files) > 1: 205 print("Stale dynamic library files in build dir. Can't install.") 206 sys.exit(1) 207 if len(files) == 1: 208 return files[0] 209 elif fname.endswith('.a') or fname.endswith('.lib'): 210 if os.stat(fname).st_size == 0: 211 (base, suffix) = os.path.splitext(fname) 212 files = glob(base + '-*' + '.rlib') 213 if len(files) > 1: 214 print("Stale static library files in build dir. Can't install.") 215 sys.exit(1) 216 if len(files) == 1: 217 return files[0] 218 return fname 219 220class Installer: 221 222 def __init__(self, options, lf): 223 self.did_install_something = False 224 self.options = options 225 self.lf = lf 226 self.preserved_file_count = 0 227 228 def log(self, msg): 229 if not self.options.quiet: 230 print(msg) 231 232 def should_preserve_existing_file(self, from_file, to_file): 233 if not self.options.only_changed: 234 return False 235 # Always replace danging symlinks 236 if os.path.islink(from_file) and not os.path.isfile(from_file): 237 return False 238 from_time = os.stat(from_file).st_mtime 239 to_time = os.stat(to_file).st_mtime 240 return from_time <= to_time 241 242 def do_copyfile(self, from_file, to_file, makedirs=None): 243 outdir = os.path.split(to_file)[0] 244 if not os.path.isfile(from_file) and not os.path.islink(from_file): 245 raise RuntimeError('Tried to install something that isn\'t a file:' 246 '{!r}'.format(from_file)) 247 # copyfile fails if the target file already exists, so remove it to 248 # allow overwriting a previous install. If the target is not a file, we 249 # want to give a readable error. 250 if os.path.exists(to_file): 251 if not os.path.isfile(to_file): 252 raise RuntimeError('Destination {!r} already exists and is not ' 253 'a file'.format(to_file)) 254 if self.should_preserve_existing_file(from_file, to_file): 255 append_to_log(self.lf, '# Preserving old file {}\n'.format(to_file)) 256 self.preserved_file_count += 1 257 return False 258 os.remove(to_file) 259 elif makedirs: 260 # Unpack tuple 261 dirmaker, outdir = makedirs 262 # Create dirs if needed 263 dirmaker.makedirs(outdir, exist_ok=True) 264 self.log('Installing {} to {}'.format(from_file, outdir)) 265 if os.path.islink(from_file): 266 if not os.path.exists(from_file): 267 # Dangling symlink. Replicate as is. 268 shutil.copy(from_file, outdir, follow_symlinks=False) 269 else: 270 # Remove this entire branch when changing the behaviour to duplicate 271 # symlinks rather than copying what they point to. 272 print(symlink_warning) 273 shutil.copyfile(from_file, to_file) 274 shutil.copystat(from_file, to_file) 275 else: 276 shutil.copyfile(from_file, to_file) 277 shutil.copystat(from_file, to_file) 278 selinux_updates.append(to_file) 279 append_to_log(self.lf, to_file) 280 return True 281 282 def do_copydir(self, data, src_dir, dst_dir, exclude, install_mode): 283 ''' 284 Copies the contents of directory @src_dir into @dst_dir. 285 286 For directory 287 /foo/ 288 bar/ 289 excluded 290 foobar 291 file 292 do_copydir(..., '/foo', '/dst/dir', {'bar/excluded'}) creates 293 /dst/ 294 dir/ 295 bar/ 296 foobar 297 file 298 299 Args: 300 src_dir: str, absolute path to the source directory 301 dst_dir: str, absolute path to the destination directory 302 exclude: (set(str), set(str)), tuple of (exclude_files, exclude_dirs), 303 each element of the set is a path relative to src_dir. 304 ''' 305 if not os.path.isabs(src_dir): 306 raise ValueError('src_dir must be absolute, got {}'.format(src_dir)) 307 if not os.path.isabs(dst_dir): 308 raise ValueError('dst_dir must be absolute, got {}'.format(dst_dir)) 309 if exclude is not None: 310 exclude_files, exclude_dirs = exclude 311 else: 312 exclude_files = exclude_dirs = set() 313 for root, dirs, files in os.walk(src_dir): 314 assert os.path.isabs(root) 315 for d in dirs[:]: 316 abs_src = os.path.join(root, d) 317 filepart = os.path.relpath(abs_src, start=src_dir) 318 abs_dst = os.path.join(dst_dir, filepart) 319 # Remove these so they aren't visited by os.walk at all. 320 if filepart in exclude_dirs: 321 dirs.remove(d) 322 continue 323 if os.path.isdir(abs_dst): 324 continue 325 if os.path.exists(abs_dst): 326 print('Tried to copy directory {} but a file of that name already exists.'.format(abs_dst)) 327 sys.exit(1) 328 data.dirmaker.makedirs(abs_dst) 329 shutil.copystat(abs_src, abs_dst) 330 sanitize_permissions(abs_dst, data.install_umask) 331 for f in files: 332 abs_src = os.path.join(root, f) 333 filepart = os.path.relpath(abs_src, start=src_dir) 334 if filepart in exclude_files: 335 continue 336 abs_dst = os.path.join(dst_dir, filepart) 337 if os.path.isdir(abs_dst): 338 print('Tried to copy file {} but a directory of that name already exists.'.format(abs_dst)) 339 sys.exit(1) 340 parent_dir = os.path.dirname(abs_dst) 341 if not os.path.isdir(parent_dir): 342 os.mkdir(parent_dir) 343 shutil.copystat(os.path.dirname(abs_src), parent_dir) 344 # FIXME: what about symlinks? 345 self.do_copyfile(abs_src, abs_dst) 346 set_mode(abs_dst, install_mode, data.install_umask) 347 348 def do_install(self, datafilename): 349 with open(datafilename, 'rb') as ifile: 350 d = pickle.load(ifile) 351 d.destdir = os.environ.get('DESTDIR', '') 352 d.fullprefix = destdir_join(d.destdir, d.prefix) 353 354 if d.install_umask != 'preserve': 355 os.umask(d.install_umask) 356 357 self.did_install_something = False 358 try: 359 d.dirmaker = DirMaker(self.lf) 360 with d.dirmaker: 361 self.install_subdirs(d) # Must be first, because it needs to delete the old subtree. 362 self.install_targets(d) 363 self.install_headers(d) 364 self.install_man(d) 365 self.install_data(d) 366 restore_selinux_contexts() 367 self.run_install_script(d) 368 if not self.did_install_something: 369 self.log('Nothing to install.') 370 if not self.options.quiet and self.preserved_file_count > 0: 371 self.log('Preserved {} unchanged files, see {} for the full list' 372 .format(self.preserved_file_count, os.path.normpath(self.lf.name))) 373 except PermissionError: 374 if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ: 375 print('Installation failed due to insufficient permissions.') 376 print('Attempting to use polkit to gain elevated privileges...') 377 os.execlp('pkexec', 'pkexec', sys.executable, main_file, *sys.argv[1:], 378 '-C', os.getcwd()) 379 else: 380 raise 381 382 def install_subdirs(self, d): 383 for (src_dir, dst_dir, mode, exclude) in d.install_subdirs: 384 self.did_install_something = True 385 full_dst_dir = get_destdir_path(d, dst_dir) 386 self.log('Installing subdir {} to {}'.format(src_dir, full_dst_dir)) 387 d.dirmaker.makedirs(full_dst_dir, exist_ok=True) 388 self.do_copydir(d, src_dir, full_dst_dir, exclude, mode) 389 390 def install_data(self, d): 391 for i in d.data: 392 fullfilename = i[0] 393 outfilename = get_destdir_path(d, i[1]) 394 mode = i[2] 395 outdir = os.path.dirname(outfilename) 396 if self.do_copyfile(fullfilename, outfilename, makedirs=(d.dirmaker, outdir)): 397 self.did_install_something = True 398 set_mode(outfilename, mode, d.install_umask) 399 400 def install_man(self, d): 401 for m in d.man: 402 full_source_filename = m[0] 403 outfilename = get_destdir_path(d, m[1]) 404 outdir = os.path.dirname(outfilename) 405 install_mode = m[2] 406 if self.do_copyfile(full_source_filename, outfilename, makedirs=(d.dirmaker, outdir)): 407 self.did_install_something = True 408 set_mode(outfilename, install_mode, d.install_umask) 409 410 def install_headers(self, d): 411 for t in d.headers: 412 fullfilename = t[0] 413 fname = os.path.basename(fullfilename) 414 outdir = get_destdir_path(d, t[1]) 415 outfilename = os.path.join(outdir, fname) 416 install_mode = t[2] 417 if self.do_copyfile(fullfilename, outfilename, makedirs=(d.dirmaker, outdir)): 418 self.did_install_something = True 419 set_mode(outfilename, install_mode, d.install_umask) 420 421 def run_install_script(self, d): 422 env = {'MESON_SOURCE_ROOT': d.source_dir, 423 'MESON_BUILD_ROOT': d.build_dir, 424 'MESON_INSTALL_PREFIX': d.prefix, 425 'MESON_INSTALL_DESTDIR_PREFIX': d.fullprefix, 426 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in d.mesonintrospect]), 427 } 428 if self.options.quiet: 429 env['MESON_INSTALL_QUIET'] = '1' 430 431 child_env = os.environ.copy() 432 child_env.update(env) 433 434 for i in d.install_scripts: 435 self.did_install_something = True # Custom script must report itself if it does nothing. 436 script = i['exe'] 437 args = i['args'] 438 name = ' '.join(script + args) 439 self.log('Running custom install script {!r}'.format(name)) 440 try: 441 rc = subprocess.call(script + args, env=child_env) 442 except OSError: 443 print('FAILED: install script \'{}\' could not be run, stopped'.format(name)) 444 # POSIX shells return 127 when a command could not be found 445 sys.exit(127) 446 if rc != 0: 447 print('FAILED: install script \'{}\' exit code {}, stopped'.format(name, rc)) 448 sys.exit(rc) 449 450 def install_targets(self, d): 451 for t in d.targets: 452 if not os.path.exists(t.fname): 453 # For example, import libraries of shared modules are optional 454 if t.optional: 455 self.log('File {!r} not found, skipping'.format(t.fname)) 456 continue 457 else: 458 raise RuntimeError('File {!r} could not be found'.format(t.fname)) 459 file_copied = False # not set when a directory is copied 460 fname = check_for_stampfile(t.fname) 461 outdir = get_destdir_path(d, t.outdir) 462 outname = os.path.join(outdir, os.path.basename(fname)) 463 final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname)) 464 aliases = t.aliases 465 should_strip = t.strip 466 install_rpath = t.install_rpath 467 install_name_mappings = t.install_name_mappings 468 install_mode = t.install_mode 469 if not os.path.exists(fname): 470 raise RuntimeError('File {!r} could not be found'.format(fname)) 471 elif os.path.isfile(fname): 472 file_copied = self.do_copyfile(fname, outname, makedirs=(d.dirmaker, outdir)) 473 set_mode(outname, install_mode, d.install_umask) 474 if should_strip and d.strip_bin is not None: 475 if fname.endswith('.jar'): 476 self.log('Not stripping jar target:', os.path.basename(fname)) 477 continue 478 self.log('Stripping target {!r} using {}.'.format(fname, d.strip_bin[0])) 479 ps, stdo, stde = Popen_safe(d.strip_bin + [outname]) 480 if ps.returncode != 0: 481 print('Could not strip file.\n') 482 print('Stdout:\n{}\n'.format(stdo)) 483 print('Stderr:\n{}\n'.format(stde)) 484 sys.exit(1) 485 if fname.endswith('.js'): 486 # Emscripten outputs js files and optionally a wasm file. 487 # If one was generated, install it as well. 488 wasm_source = os.path.splitext(fname)[0] + '.wasm' 489 if os.path.exists(wasm_source): 490 wasm_output = os.path.splitext(outname)[0] + '.wasm' 491 file_copied = self.do_copyfile(wasm_source, wasm_output) 492 elif os.path.isdir(fname): 493 fname = os.path.join(d.build_dir, fname.rstrip('/')) 494 outname = os.path.join(outdir, os.path.basename(fname)) 495 d.dirmaker.makedirs(outdir, exist_ok=True) 496 self.do_copydir(d, fname, outname, None, install_mode) 497 else: 498 raise RuntimeError('Unknown file type for {!r}'.format(fname)) 499 printed_symlink_error = False 500 for alias, to in aliases.items(): 501 try: 502 symlinkfilename = os.path.join(outdir, alias) 503 try: 504 os.remove(symlinkfilename) 505 except FileNotFoundError: 506 pass 507 os.symlink(to, symlinkfilename) 508 append_to_log(self.lf, symlinkfilename) 509 except (NotImplementedError, OSError): 510 if not printed_symlink_error: 511 print("Symlink creation does not work on this platform. " 512 "Skipping all symlinking.") 513 printed_symlink_error = True 514 if file_copied: 515 self.did_install_something = True 516 try: 517 depfixer.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path, 518 install_name_mappings, verbose=False) 519 except SystemExit as e: 520 if isinstance(e.code, int) and e.code == 0: 521 pass 522 else: 523 raise 524 525def run(opts): 526 datafilename = 'meson-private/install.dat' 527 private_dir = os.path.dirname(datafilename) 528 log_dir = os.path.join(private_dir, '../meson-logs') 529 if not os.path.exists(os.path.join(opts.wd, datafilename)): 530 sys.exit('Install data not found. Run this command in build directory root.') 531 if not opts.no_rebuild: 532 if not rebuild_all(opts.wd): 533 sys.exit(-1) 534 os.chdir(opts.wd) 535 with open(os.path.join(log_dir, 'install-log.txt'), 'w') as lf: 536 installer = Installer(opts, lf) 537 append_to_log(lf, '# List of files installed by Meson') 538 append_to_log(lf, '# Does not contain files installed by custom scripts.') 539 if opts.profile: 540 import cProfile as profile 541 fname = os.path.join(private_dir, 'profile-installer.log') 542 profile.runctx('installer.do_install(datafilename)', globals(), locals(), filename=fname) 543 else: 544 installer.do_install(datafilename) 545 return 0 546