Merge pull request #1302 from centricular/install-mode

Support file perms for install_data and install_subdir
pull/1332/head
Jussi Pakkanen 8 years ago committed by GitHub
commit 4c9c14121f
  1. 3
      mesonbuild/backend/backends.py
  2. 4
      mesonbuild/backend/ninjabackend.py
  3. 3
      mesonbuild/build.py
  4. 1
      mesonbuild/dependencies.py
  5. 36
      mesonbuild/interpreter.py
  6. 104
      mesonbuild/mesonlib.py
  7. 32
      mesonbuild/scripts/meson_install.py
  8. 89
      run_unittests.py
  9. 1
      test cases/common/12 data/installed_files.txt
  10. 11
      test cases/common/12 data/meson.build
  11. 3
      test cases/common/12 data/runscript.sh
  12. 4
      test cases/common/66 install subdir/meson.build
  13. 4
      test cases/common/66 install subdir/subdir/meson.build

@ -459,7 +459,8 @@ class Backend:
mfobj['projects'] = self.build.dep_manifest
with open(ifilename, 'w') as f:
f.write(json.dumps(mfobj))
d.data.append([ifilename, ofilename])
# Copy file from, to, and with mode unchanged
d.data.append([ifilename, ofilename, None])
def get_regen_filelist(self):
'''List of all files whose alteration means that the build

@ -700,7 +700,7 @@ int dummy;
assert(isinstance(f, mesonlib.File))
plain_f = os.path.split(f.fname)[1]
dstabs = os.path.join(subdir, plain_f)
i = [f.absolute_path(srcdir, builddir), dstabs]
i = [f.absolute_path(srcdir, builddir), dstabs, de.install_mode]
d.data.append(i)
def generate_subdir_install(self, d):
@ -715,7 +715,7 @@ int dummy;
inst_dir = sd.installable_subdir
src_dir = os.path.join(self.environment.get_source_dir(), subdir)
dst_dir = os.path.join(self.environment.get_prefix(), sd.install_dir)
d.install_subdirs.append([src_dir, inst_dir, dst_dir])
d.install_subdirs.append([src_dir, inst_dir, dst_dir, sd.install_mode])
def generate_tests(self, outfile):
self.serialise_tests()

@ -1502,9 +1502,10 @@ class ConfigurationData:
# A bit poorly named, but this represents plain data files to copy
# during install.
class Data:
def __init__(self, sources, install_dir):
def __init__(self, sources, install_dir, install_mode=None):
self.sources = sources
self.install_dir = install_dir
self.install_mode = install_mode
if not isinstance(self.sources, list):
self.sources = [self.sources]
for s in self.sources:

@ -318,6 +318,7 @@ class WxDependency(Dependency):
def __init__(self, environment, kwargs):
Dependency.__init__(self, 'wx')
self.is_found = False
self.modversion = 'none'
if WxDependency.wx_found is None:
self.check_wxconfig()
if not WxDependency.wx_found:

@ -22,7 +22,7 @@ from . import optinterpreter
from . import compilers
from .wrap import wrap
from . import mesonlib
from .mesonlib import Popen_safe
from .mesonlib import FileMode, Popen_safe
from .dependencies import InternalDependency, Dependency
from .interpreterbase import InterpreterBase
from .interpreterbase import check_stringlist, noPosargs, noKwargs, stringArgs
@ -453,11 +453,12 @@ class DataHolder(InterpreterObject):
return self.held_object.install_dir
class InstallDir(InterpreterObject):
def __init__(self, source_subdir, installable_subdir, install_dir):
def __init__(self, src_subdir, inst_subdir, install_dir, install_mode):
InterpreterObject.__init__(self)
self.source_subdir = source_subdir
self.installable_subdir = installable_subdir
self.source_subdir = src_subdir
self.installable_subdir = inst_subdir
self.install_dir = install_dir
self.install_mode = install_mode
class Man(InterpreterObject):
@ -2141,6 +2142,25 @@ requirements use the version keyword argument instead.''')
self.evaluate_codeblock(codeblock)
self.subdir = prev_subdir
def _get_kwarg_install_mode(self, kwargs):
if 'install_mode' not in kwargs:
return None
install_mode = []
mode = mesonlib.stringintlistify(kwargs.get('install_mode', []))
for m in mode:
# We skip any arguments that are set to `false`
if m is False:
m = None
install_mode.append(m)
if len(install_mode) > 3:
raise InvalidArguments('Keyword argument install_mode takes at '
'most 3 arguments.')
if len(install_mode) > 0 and install_mode[0] is not None and \
not isinstance(install_mode[0], str):
raise InvalidArguments('Keyword argument install_mode requires the '
'permissions arg to be a string or false')
return FileMode(*install_mode)
def func_install_data(self, node, args, kwargs):
kwsource = mesonlib.stringlistify(kwargs.get('sources', []))
raw_sources = args + kwsource
@ -2153,7 +2173,10 @@ requirements use the version keyword argument instead.''')
source_strings.append(s)
sources += self.source_strings_to_files(source_strings)
install_dir = kwargs.get('install_dir', None)
data = DataHolder(build.Data(sources, install_dir))
if not isinstance(install_dir, (str, type(None))):
raise InvalidArguments('Keyword argument install_dir not a string.')
install_mode = self._get_kwarg_install_mode(kwargs)
data = DataHolder(build.Data(sources, install_dir, install_mode))
self.build.data.append(data.held_object)
return data
@ -2166,7 +2189,8 @@ requirements use the version keyword argument instead.''')
install_dir = kwargs['install_dir']
if not isinstance(install_dir, str):
raise InvalidArguments('Keyword argument install_dir not a string.')
idir = InstallDir(self.subdir, args[0], install_dir)
install_mode = self._get_kwarg_install_mode(kwargs)
idir = InstallDir(self.subdir, args[0], install_dir, install_mode)
self.build.install_dirs.append(idir)
return idir

