Search code examples
pythonarchlinuxpacman-package-manager

python script for installing aur packages


I'm trying to make a python script for installing aur packages such as google-chrome, sublime-text etc for a fresh version of arch linux. I can clone the git url just fine but I have problems using the makepkg program. I can't run the script as sudo because makepkg doesn't allow sudo authority because it can cause damage to the system but I need sudo authority to install it with pacman. I'll post what I have for the function so far and I would greatly appreciate if anyone can help me. (apologies in advance if it's a stupid question or the answer is easy, but I've spent a couple of hours on google and couldn't find the answer)

def clone_and_makepkg(package_name, aur_folder_path, password):
    git_url = "https://aur.archlinux.org/" + package_name + ".git"
    new_package_path = os.path.join(aur_folder_path, package_name)

    print("Cloning " + git_url + " to " + new_package_path)

    Popen(["git", "clone", git_url, new_package_path]).wait()

    os.chdir(new_package_path)

Solution

  • Option 1: Executing script as user, escalating privileges to root for pacman -U

    I think this is a bad idea. It is a bit sleazy for scripts to read passwords and drag them along for quite a while through insecure memory because they want to execute something as root in the end. If you insist, it will, however, work analogous to option 2.

    Option 2: Executing script as root, demoting subprocesses to user for git clone and makepkg

    You can use subprocess.Popen's preexec_fn option, which does, according to help(subprocess.Popen):

    preexec_fn: (POSIX only) An object to be called in the child process just before the child is executed.

    The only tricky thing with this is to hand over arguments. If we did not have to do that, we could simply state the function (os.setuid) as preexec_fn option in the Popen() constructor call. Unfortunately, it would then be executed when we define the subprocess, not when we run it as intended. We would therefore end up demoting the master process.

    So we have to define a little wrapper function:

    def demote():
        os.setuid(1000)
    

    and can then define our subprocess:

    process = Popen(["git", "clone", git_url, new_package_path], preexec_fn=demote)
    process.wait()
    

    Of course, this is not a nice way to do it - hardcoding uid's and all. If we want demote() to accept arguments, however, we are back at square one. So we have to make it a bit more difficult and define a nested function (a function within a function) where the outer one is called to define the subprocess, accepts arguments, defines uid and gid and returns the inner function that applies the demotion. Sounds complicated? Strictly speaking, you do not have to do that, but let's say we want to be flexible, so here we go:

    def demote(user_uid, user_gid):
        def apply_demotion():
            os.setgid(user_gid)
            os.setuid(user_uid)
        return apply_demotion
    

    A second option is to use su -c (command) (user) which can be called from python using the os.system() function:

    os.system("su -c makepkg " + user)
    

    You will probably want to run both the git clone and the makepkg command with the same method for consistency.

    A working example

    import os
    from subprocess import Popen
    import glob
    import sys
    
    def clone_and_makepkg(package_name, aur_folder_path="/tmp/build/", uid=1000, gid=1000):
    
        """prepare urls and paths"""
        git_url = "https://aur.archlinux.org/" + package_name + ".git"
        new_package_path = os.path.join(aur_folder_path, package_name)
    
        """ensure the build directory exists and user has correct privileges to work there"""
        if not os.path.exists(aur_folder_path):
            os.mkdir(aur_folder_path)
            os.chmod(aur_folder_path, 0o777)
    
        """perform git clone"""
        print("Cloning " + git_url + " to " + new_package_path)
        Popen(["git", "clone", git_url, new_package_path], preexec_fn=demote(uid, gid)).wait()
    
        """change to make directory"""
        os.chdir(new_package_path)
    
        """run makepkg"""
        Popen("makepkg", preexec_fn=demote(uid, gid)).wait()
    
        """collect built packages"""
        built_packages = glob.glob(new_package_path + os.sep + "*.pkg.tar.xz")
    
        """install each package"""
        for package in built_packages:
            print("Installing package {}".format(package))
            os.system("pacman -U " + package + " --noconfirm")
    
    def demote(user_uid, user_gid):
        def apply_demotion():
            os.setgid(user_gid)
            os.setuid(user_uid)
        return apply_demotion
    
    
    if __name__ == "__main__":
        """Example call. Will by default install package 3to2 (This example does not have any further dependencies
           except for python. Which is evidently already installed. So this example is hassle-free. For 
           examples with dependencies, you may want to find a way to deal with those in the script.)"""
        pkgname = "3to2"
        if len(sys.argv) > 1:
            pkgname = sys.argv[1]
        clone_and_makepkg(package_name=pkgname)