diff options
-rw-r--r-- | mesonbuild/backend/backends.py | 3 | ||||
-rw-r--r-- | mesonbuild/backend/ninjabackend.py | 4 | ||||
-rw-r--r-- | mesonbuild/build.py | 3 | ||||
-rw-r--r-- | mesonbuild/interpreter.py | 36 | ||||
-rw-r--r-- | mesonbuild/mesonlib.py | 104 | ||||
-rw-r--r-- | mesonbuild/scripts/meson_install.py | 32 | ||||
-rwxr-xr-x | run_unittests.py | 89 | ||||
-rw-r--r-- | test cases/common/12 data/installed_files.txt | 1 | ||||
-rw-r--r-- | test cases/common/12 data/meson.build | 11 | ||||
-rw-r--r-- | test cases/common/12 data/runscript.sh | 3 | ||||
-rw-r--r-- | test cases/common/66 install subdir/meson.build | 4 | ||||
-rw-r--r-- | test cases/common/66 install subdir/subdir/meson.build | 4 |
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---') |