From 8651d55c6a317de37dcaa9965157151095a88292 Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Fri, 9 Mar 2018 10:38:02 -0800 Subject: [PATCH 1/5] Add new builtin option --install-umask This option controls the permissions of installed files (except for those specified explicitly using install_mode option, e.g. in install_data rules.) An install-umask of 022 will install all binaries, directories and executable files with mode rwxr-xr-x, while all data and non-executable files will be installed with mode rw-r--r--. Setting install-umask to the string 'preserve' will disable this feature, keeping the permissions of installed files same as the files in the build tree (or source tree for install_data and install_subdir.) Note that, in this case, the umask used when building and that used when checking out the source tree will leak into the install tree. Keep the default as 'preserve', to show that no behavior is changed and all tests keep passing unchanged. Tested: ./run_tests.py --- mesonbuild/backend/backends.py | 4 ++- mesonbuild/backend/ninjabackend.py | 4 ++- mesonbuild/coredata.py | 25 +++++++++++++++--- mesonbuild/scripts/meson_install.py | 41 +++++++++++++++++++++++------ 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 8f75dacf9..bc7c29504 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -37,11 +37,13 @@ class CleanTrees: self.trees = trees class InstallData: - def __init__(self, source_dir, build_dir, prefix, strip_bin, mesonintrospect): + def __init__(self, source_dir, build_dir, prefix, strip_bin, + install_umask, mesonintrospect): self.source_dir = source_dir self.build_dir = build_dir self.prefix = prefix self.strip_bin = strip_bin + self.install_umask = install_umask self.targets = [] self.headers = [] self.man = [] diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index bc3a8ef96..fb5b4b724 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -668,7 +668,9 @@ int dummy; d = InstallData(self.environment.get_source_dir(), self.environment.get_build_dir(), self.environment.get_prefix(), - strip_bin, self.environment.get_build_command() + ['introspect']) + strip_bin, + self.environment.coredata.get_builtin_option('install_umask'), + self.environment.get_build_command() + ['introspect']) elem = NinjaBuildElement(self.all_outputs, 'meson-install', 'CUSTOM_COMMAND', 'PHONY') elem.add_dep('all') elem.add_item('DESC', 'Installing files.') diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index ba4f49552..17b28a802 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -105,6 +105,22 @@ class UserIntegerOption(UserOption): except ValueError: raise MesonException('Value string "%s" is not convertable to an integer.' % valuestring) +class UserUmaskOption(UserIntegerOption): + def __init__(self, name, description, value, yielding=None): + super().__init__(name, description, 0, 0o777, value, yielding) + + def set_value(self, newvalue): + if newvalue is None or newvalue == 'preserve': + self.value = None + else: + super().set_value(newvalue) + + def toint(self, valuestring): + try: + return int(valuestring, 8) + except ValueError as e: + raise MesonException('Invalid mode: {}'.format(e)) + class UserComboOption(UserOption): def __init__(self, name, description, choices, value, yielding=None): super().__init__(name, description, choices, yielding) @@ -345,12 +361,12 @@ def is_builtin_option(optname): def get_builtin_option_choices(optname): if is_builtin_option(optname): - if builtin_options[optname][0] == UserStringOption: - return None + if builtin_options[optname][0] == UserComboOption: + return builtin_options[optname][2] elif builtin_options[optname][0] == UserBooleanOption: return [True, False] else: - return builtin_options[optname][2] + return None else: raise RuntimeError('Tried to get the supported values for an unknown builtin option \'%s\'.' % optname) @@ -379,6 +395,8 @@ def get_builtin_option_default(optname, prefix='', noneIfSuppress=False): o = builtin_options[optname] if o[0] == UserComboOption: return o[3] + if o[0] == UserIntegerOption: + return o[4] if optname in builtin_dir_noprefix_options: if noneIfSuppress: # Return None if argparse defaulting should be suppressed for @@ -438,6 +456,7 @@ builtin_options = { 'backend': [UserComboOption, 'Backend to use.', backendlist, 'ninja'], 'stdsplit': [UserBooleanOption, 'Split stdout and stderr in test logs.', True], 'errorlogs': [UserBooleanOption, "Whether to print the logs from failing tests.", True], + 'install_umask': [UserUmaskOption, 'Default umask to apply on permissions of installed files.', None], } # Special prefix-dependent defaults for installation directories that reside in diff --git a/mesonbuild/scripts/meson_install.py b/mesonbuild/scripts/meson_install.py index 013f2a001..b45426005 100644 --- a/mesonbuild/scripts/meson_install.py +++ b/mesonbuild/scripts/meson_install.py @@ -51,12 +51,25 @@ class DirMaker: for d in self.dirs: append_to_log(d) -def set_mode(path, mode): - if mode is None: - # Keep mode unchanged +def is_executable(path): + '''Checks whether any of the "x" bits are set in the source file mode.''' + return bool(os.stat(path).st_mode & 0o111) + +def sanitize_permissions(path, umask): + if umask is None: return - if (mode.perms_s or mode.owner or mode.group) is None: - # Nothing to set + new_perms = 0o777 if is_executable(path) else 0o666 + new_perms &= ~umask + try: + os.chmod(path, new_perms) + except PermissionError as e: + msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...' + print(msg.format(path, new_perms, e.strerror)) + +def set_mode(path, mode, default_umask): + if mode is None or (mode.perms_s or mode.owner or mode.group) is None: + # Just sanitize permissions with the default umask + sanitize_permissions(path, default_umask) return # No chown() on Windows, and must set one of owner/group if not is_windows() and (mode.owner or mode.group) is not None: @@ -83,6 +96,8 @@ def set_mode(path, mode): except PermissionError as e: msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...' print(msg.format(path, mode.perms_s, e.strerror)) + else: + sanitize_permissions(path, default_umask) def restore_selinux_contexts(): ''' @@ -180,6 +195,7 @@ def do_copydir(data, src_dir, dst_dir, exclude): sys.exit(1) data.dirmaker.makedirs(abs_dst) shutil.copystat(abs_src, abs_dst) + 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) @@ -195,6 +211,7 @@ def do_copydir(data, src_dir, dst_dir, exclude): os.mkdir(parent_dir) shutil.copystat(os.path.dirname(abs_src), parent_dir) shutil.copy2(abs_src, abs_dst, follow_symlinks=False) + sanitize_permissions(abs_dst, data.install_umask) append_to_log(abs_dst) def get_destdir_path(d, path): @@ -210,6 +227,8 @@ def do_install(datafilename): d.destdir = os.environ.get('DESTDIR', '') d.fullprefix = destdir_join(d.destdir, d.prefix) + if d.install_umask is not None: + os.umask(d.install_umask) d.dirmaker = DirMaker() with d.dirmaker: install_subdirs(d) # Must be first, because it needs to delete the old subtree. @@ -226,7 +245,7 @@ def install_subdirs(d): print('Installing subdir %s to %s' % (src_dir, full_dst_dir)) d.dirmaker.makedirs(full_dst_dir, exist_ok=True) do_copydir(d, src_dir, full_dst_dir, exclude) - set_mode(full_dst_dir, mode) + set_mode(full_dst_dir, mode, d.install_umask) def install_data(d): for i in d.data: @@ -237,7 +256,7 @@ def install_data(d): d.dirmaker.makedirs(outdir, exist_ok=True) print('Installing %s to %s' % (fullfilename, outdir)) do_copyfile(fullfilename, outfilename) - set_mode(outfilename, mode) + set_mode(outfilename, mode, d.install_umask) def install_man(d): for m in d.man: @@ -256,6 +275,7 @@ def install_man(d): append_to_log(outfilename) else: do_copyfile(full_source_filename, outfilename) + sanitize_permissions(outfilename, d.install_umask) def install_headers(d): for t in d.headers: @@ -266,6 +286,7 @@ def install_headers(d): print('Installing %s to %s' % (fname, outdir)) d.dirmaker.makedirs(outdir, exist_ok=True) do_copyfile(fullfilename, outfilename) + sanitize_permissions(outfilename, d.install_umask) def run_install_script(d): env = {'MESON_SOURCE_ROOT': d.source_dir, @@ -330,6 +351,7 @@ def install_targets(d): raise RuntimeError('File {!r} could not be found'.format(fname)) elif os.path.isfile(fname): do_copyfile(fname, outname) + sanitize_permissions(outname, d.install_umask) if should_strip and d.strip_bin is not None: if fname.endswith('.jar'): print('Not stripping jar target:', os.path.basename(fname)) @@ -346,9 +368,12 @@ def install_targets(d): pdb_outname = os.path.splitext(outname)[0] + '.pdb' print('Installing pdb file %s to %s' % (pdb_filename, pdb_outname)) do_copyfile(pdb_filename, pdb_outname) + sanitize_permissions(pdb_outname, d.install_umask) elif os.path.isdir(fname): fname = os.path.join(d.build_dir, fname.rstrip('/')) - do_copydir(d, fname, os.path.join(outdir, os.path.basename(fname)), None) + outname = os.path.join(outdir, os.path.basename(fname)) + do_copydir(d, fname, outname, None) + sanitize_permissions(outname, d.install_umask) else: raise RuntimeError('Unknown file type for {!r}'.format(fname)) printed_symlink_error = False From b0382733d80e4963036a6abd4f475ebbea67d72c Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Fri, 9 Mar 2018 23:27:55 -0800 Subject: [PATCH 2/5] Update default of install-umask to 022 And, with that, update the test cases that checked that preserving the original permissions worked to set install_umask=preserve explicitly in those projects' default_options. Tested: ./run_tests.py --- mesonbuild/coredata.py | 2 +- test cases/common/12 data/meson.build | 3 ++- test cases/common/66 install subdir/meson.build | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 17b28a802..3484421fe 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -456,7 +456,7 @@ builtin_options = { 'backend': [UserComboOption, 'Backend to use.', backendlist, 'ninja'], 'stdsplit': [UserBooleanOption, 'Split stdout and stderr in test logs.', True], 'errorlogs': [UserBooleanOption, "Whether to print the logs from failing tests.", True], - 'install_umask': [UserUmaskOption, 'Default umask to apply on permissions of installed files.', None], + 'install_umask': [UserUmaskOption, 'Default umask to apply on permissions of installed files.', '022'], } # Special prefix-dependent defaults for installation directories that reside in diff --git a/test cases/common/12 data/meson.build b/test cases/common/12 data/meson.build index d855bbaf4..b5b1e8a62 100644 --- a/test cases/common/12 data/meson.build +++ b/test cases/common/12 data/meson.build @@ -1,4 +1,5 @@ -project('data install test', 'c') +project('data install test', 'c', + default_options : ['install_umask=preserve']) install_data(sources : 'datafile.dat', install_dir : 'share/progname') # Some file in /etc that is only read-write by root; add a sticky bit for testing install_data(sources : 'etcfile.dat', install_dir : '/etc', install_mode : 'rw------T') diff --git a/test cases/common/66 install subdir/meson.build b/test cases/common/66 install subdir/meson.build index 403b6f04d..6f92efdb8 100644 --- a/test cases/common/66 install subdir/meson.build +++ b/test cases/common/66 install subdir/meson.build @@ -1,4 +1,5 @@ -project('install a whole subdir', 'c') +project('install a whole subdir', 'c', + default_options : ['install_umask=preserve']) # A subdir with an exclusion: install_subdir('sub2', From 59b0fa9722c92f2df76dc87f7a4f0afbce4bd6fa Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Mon, 12 Mar 2018 00:23:21 -0700 Subject: [PATCH 3/5] Add a unit test for install_umask. This test copies a src tree using umask of 002, then runs the build and install under umask 027. It ensures that the default install_umask of 022 is still applied to all files and directories in the install tree. --- run_unittests.py | 68 +++++++++++++++++++ test cases/unit/24 install umask/datafile.cat | 1 + test cases/unit/24 install umask/meson.build | 7 ++ test cases/unit/24 install umask/myinstall.py | 17 +++++ test cases/unit/24 install umask/prog.1 | 1 + test cases/unit/24 install umask/prog.c | 3 + test cases/unit/24 install umask/sample.h | 6 ++ .../unit/24 install umask/subdir/datafile.dog | 1 + .../unit/24 install umask/subdir/sayhello | 2 + 9 files changed, 106 insertions(+) create mode 100644 test cases/unit/24 install umask/datafile.cat create mode 100644 test cases/unit/24 install umask/meson.build create mode 100644 test cases/unit/24 install umask/myinstall.py create mode 100644 test cases/unit/24 install umask/prog.1 create mode 100644 test cases/unit/24 install umask/prog.c create mode 100644 test cases/unit/24 install umask/sample.h create mode 100644 test cases/unit/24 install umask/subdir/datafile.dog create mode 100755 test cases/unit/24 install umask/subdir/sayhello diff --git a/run_unittests.py b/run_unittests.py index e0cd1ec50..bb18f1137 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -2669,6 +2669,74 @@ class LinuxlikeTests(BasePlatformTests): # The chown failed nonfatally if we're not root self.assertEqual(0, statf.st_uid) + def test_install_umask(self): + ''' + Test that files are installed with correct permissions using default + install umask of 022, regardless of the umask at time the worktree + was checked out or the build was executed. + ''' + # Copy source tree to a temporary directory and change permissions + # there to simulate a checkout with umask 002. + orig_testdir = os.path.join(self.unit_test_dir, '24 install umask') + # Create a new testdir under tmpdir. + tmpdir = os.path.realpath(tempfile.mkdtemp()) + self.addCleanup(windows_proof_rmtree, tmpdir) + testdir = os.path.join(tmpdir, '24 install umask') + # Copy the tree using shutil.copyfile, which will use the current umask + # instead of preserving permissions of the old tree. + save_umask = os.umask(0o002) + self.addCleanup(os.umask, save_umask) + shutil.copytree(orig_testdir, testdir, copy_function=shutil.copyfile) + # Preserve the executable status of subdir/sayhello though. + os.chmod(os.path.join(testdir, 'subdir', 'sayhello'), 0o775) + self.init(testdir) + # Run the build under a 027 umask now. + os.umask(0o027) + self.build() + # And keep umask 027 for the install step too. + self.install() + + for executable in [ + 'bin/prog', + 'share/subdir/sayhello', + ]: + f = os.path.join(self.installdir, 'usr', *executable.split('/')) + found_mode = stat.filemode(os.stat(f).st_mode) + want_mode = '-rwxr-xr-x' + self.assertEqual(want_mode, found_mode, + msg=('Expected file %s to have mode %s but found %s instead.' % + (executable, want_mode, found_mode))) + + for directory in [ + 'usr', + 'usr/bin', + 'usr/include', + 'usr/share', + 'usr/share/man', + 'usr/share/man/man1', + 'usr/share/subdir', + ]: + f = os.path.join(self.installdir, *directory.split('/')) + found_mode = stat.filemode(os.stat(f).st_mode) + want_mode = 'drwxr-xr-x' + self.assertEqual(want_mode, found_mode, + msg=('Expected directory %s to have mode %s but found %s instead.' % + (directory, want_mode, found_mode))) + + for datafile in [ + 'include/sample.h', + 'share/datafile.cat', + 'share/file.dat', + 'share/man/man1/prog.1.gz', + 'share/subdir/datafile.dog', + ]: + f = os.path.join(self.installdir, 'usr', *datafile.split('/')) + found_mode = stat.filemode(os.stat(f).st_mode) + want_mode = '-rw-r--r--' + self.assertEqual(want_mode, found_mode, + msg=('Expected file %s to have mode %s but found %s instead.' % + (datafile, want_mode, found_mode))) + def test_cpp_std_override(self): testdir = os.path.join(self.unit_test_dir, '6 std override') self.init(testdir) diff --git a/test cases/unit/24 install umask/datafile.cat b/test cases/unit/24 install umask/datafile.cat new file mode 100644 index 000000000..53d81fc2a --- /dev/null +++ b/test cases/unit/24 install umask/datafile.cat @@ -0,0 +1 @@ +Installed cat is installed. diff --git a/test cases/unit/24 install umask/meson.build b/test cases/unit/24 install umask/meson.build new file mode 100644 index 000000000..225f71c67 --- /dev/null +++ b/test cases/unit/24 install umask/meson.build @@ -0,0 +1,7 @@ +project('install umask', 'c') +executable('prog', 'prog.c', install : true) +install_headers('sample.h') +install_man('prog.1') +install_data('datafile.cat', install_dir : get_option('prefix') + '/share') +install_subdir('subdir', install_dir : get_option('prefix') + '/share') +meson.add_install_script('myinstall.py', 'share', 'file.dat') diff --git a/test cases/unit/24 install umask/myinstall.py b/test cases/unit/24 install umask/myinstall.py new file mode 100644 index 000000000..db6a51c2b --- /dev/null +++ b/test cases/unit/24 install umask/myinstall.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import os +import sys + +prefix = os.environ['MESON_INSTALL_DESTDIR_PREFIX'] + +dirname = os.path.join(prefix, sys.argv[1]) + +try: + os.makedirs(dirname) +except FileExistsError: + if not os.path.isdir(dirname): + raise + +with open(os.path.join(dirname, sys.argv[2]), 'w') as f: + f.write('') diff --git a/test cases/unit/24 install umask/prog.1 b/test cases/unit/24 install umask/prog.1 new file mode 100644 index 000000000..08ef7da62 --- /dev/null +++ b/test cases/unit/24 install umask/prog.1 @@ -0,0 +1 @@ +Man up, you. diff --git a/test cases/unit/24 install umask/prog.c b/test cases/unit/24 install umask/prog.c new file mode 100644 index 000000000..0f0061d2a --- /dev/null +++ b/test cases/unit/24 install umask/prog.c @@ -0,0 +1,3 @@ +int main(int argc, char **arv) { + return 0; +} diff --git a/test cases/unit/24 install umask/sample.h b/test cases/unit/24 install umask/sample.h new file mode 100644 index 000000000..dc030dac1 --- /dev/null +++ b/test cases/unit/24 install umask/sample.h @@ -0,0 +1,6 @@ +#ifndef SAMPLE_H +#define SAMPLE_H + +int wackiness(); + +#endif diff --git a/test cases/unit/24 install umask/subdir/datafile.dog b/test cases/unit/24 install umask/subdir/datafile.dog new file mode 100644 index 000000000..7a5bcb765 --- /dev/null +++ b/test cases/unit/24 install umask/subdir/datafile.dog @@ -0,0 +1 @@ +Installed dog is installed. diff --git a/test cases/unit/24 install umask/subdir/sayhello b/test cases/unit/24 install umask/subdir/sayhello new file mode 100755 index 000000000..1e1c90a85 --- /dev/null +++ b/test cases/unit/24 install umask/subdir/sayhello @@ -0,0 +1,2 @@ +#!/bin/sh +echo 'Hello, World!' From a98e9a1b70b43c06bb634521691aa516f7d2387d Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Mon, 12 Mar 2018 00:38:51 -0700 Subject: [PATCH 4/5] Add release-notes snippet for install_umask --- docs/markdown/snippets/install_umask.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/markdown/snippets/install_umask.md diff --git a/docs/markdown/snippets/install_umask.md b/docs/markdown/snippets/install_umask.md new file mode 100644 index 000000000..b3a242787 --- /dev/null +++ b/docs/markdown/snippets/install_umask.md @@ -0,0 +1,17 @@ +## New built-in option install_umask with a default value 022 + +This umask is used to define the default permissions of files and directories +created in the install tree. Files will preserve their executable mode, but the +exact permissions will obey the install_umask. + +The install_umask can be overridden in the meson command-line: + + $ meson --install-umask=027 builddir/ + +A project can also override the default in the project() call: + + project('myproject', 'c', + default_options : ['install_umask=027']) + +To disable the install_umask, set it to 'preserve', in which case permissions +are copied from the files in their origin. From 170776d626373762b220aad8ac7e5b57f18156ed Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Wed, 25 Apr 2018 14:11:06 -0700 Subject: [PATCH 5/5] Add install_umask to list of options of `meson configure` Tested: $ ./meson.py configure --help [...] --install-umask INSTALL_UMASK Default umask to apply on permissions of installed files (default: 022). --- mesonbuild/mconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py index 9a113327a..fd4c141af 100644 --- a/mesonbuild/mconf.py +++ b/mesonbuild/mconf.py @@ -152,7 +152,7 @@ class Conf: print(' Build dir ', self.build.environment.build_dir) print('\nCore options:\n') carr = [] - for key in ['buildtype', 'warning_level', 'werror', 'strip', 'unity', 'default_library']: + for key in ['buildtype', 'warning_level', 'werror', 'strip', 'unity', 'default_library', 'install_umask']: carr.append({'name': key, 'descr': coredata.get_builtin_option_description(key), 'value': self.coredata.get_builtin_option(key),