Merge pull request #3225 from filbranden/fixperms3

Introduce install_umask to determine permissions of files in install tree. Default it to 022
pull/3408/merge
Jussi Pakkanen 7 years ago committed by GitHub
commit 9b0453d3e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      docs/markdown/snippets/install_umask.md
  2. 4
      mesonbuild/backend/backends.py
  3. 4
      mesonbuild/backend/ninjabackend.py
  4. 25
      mesonbuild/coredata.py
  5. 2
      mesonbuild/mconf.py
  6. 41
      mesonbuild/scripts/meson_install.py
  7. 68
      run_unittests.py
  8. 3
      test cases/common/12 data/meson.build
  9. 3
      test cases/common/66 install subdir/meson.build
  10. 1
      test cases/unit/24 install umask/datafile.cat
  11. 7
      test cases/unit/24 install umask/meson.build
  12. 17
      test cases/unit/24 install umask/myinstall.py
  13. 1
      test cases/unit/24 install umask/prog.1
  14. 3
      test cases/unit/24 install umask/prog.c
  15. 6
      test cases/unit/24 install umask/sample.h
  16. 1
      test cases/unit/24 install umask/subdir/datafile.dog
  17. 2
      test cases/unit/24 install umask/subdir/sayhello

@ -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.

@ -37,11 +37,13 @@ class CleanTrees:
self.trees = trees self.trees = trees
class InstallData: 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.source_dir = source_dir
self.build_dir = build_dir self.build_dir = build_dir
self.prefix = prefix self.prefix = prefix
self.strip_bin = strip_bin self.strip_bin = strip_bin
self.install_umask = install_umask
self.targets = [] self.targets = []
self.headers = [] self.headers = []
self.man = [] self.man = []

@ -671,7 +671,9 @@ int dummy;
d = InstallData(self.environment.get_source_dir(), d = InstallData(self.environment.get_source_dir(),
self.environment.get_build_dir(), self.environment.get_build_dir(),
self.environment.get_prefix(), 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 = NinjaBuildElement(self.all_outputs, 'meson-install', 'CUSTOM_COMMAND', 'PHONY')
elem.add_dep('all') elem.add_dep('all')
elem.add_item('DESC', 'Installing files.') elem.add_item('DESC', 'Installing files.')

@ -105,6 +105,22 @@ class UserIntegerOption(UserOption):
except ValueError: except ValueError:
raise MesonException('Value string "%s" is not convertable to an integer.' % valuestring) 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): class UserComboOption(UserOption):
def __init__(self, name, description, choices, value, yielding=None): def __init__(self, name, description, choices, value, yielding=None):
super().__init__(name, description, choices, yielding) super().__init__(name, description, choices, yielding)
@ -351,12 +367,12 @@ def is_builtin_option(optname):
def get_builtin_option_choices(optname): def get_builtin_option_choices(optname):
if is_builtin_option(optname): if is_builtin_option(optname):
if builtin_options[optname][0] == UserStringOption: if builtin_options[optname][0] == UserComboOption:
return None return builtin_options[optname][2]
elif builtin_options[optname][0] == UserBooleanOption: elif builtin_options[optname][0] == UserBooleanOption:
return [True, False] return [True, False]
else: else:
return builtin_options[optname][2] return None
else: else:
raise RuntimeError('Tried to get the supported values for an unknown builtin option \'%s\'.' % optname) raise RuntimeError('Tried to get the supported values for an unknown builtin option \'%s\'.' % optname)
@ -385,6 +401,8 @@ def get_builtin_option_default(optname, prefix='', noneIfSuppress=False):
o = builtin_options[optname] o = builtin_options[optname]
if o[0] == UserComboOption: if o[0] == UserComboOption:
return o[3] return o[3]
if o[0] == UserIntegerOption:
return o[4]
if optname in builtin_dir_noprefix_options: if optname in builtin_dir_noprefix_options:
if noneIfSuppress: if noneIfSuppress:
# Return None if argparse defaulting should be suppressed for # Return None if argparse defaulting should be suppressed for
@ -444,6 +462,7 @@ builtin_options = {
'backend': [UserComboOption, 'Backend to use.', backendlist, 'ninja'], 'backend': [UserComboOption, 'Backend to use.', backendlist, 'ninja'],
'stdsplit': [UserBooleanOption, 'Split stdout and stderr in test logs.', True], 'stdsplit': [UserBooleanOption, 'Split stdout and stderr in test logs.', True],
'errorlogs': [UserBooleanOption, "Whether to print the logs from failing tests.", True], 'errorlogs': [UserBooleanOption, "Whether to print the logs from failing tests.", True],
'install_umask': [UserUmaskOption, 'Default umask to apply on permissions of installed files.', '022'],
} }
# Special prefix-dependent defaults for installation directories that reside in # Special prefix-dependent defaults for installation directories that reside in

