aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mesonbuild/backend/backends.py3
-rw-r--r--mesonbuild/backend/ninjabackend.py4
-rw-r--r--mesonbuild/build.py3
-rw-r--r--mesonbuild/interpreter.py36
-rw-r--r--mesonbuild/mesonlib.py104
-rw-r--r--mesonbuild/scripts/meson_install.py32
-rwxr-xr-xrun_unittests.py89
-rw-r--r--test cases/common/12 data/installed_files.txt1
-rw-r--r--test cases/common/12 data/meson.build11
-rw-r--r--test cases/common/12 data/runscript.sh3
-rw-r--r--test cases/common/66 install subdir/meson.build4
-rw-r--r--test cases/common/66 install subdir/subdir/meson.build4
12 files changed, 277 insertions, 17 deletions
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
index e46c2c5..6f8a50e 100644
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -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
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
index e1a478c..628718f 100644
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -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()
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
index ceae49b..dc072a5 100644
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -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:
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index b5cd0b6..cb5b617 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -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
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py
index 2587d6f..2ad43c8 100644
--- a/mesonbuild/mesonlib.py
+++ b/mesonbuild/mesonlib.py
@@ -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.')
diff --git a/mesonbuild/scripts/meson_install.py b/mesonbuild/scripts/meson_install.py
index 676a1e5..a74573e 100644
--- a/mesonbuild/scripts/meson_install.py
+++ b/mesonbuild/scripts/meson_install.py
@@ -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:
diff --git a/run_unittests.py b/run_unittests.py
index 6aa5b2b..5eba222 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -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):
diff --git a/test cases/common/12 data/installed_files.txt b/test cases/common/12 data/installed_files.txt
index 8651e3a..af1a735 100644
--- a/test cases/common/12 data/installed_files.txt
+++ b/test cases/common/12 data/installed_files.txt
@@ -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
diff --git a/test cases/common/12 data/meson.build b/test cases/common/12 data/meson.build
index 7494abc..d3407d1 100644
--- a/test cases/common/12 data/meson.build
+++ b/test cases/common/12 data/meson.build
@@ -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')
diff --git a/test cases/common/12 data/runscript.sh b/test cases/common/12 data/runscript.sh
new file mode 100644
index 0000000..8bc5ca6
--- /dev/null
+++ b/test cases/common/12 data/runscript.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Runscript"
diff --git a/test cases/common/66 install subdir/meson.build b/test cases/common/66 install subdir/meson.build
index 669cf09..fed2f89 100644
--- a/test cases/common/66 install subdir/meson.build
+++ b/test cases/common/66 install subdir/meson.build
@@ -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')
diff --git a/test cases/common/66 install subdir/subdir/meson.build b/test cases/common/66 install subdir/subdir/meson.build
index 08b417d..37d2da4 100644
--- a/test cases/common/66 install subdir/subdir/meson.build
+++ b/test cases/common/66 install subdir/subdir/meson.build
@@ -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---')