DEV Community

Cover image for Installing Apt Packages Without Sudo
Ryan Bevin
Ryan Bevin

Posted on

Installing Apt Packages Without Sudo

To whom it may concern,

This is my first post so apologies for the fan fair!

Recently our company revoked our sudo access (for reasons I will not go into as it may give me an ulcer). Yep. No mo' sudo.

This means any apt package (we use a system based on Ubuntu/Debian) we want to install, whether it be for testing purposes or to add it as part of our list of tools, we will need to get the password manually typed in by another person. This can slow down our day to day work and it turns out it has even added a road block into our automation. As you can imagine this can be frustrating for everyone involved.

I am writing to inform you that if you have a similar problem there is a way around this issue. It requires a few commands in sequence and you will require sudo one, last, time. So here goes!

Let's get the sudo install out of the way one last time.

Step 1: Install apt-rdepends

We need this package to get the other apt package dependancies.
sudo apt-get install apt-rdepends -y

Step 2: Download the .deb pacakge

So now you should navigate to the folder you wish to install these packages under. Once you do this you can now download the pakcage and dependencies. In this example I use cflow as the package I wish to install. Replace this with whatever you wish.
apt-get download $(apt-rdepends cflow|grep -v "^ ")

Step 3: Extract the .deb packages.

You will need to extract each deb package individually which is annoying but this can be automated. You can extract the debian packages in the current directory using the following
dpkg -x deb_package.deb .

Step 4: Adding the Packages to the PATH

This can be done by adding the by updating the .profile file
export PATH="$PATH:$HOME/path/to/executable"

Step 5: Refresh the .profile File

When we update the .profile file with the PATH changes, this needs to be then refreshed. This can be done by running the following
source .profile

As a bonus for making it to the end here is the automated python version.

""" This is used to download and install apt packages and install them under the current user.
    This script is particularly useful for those who don't have sudo access."""

#!/usr/bin/python3.10.6

# we are disabling pylint checking some imports to save action minutes on github.
import subprocess
import os
import glob
import logging
import click  # pylint: disable=import-error


class LocalAptInstaller:
    """ This class is has all the functionality to download
        and install debian packages and add them to the PATH."""

    def __init__(self, apt_pkg, install_dir):
        self.apt_pkg_name = apt_pkg
        self.home_path = ""
        self.local_apps_folder_name = install_dir
        self.packages_list = [""]
        self.executables_list = [""]

    def download_package(self):
        """ This function uses apt-get to download our debian package locally."""
        logging.info("Downloading %s", self.apt_pkg_name)

        # We want to parse the package name here to get it's dependencies.
        # This allows us to pass in this string easier to subprocess.
        # Apparently this can download a package and all it's
        # dependancies = apt-get download $(apt-rdepends <package>|grep -v "^ ")
        package_parsed = f'$(apt-rdepends {self.apt_pkg_name}|grep -v "^ ")'

        # Need to navigate to the folder we want to install these packages too.
        # In our case its the user directory
        self.home_path = os.path.expanduser('~')
        os.chdir(self.home_path)

        # Then we want to make an local apps directory
        subprocess.call(
            ["mkdir", "-p", os.path.expanduser(f'~/{self.local_apps_folder_name}')])

        # Then we can go into that directory.
        os.chdir(f"{self.home_path}/{self.local_apps_folder_name}/")

        # And finally we can download our package and it's dependencies.
        cmd = subprocess.Popen(
            f'apt-get download {package_parsed}', shell=True)
        cmd.communicate()

    def install_deb_package(self):
        """ This function uses dpkg to install the package locally."""
        logging.info("Installing %s", self.apt_pkg_name)

        # first we need to get a list of all the packages and dependencies downloaded.
        self.packages_list = glob.glob(
            f"{self.home_path}/{self.local_apps_folder_name}/*.deb")

        # next we need to unpackage them.
        for package in self.packages_list:
            cmd = subprocess.Popen(
                f'dpkg -x {package} .', shell=True)
            cmd.communicate()

    def add_package_to_path(self):
        """ This function adds the package we've installed to PATH so it can be used."""
        logging.info("Adding %s to PATH", self.apt_pkg_name)

        # We need to get a list of executables from the packages
        # we've downloaded and the dependencies.
        executables = subprocess.check_output(
            "find . -type f -executable -print", stderr=subprocess.STDOUT,
            shell=True).decode("utf-8")

        # we need to get the executables into a list so they are manageable.
        self.executables_list = executables.splitlines()

        for idx, _ in enumerate(self.executables_list):
            # need to remove the file name and just point the path to the folder.
            executable_str = str(self.executables_list[idx]).rsplit('/', 1)[0]

            # we need to then concatenate the file executables together to the PATH variable
            self.executables_list[idx] = f"{self.local_apps_folder_name}/{executable_str[2:]}"

        # Now we need to open our .profile file
        with open(f"{self.home_path}/.profile", "a", encoding="utf8") as myfile:
            for idx, _ in enumerate(self.executables_list):
                # Then finally we can insert these into the
                # .profile file for these to be added to the PATH
                myfile.write(
                    f'\nexport PATH="$PATH:$HOME/{self.executables_list[idx]}"')


@click.command()
@click.option("--apt_pkg", default="cflow",
              help="The apt package you wish to download and install under the current user.")
@click.option("--install_dir", default="apps",
              help="You can give the name of the directroy you'd like to install the apps too.")
# pre-requisite run this once: sudo apt-get install apt-rdepends
def install_apt_pkg(apt_pkg, install_dir):
    """ This script is used download and dpkg apt packages
        locally if the user doesn't have sudo permissions """

    installer = LocalAptInstaller(apt_pkg, install_dir)
    try:
        installer.download_package()
        installer.install_deb_package()
        installer.add_package_to_path()
    except RuntimeError:
        logging.error("Install Failed!")


if __name__ == '__main__':
     # use this to log to a file.
     # logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.INFO)
     # logging.basicConfig(level=logging.INFO) # use this to log to the terminal.
     install_apt_pkg()
Enter fullscreen mode Exit fullscreen mode

Hopefully you get as much use from this as I am getting!

Thanks for reading 😁

Top comments (3)

Collapse
 
phlash profile image
Phil Ashby

This is certainly one way to reduce the friction of getting packages into your home directory, although it may have issues if the package requires configuration setup (eg: in /etc) or links via /etc/alternatives, basically any package with a post-inst.sh script may not work after simply unpacking... YMMV!

I note that this is not a new issue (that's 14 years ago!) and there are more heavyweight solutions, usually involving increasing levels of virtualisation such as fakechroot, docker/lxc or qemu-user.

When faced with a similar issue a few years ago in a large UK telco, I worked with the IT services team to create a viable virtualisation solution - where they managed the base OS and various endpoint protection services on it, but provided users with the means to create and operate within any number of full VMs (all flavours of OS). This met the needs of both parties, to provide IT with detection and remote isolation/off switches for misbehaving machines, and allow users to install almost anything in sandboxed VMs so they can get on with life/work. It took a while for corporate machines to all have enough grunt to run this arrangement, but devs tended to have faster machines so they got early benefit.

I hope you are still on speaking terms with your IT folks! :)

Collapse
 
phlash profile image
Phil Ashby

Ohh, I forgot to mention that this approach gave additional benefit to IT folks later on as they could leverage the VM snapshot capabilities to provide both automated backup and fast restore to new hardware for every employee, this plus encrypted VM filesystems meant that laptops left on trains were way less of a problem for them.

Collapse
 
rbevin777 profile image
Ryan Bevin

Thanks for the reply and sharing your experience!