dnf: distro-syncとupgradeの違いを調べてみる

Fedora 22から公式に採用されたパッケージマネージャのdnfで、distro-syncとupgradeは何が違うのかというのを調べてみました。

まずはお約束通りにmanから。

distro-syncの説明。

   Distro-sync command
       dnf distro-sync [<package-spec>...]
              As necessary upgrades, downgrades or keeps selected installed packages to match the latest version available from any enabled repository. If no package is given, all installed packages are con‐
              sidered.

              See also Configuration Files Replacement Policy.

upgradeの説明。

   Upgrade Command
       dnf [options] upgrade
              Updates each package to a highest version that is both available and resolvable.

       dnf [options] upgrade <package-specs>...
              Updates each specified package to the latest available version. Updates dependencies as necessary.

       See also Configuration Files Replacement Policy.

updateコマンドもありますが、これはupgradeへのaliasとのことです。

これを見ると両者の違いは大体わかりますね。distro-syncの方は必要ならdowngradeもするんだけど、upgradeはバージョンが一番新しいものへの更新のみという感じですね。

ちょっとこれだけだとなんなので、ついでにソースも見てます。 まずはdistro-syncの方から。これはdnf/cli/commands/distrosync.pyにあります。ディレクトリを見るとdnfのinstall、upgrade等のコマンドはdnf/cli/commands/にありますね。

このファイルでdistro-syncの実行をするのはこのrun()で、実際の処理は別のところで行っています。extcmdsはdistro-syncコマンドに対するオプションが渡された場合に使われるのでdnf distro-syncとやった場合はextcmdsはNoneになっていると思います。

    def run(self, extcmds):
        return self.base.distro_sync_userlist(extcmds)

run()で呼んでいるdistro_sync_userlist()はdnf/cli/cli.pyにあります。

    def distro_sync_userlist(self, userlist):
        """ Upgrade or downgrade packages to match the latest versions available
            in the enabled repositories.
            :return: (exit_code, [ errors ])
            exit_code is::
                0 = we're done, exit
                1 = we've errored, exit with error string
                2 = we've got work yet to do, onto the next stage
        """
        oldcount = self._goal.req_length()
        if len(userlist) == 0:
            self.distro_sync()
        else:
            for pkg_spec in userlist:
                self.distro_sync(pkg_spec)

        cnt = self._goal.req_length() - oldcount
        if cnt <= 0 and not self._goal.req_has_distupgrade_all():
            msg = _('No packages marked for distribution synchronization.')
            raise dnf.exceptions.Error(msg)

distro-syncコマンドにオプションを渡さなかった場合はuserlistのサイズは0なので、self.distro_sync()が呼ばれます。ちなみにselfはBaseCliクラスです。

distro_sync()はBaseクラスのdnf/base.pyにあります。

    def distro_sync(self, pkg_spec=None):
        if pkg_spec is None:
            self._goal.distupgrade_all()
        else:
            sltrs = dnf.subject.Subject(pkg_spec).get_best_selectors(self.sack)
            match = reduce(lambda x, y: y.matches() or x, sltrs, [])
            if not match:
                logger.info(_('No package %s installed.'), pkg_spec)
                return 0
            for sltr in sltrs:
                if not sltr.matches():
                    continue
                self._goal.distupgrade(select=sltr)
        return 1

pkg_specはdistrosync.py.pyでのextcmdsなので、オプションを渡していなければself._goal.distupgrade_all()を呼びます。

ここで、distupgrade_all()ですが、これはdnfのソースツリーにはいません。hawkeyという別のライブラリにあります。dnfもhawkeyもrpm-software-managementの配下にあるので役割の違いというとこでしょうね。で、distupgrade_all()ですが、src/python/goal-py.cにあります。ここからはcのコードです。

static PyObject *
distupgrade_all(_GoalObject *self, PyObject *unused)
{
    int ret = hy_goal_distupgrade_all(self->goal);
    return op_ret2exc(ret);
}

メインはhy_goal_distupgrade_all()っぽいですね。探してみるとsrc/goal.c にあります。

int
hy_goal_distupgrade_all(HyGoal goal)
{
    goal->actions |= HY_DISTUPGRADE_ALL;
    queue_push2(&goal->staging, SOLVER_DISTUPGRADE|SOLVER_SOLVABLE_ALL, 0);
    return 0;
}

queue_push2はlibsolvというライブラリの関数みたいですね。このlibsolvはREADMEファイルで「This is libsolv, a free package dependency solver using a satisfiability algorithm.」と書かれています。サポートしているパッケージフォーマットはrpmdebなどがあるようです。 ということなので、dnfはパッケージの依存関係に関しては自前で処理をするのではなくて、外部のライブラリ(libsolv)を使っているということがわかりました。

ここで、なんとなく想像はついていますが、upgradeの方も見てみましょう。

upgradeコマンドの入り口はdnf/cli/commands/upgrade.pyで、distro-syncと同様にdnf/cli/commands以下にあります。

    def run(self, extcmds):
        pkg_specs, filenames = self.parse_extcmds(extcmds)

        if not pkg_specs and not filenames:
            # Update all packages.
            self.base.upgrade_all()
            done = True
        else:
            # Update files.
            local_pkgs = map(self.base.add_remote_rpm, filenames)
            results = map(self.base.package_upgrade, local_pkgs)
            done = functools.reduce(operator.or_, results, False)

            # Update packages.
            for pkg_spec in pkg_specs:
                try:
                    self.base.upgrade(pkg_spec)
                except dnf.exceptions.MarkingError:
                    logger.info(_('No match for argument: %s'), pkg_spec)
                else:

処理はこんな感じです。これもオプションでパッケージの指定をできますが、なければupgrade_all()を呼びます。upgrade_all()はdnf/base.pyにあります。

    def upgrade_all(self, reponame=None):
        # :api
        if reponame is None:
            self._goal.upgrade_all()
        else:
            try:
                self.upgrade('*', reponame)
            except dnf.exceptions.MarkingError:
                pass
        return 1

まあ、動作はdistro-syncと同じですね。upgrade_all()はhawkeyのsrc/python/goal-py.cにあります。

static PyObject *
upgrade_all(_GoalObject *self, PyObject *unused)
{
    int ret = hy_goal_upgrade_all(self->goal);
    return op_ret2exc(ret);
}

これもdistro-syncの場合と全く一緒です。hy_goal_upgrade_all()はsrc/goal.cにあります。

int
hy_goal_upgrade_all(HyGoal goal)
{
    goal->actions |= HY_UPGRADE_ALL;
    queue_push2(&goal->staging, SOLVER_UPDATE|SOLVER_SOLVABLE_ALL, 0);
    return 0;
}

この辺まででdistro-sync、upgradeがlibsolvを使って処理するというところまでわかりましたが、ここから先はデバッガでも動かしつつちゃんと動作を追わないと流れが見えないので今日はここまで/(^o^)\