Merge pull request #3225 from filbranden/fixperms3

Introduce install_umask to determine permissions of files in install tree. Default it to 022
This commit is contained in:
Jussi Pakkanen 2018-04-26 23:14:00 +03:00 committed by GitHub
commit 9b0453d3e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 189 additions and 16 deletions

View File

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

View File

@ -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 = []

View File

@ -671,7 +671,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.')

View File

@ -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)
@ -351,12 +367,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)
@ -385,6 +401,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
@ -444,6 +462,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.', '022'],
}
# Special prefix-dependent defaults for installation directories that reside in

View File

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

View File

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

View File

@ -2731,6 +2731,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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Man up, you.

View File

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

View File

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

View File

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

View File

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