Apt-get commands from within a deb postinst

7k views Asked by At

I have a deb package that I've created. From the postinst script, I would like to run:

apt-get update

The package adds a proxy to the apt system by dropping a file in /etc/apt/apt.conf.d/. I would like to force the apt system to do the equivalent of "apt-get update". However, I cannot run that command directly from postinst, since the apt lock file has already been placed by dpkg which is installing this package! Is there some debconf tools/commands to do this?

As a bonus, I would love to be able to remove a package from within preinst/postinst:

apt-get remove popularitycontest

NOTE - this package is for an internal project - not a deb that will ever be released into the wild or submitted to Debian.

4

There are 4 answers

2
Franklin Piat On BEST ANSWER

It is not possible to invoke an APT command (apt-get, aptitude..) from within a package script (preinst, postinst, prerm, postrm...).

Enabling so would raise lots of problems, especially for dependency and ordering of package installation.

Various workaround have been used, either by using proper package (pre) dependencies or by providing an easy-to-use tool for your users (like module-assistant and other tools).

In your case, your package could just conflict with popularitycontest to uninstall it. Also, if your user have "your" package, it means they have already added an entry to their sources.list, so they can add another one!

0
Javier López On

Nested dpkg/apt-get approach

As Franklin point it out, enabling it would create lots of problems, however if it's for internal use your package can deal with them, still will be scary so please try to avoid it, having said that the approach would involve:

  1. Disable temporarily apt/dpkg locks
  2. Update / Install / Remove software
  3. Merge changes to the dpkg database
  4. Enable back apt/dpkg locks
  5. Profit

1. Disable temporarily apt/dpkg locks

You need to move temporarily the following files:

  • /var/lib/dpkg/lock
  • /var/lib/dpkg/updates/
  • /var/cache/apt/archives/lock

2. Update / Install / Remove software

Now you can run apt-get/dpkg, if you only want to update the index you can run apt-get update, re-enable the locks and continue, otherwise be ready to deal with the dpkg database.

If you want to install/remove software then you will need take in consideration that apt-get/dpkg write its final state to:

  • /var/lib/dpkg/status

Let's suppose you have a system with the following packages: firefox, htop, curl, and let's suppose that your package foo removes curl, so upon installation of your package you should have firefox, htop, foo however since dpkg updates its state once per instance your nested state will be overrided by the parent process leaving the following status firefox, htop, curl, foo

So, you won't have the curl files around but to dpkg the package will still be installed, this will also happen with new software and dependencies.

Let's suppose your foo packages installs apache2 which depends in apache2-data, you would expect them in your dpkg database as: firefox, htop, curl, foo, apache2, apache2-data , however you'll have firefox, htop, curl, foo, the nested output was overrided by the parent process which was only aware of the installation of foo

3. Merge changes to the dpkg database

In order to avoid this mix-up, you'll need to take care of the dpkg changes manually, also since the file is overrided with ever apt-get/dpkg instance you would need to save the changes in a different location and apply them to the original file only after the main apt-get/dpkg instance is done, since your script will end before that happen, you'll need to leave behind a cronjob entry or a hand-made daemon.

4. Enable back apt/dpkg locks

5. Profit

Since the above process can be somehow troublesome, I'm leaving a naive implementation, be sure to understand it before using it and expect corner cases.

package=my-pkg

_dpkg_suspend_process() {
    #unlock standard files
    busybox mv     /var/lib/dpkg/lock           /var/lib/dpkg/lock.suspended
    busybox rm -rf /var/lib/dpkg/updates.suspended/
    busybox mv     /var/lib/dpkg/updates/       /var/lib/dpkg/updates.suspended
    busybox mkdir  /var/lib/dpkg/updates/
    busybox mv     /var/cache/apt/archives/lock /var/cache/apt/archives/lock.suspended

    #debconf missing file descriptors workaround
    busybox cp /usr/share/debconf/confmodule       /usr/share/debconf/confmodule.bk  
    busybox cp /usr/share/minos/debconf/confmodule /usr/share/debconf/confmodule

    #while apt is being executed it modifies the status file which brings conflicts
    #to new packages if they're installed/removed in abused apt instances, therefore
    #the status-old file (which represent the original state in which the first
    #apt instance was launched) is used to create temporal diffs which will be merged
    #at the end
    busybox cp     /var/lib/dpkg/status         /var/lib/dpkg/status.suspended
    busybox cp     /var/lib/dpkg/status-old     /var/lib/dpkg/status-orig
    busybox cp     /var/lib/dpkg/status-orig    /var/lib/dpkg/status
}