@ -14,6 +14,7 @@
"""A library of random helper functionality."""
import stat
import platform, subprocess, operator, os, shutil, re
from glob import glob
@ -24,6 +25,97 @@ class MesonException(Exception):
class EnvironmentException(MesonException):
'''Exceptions thrown while processing and creating the build environment'''
class FileMode:
# The first triad is for owner permissions, the second for group permissions,
# and the third for others (everyone else).
# For the 1st character:
# 'r' means can read
# '-' means not allowed
# For the 2nd character:
# 'w' means can write
# '-' means not allowed
# For the 3rd character:
# 'x' means can execute
# 's' means can execute and setuid/setgid is set (owner/group triads only)
# 'S' means cannot execute and setuid/setgid is set (owner/group triads only)
# 't' means can execute and sticky bit is set ("others" triads only)
# 'T' means cannot execute and sticky bit is set ("others" triads only)
# '-' means none of these are allowed
#
# The meanings of 'rwx' perms is not obvious for directories; see:
# https://www.hackinglinuxexposed.com/articles/20030424.html
#
# For information on this notation such as setuid/setgid/sticky bits, see:
# https://en.wikipedia.org/wiki/File_system_permissions#Symbolic_notation
symbolic_perms_regex = re.compile('[r-][w-][xsS-]' # Owner perms
'[r-][w-][xsS-]' # Group perms
'[r-][w-][xtT-]') # Others perms
def __init__(self, perms=None, owner=None, group=None):
self.perms_s = perms
self.perms = self.perms_s_to_bits(perms)
self.owner = owner
self.group = group
def __repr__(self):
ret = '<FileMode: {!r} owner={} group={}'
return ret.format(self.perms_s, self.owner, self.group)
@classmethod
def perms_s_to_bits(cls, perms_s):
'''
Does the opposite of stat.filemode(), converts strings of the form
'rwxr-xr-x' to st_mode enums which can be passed to os.chmod()
'''
if perms_s is None:
# No perms specified, we will not touch the permissions
return -1
eg = 'rwxr-xr-x'
if not isinstance(perms_s, str):
msg = 'Install perms must be a string. For example, {!r}'
raise MesonException(msg.format(eg))
if len(perms_s) != 9 or not cls.symbolic_perms_regex.match(perms_s):
msg = 'File perms {!r} must be exactly 9 chars. For example, {!r}'
raise MesonException(msg.format(perms_s, eg))
perms = 0
# Owner perms
if perms_s[0] == 'r':
perms |= stat.S_IRUSR
if perms_s[1] == 'w':
perms |= stat.S_IWUSR
if perms_s[2] == 'x':
perms |= stat.S_IXUSR
elif perms_s[2] == 'S':
perms |= stat.S_ISUID
elif perms_s[2] == 's':
perms |= stat.S_IXUSR
perms |= stat.S_ISUID
# Group perms
if perms_s[3] == 'r':
perms |= stat.S_IRGRP
if perms_s[4] == 'w':
perms |= stat.S_IWGRP
if perms_s[5] == 'x':
perms |= stat.S_IXGRP
elif perms_s[5] == 'S':
perms |= stat.S_ISGID
elif perms_s[5] == 's':
perms |= stat.S_IXGRP
perms |= stat.S_ISGID
# Others perms
if perms_s[6] == 'r':
perms |= stat.S_IROTH
if perms_s[7] == 'w':
perms |= stat.S_IWOTH
if perms_s[8] == 'x':
perms |= stat.S_IXOTH
elif perms_s[8] == 'T':
perms |= stat.S_ISVTX
elif perms_s[8] == 't':
perms |= stat.S_IXOTH
perms |= stat.S_ISVTX
return perms
class File:
def __init__(self, is_built, subdir, fname):
self.is_built = is_built
@ -360,11 +452,21 @@ def replace_if_different(dst, dst_tmp):
else:
os.unlink(dst_tmp)
def stringintlistify(item):
if isinstance(item, (str, int)):
item = [item]
if not isinstance(item, list):
raise MesonException('Item must be a list, a string, or an int')
for i in item:
if not isinstance(i, (str, int, type(None))):
raise MesonException('List item must be a string or an int')
return item
def stringlistify(item):
if isinstance(item, str):
item = [item]
if not isinstance(item, list):
raise MesonException('Item is not an array')
raise MesonException('Item is not a list')
for i in item:
if not isinstance(i, str):
raise MesonException('List item not a string.')

