Package Installation Scripts: Pre and Post Install Hooks

By DistroPack Team 9 min read

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.

Try DistroPack Free

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.

View Pricing

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.

Try DistroPack Free