_dpkg_continue_process() {
    #relock standard files
    busybox rm -rf /var/lib/dpkg/updates
    busybox mv     /var/lib/dpkg/lock.suspended           /var/lib/dpkg/lock
    busybox mv     /var/lib/dpkg/updates.suspended        /var/lib/dpkg/updates
    busybox mv     /var/cache/apt/archives/lock.suspended /var/cache/apt/archives/lock
    busybox mv     /var/lib/dpkg/status.suspended         /var/lib/dpkg/status

    #debconf missing file descriptors workaround
    busybox mv     /usr/share/debconf/confmodule.bk       /usr/share/debconf/confmodule

    #keep status-old file to survive multiple abused apt instances
    busybox mv     /var/lib/dpkg/status-orig              /var/lib/dpkg/status-old
}

_dpkg_sync_status_db() {
    _dpkg_sync_status_db_script="/var/lib/dpkg/dpkg-sync-status-db"
    _dpkg_sync_status_db_script_generator() {
        printf "%s\\n" "#!/bin/sh"
        printf "%s\\n" "#autogenerated by ${package}: $(date +%d-%m-%Y:%H:%M)"
        printf "\\n"
        printf "%s\\n" '##close stdout'
        printf "%s\\n" '#exec 1<&-'
        printf "%s\\n" '##close stderr'
        printf "%s\\n" '#exec 2<&-'
        printf "%s\\n" '##open stdout as $log_file file for read and write.'
        printf "%s\\n" "#exec 1<> /tmp/${package}.\${$}.debug"
        printf "%s\\n" '##redirect stderr to stdout'
        printf "%s\\n" '#exec 2>&1'
        printf "%s\\n" '#set -x #enable trace mode'
        printf "\\n"
        printf "%s\\n" "while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do sleep 1; done"
        printf "\\n"
        printf "%s\\n" 'pkgs__add="$(cat /var/lib/apt/apt-add-queue)"'
        printf "%s\\n" 'if [ -n "${pkgs__add}" ]; then'
        printf "%s\\n" '  for pkg in $pkgs__add; do'
        printf "%s\\n" '    if ! busybox grep "^Package: ${pkg}$" /var/lib/dpkg/status >/dev/null 2>&1; then'
        printf "%s\\n" '      busybox sed -n "/Package: ${pkg}$/,/^$/p" \'
        printf "%s\\n" "      /var/lib/dpkg/status-append-queue >> /var/lib/dpkg/status"
        printf "%s\\n" "    fi"
        printf "%s\\n" "  done"
        printf "%s\\n" "fi"
        printf "\\n"
        printf "%s\\n" 'pkgs__rm="$(cat /var/lib/apt/apt-rm-queue)"'
        printf "%s\\n" 'if [ -n "${pkgs__rm}" ]; then'
        printf "%s\\n" '  for pkg in $pkgs__rm; do'
        printf "%s\\n" '    busybox sed -i "/Package: ${pkg}$/,/^$/d" /var/lib/dpkg/status'
        printf "%s\\n" "  done"
        printf "%s\\n" "fi"
        printf "\\n"
        printf "%s\\n" "mv /var/lib/apt/apt-add-queue /var/lib/apt/apt-add-queue.bk"
        printf "%s\\n" "mv /var/lib/apt/apt-rm-queue  /var/lib/apt/apt-rm-queue.bk"
        printf "%s\\n" "mv /var/lib/dpkg/status-append-queue /var/lib/dpkg/status-append-queue.bk"
        printf "\\n"
        printf "%s\\n" "rm -rf /var/lib/apt/apt-add-queue /var/lib/apt/apt-rm-queue"
        printf "%s\\n" "rm -rf ${_dpkg_sync_status_db_script}"
    }

    _dpkg_sync_status_db_script_generator > "${_dpkg_sync_status_db_script}"
    chmod +x "${_dpkg_sync_status_db_script}"
    _daemonize /bin/sh -c "${_dpkg_sync_status_db_script}"
}

