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:
commit
9b0453d3e9
|
@ -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
|
||||
|
||||
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 = []
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 @@
|
|||
Man up, you.
|
|
@ -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 @@
|
|||
Installed dog is installed.
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
echo 'Hello, World!'
|
Loading…
Reference in New Issue