Installing Apt Packages Without Sudo
Tags: linux, coding, automation, beginners
Published: 2023-09-06 17:31:52

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()
```

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

Thanks for reading 😁