_daemonize() {
    #http://blog.n01se.net/blog-n01se-net-p-145.html
    [ -z "${1}" ] && return 1
    (   #1. fork, to guarantee the child is not a process
        #group leader, necessary for setsid) and have the
        #parent exit (to allow control to return to the shell)

        #2. redirect stdin/stdout/stderr before running child
        [ -t 0 ] && exec  </dev/null
        [ -t 1 ] && exec  >/dev/null
        [ -t 2 ] && exec 2>/dev/null
        if ! command -v "setsid" >/dev/null 2>&1; then
            #2.1 guard against HUP and INT (in child)
            trap '' 1 2
        fi

        #3. ensure cwd isn't a mounted fs so it does't block
        #umount invocations
        cd /

        #4. umask (leave this to caller)
        #umask 0

        #5. close unneeded fds
        #XCU 2.7 Redirection says: open files are represented by
        #decimal numbers starting with zero. The largest possible
        #value is implementation-defined; however, all
        #implementations shall support at least 0 to 9, inclusive,
        #for use by the application.
        i=3; while [ "${i}" -le "9" ]; do
            eval "exec ${i}>&-"
            i="$(($i + 1))"
        done

        #6. create new session, so the child has no
        #controlling terminal, this prevents the child from
        #accesing a terminal (using /dev/tty) and getting
        #signals from the controlling terminal (e.g. HUP, INT)
        if command -v "setsid" >/dev/null 2>&1; then
            exec setsid "$@"
        elif command -v "nohup" >/dev/null 2>&1; then
            exec nohup "$@" >/dev/null 2>&1
        else
            if [ ! -f "${1}" ]; then
                "$@"
            else
                exec "$@"
            fi
        fi
    ) &
    #2.2 guard against HUP (in parent)
    if ! command -v "setsid" >/dev/null 2>&1 \ &&
       ! command -v "nohup"  >/dev/null 2>&1; then
        disown -h "${!}"
    fi
}

_apt_add_queue() {
    for pkg in "${@}"; do
        if  busybox grep "${pkg}" /var/lib/apt/apt-rm-queue >/dev/null 2>&1; then
            busybox sed -i "/^${pkg}$/d" /var/lib/apt/apt-rm-queue
        else
            if ! busybox grep "^Package: ${pkg}$" /var/lib/dpkg/status >/dev/null 2>&1; then
                printf "%s\\n" "${pkg}" >> /var/lib/apt/apt-add-queue
            fi
        fi
    done; unset pkg
}

_apt_rm_queue() {
    for pkg in "${@}"; do
        if  busybox grep "${pkg}" /var/lib/apt/apt-add-queue >/dev/null 2>&1; then
            busybox sed -i "/^${pkg}$/d" /var/lib/apt/apt-add-queue
        else
            if busybox grep "^Package: ${pkg}$" /var/lib/dpkg/status >/dev/null 2>&1; then
                printf "%s\\n" "${pkg}" >> /var/lib/apt/apt-rm-queue
            fi
        fi
    done; unset pkg
}

_apt_install() {
    [ -z "${1}" ] && return
    _apt_add_queue $(printf "%s\\n" "${@}" | busybox sed "s:${package}::g")
}

_apt_purge() {
    [ -z "${1}" ] && return
    _apt_rm_queue $(printf "%s\\n" "${@}" | busybox sed "s:${package}::g")
}

