Intro
Consider the situation: you've spent a considerable amount of time developing an app, either as a personal project or as part of your job, and now it's time to share it with your target audience.
Note: here I focus mostly on console apps running on π§ Linux and π MacOS.
CLI apps can be executed either from a directory where the binary is located and it is fine at the development stage or if you share your app with your team, but as an end user, you want the app usage to be as straightforward as possible. Recently, I developed an app for extracting images from PDF documents pdfjuicer which was intended for both manual usage and being integrated into data processing pipelines.
Calling it like this
/Users/qwe/code/cli_apps/pdfjuicer/bin/pdfjuicer
is not a convenient way to run the app. I want users to be able to call the app just like any others, simply typing its name in the terminal
pdfjuicer
This way, it is convenient both for users and for calling the app from other software without bothering about the app location.
Problem analysis
Making the app run simply by calling its name in a terminal is achieved by adding your app path to the environment PATH. You can also add your app to software repositories like apt repositories, homebrew, etc. However, especially at the first stages it might be an overwhelming thing to do, and you might want to strike a balance between installation convenience and ease of solution maintainability. Here, I'll explore the option of developing a custom installation script.
However, here we need to look at the situation on a larger scale and consider the following questions:
How tech-savvy is our audience?
Is providing manual instructions enough?
What are the installation steps?
If choosing automated installation, should the whole process of installation be automated?
Can we test the installation process before actually installing an app?
What tool or language should be used for automation?
All these questions naturally arise one from another. First, we need to understand how complex the process is. We need to answer the question of whether our target audience has enough expertise to follow manual instructions or automation is needed. If an automated route is taken, maybe not all steps should be automated, giving user options or protecting user from unintended changes in system settings. And finally, if something goes wrong, can we find potential issues in advance by performing a test installation?
Working out the plan π§
So my next step was to get answers to these questions.
How tech-savvy is our audience?
You might think that users of console apps are mostly tech savvy, however, it is more like a spectrum. Consider CLI apps like ffmpeg
or imagemagic
which can be used by a wide range of users doing tasks like converting videos to certain formats or resizing photos from family photoarchive. I my case pdfjuicer was made for users who need to extract pages from PDF files as images with custom image size, format and thumbnails generation. The major target audience are educators and content creators. Users in these categories do not always want or have time for digging into the intricacies of application setup. Even if we consider users like devops engineers and software engineers, they also will appreciate quick and easy ways to install an app rather than delving into documentation and figuring out how to do it themselfes.
Is providing manual instructions enough? π
This question naturally arises from the previous one and my decision was not to go with the manual route. Even many tech-savvy users will prefer to save time by using a ready automated solution instead of installing the app manually or developing their own automation script. However, by taking the route which involves automated installation script, there is a problem of the installation process being hidden, which might concern some users: What will happen after installation? Where the app will be installed?
Solution is to print messages during installation showing which steps are performed now with additional information like file and folder names which are involved in the installation process, script status (start, finish, error); also adding test mode or dry-run
mode will introduce additional layer of comfort and safety for a user avoiding concerns like What will actually happen after I'll run the script? and answers our question Can we test installation process before actually installing an app?
Also user can check the source code of an installation script to check the process in more detail. And finally, making script output formatted with ANSI Escape Codes will add more clarity to output messages, for example, making diagnostic information green and errors red.
Installation steps π»
First, let's lay down steps which must be performed to install the CLI app with brief considerations for each step:
Find binary of an app - can be done by entering a path to the binary as a parameter to the installation script or app can be put into the same directory with the installation script.
Transfer binary to bin directory - at this step 2 new questions arise.
Transfer binary to system
bin
directory or in user-specific~/bin
?Move binary or copy to
bin
directory?
Let's address both.
Since this app is not installed from official repositories and can be considered as custom software, user-specific ~/bin
seems like a preferable choice. It will also avoid permissions issues that might arise when installing in the system bin
and potential conflicts of overwriting binaries there. Overall, in this case, custom ~/bin
simplifies app management and avoids risks of interfering with operating system.
Regarding whether it is better to move or copy binary - better to choose copying. In this way, if installation fails, it can be started again, otherwise, if the binary was moved, user will need to take the binary from ~/bin
and put it into an original folder or download it again to retry the installation process.
Also, going with user-specific ~/bin
directory creates extra optional step of creating such a directory if I did not exist.
Set permissions to binary - binary should be executable and execution permission will be limited to its owner since the app is installed in user user-specific binary directory, which can be achieved by using chmod u+x app_binary
which makes user (owner) u
right to execute x
the binary.
Adding binary to PATH - this step makes app executable by simply calling it by name in terminal. To achieve that path to app must be added to shell configuration file (.bashrc, .bash_profile, .profile or .zshrc) using export
command export PATH="$PATH:$target_bin_dir"
. In order to apply changes user will need to reload terminal which can be done by executing source ~/.bashrc
(or targeting another appropriate file for particular user's shell config).
Should the whole process of installation be automated?
Here it is crucial to focus on steps which have the most uncertainty in the produced outcome and risk of interfering with the operation system functioning and let the user manage these steps. In this installation process, this is the last step of adding the app to PATH. It is better to allow user to edit shell config file manually, choosing where to add new path - precede other paths or make it the last one. It will also help to avoid risks of potential overwriting of existing settings or duplicating settings or writing into the wrong shell configuration file.
Which tool or language should be used for automation?
To make installation compatible among Linux, Mac OS and BSD systems, it makes sense to implement installation using bash
script. However, my app is intended to be used not only by users but also by other software with scenarios of the app being installed on VPS or in Docker containers, including lightweight containers based on operating systems like Alpine Linux, which provides only sh
but not bash
.
Additionally, such an approach will improve overall compatibility across different operating systems and their distributives. However, it means that while implementing the installation script, it is important to be mindful about using syntax that is supported in sh
and avoid bash
specific syntax, which might mean potential tradeoffs like certain parts of code being more verbose.
What about other scripting languages?
One can ask that there are other options for scripting languages which will get this job done, like Python, Ruby or Lua and in terms of syntax, they will make the script easier to develop and easier to maintain if in the future it will require upgrades and extensions. However, we will pay the price of portability complexity. Choosing sh
(Bourne shell) allows us to be as much agnostic about user's operating system environment as possible. If we choose bash
, we add a layer of uncertainty: what if the host operating system has only sh
installed?
With other scripting languages like Python, Ruby or Lua uncertainty rises even higher. Now we need to take care of interpreter version (Python 2 and 3), guess wether chosen scripting language is installed on the user's machine at all, manage situations like installing scripting language if it is not presented in operating system which will significantly increase installation complexity like extra installation steps, longer installation time, need to download additional packages, more risks of things go wrong due to extra steps in installation.
So in this case my choice is sh
- the most available scripting language on UNIX-like systems, including minimal distributives used in Docker containers.
Implementation π
Add ANSI escape codes for formatting messages
reset="\033[0m"
bold="\033[1m"
color_green="\033[32m"
color_red="\033[31m"
In this script we will use green and red colors, bold font and also reset code that will return output format to default.
Red color will be used to notify user that installation script is running in testing mode when no actual operations are performed. Green will be used to print message about installation success. Bold text will be used in large chunks of text to highlights key moments.
Set work directory
workdir=$(pwd)
Multiple flags
Since script has 2 flags --dev
and --dry-run
and they can be used either both or just one of them and in any order to check for current flag the following construction will be used which will check whether target flag is 1st or the 2nd argument:
if [ "$1" = "--flag" ] || [ "$2" = "--flag" ]; then
...
fi
Dev flag
In addition to regular installation, script supports another mode which is activated by --dev
flag and intended to be used when you compile app from source.
App was written in Go and project contains bin directory where compiled binary is stored. In regular mode, installation script expects binary to be in the same directory with script, or in case of calling install.sh from source code (script is located in the root of the project) -- dev flag modifies path to binary adding current workdir/bin as folder.
if [ "$1" = "--dev" ] || [ "$2" = "--dev" ]; then
binary_file="bin/pdfjuicer"
else
binary_file="pdfjuicer"
fi
Dry-run
Check if this is a test/dry run:
dry_run=false
if [ "$1" = "--dry-run" ] || [ "$2" = "--dry-run" ]; then
printf "${bold}${color_red}This is dry run, no actual actions will be performed. Use this for testing installation script.${reset}\n"
dry_run=true
fi
If dry run is enabled show message about it in red color and with bold color using defined previously ANSI escape codes.
Setting paths to target path where it will be copied (~/bin
) and original binary location.
target_bin_dir="$HOME/bin/"
source="$workdir/$binary_file"
Create ~/bin
directory if it doesn't exist
echo "Creating directory $target_bin_dir for binary (if not exist)"
if ! $dry_run; then
mkdir -p "$target_bin_dir"
fi
Copy binary to ~/bin
echo "Copying $source to $target_bin_dir"
if ! $dry_run; then
cp "$source" "$target_bin_dir"
fi
Add execute permission using chmod
, in the message to user script will show user name who will be able to execute binary using whoami
command. If everything is running correctly it will show name of a user running the script.
echo "Adding execute permission for user $(whoami)"
if ! $dry_run; then
chmod u+x "$target_bin_dir$binary_file"
fi
And finally installation script will show instructions for the final step which user will do manually: adding binary to PATH.
Calling installation script
The install script has 2 modes of running: real installation and test (dry-run) when the user sees messages for each step of an installation process but no actual operations are performed.
# testing script
bash ./install.sh --dry-run
# actual installation
bash ./install.sh
And in case of installing compiled binary from source code:
# testing script
bash ./install.sh --dev --dry-run
# actual installation from
bash ./install.sh --dev
Such practice enables user to check if script is working properly: binary is found in correct folder, it will be copied to correct path, script doesn't fail during the execution.
Conclusions
Shipping app with installation script simplifies app installation compared to providing manual instructions. At the same time it's important to identify steps which are better to be performed manually for installation safety reasons. In this case user will manually add app to PATH to avoid potential mistakes and conflicts in shell configuration. Having an installation script provides a balance between manual setup and adding app to official repositories which can be time consuming especially at early stages of app development or when app is developed for internal usage in a company.
Complete source code of installation script can be found in pdfjuicer project here: install.sh
Top comments (0)