diff --git a/tests/install_group/wscript b/tests/install_group/wscript new file mode 100644 index 00000000..7ca72e3b --- /dev/null +++ b/tests/install_group/wscript @@ -0,0 +1,73 @@ +#! /usr/bin/env python + +from waflib import Utils, Build, Logs +import os + +GRP = 'adm' + +def test_chown(bld): + def create_and_chown(tsk): + tsk.outputs[0].write('test') + Utils.lchown(tsk.outputs[0].abspath(), -1, GRP) + bld.conf.env.CAN_CHOWN = True + bld(rule=create_and_chown, target='foo.txt', always=True) + +def test_grp(bld): + def check_path(tsk): + import grp + entry = grp.getgrnam(GRP) + assert entry[0] == GRP + bld.conf.env.CAN_GRP = True + bld(rule=check_path, always=True) + +def test_chown_install(bld): + bld.is_install = Build.INSTALL + + dest_file = bld.bldnode.make_node('test/foo') + dest_link = bld.bldnode.make_node('test/foo_link') + + tmpfile = bld.bldnode.make_node('foo.txt') + tmpfile.write('this is a test') + + bld.install_as(dest_file, + tmpfile, + install_group=GRP) + bld.symlink_as(dest_link, + 'foo', + install_group=GRP) + + bld.add_group() + + def check_path(tsk): + import grp + gid = grp.getgrnam(GRP)[2] + assert os.stat(dest_file.abspath()).st_gid == gid + assert os.stat(dest_link.abspath()).st_gid == gid + bld(rule=check_path, always=True) + +def configure(conf): + conf.test(build_fun=test_grp, + msg='Checking for the python module grp', + okmsg='ok', + errmsg='grp is missing', + mandatory=False) + if not conf.env.CAN_GRP: + return + + conf.test(build_fun=test_chown, + msg='Checking for Utils.lchown', + okmsg='ok', + errmsg='chown does not seem to work', + mandatory=False) + if not conf.env.CAN_CHOWN: + return + + Logs.info = Utils.nada + conf.test(build_fun=test_chown_install, + msg='Testing install_group="adm"', + okmsg='ok', + errmsg='there is a regression') + +def build(bld): + pass + diff --git a/waflib/Build.py b/waflib/Build.py index 07ccf850..af5c14e4 100644 --- a/waflib/Build.py +++ b/waflib/Build.py @@ -956,6 +956,8 @@ def add_install_task(self, **kw): tsk.install_to = tsk.dest = kw['install_to'] tsk.install_from = kw['install_from'] tsk.relative_base = kw.get('cwd') or kw.get('relative_base', self.path) + tsk.install_user = kw.get('install_user') + tsk.install_group = kw.get('install_group') tsk.init_files() if not kw.get('postpone', True): tsk.run_now() @@ -1065,7 +1067,9 @@ class inst(Task.Task): def copy_fun(self, src, tgt): """ - Copies a file from src to tgt, preserving permissions and trying to work around path limitations on Windows platforms + Copies a file from src to tgt, preserving permissions and trying to work + around path limitations on Windows platforms. On Unix-like platforms, + the owner/group of the target file may be set through install_user/install_group :param src: absolute path :type src: string @@ -1077,7 +1081,7 @@ class inst(Task.Task): if Utils.is_win32 and len(tgt) > 259 and not tgt.startswith('\\\\?\\'): tgt = '\\\\?\\' + tgt shutil.copy2(src, tgt) - os.chmod(tgt, self.chmod) + self.fix_perms(tgt) def rm_empty_dirs(self, tgt): """ @@ -1180,6 +1184,32 @@ class inst(Task.Task): Logs.error('Input %r is not a file', src) raise Errors.WafError('Could not install the file %r' % tgt, e) + def fix_perms(self, tgt): + """ + Change the ownership of the file/folder/link pointed by the given path + This looks up for `install_user` or `install_group` attributes + on the task or on the task generator:: + + def build(bld): + bld.install_as('${PREFIX}/wscript', + 'wscript', + install_user='nobody', install_group='nogroup') + bld.symlink_as('${PREFIX}/wscript_link', + Utils.subst_vars('${PREFIX}/wscript', bld.env), + install_user='nobody', install_group='nogroup') + """ + if not Utils.is_win32: + user = getattr(self, 'install_user', None) or getattr(self.generator, 'install_user', None) + group = getattr(self, 'install_group', None) or getattr(self.generator, 'install_group', None) + if user or group: + Utils.lchown(tgt, user or -1, group or -1) + if os.path.islink(tgt): + # BSD-specific + if hasattr(os, 'lchmod'): + os.lchmod(tgt, self.chmod) + else: + os.chmod(tgt, self.chmod) + def do_link(self, src, tgt, **kw): """ Creates a symlink from tgt to src. @@ -1200,6 +1230,7 @@ class inst(Task.Task): if not self.generator.bld.progress_bar: Logs.info('+ symlink %s (to %s)', tgt, src) os.symlink(src, tgt) + self.fix_perms(tgt) def do_uninstall(self, src, tgt, lbl, **kw): """ diff --git a/waflib/Context.py b/waflib/Context.py index 4c9e7bb7..3a65e986 100644 --- a/waflib/Context.py +++ b/waflib/Context.py @@ -283,7 +283,7 @@ class Context(ctx): if not user_function: if not mandatory: continue - raise Errors.WafError('No function %s defined in %s' % (name or self.fun, node.abspath())) + raise Errors.WafError('No function %r defined in %s' % (name or self.fun, node.abspath())) user_function(self) finally: self.post_recurse(node) diff --git a/waflib/Utils.py b/waflib/Utils.py index e461ad5b..230267e9 100644 --- a/waflib/Utils.py +++ b/waflib/Utils.py @@ -860,6 +860,30 @@ def run_prefork_process(cmd, kwargs, cargs): raise Exception(trace) return ret, out, err +def lchown(path, user=-1, group=-1): + """ + Change the owner/group of a path, raises an OSError if the + ownership change fails. + + :param user: user to change + :type user: int or str + :param group: group to change + :type group: int or str + """ + if isinstance(user, str): + import pwd + entry = pwd.getpwnam(user) + if not entry: + raise OSError('Unknown user %r' % user) + user = entry[2] + if isinstance(group, str): + import grp + entry = grp.getgrnam(group) + if not entry: + raise OSError('Unknown group %r' % group) + group = entry[2] + return os.lchown(path, user, group) + def run_regular_process(cmd, kwargs, cargs={}): """ Executes a subprocess command by using subprocess.Popen