_apt_run() {
    [ ! -f /var/lib/apt/apt-add-queue ] && [ ! -f /var/lib/apt/apt-rm-queue ] && return

    pkgs__add="$(cat /var/lib/apt/apt-add-queue 2>/dev/null)"
    if [ -n "${pkgs__add}" ]; then
        _dpkg_suspend_process
        busybox awk '/^Package: /{print $2}' /var/lib/dpkg/status | \
            busybox sort > /var/lib/dpkg/status-pkgs.orig
        _apt_run__output="$(DEBIAN_FRONTEND=noninteractive apt-get install  \
            --no-install-recommends -y -o Dpkg::Options::="--force-confdef" \
            -o Dpkg::Options::="--force-confold" --force-yes ${pkgs__add} 2>&1)" || \
            printf "%s\\n" "${_apt_run__output}" >&2
        busybox awk '/^Package: /{print $2}' /var/lib/dpkg/status | \
            busybox sort > /var/lib/dpkg/status-pkgs.current
        _dpkg__added_pkgs="$(busybox diff -Naur /var/lib/dpkg/status-pkgs.orig \
            /var/lib/dpkg/status-pkgs.current | busybox awk '/^\+[a-zA-Z]/{gsub("^+","");print;}')"
        busybox rm -rf /var/lib/dpkg/status-pkgs*
        #add dependencies
        if [ -n "${_dpkg__added_pkgs}" ]; then
            printf "%s\\n" "${_dpkg__added_pkgs}" >> /var/lib/apt/apt-add-queue
            printf "%s\\n" "$(busybox sort /var/lib/apt/apt-add-queue | busybox uniq)" \
                > /var/lib/apt/apt-add-queue
        fi

        #extract dpkg status output to append it at the end
        for pkg in $_dpkg__added_pkgs; do
            busybox sed -n '/Package: '"${pkg}"'$/,/^$/p' /var/lib/dpkg/status \
                >> /var/lib/dpkg/status-append-queue
        done
        _dpkg_continue_process
    fi

    pkgs__rm="$(cat /var/lib/apt/apt-rm-queue 2>/dev/null)"
    if [ -n "${pkgs__rm}" ]; then
        _dpkg_suspend_process
        busybox awk '/^Package: /{print $2}' /var/lib/dpkg/status | \
            busybox sort > /var/lib/dpkg/status-pkgs.orig
        _apt_run__output="$(DEBIAN_FRONTEND=noninteractive apt-get purge \
            -y ${pkgs__rm} 2>&1)" || printf "%s\\n" "${_apt_run__output}" >&2
        busybox awk '/^Package: /{print $2}' /var/lib/dpkg/status | \
            busybox sort > /var/lib/dpkg/status-pkgs.current
        _dpkg__removed_pkgs="$(busybox diff -Naur /var/lib/dpkg/status-pkgs.orig \
            /var/lib/dpkg/status-pkgs.current | busybox awk '/^-[a-zA-Z]/{gsub("^-","");print;}')"
        busybox rm -rf /var/lib/dpkg/status-pkgs*
        #remove dependencies
        if [ -n "${_dpkg__removed_pkgs}" ]; then
            printf "%s\\n" "${_dpkg__removed_pkgs}" >> /var/lib/apt/apt-rm-queue
            printf "%s\\n" "$(busybox sort /var/lib/apt/apt-rm-queue | busybox uniq)" \
                > /var/lib/apt/apt-rm-queue
        fi
        _dpkg_continue_process
    fi

    _dpkg_sync_status_db
}

_apt_install foo bar
_apt_purge   ugly packages
_apt_run
0
Javier López On

Daemon dpkg/apt-get approach

As Franklin point it out, enabling it would create lots of problems, however if it's for internal use your package can deal with them, still will be scary so please try to avoid it, having said that the approach would involve:

  1. Define and populate install / remove apt-get queues
  2. Alternatively depend on aptdaemon or a similar tool [optional]
  3. Leave behind a daemon which acts on the defined apt-get/dpkg queues post installation
  4. Alternatively add a cronjob entry who deletes itself [optional]
  5. Profit

1. Define and populate install / remove apt-get queues

