Package Installation Scripts: Pre and Post Install Hooks
Have you ever wondered what happens behind the scenes when you install a software package on your Linux system? While package managers like apt, yum, or pacman handle the basic file copying, the real magic happens through package scripts that execute at strategic points during the installation process. These powerful hooks allow package maintainers to perform complex setup tasks, ensuring your software works correctly the moment installation completes.
In this comprehensive guide, we'll dive deep into the world of package installation scripts, exploring the different types of maintainer scripts available across various distributions, their specific use cases, and best practices for implementation. Whether you're a system administrator troubleshooting installation issues or a developer creating your first package, understanding these scripts is crucial for mastering Linux package management.
Understanding Package Lifecycle Scripts
Package installation isn't just about copying files to the correct locations. Modern software often requires additional setup such as creating users, starting services, migrating configurations, or updating system databases. This is where installation scripts come into play, providing hooks that execute at specific points in the package lifecycle.
Script Types and Their Execution Order
Package managers support various scripts that run at different stages of package lifecycle. The exact names and execution order vary between package formats, but the concepts remain similar across distributions.
Pre-Installation Scripts
preinst scripts run before the package files are actually installed on the system. This is your opportunity to prepare the environment for the incoming software.
Common preinst use cases include:
- Stopping existing services that might conflict with the installation
- Creating necessary system users or groups
- Backing up existing configuration files
- Checking system prerequisites and dependencies
- Validating that the system meets minimum requirements
Here's a simple example of a Debian preinst script that checks for available disk space:
#!/bin/bash
set -e
# Check available disk space in /usr
AVAILABLE_SPACE=$(df /usr | awk 'NR==2 {print $4}')
REQUIRED_SPACE=50000000 # 50MB in KB
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
echo "Error: Insufficient disk space. Required: $((REQUIRED_SPACE/1024))MB, Available: $((AVAILABLE_SPACE/1024))MB" >&2
exit 1
fi
echo "Disk space check passed"
Post-Installation Scripts
postinst scripts execute after the package files have been installed. This is where most of the post-installation configuration happens.
Typical postinst tasks include:
- Starting and enabling system services
- Updating system databases (like font caches or application menus)
- Setting up default configurations
- Running database migrations
- Generating SSL certificates or other security artifacts
Example postinst script for enabling a systemd service:
#!/bin/bash
set -e
case "$1" in
configure)
# Only run on fresh installs, not on upgrades
if [ "$2" = "" ]; then
# Create a system user for our service
if ! getent passwd myapp >/dev/null; then
adduser --system --group --no-create-home myapp
fi
# Set up directories with correct permissions
mkdir -p /var/lib/myapp
chown myapp:myapp /var/lib/myapp
chmod 750 /var/lib/myapp
# Enable and start the service
systemctl enable myapp.service
systemctl start myapp.service
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
# Handle abnormal termination scenarios
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# Update application menus
update-desktop-database /usr/share/applications
Pre-Removal and Post-Removal Scripts
Removal scripts handle the cleanup process when a package is uninstalled. prerm runs before file removal, while postrm executes after the files are gone.
prerm responsibilities:
- Stopping services gracefully
- Backing up user data before removal
- Warning users about data loss
- Checking if other packages depend on this one
postrm cleanup tasks:
- Removing created users and groups
- Deleting log files and temporary data
- Cleaning up configuration files (if purge is requested)
- Updating system caches and databases
Distribution-Specific Script Implementation
While the concepts are similar, each package format implements these scripts differently. Understanding these differences is crucial when creating cross-distribution packages.
Debian/Ubuntu (.deb Packages)
Debian-based systems use scripts placed in the DEBIAN/ directory of the package build structure:
my-package/
├── DEBIAN/
│ ├── control
│ ├── preinst
│ ├── postinst
│ ├── prerm
│ └── postrm
└── usr/
└── ... (package files)
Each script must be executable and start with a proper shebang line. The scripts receive different arguments depending on the operation (install, upgrade, remove).
RPM-Based Systems (.rpm Packages)
RPM packages define scripts directly in the .spec file using specific sections:
# Example .spec file script sections
%pre
#!/bin/bash
# Pre-installation script
echo "Running pre-installation tasks"
%post
#!/bin/bash
# Post-installation script
systemctl daemon-reload
%preun
#!/bin/bash
# Pre-removal script
if [ $1 -eq 0 ]; then # Only on complete removal
systemctl stop myapp.service
fi
%postun
#!/bin/bash
# Post-removal script
if [ $1 -ge 1 ]; then # On upgrade, not removal
systemctl try-restart myapp.service
fi
Arch Linux (PKGBUILD)
Arch uses an .INSTALL file that contains functions for different lifecycle events:
# Example .INSTALL file
pre_install() {
echo "Preparing for installation..."
}
post_install() {
echo "Configuring package..."
systemctl enable myapp.service
}
pre_upgrade() {
echo "Preparing for upgrade..."
}
post_upgrade() {
echo "Finalizing upgrade..."
}
pre_remove() {
echo "Preparing for removal..."
systemctl stop myapp.service
}
post_remove() {
echo "Cleaning up..."
}
Best Practices for Package Scripts
Writing reliable maintainer scripts requires careful consideration of various edge cases and potential failure scenarios.
Idempotency: The Golden Rule
Your scripts should be idempotent – safe to run multiple times without causing errors or duplicate actions. This is crucial because scripts might be re-executed during failed installation recovery.
#!/bin/bash
# Good: Idempotent user creation
if ! getent passwd myapp >/dev/null; then
adduser --system --group --no-create-home myapp
echo "Created myapp user"
else
echo "myapp user already exists"
fi
# Bad: Non-idempotent - will fail on second run
adduser --system --group --no-create-home myapp
Robust Error Handling
Always include proper error handling to prevent partial installations and provide useful error messages.
#!/bin/bash
set -e # Exit on any error
echo "Starting post-installation configuration"
# Check if required commands are available
for cmd in systemctl update-desktop-database; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: Required command '$cmd' not found" >&2
exit 1
fi
done
# Attempt configuration with error handling
if ! systemctl enable myapp.service; then
echo "Warning: Failed to enable myapp.service" >&2
# Continue with other tasks instead of failing completely
fi
Minimal User Interaction
Package scripts should avoid interactive prompts whenever possible. Automated installation systems and package managers expect scripts to run without user input.
Comprehensive Logging
Implement detailed logging to help with debugging installation issues. Log to standard output/error and consider writing to a package-specific log file.
#!/bin/bash
LOG_FILE="/var/log/myapp-install.log"
log_message() {
echo "$(date): $1" | tee -a "$LOG_FILE"
}
log_message "Starting pre-installation script"
# Your script logic here
if [ -f "/etc/myapp/old-config.conf" ]; then
log_message "Found old configuration, backing up"
cp "/etc/myapp/old-config.conf" "/etc/myapp/old-config.conf.backup"
fi
log_message "Pre-installation script completed"
Managing these complexities across different distributions can be challenging. Tools like DistroPack simplify this process by providing a unified interface for package script management across multiple platforms.
Common Script Tasks and Examples
Let's explore some practical examples of common tasks you might implement in your installation scripts.
Service Management
Managing system services is one of the most common tasks in package scripts.
#!/bin/bash
# Service management in postinst
case "$1" in
configure)
# Reload systemd to recognize new unit files
systemctl daemon-reload
# Only on fresh install, not upgrade
if [ -z "$2" ]; then
systemctl enable myapp.service
systemctl start myapp.service
echo "Service myapp enabled and started"
else
# On upgrade, try to restart if running
if systemctl is-active --quiet myapp.service; then
systemctl restart myapp.service
echo "Service myapp restarted"
fi
fi
;;
esac
User and Group Management
Creating dedicated users for your application improves security and isolation.
#!/bin/bash
# User creation in postinst
APP_USER="myapp"
APP_GROUP="myapp"
if ! getent group "$APP_GROUP" >/dev/null; then
groupadd --system "$APP_GROUP"
echo "Created group $APP_GROUP"
fi
if ! getent passwd "$APP_USER" >/dev/null; then
useradd --system --gid "$APP_GROUP" --home-dir /var/lib/myapp --shell /bin/false "$APP_USER"
echo "Created user $APP_USER"
fi
Configuration File Management
Handling configuration files requires careful consideration of existing user configurations.
#!/bin/bash
# Configuration management in postinst
CONFIG_FILE="/etc/myapp/config.conf"
DEFAULT_CONFIG="/etc/myapp/config.conf.default"
# If no config exists, create from default
if [ ! -f "$CONFIG_FILE" ]; then
cp "$DEFAULT_CONFIG" "$CONFIG_FILE"
chmod 600 "$CONFIG_FILE"
echo "Created default configuration at $CONFIG_FILE"
else
# Compare with default and warn about new options
if ! diff -q "$DEFAULT_CONFIG" "$CONFIG_FILE" >/dev/null; then
echo "Note: Custom configuration detected at $CONFIG_FILE"
echo "Check $DEFAULT_CONFIG for new configuration options"
fi
fi
Testing Your Package Scripts
Thorough testing is essential for reliable package scripts. Implement a comprehensive testing strategy that covers various scenarios.
Testing Environments
Test your packages in environments that mirror your target systems:
- Clean installations – Fresh OS installs with minimal packages
- Upgrade scenarios – Systems with previous versions installed
- Container testing – Docker containers for isolated, reproducible tests
- Multiple distributions – Test on all supported OS versions
Automated Testing Strategies
Implement automated testing in your CI/CD pipeline:
#!/bin/bash
# Example test script for package installation
# Test fresh installation
echo "Testing fresh installation..."
dpkg -i myapp_1.0-1_amd64.deb
systemctl is-active myapp.service || exit 1
# Test configuration
curl -f http://localhost:8080/health || exit 1
# Test upgrade from previous version
echo "Testing upgrade..."
dpkg -i myapp_1.1-1_amd64.deb
systemctl is-active myapp.service || exit 1
# Test removal
echo "Testing removal..."
dpkg -r myapp
systemctl is-active myapp.service && exit 1
echo "All tests passed!"
Conclusion
Mastering package installation scripts is essential for creating professional, reliable software packages. From preinst scripts that prepare the environment to postinst scripts that configure your application, these hooks provide the flexibility needed for complex installation scenarios.
Remember the key principles: write idempotent scripts, implement robust error handling, minimize user interaction, and test thoroughly across all target environments. Whether you're working with Debian's maintainer scripts, RPM's spec file sections, or Arch's PKGBUILD functions, the concepts remain consistent even if the implementation details differ.
By following the best practices outlined in this guide and leveraging tools like DistroPack to streamline your packaging workflow, you can create packages that install seamlessly and reliably across diverse Linux environments.