diff options
author | Nirbheek Chauhan <nirbheek@centricular.com> | 2017-01-14 14:41:18 +0530 |
---|---|---|
committer | Nirbheek Chauhan <nirbheek@centricular.com> | 2017-01-24 00:20:51 +0530 |
commit | 7d6f628ed4c3c3dca32bef01b2581f2e9bcde189 (patch) | |
tree | 995cb632db1f11e94209bfc18205f7c8e81f52f2 | |
parent | 905ff356118d3317aa1922bbfee3dd4c3cb71a6f (diff) | |
download | meson-7d6f628ed4c3c3dca32bef01b2581f2e9bcde189.zip meson-7d6f628ed4c3c3dca32bef01b2581f2e9bcde189.tar.gz meson-7d6f628ed4c3c3dca32bef01b2581f2e9bcde189.tar.bz2 |
Support file perms for install_data and install_subdir
With the 'install_mode' kwarg, you can now specify the file and
directory permissions and the owner and the group to be used while
installing. You can pass either:
* A single string specifying just the permissions
* A list of strings with:
- The first argument a string of permissions
- The second argument a string specifying the owner or
an int specifying the uid
- The third argument a string specifying the group or
an int specifying the gid
Specifying `false` as any of the arguments skips setting that one.
The format of the permissions kwarg is the same as the symbolic
notation used by ls -l with the first character that specifies 'd',
'-', 'c', etc for the file type omitted since that is always obvious
from the context.
Includes unit tests for the same. Sadly these only run on Linux right
now, but we want them to run on all platforms. We do set the mode in the
integration tests for all platforms but we don't check if they were
actually set correctly.
-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---') |