Since you won't run additional dpkg/apt-get instances, you can define temporal files to use as apt/dpkg queues, e.g:

  • /var/lib/apt/apt-add-queue
  • /var/lib/apt/apt-rm-queue

2. Depend on aptdaemon

If you choice to depend on aptdaemon or a similar tool, you'll need to add it to the PreDepends field in you control file and call it from your maintainer script.

3. Leave behind a daemon which acts on the defined apt-get/dpkg queues post installation

Your daemon will need to wait the /var/lib/dpkg/lock file to unlock to run its apt-get/dpkg instances.

4. Add a cronjob entry who deletes itself

5. Profit

Since the above process can be somehow troublesome, I'm leaving a naive implementation, be sure to understand it before using it and expect corner cases.

package=my-pkg

_daemonize() {
    #http://blog.n01se.net/blog-n01se-net-p-145.html
    [ -z "${1}" ] && return 1
    (   #1. fork, to guarantee the child is not a process
        #group leader, necessary for setsid) and have the
        #parent exit (to allow control to return to the shell)

        #2. redirect stdin/stdout/stderr before running child
        [ -t 0 ] && exec  </dev/null
        [ -t 1 ] && exec  >/dev/null
        [ -t 2 ] && exec 2>/dev/null
        if ! command -v "setsid" >/dev/null 2>&1; then
            #2.1 guard against HUP and INT (in child)
            trap '' 1 2
        fi

        #3. ensure cwd isn't a mounted fs so it does't block
        #umount invocations
        cd /

        #4. umask (leave this to caller)
        #umask 0

        #5. close unneeded fds
        #XCU 2.7 Redirection says: open files are represented by
        #decimal numbers starting with zero. The largest possible
        #value is implementation-defined; however, all
        #implementations shall support at least 0 to 9, inclusive,
        #for use by the application.
        i=3; while [ "${i}" -le "9" ]; do
            eval "exec ${i}>&-"
            i="$(($i + 1))"
        done

        #6. create new session, so the child has no
        #controlling terminal, this prevents the child from
        #accesing a terminal (using /dev/tty) and getting
        #signals from the controlling terminal (e.g. HUP, INT)
        if command -v "setsid" >/dev/null 2>&1; then
            exec setsid "$@"
        elif command -v "nohup" >/dev/null 2>&1; then
            exec nohup "$@" >/dev/null 2>&1
        else
            if [ ! -f "${1}" ]; then
                "$@"
            else
                exec "$@"
            fi
        fi
    ) &
    #2.2 guard against HUP (in parent)
    if ! command -v "setsid" >/dev/null 2>&1 \ &&
       ! command -v "nohup"  >/dev/null 2>&1; then
        disown -h "${!}"
    fi
}

_apt_add_queue() {
    for pkg in "${@}"; do
        if  busybox grep "${pkg}" /var/lib/apt/apt-rm-queue >/dev/null 2>&1; then
            busybox sed -i "/^${pkg}$/d" /var/lib/apt/apt-rm-queue
        else
            if ! busybox grep "^Package: ${pkg}$" /var/lib/dpkg/status >/dev/null 2>&1; then
                printf "%s\\n" "${pkg}" >> /var/lib/apt/apt-add-queue
            fi
        fi
    done; unset pkg
}

_apt_rm_queue() {
    for pkg in "${@}"; do
        if  busybox grep "${pkg}" /var/lib/apt/apt-add-queue >/dev/null 2>&1; then
            busybox sed -i "/^${pkg}$/d" /var/lib/apt/apt-add-queue
        else
            if busybox grep "^Package: ${pkg}$" /var/lib/dpkg/status >/dev/null 2>&1; then
                printf "%s\\n" "${pkg}" >> /var/lib/apt/apt-rm-queue
            fi
        fi
    done; unset pkg
}

_apt_install() {
    [ -z "${1}" ] && return
    _apt_add_queue $(printf "%s\\n" "${@}" | busybox sed "s:${package}::g")
}

_apt_purge() {
    [ -z "${1}" ] && return
    _apt_rm_queue $(printf "%s\\n" "${@}" | busybox sed "s:${package}::g")
}

