diff options
author | Xavier Claessens <xavier.claessens@collabora.com> | 2020-04-24 17:57:19 -0400 |
---|---|---|
committer | Jussi Pakkanen <jpakkane@gmail.com> | 2021-02-04 16:24:38 +0000 |
commit | 95c079071118abc9078b0931ee86f51b1b4f8e05 (patch) | |
tree | 6535a45ede91198c5a346fd4c5b69dbffc4aebaa | |
parent | 673aff3595d04a82d431dc4903c95227481565ca (diff) | |
download | meson-95c079071118abc9078b0931ee86f51b1b4f8e05.zip meson-95c079071118abc9078b0931ee86f51b1b4f8e05.tar.gz meson-95c079071118abc9078b0931ee86f51b1b4f8e05.tar.bz2 |
minstall: Add --dry-run option
Closes: #1281
-rw-r--r-- | docs/markdown/snippets/install_dry_run.md | 4 | ||||
-rw-r--r-- | mesonbuild/minstall.py | 124 | ||||
-rwxr-xr-x | run_unittests.py | 19 |
3 files changed, 118 insertions, 29 deletions
diff --git a/docs/markdown/snippets/install_dry_run.md b/docs/markdown/snippets/install_dry_run.md new file mode 100644 index 0000000..8106a06 --- /dev/null +++ b/docs/markdown/snippets/install_dry_run.md @@ -0,0 +1,4 @@ +## meson install --dry-run + +New option to meson install command that does not actually install files, but +only print messages. diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 18dca8b..6c1810a 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -54,6 +54,7 @@ if T.TYPE_CHECKING: quiet: bool wd: str destdir: str + dry_run: bool symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, @@ -75,20 +76,27 @@ def add_arguments(parser: argparse.Namespace) -> None: help='Do not print every file that was installed.') parser.add_argument('--destdir', default=None, help='Sets or overrides DESTDIR environment. (Since 0.57.0)') + parser.add_argument('--dry-run', '-n', action='store_true', + help='Doesn\'t actually install, but print logs.') class DirMaker: - def __init__(self, lf: T.TextIO): + def __init__(self, lf: T.TextIO, makedirs: T.Callable[..., None]): self.lf = lf self.dirs: T.List[str] = [] + self.makedirs_impl = makedirs def makedirs(self, path: str, exist_ok: bool = False) -> None: dirname = os.path.normpath(path) dirs = [] while dirname != os.path.dirname(dirname): + if dirname in self.dirs: + # In dry-run mode the directory does not exist but we would have + # created it with all its parents otherwise. + break if not os.path.exists(dirname): dirs.append(dirname) dirname = os.path.dirname(dirname) - os.makedirs(path, exist_ok=exist_ok) + self.makedirs_impl(path, exist_ok=exist_ok) # store the directories in creation order, with the parent directory # before the child directories. Future calls of makedir() will not @@ -275,6 +283,74 @@ class Installer: self.options = options self.lf = lf self.preserved_file_count = 0 + self.dry_run = options.dry_run + + def mkdir(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.mkdir(*args, **kwargs) + + def remove(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.remove(*args, **kwargs) + + def symlink(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.symlink(*args, **kwargs) + + def makedirs(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.makedirs(*args, **kwargs) + + def copy(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copy(*args, **kwargs) + + def copy2(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copy2(*args, **kwargs) + + def copyfile(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copyfile(*args, **kwargs) + + def copystat(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copystat(*args, **kwargs) + + def fix_rpath(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + depfixer.fix_rpath(*args, **kwargs) + + def set_chown(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_chown(*args, **kwargs) + + def set_chmod(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_chmod(*args, **kwargs) + + def sanitize_permissions(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + sanitize_permissions(*args, **kwargs) + + def set_mode(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_mode(*args, **kwargs) + + def restore_selinux_contexts(self) -> None: + if not self.dry_run: + restore_selinux_contexts() + + def Popen_safe(self, *args: T.Any, **kwargs: T.Any) -> T.Tuple[int, str, str]: + if not self.dry_run: + p, o, e = Popen_safe(*args, **kwargs) + return p.returncode, o, e + return 0, '', '' + + def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int: + if not self.dry_run: + return run_exe(*args, **kwargs) + return 0 def log(self, msg: str) -> None: if not self.options.quiet: @@ -307,7 +383,7 @@ class Installer: append_to_log(self.lf, '# Preserving old file {}\n'.format(to_file)) self.preserved_file_count += 1 return False - os.remove(to_file) + self.remove(to_file) elif makedirs: # Unpack tuple dirmaker, outdir = makedirs @@ -317,14 +393,14 @@ class Installer: if os.path.islink(from_file): if not os.path.exists(from_file): # Dangling symlink. Replicate as is. - shutil.copy(from_file, outdir, follow_symlinks=False) + self.copy(from_file, outdir, follow_symlinks=False) else: # Remove this entire branch when changing the behaviour to duplicate # symlinks rather than copying what they point to. print(symlink_warning) - shutil.copy2(from_file, to_file) + self.copy2(from_file, to_file) else: - shutil.copy2(from_file, to_file) + self.copy2(from_file, to_file) selinux_updates.append(to_file) append_to_log(self.lf, to_file) return True @@ -378,8 +454,8 @@ class Installer: print('Tried to copy directory {} but a file of that name already exists.'.format(abs_dst)) sys.exit(1) dm.makedirs(abs_dst) - shutil.copystat(abs_src, abs_dst) - sanitize_permissions(abs_dst, data.install_umask) + self.copystat(abs_src, abs_dst) + self.sanitize_permissions(abs_dst, data.install_umask) for f in files: abs_src = os.path.join(root, f) filepart = os.path.relpath(abs_src, start=src_dir) @@ -391,11 +467,11 @@ class Installer: sys.exit(1) parent_dir = os.path.dirname(abs_dst) if not os.path.isdir(parent_dir): - os.mkdir(parent_dir) - shutil.copystat(os.path.dirname(abs_src), parent_dir) + self.mkdir(parent_dir) + self.copystat(os.path.dirname(abs_src), parent_dir) # FIXME: what about symlinks? self.do_copyfile(abs_src, abs_dst) - set_mode(abs_dst, install_mode, data.install_umask) + self.set_mode(abs_dst, install_mode, data.install_umask) @staticmethod def check_installdata(obj: InstallData) -> InstallData: @@ -422,13 +498,13 @@ class Installer: self.did_install_something = False try: - with DirMaker(self.lf) as dm: + with DirMaker(self.lf, self.makedirs) as dm: self.install_subdirs(d, dm, destdir, fullprefix) # Must be first, because it needs to delete the old subtree. self.install_targets(d, dm, destdir, fullprefix) self.install_headers(d, dm, destdir, fullprefix) self.install_man(d, dm, destdir, fullprefix) self.install_data(d, dm, destdir, fullprefix) - restore_selinux_contexts() + self.restore_selinux_contexts() self.run_install_script(d, destdir, fullprefix) if not self.did_install_something: self.log('Nothing to install.') @@ -460,7 +536,7 @@ class Installer: outdir = os.path.dirname(outfilename) if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, mode, d.install_umask) + self.set_mode(outfilename, mode, d.install_umask) def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for m in d.man: @@ -470,7 +546,7 @@ class Installer: install_mode = m[2] if self.do_copyfile(full_source_filename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, install_mode, d.install_umask) + self.set_mode(outfilename, install_mode, d.install_umask) def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for t in d.headers: @@ -481,7 +557,7 @@ class Installer: install_mode = t[2] if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, install_mode, d.install_umask) + self.set_mode(outfilename, install_mode, d.install_umask) def run_install_script(self, d: InstallData, destdir: str, fullprefix: str) -> None: env = {'MESON_SOURCE_ROOT': d.source_dir, @@ -501,7 +577,7 @@ class Installer: self.did_install_something = True # Custom script must report itself if it does nothing. self.log('Running custom install script {!r}'.format(name)) try: - rc = run_exe(i, env) + rc = self.run_exe(i, env) except OSError: print('FAILED: install script \'{}\' could not be run, stopped'.format(name)) # POSIX shells return 127 when a command could not be found @@ -533,14 +609,14 @@ class Installer: raise RuntimeError('File {!r} could not be found'.format(fname)) elif os.path.isfile(fname): file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir)) - set_mode(outname, install_mode, d.install_umask) + self.set_mode(outname, install_mode, d.install_umask) if should_strip and d.strip_bin is not None: if fname.endswith('.jar'): self.log('Not stripping jar target: {}'.format(os.path.basename(fname))) continue self.log('Stripping target {!r} using {}.'.format(fname, d.strip_bin[0])) - ps, stdo, stde = Popen_safe(d.strip_bin + [outname]) - if ps.returncode != 0: + returncode, stdo, stde = self.Popen_safe(d.strip_bin + [outname]) + if returncode != 0: print('Could not strip file.\n') print('Stdout:\n{}\n'.format(stdo)) print('Stderr:\n{}\n'.format(stde)) @@ -564,10 +640,10 @@ class Installer: try: symlinkfilename = os.path.join(outdir, alias) try: - os.remove(symlinkfilename) + self.remove(symlinkfilename) except FileNotFoundError: pass - os.symlink(to, symlinkfilename) + self.symlink(to, symlinkfilename) append_to_log(self.lf, symlinkfilename) except (NotImplementedError, OSError): if not printed_symlink_error: @@ -577,8 +653,8 @@ class Installer: if file_copied: self.did_install_something = True try: - depfixer.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path, - install_name_mappings, verbose=False) + self.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path, + install_name_mappings, verbose=False) except SystemExit as e: if isinstance(e.code, int) and e.code == 0: pass diff --git a/run_unittests.py b/run_unittests.py index 2b8812a..da13753 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -2270,11 +2270,13 @@ class AllPlatformTests(BasePlatformTests): expected = {installpath: 0} for name in installpath.rglob('*'): expected[name] = 0 - # Find logged files and directories - with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f: - logged = list(map(lambda l: Path(l.strip()), - filter(lambda l: not l.startswith('#'), - f.readlines()))) + def read_logs(): + # Find logged files and directories + with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f: + return list(map(lambda l: Path(l.strip()), + filter(lambda l: not l.startswith('#'), + f.readlines()))) + logged = read_logs() for name in logged: self.assertTrue(name in expected, 'Log contains extra entry {}'.format(name)) expected[name] += 1 @@ -2283,6 +2285,13 @@ class AllPlatformTests(BasePlatformTests): self.assertGreater(count, 0, 'Log is missing entry for {}'.format(name)) self.assertLess(count, 2, 'Log has multiple entries for {}'.format(name)) + # Verify that with --dry-run we obtain the same logs but with nothing + # actually installed + windows_proof_rmtree(self.installdir) + self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir], workdir=self.builddir) + self.assertEqual(logged, read_logs()) + self.assertFalse(os.path.exists(self.installdir)) + def test_uninstall(self): exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix) testdir = os.path.join(self.common_test_dir, '8 install') |