@ -152,7 +152,7 @@ class Conf:
print(' Build dir ', self.build.environment.build_dir) print(' Build dir ', self.build.environment.build_dir)
print('\nCore options:\n') print('\nCore options:\n')
carr = [] 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, carr.append({'name': key,
'descr': coredata.get_builtin_option_description(key), 'descr': coredata.get_builtin_option_description(key),
'value': self.coredata.get_builtin_option(key), 'value': self.coredata.get_builtin_option(key),

@ -51,12 +51,25 @@ class DirMaker:
for d in self.dirs: for d in self.dirs:
append_to_log(d) append_to_log(d)
def set_mode(path, mode): def is_executable(path):
if mode is None: '''Checks whether any of the "x" bits are set in the source file mode.'''
# Keep mode unchanged return bool(os.stat(path).st_mode & 0o111)
def sanitize_permissions(path, umask):
if umask is None:
return return
if (mode.perms_s or mode.owner or mode.group) is None: new_perms = 0o777 if is_executable(path) else 0o666
# Nothing to set 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 return
# No chown() on Windows, and must set one of owner/group # No chown() on Windows, and must set one of owner/group
if not is_windows() and (mode.owner or mode.group) is not None: 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: except PermissionError as e:
msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...' msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
print(msg.format(path, mode.perms_s, e.strerror)) print(msg.format(path, mode.perms_s, e.strerror))
else:
sanitize_permissions(path, default_umask)
def restore_selinux_contexts(): def restore_selinux_contexts():
''' '''
@ -180,6 +195,7 @@ def do_copydir(data, src_dir, dst_dir, exclude):
sys.exit(1) sys.exit(1)
data.dirmaker.makedirs(abs_dst) data.dirmaker.makedirs(abs_dst)
shutil.copystat(abs_src, abs_dst) shutil.copystat(abs_src, abs_dst)
sanitize_permissions(abs_dst, data.install_umask)
for f in files: for f in files:
abs_src = os.path.join(root, f) abs_src = os.path.join(root, f)
filepart = os.path.relpath(abs_src, start=src_dir) 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) os.mkdir(parent_dir)
shutil.copystat(os.path.dirname(abs_src), parent_dir) shutil.copystat(os.path.dirname(abs_src), parent_dir)
shutil.copy2(abs_src, abs_dst, follow_symlinks=False) shutil.copy2(abs_src, abs_dst, follow_symlinks=False)
sanitize_permissions(abs_dst, data.install_umask)
append_to_log(abs_dst) append_to_log(abs_dst)
def get_destdir_path(d, path): def get_destdir_path(d, path):
@ -210,6 +227,8 @@ def do_install(datafilename):
d.destdir = os.environ.get('DESTDIR', '') d.destdir = os.environ.get('DESTDIR', '')
d.fullprefix = destdir_join(d.destdir, d.prefix) d.fullprefix = destdir_join(d.destdir, d.prefix)
if d.install_umask is not None:
os.umask(d.install_umask)
d.dirmaker = DirMaker() d.dirmaker = DirMaker()
with d.dirmaker: with d.dirmaker:
install_subdirs(d) # Must be first, because it needs to delete the old subtree. 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)) print('Installing subdir %s to %s' % (src_dir, full_dst_dir))
d.dirmaker.makedirs(full_dst_dir, exist_ok=True) d.dirmaker.makedirs(full_dst_dir, exist_ok=True)
do_copydir(d, src_dir, full_dst_dir, exclude) 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): def install_data(d):
for i in d.data: for i in d.data:
@ -237,7 +256,7 @@ def install_data(d):
d.dirmaker.makedirs(outdir, exist_ok=True) d.dirmaker.makedirs(outdir, exist_ok=True)
print('Installing %s to %s' % (fullfilename, outdir)) print('Installing %s to %s' % (fullfilename, outdir))
do_copyfile(fullfilename, outfilename) do_copyfile(fullfilename, outfilename)
set_mode(outfilename, mode) set_mode(outfilename, mode, d.install_umask)
def install_man(d): def install_man(d):
for m in d.man: for m in d.man:
@ -256,6 +275,7 @@ def install_man(d):
append_to_log(outfilename) append_to_log(outfilename)
else: else:
do_copyfile(full_source_filename, outfilename) do_copyfile(full_source_filename, outfilename)
sanitize_permissions(outfilename, d.install_umask)
def install_headers(d): def install_headers(d):
for t in d.headers: for t in d.headers:
@ -266,6 +286,7 @@ def install_headers(d):
print('Installing %s to %s' % (fname, outdir)) print('Installing %s to %s' % (fname, outdir))
d.dirmaker.makedirs(outdir, exist_ok=True) d.dirmaker.makedirs(outdir, exist_ok=True)
do_copyfile(fullfilename, outfilename) do_copyfile(fullfilename, outfilename)
sanitize_permissions(outfilename, d.install_umask)
def run_install_script(d): def run_install_script(d):
env = {'MESON_SOURCE_ROOT': d.source_dir, 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)) raise RuntimeError('File {!r} could not be found'.format(fname))
elif os.path.isfile(fname): elif os.path.isfile(fname):
do_copyfile(fname, outname) do_copyfile(fname, outname)
sanitize_permissions(outname, d.install_umask)
if should_strip and d.strip_bin is not None: if should_strip and d.strip_bin is not None:
if fname.endswith('.jar'): if fname.endswith('.jar'):
print('Not stripping jar target:', os.path.basename(fname)) 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' pdb_outname = os.path.splitext(outname)[0] + '.pdb'
print('Installing pdb file %s to %s' % (pdb_filename, pdb_outname)) print('Installing pdb file %s to %s' % (pdb_filename, pdb_outname))
do_copyfile(pdb_filename, pdb_outname) do_copyfile(pdb_filename, pdb_outname)
sanitize_permissions(pdb_outname, d.install_umask)
elif os.path.isdir(fname): elif os.path.isdir(fname):
fname = os.path.join(d.build_dir, fname.rstrip('/')) 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: else:
raise RuntimeError('Unknown file type for {!r}'.format(fname)) raise RuntimeError('Unknown file type for {!r}'.format(fname))
printed_symlink_error = False printed_symlink_error = False

@ -2731,6 +2731,74 @@ class LinuxlikeTests(BasePlatformTests):
# The chown failed nonfatally if we're not root # The chown failed nonfatally if we're not root
self.assertEqual(0, statf.st_uid) 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): def test_cpp_std_override(self):
testdir = os.path.join(self.unit_test_dir, '6 std override') testdir = os.path.join(self.unit_test_dir, '6 std override')
self.init(testdir) self.init(testdir)

@ -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') 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 # 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') install_data(sources : 'etcfile.dat', install_dir : '/etc', install_mode : 'rw------T')

@ -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: # A subdir with an exclusion:
install_subdir('sub2', install_subdir('sub2',

@ -0,0 +1 @@
Installed cat is installed.

@ -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')

@ -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('')

@ -0,0 +1,3 @@
int main(int argc, char **arv) {
return 0;
}

@ -0,0 +1,6 @@
#ifndef SAMPLE_H
#define SAMPLE_H
int wackiness();
#endif

@ -0,0 +1,2 @@
#!/bin/sh
echo 'Hello, World!'
Loading…
Cancel
Save