@ -16,10 +16,34 @@ import sys, pickle, os, shutil, subprocess, gzip, platform
from glob import glob
from . import depfixer
from . import destdir_join
from ..mesonlib import Popen_safe
from ..mesonlib import is_windows, Popen_safe
install_log_file = None
def set_mode(path, mode):
if mode is None:
# Keep mode unchanged
return
if (mode.perms_s or mode.owner or mode.group) is None:
# Nothing to set
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:
try:
shutil.chown(path, mode.owner, mode.group)
except PermissionError as e:
msg = '{!r}: Unable to set owner {!r} and group {!r}: {}, ignoring...'
print(msg.format(path, mode.owner, mode.group, e.strerror))
# Must set permissions *after* setting owner/group otherwise the
# setuid/setgid bits will get wiped by chmod
# NOTE: On Windows you can set read/write perms; the rest are ignored
if mode.perms_s is not None:
try:
os.chmod(path, mode.perms)
except PermissionError as e:
msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
print(msg.format(path, mode.perms_s, e.strerror))
def append_to_log(line):
install_log_file.write(line)
if not line.endswith('\n'):
@ -96,7 +120,7 @@ def do_install(datafilename):
run_install_script(d)
def install_subdirs(data):
for (src_dir, inst_dir, dst_dir) in data.install_subdirs:
for (src_dir, inst_dir, dst_dir, mode) in data.install_subdirs:
if src_dir.endswith('/') or src_dir.endswith('\\'):
src_dir = src_dir[:-1]
src_prefix = os.path.join(src_dir, inst_dir)
@ -105,15 +129,19 @@ def install_subdirs(data):
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
do_copydir(src_prefix, src_dir, dst_dir)
dst_prefix = os.path.join(dst_dir, inst_dir)
set_mode(dst_prefix, mode)
def install_data(d):
for i in d.data:
fullfilename = i[0]
outfilename = get_destdir_path(d, i[1])
mode = i[2]
outdir = os.path.split(outfilename)[0]
os.makedirs(outdir, exist_ok=True)
print('Installing %s to %s.' % (fullfilename, outdir))
do_copyfile(fullfilename, outfilename)
set_mode(outfilename, mode)
def install_man(d):
for m in d.man:

@ -13,12 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import stat
import unittest, os, sys, shutil, time
import subprocess
import re, json
import tempfile
from glob import glob
import mesonbuild.environment
import mesonbuild.mesonlib
from mesonbuild.environment import detect_ninja, Environment
from mesonbuild.dependencies import PkgConfigDependency
@ -58,6 +60,42 @@ class InternalTests(unittest.TestCase):
self.assertEqual(searchfunc('foobar 2016.10.128'), 'unknown version')
self.assertEqual(searchfunc('2016.10.128'), 'unknown version')
def test_mode_symbolic_to_bits(self):
modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
self.assertEqual(modefunc('---------'), 0)
self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
self.assertEqual(modefunc('------r--'), stat.S_IROTH)
self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
self.assertEqual(modefunc('--S------'), stat.S_ISUID)
self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
# We could keep listing combinations exhaustively but that seems
# tedious and pointless. Just test a few more.
self.assertEqual(modefunc('rwxr-xr-x'),
stat.S_IRWXU |
stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
self.assertEqual(modefunc('rw-r--r--'),
stat.S_IRUSR | stat.S_IWUSR |
stat.S_IRGRP |
stat.S_IROTH)
self.assertEqual(modefunc('rwsr-x---'),
stat.S_IRWXU | stat.S_ISUID |
stat.S_IRGRP | stat.S_IXGRP)
class LinuxlikeTests(unittest.TestCase):
def setUp(self):
super().setUp()
@ -552,6 +590,57 @@ class LinuxlikeTests(unittest.TestCase):
self.init(testdir)
self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt')
def test_installed_modes(self):
'''
Test that files installed by these tests have the correct permissions.
Can't be an ordinary test because our installed_files.txt is very basic.
'''
# Test file modes
testdir = os.path.join(self.common_test_dir, '12 data')
self.init(testdir)
self.install()
f = os.path.join(self.installdir, 'etc', 'etcfile.dat')
found_mode = stat.filemode(os.stat(f).st_mode)
want_mode = 'rw------T'
self.assertEqual(want_mode, found_mode[1:])
f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh')
statf = os.stat(f)
found_mode = stat.filemode(statf.st_mode)
want_mode = 'rwxr-sr-x'
self.assertEqual(want_mode, found_mode[1:])
if os.getuid() == 0:
# The chown failed nonfatally if we're not root
self.assertEqual(0, statf.st_uid)
self.assertEqual(0, statf.st_gid)
f = os.path.join(self.installdir, 'usr', 'share', 'progname',
'fileobject_datafile.dat')
statf = os.stat(f)
found_mode = stat.filemode(statf.st_mode)
want_mode = 'rw-rw-r--'
self.assertEqual(want_mode, found_mode[1:])
self.assertEqual(os.getuid(), statf.st_uid)
if os.getuid() == 0:
# The chown failed nonfatally if we're not root
self.assertEqual(0, statf.st_gid)
self.wipe()
# Test directory modes
testdir = os.path.join(self.common_test_dir, '66 install subdir')
self.init(testdir)
self.install()
f = os.path.join(self.installdir, 'usr', 'share', 'sub1')
statf = os.stat(f)
found_mode = stat.filemode(statf.st_mode)
want_mode = 'rwxr-x--t'
self.assertEqual(want_mode, found_mode[1:])
if os.getuid() == 0:
# The chown failed nonfatally if we're not root
self.assertEqual(0, statf.st_uid)
class RewriterTests(unittest.TestCase):

@ -3,3 +3,4 @@ usr/share/progname/fileobject_datafile.dat
usr/share/progname/vanishing.dat
usr/share/progname/vanishing2.dat
etc/etcfile.dat
usr/bin/runscript.sh

@ -1,7 +1,14 @@
project('data install test', 'c')
install_data(sources : 'datafile.dat', install_dir : 'share/progname')
install_data(sources : 'etcfile.dat', install_dir : '/etc')
install_data(files('fileobject_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')
# Some script that needs to be executable by the group
install_data('runscript.sh',
install_dir : get_option('bindir'),
install_mode : ['rwxr-sr-x', 'root', 0])
install_data(files('fileobject_datafile.dat'),
install_dir : 'share/progname',
install_mode : [false, false, 0])
subdir('vanishing')

@ -0,0 +1,3 @@
#!/bin/sh
echo "Runscript"

@ -1,5 +1,7 @@
project('install a whole subdir', 'c')
subdir('subdir')
install_subdir('sub1', install_dir : 'share')
# A subdir with write perms only for the owner
# and read-list perms for owner and group
install_subdir('sub1', install_dir : 'share', install_mode : ['rwxr-x--t', 'root'])
install_subdir('sub/sub1', install_dir : 'share')

@ -1 +1,3 @@
install_subdir('sub1', install_dir : 'share')
install_subdir('sub1', install_dir : 'share',
# This mode will be overriden by the mode set in the outer install_subdir
install_mode : 'rwxr-x---')

Loading…
Cancel
Save