Skip to content

Commit

Permalink
Add Polkit support for installation
Browse files Browse the repository at this point in the history
  • Loading branch information
refi64 committed Jan 25, 2018
1 parent e14dcc6 commit d0f0585
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 20 deletions.
7 changes: 6 additions & 1 deletion lib/fbuild/context.py
Expand Up @@ -252,7 +252,12 @@ def timeout_function(p):
return stdout, stderr

def install(self, path, target, *, rename=None, perms=None):
"""Set the given file to be installed after the build completes."""
"""Set the given file to be installed after the build completes.
*path* is the path of the file to install, and *target* is a subdirectory
of the installation prefix where the file should be installed to. If *rename*
is given, it should be a new basename for the file when installed into the
target directory."""
self.to_install.append((Path(path).abspath(), target, rename, perms))

# ------------------------------------------------------------------------------
Expand Down
163 changes: 163 additions & 0 deletions lib/fbuild/install.py
@@ -0,0 +1,163 @@
import base64
import os
import pickle
import subprocess
import sys

import fbuild
import fbuild.builders
import fbuild.path
import fbuild.subprocess.killableprocess

# ------------------------------------------------------------------------------


class Commander: pass


class LocalCommander:
def install(self, file, target, perms=None):
file.copy(target)
if perms is not None:
file.chmod(perms)

def close(self): pass


class PolkitCommander:
def __init__(self, proc):
self.proc = proc

self._send('chdir', os.getcwd())

def _send(self, *message):
encoded = self._encode(message)
self.proc.stdin.write(encoded)
self.proc.stdin.write('\n')
self.proc.stdin.flush()

@staticmethod
def _encode(data):
return base64.b64encode(pickle.dumps(data)).decode('ascii')

@staticmethod
def _decode(data):
return pickle.loads(base64.b64decode(data.encode('ascii')))

def install(self, file, target, perms=None):
self._send('install', file, target, perms)

def close(self):
self._send('close')
self.proc.wait()


def polkit_process():
commander = LocalCommander()

sys.stdout.write('\n')
sys.stdout.close()
sys.stdout = sys.stderr

for line in sys.stdin:
message = PolkitCommander._decode(line.strip())
command, args = message[0], message[1:]

if command == 'chdir':
os.chdir(args[0])
elif command == 'install':
commander.install(*args)
elif command == 'close':
sys.exit()


class Installer:
"""An Installer manages installation of all the files marked for installation."""

def __init__(self, ctx):
self.ctx = ctx
self.privileged = False

def _install(self, commander):
for file, subdir, rename, perms in self.ctx.to_install:
# Generate the full subdirectory.
target_root = fbuild.path.Path(subdir).addroot(self.ctx.install_prefix)
target_root.makedirs(exist_ok=True)

# Generate the target path.
target = target_root / (rename or file.basename())
file = file.relpath(file.getcwd())

# Install the file.
self.ctx.logger.check(' * install', '%s -> %s' % (file, target),
color='yellow')
commander.install(file, target, perms)

def _find_pkexec(self):
try:
return fbuild.builders.find_program(self.ctx, ['pkexec'], quieter=1)
except fbuild.builders.MissingProgram:
return None

def _load_polkit_process(self, pkexec):
root_module = fbuild.path.Path(fbuild.__file__)
root_directory = root_module.parent.parent

# Make sure Fbuild can be found.
env = os.environ.copy()
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = '%s:%s' % (root_directory, env['PYTHONPATH'])
else:
env['PYTHONPATH'] = root_directory

# Run the process.
cmd = [pkexec, sys.executable, '-m', 'fbuild.install', 'polkit']
proc = fbuild.subprocess.killableprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
universal_newlines=True)

# Make sure it's ready first.
proc.stdout.readline()
return proc

# try:
# p.wait()
# except KeyboardInterrupt:
# os.kill(os.getpid(), signal.SIGKILL)
# # p.wait()
# raise