_apt_daemon() {
    [ ! -f /var/lib/apt/apt-add-queue ] && [ ! -f /var/lib/apt/apt-rm-queue ] && return

    _apt_daemon__script="/var/lib/apt/apt-daemon"
    _apt_daemon_generator() {
        printf "%s\\n" "#!/bin/sh"
        printf "%s\\n" "#autogenerated by ${package}: $(date +%d-%m-%Y:%H:%M)"
        printf "\\n"
        printf "%s\\n" "if [ -f /var/lib/apt/apt-add-queue ]; then"
        printf "%s\\n" "  cp /usr/share/debconf/confmodule /usr/share/debconf/confmodule.bk"
        printf "%s\\n" "  cp /usr/share/minos/debconf/confmodule /usr/share/debconf/confmodule"
        printf "%s\\n" "  while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do sleep 1; done"
        printf "%s\\n" "  DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \\"
        printf "%s\\n" "      -o Dpkg::Options::=\"--force-confdef\" \\"
        printf "%s\\n" "      -o Dpkg::Options::=\"--force-confold\" \\"
        printf "%s\\n" "      --force-yes \$(cat /var/lib/apt/apt-add-queue)"
        printf "%s\\n" "  mv /usr/share/debconf/confmodule.bk /usr/share/debconf/confmodule"
        printf "%s\\n" "fi"
        printf "\\n"
        printf "%s\\n" "if [ -f /var/lib/apt/apt-rm-queue ]; then"
        printf "%s\\n" "  cp /usr/share/debconf/confmodule /usr/share/debconf/confmodule.bk"
        printf "%s\\n" "  cp /usr/share/minos/debconf/confmodule /usr/share/debconf/confmodule"
        printf "%s\\n" "  while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do sleep 1; done"
        printf "%s\\n" "    DEBIAN_FRONTEND=noninteractive apt-get purge -y \$(cat /var/lib/apt/apt-rm-queue)"
        printf "%s\\n" "  mv /usr/share/debconf/confmodule.bk /usr/share/debconf/confmodule"
        printf "%s\\n" "fi"
        printf "\\n"
        printf "%s\\n" "mv /var/lib/apt/apt-add-queue /var/lib/apt/apt-add-queue.bk"
        printf "%s\\n" "mv /var/lib/apt/apt-rm-queue  /var/lib/apt/apt-rm-queue.bk"
        printf "\\n"
        printf "%s\\n" "rm -rf /var/lib/apt/apt-add-queue"
        printf "%s\\n" "rm -rf /var/lib/apt/apt-rm-queue"
        printf "%s\\n" "rm -rf \"${_apt_daemon__script}\""
    }
    _apt_daemon_generator > "${_apt_daemon__script}" && chmod +x "${_apt_daemon__script}"
    _daemonize /bin/sh   -c "${_apt_daemon__script}"
    printf "%s\\n" "${package}: package changes will be applied shortly upon completion of this apt/dpkg instance"
    printf "%s\\n" "${package}: DO NOT POWEROFF the system until it completes"
}

_apt_install foo bar
_apt_purge   ugly packages
_apt_daemon
0
Tobi Oetiker On

Yes, you should not do this -- but if you have to -- here is how you could call apt-get from inside postinst script. The same approach would work for calling dpkg directly.

The idea is to launch a subshell which patiently waits in the background until all install activity has ended and then does its thing.

((
   # wait for install to finish
   while pgrep -x 'dpkg|apt|apt-get' > /dev/null; do sleep 1; done
   # clear environment
   eval $(env|grep -E 'DPKG|DEB'|awk -F= '{print "unset "$1}')
   export DEBIAN_FRONTEND=noninteractive
   # make sure no one else is locking the dpkg database
   flock --exclusive --close /var/lib/dpkg/lock \
       apt-get remove popularitycontest
) 2>&1 >/dev/null </dev/null &)

Obviously this will cause problems if there are multiple packages which try this trick. So as I said above, you should really not do this.