# if p.returncode != 0:
# self.ctx.logger.log("Failed to run 'fbuild install' via pkexec.", color='red')
# sys.exit(p.returncode)

def install(self):
"""Install all the files that are marked for installation. If the user does
not have permissions to install to the installation prefix, but Polkit's pkexec
is present, then pkexec will be used to spawn a privileged Fbuild process that
can install the files."""

commander = LocalCommander()

if not self.ctx.install_prefix.access(os.W_OK):
pkexec = None

if not self.privileged:
pkexec = self._find_pkexec()

if pkexec is None:
self.ctx.logger.log('Warning: You do not have write access for the ' \
'installation directory.', color='red')
else:
proc = self._load_polkit_process(pkexec)
commander = PolkitCommander(proc)

try:
self._install(commander)
finally:
commander.close()


if __name__ == '__main__':
assert sys.argv[1] == 'polkit'
polkit_process()
19 changes: 3 additions & 16 deletions lib/fbuild/main.py
Expand Up @@ -14,6 +14,7 @@
import fbuild.context
import fbuild.options
import fbuild.builders.file
import fbuild.install

# If we can't import fbuildroot, save the exception and raise it later.
try:
Expand Down Expand Up @@ -72,22 +73,8 @@ def parse_args(argv):
# ------------------------------------------------------------------------------

def install_files(ctx):
for file, subdir, rename, perms in ctx.to_install:
# Generate the full subdirectory.
target_root = fbuild.path.Path(subdir).addroot(ctx.install_prefix)
target_root.makedirs(exist_ok=True)

# Generate the target path.
target = target_root / (rename or file.basename())
file = file.relpath(file.getcwd())

# Copy the file.
ctx.logger.check(' * install', '%s -> %s' % (file, target), color='yellow')
file.copy(target)

# Set permissions.
if perms is not None:
file.chmod(perms)
installer = fbuild.install.Installer(ctx)
installer.install()

# ------------------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions lib/fbuild/path.py
Expand Up @@ -98,8 +98,8 @@ def parent(self):
# -------------------------------------------------------------------------
# methods

def access(self):
return os.access(self)
def access(self, *args, **kw):
return os.access(self, *args, **kw)

def addprefix(self, prefix):
"""Add the prefix before the basename of the path.
Expand Down
1 change: 1 addition & 0 deletions lib/fbuild/subprocess/killableprocess.py
Expand Up @@ -250,6 +250,7 @@ def wait(self, timeout=-1, group=True):
# time.sleep is interrupted by signals (good!)
newtimeout = timeout - time.time() + starttime
time.sleep(newtimeout)
time.sleep(0.1)

self.kill(group)
signal.signal(signal.SIGCHLD, oldsignal)
Expand Down
23 changes: 23 additions & 0 deletions misc/com.github.fbuild.install.policy
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD polkit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd">
<policyconfig>

<vendor>The Fbuild Build System</vendor>
<vendor_url>https://github.com/felix-lang/fbuild</vendor_url>

<action id="org.github.fbuild.install.run">
<description>Install the given project via Fbuild</description>
<message>Authentication is required to install the project via Fbuild</message>
<icon_name>audio-x-generic</icon_name>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/python</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">-m</annotate>
<annotate key="org.freedesktop.policykit.exec.argv2">fbuild.install</annotate>
</action>

</policyconfig>
4 changes: 3 additions & 1 deletion setup.py
Expand Up @@ -11,8 +11,10 @@
cmdclass = {}

data_files = []
if sys.platform != 'win32':
if sys.platform.startswith('linux'):
data_files.append(('/usr/local/share/uprocd/modules', ['misc/fbuild.module']))
data_files.append(('/usr/share/polkit-1/actions',
['misc/com.github.fbuild.install.policy']))

setup(
name='fbuild',
Expand Down

0 comments on commit d0f0585

Please sign in to comment.