#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import argparse
import errno
import os
import os.path
import pickle
import subprocess
import sys


if sys.hexversion >= 0x03050000:
    decode_error_handler = 'backslashreplace'
else:
    decode_error_handler = 'replace'


def decode(value):
    if isinstance(value, bytes):
        return value.decode('utf-8', errors=decode_error_handler)
    return value


def _log(template, *args, **kwargs):
    print(
        template.format(*(decode(arg) for arg in args)),
        **kwargs
    )


def log(template, *args, **kwargs):
    kwargs['file'] = sys.stderr
    _log(template, *args, **kwargs)


def out(template, *args, **kwargs):
    kwargs['file'] = sys.stdout
    _log(template, *args, **kwargs)


def verify_initial(prefix):
    if not os.path.isdir(prefix):
        log('{} does not exist! wtf?!', prefix)
        raise SystemExit(1)

    lib64 = os.path.join(prefix, b'lib64')
    lib32 = os.path.join(prefix, b'lib32')
    lib = os.path.join(prefix, b'lib')
    lib_new = os.path.join(prefix, b'lib.new')

    if not os.path.isdir(lib64) and not os.path.islink(lib64):
        log('{} needs to exist as a real directory!', lib64)
        raise SystemExit(1)

    if os.path.islink(lib32):
        log('{} is a symlink! was the migration done already?', lib32)
        raise SystemExit(1)

    if os.path.isdir(lib) and not os.path.islink(lib):
        log('{} is a real directory! was the migration done already?', lib)
        raise SystemExit(1)

    if os.path.islink(lib) and os.readlink(lib) == b'lib.new':
        log('{} is a symlink to lib.new! did you want to --finish?', lib)
        raise SystemExit(1)

    if not os.path.islink(lib) or os.readlink(lib) not in (b'lib64',
            os.path.join(prefix, b'lib64')):
        log('{} needs to be a symlink to lib64!', lib)
        raise SystemExit(1)

    if os.path.isdir(lib_new):
        log('{} exists! do you need to remove failed migration?', lib_new)
        raise SystemExit(1)


def verify_migrated(prefix):
    if not os.path.isdir(prefix):
        log('{} does not exist! wtf?!', prefix)
        raise SystemExit(1)

    lib64 = os.path.join(prefix, b'lib64')
    lib32 = os.path.join(prefix, b'lib32')
    lib = os.path.join(prefix, b'lib')
    lib_new = os.path.join(prefix, b'lib.new')

    if not os.path.isdir(lib64) and not os.path.islink(lib64):
        log('{} needs to exist as a real directory!', lib64)
        raise SystemExit(1)

    if os.path.islink(lib32):
        log('{} is a symlink! was the migration finished already?', lib32)
        raise SystemExit(1)

    if os.path.isdir(lib) and not os.path.islink(lib):
        log('{} is a real directory! was the migration finished already?', lib)
        raise SystemExit(1)

    if os.path.islink(lib) and os.readlink(lib) == b'lib64':
        log('{} is a symlink to lib64! did the migration succeed?', lib)
        raise SystemExit(1)

    if not os.path.isdir(lib_new):
        log('{} does not exist! did you --migrate?', lib_new)
        raise SystemExit(1)

    if not os.path.islink(lib) or os.readlink(lib) != b'lib.new':
        log('{} needs to be a symlink to lib.new!', lib)
        raise SystemExit(1)


def path_get_leftmost_dirs(paths):
    for p in paths:
        if b'/' in p:
            yield p.split(b'/', 1)[0]


def path_get_top_files(paths):
    for p in paths:
        if b'/' not in p:
            yield p


def path_starts_with(haystack, needle):
    return (haystack + b'/').startswith(needle + b'/')


def nonfatal_remove(fp):
    try:
        os.remove(fp)
    except OSError as e:
        if e.errno in (errno.EISDIR, errno.EPERM):
            try:
                os.rmdir(fp)
            except OSError as e:
                if e.errno not in (errno.EEXIST, errno.ENOTEMPTY):
                    log('Removing {} failed: {}', fp, e)
                    return False
        elif e.errno == errno.ENOENT:
            pass
        else:
            log('Removing {} failed: {}', fp, e)
            return False
    return True


def is_lib64_candidate(path):
    if os.path.splitext(path)[1] in (b'.a', b'.chk', b'.la', b'.so'):
        return True
    if b'.so.' in path:
        return True
    if path in (b'locale',):
        return True
    return False


class MigrationState(object):
    __slots__ = ('eroot', 'excludes', 'includes', 'prefixes', 'has_lib32')

    def __init__(self, eroot):
        self.eroot = os.path.normpath(eroot)

    def analyze(self, usr_merge, real_prefixes):
        from portage import create_trees, _encodings

        log('Analyzing files installed into lib & lib64...')

        # use all canonical prefixes that could be present in vdb
        usr_prefix = os.path.join(self.eroot, b'usr')
        subprefixes = (
            self.eroot,
            usr_prefix,
            os.path.join(usr_prefix, b'local'),
        )

        lib_path = dict((prefix, os.path.join(prefix, b'lib/')) for prefix in subprefixes)
        lib32_path = dict((prefix, os.path.join(prefix, b'lib32/')) for prefix in subprefixes)
        lib64_path = dict((prefix, os.path.join(prefix, b'lib64/')) for prefix in subprefixes)

        lib_paths = dict((prefix, set()) for prefix in subprefixes)
        lib32_paths = dict((prefix, set()) for prefix in subprefixes)
        lib64_paths = dict((prefix, set()) for prefix in subprefixes)

        trees = create_trees(config_root=self.eroot)
        vartree = trees[max(trees)]['vartree']
        settings = vartree.settings
        eprefix = settings['EPREFIX'].encode(_encodings['fs'])
        assert self.eroot.endswith(eprefix), 'EROOT must be ROOT/EPREFIX.'
        root = self.eroot[:-len(eprefix)] or b'/'

        vardb = vartree.dbapi
        missing_files = set()
        for p in vardb.cpv_all():
            for f, details in vardb._dblink(p).getcontents().items():
                # skip directories; we will get them implicitly via
                # files contained within them
                if details[0] == 'dir':
                    continue
                f = os.path.join(root, f.encode(_encodings['fs']).lstrip(b'/'))
                for prefix in subprefixes:
                    for libdir, dest in (
                            (lib_path[prefix], lib_paths[prefix]),
                            (lib32_path[prefix], lib32_paths[prefix]),
                            (lib64_path[prefix], lib64_paths[prefix])):
                        if f.startswith(libdir):
                            if not os.path.exists(f):
                                missing_files.add(f)
                            else:
                                dest.add(f[len(libdir):])
                            break

        pure_lib = {}
        mixed_lib = {}

        if usr_merge:
            log('')
            log('/usr merge detected!')

            # figure out which of the prefixes is real, and the other
            # will be the aliased one
            real_prefix = real_prefixes[0]
            if real_prefix == usr_prefix:
                alias_prefix = self.eroot
            else:
                alias_prefix = usr_prefix

            for d in lib_paths, lib32_paths, lib64_paths:
                d[real_prefix].update(d[alias_prefix])
                del d[alias_prefix]

        self.prefixes = real_prefixes
        self.excludes = {}
        self.includes = {}

        for prefix in real_prefixes:
            log('')
            lib_paths[prefix] = frozenset(lib_paths[prefix])
            lib32_paths[prefix] = frozenset(lib32_paths[prefix])
            lib64_paths[prefix] = frozenset(lib64_paths[prefix])

            lib_prefixes = frozenset(path_get_leftmost_dirs(lib_paths[prefix]))
            lib64_prefixes = frozenset(path_get_leftmost_dirs(lib64_paths[prefix]))
            lib_files = frozenset(path_get_top_files(lib_paths[prefix]))
            lib64_files = frozenset(path_get_top_files(lib64_paths[prefix]))

            pure_lib = lib_prefixes - lib64_prefixes
            mixed_lib = lib_prefixes & lib64_prefixes

            unowned_files = (frozenset(os.listdir(lib_path[prefix]))
                             - lib_prefixes - lib64_prefixes
                             - lib_files - lib64_files)
            # library symlinks go to lib64
            lib64_unowned = frozenset(x for x in unowned_files
                                      if is_lib64_candidate(x))
            lib_unowned = unowned_files - lib64_unowned

            log('directories that will be moved to {}:', lib_path[prefix])
            for p in sorted(pure_lib):
                log('\t{}', p)
            log('\t(+ {} files)', len(lib_files))
            log('')

            log('directories whose contents will be split between {} and {}:', lib_path[prefix], lib64_path[prefix])
            for p in sorted(mixed_lib):
                log('\t{}', p)

            if lib_unowned:
                log('')
                log('orphan dirs/files (not owned by any package) that will be moved to {}:', lib_path[prefix])
                for p in sorted(lib_unowned):
                    log('\t{}', p)

            if lib64_unowned:
                log('')
                log('orphan dirs/files (not owned by any package) that will be kept in {}:', lib64_path[prefix])
                for p in sorted(lib64_unowned):
                    log('\t{}', p)

            # prepare the exclude lists
            excludes = set()
            for p in lib64_paths[prefix]:
                for tp in mixed_lib:
                    if path_starts_with(p, tp):
                        excludes.add(p)
                        break

            # store the data
            self.includes[prefix] = lib_prefixes | lib_files | lib_unowned
            self.excludes[prefix] = frozenset(excludes)

            # verify for conflicts
            # 1. lib/foo and lib32/foo are going to be the same path now
            conflicts = ((lib_paths[prefix] | lib_unowned)
                         & lib32_paths[prefix])
            if conflicts:
                log('')
                log('')
                log('One or more files are both in {} and {}, making the conversion impossible.',
                    lib_path[prefix], lib32_path[prefix])
                log('')
                for p in sorted(conflicts):
                    log('\t{}', p)
                log('')
                log('Please report a bug at https://bugs.gentoo.org/, and do not proceed with')
                log('the migration until a proper solution is found.')
                raise SystemExit(1)

            # 2. lib/foo and lib64/foo were *supposed* to be the same path now
            # so we must not have two entries for each
            conflicts = lib_paths[prefix] & lib64_paths[prefix]
            if conflicts:
                log('')
                log('')
                log('One or more files are both in {} and {}, making the conversion impossible.',
                    lib_path[prefix], lib64_path[prefix])
                log('')
                for p in sorted(conflicts):
                    log('\t{}', p)
                log('')
                log('Please report a bug at https://bugs.gentoo.org/, and do not proceed with')
                log('the migration until a proper solution is found.')
                raise SystemExit(1)

        self.has_lib32 = any(lib32_paths.values())
        if not self.has_lib32:
            log('')
            log('')
            log('Warning: no lib32 paths found. This is fine if you are running no-multilib,')
            log('otherwise this is suspicious.')

        if missing_files:
            log('')
            log('')
            log('One or more package files are missing from the system. This should not')
            log('cause any problems but you may want to reinstall the packages')
            log('that installed them. The missing files are:')
            log('')
            for p in sorted(missing_files):
                log('\t{}', p)

        # check for mountpoints, they are trouble
        mountpoints_staying = set()
        mountpoints_moved = set()
        for prefix in real_prefixes:
            for topdir in (lib64_path[prefix], lib32_path[prefix]):
                if not os.path.isdir(topdir):
                    continue
                for dirpath, dirnames, _ in os.walk(topdir):
                    for d in list(dirnames):
                        dp = os.path.join(dirpath, d)
                        if os.path.ismount(dp):
                            for x in self.includes[prefix]:
                                if path_starts_with(os.path.relpath(dp, topdir), x):
                                    mountpoints_moved.add(dp)
                                    break
                            else:
                                mountpoints_staying.add(dp)
                            # do not process filesystems recursively
                            dirnames.remove(d)

        if mountpoints_staying:
            log('')
            log('')
            log('One or more mount points (or subvolumes) detected:')
            log('')
            for p in sorted(mountpoints_staying):
                log('\t{}', p)
            log('')
            log('Those directories do not need to be migrated. However, please make sure')
            log('that fstab is using correct (real) paths to them.')

        if mountpoints_moved:
            log('')
            log('')
            log('One or more mount points (or subvolumes) need to be migrated:')
            log('')
            for p in sorted(mountpoints_moved):
                log('\t{}', p)
            log('')
            log('Migration of mount points is not supported by the script. The files from them')
            log('will be copied to the parent filesystem. The --finish subcommand will fail')
            log('at their removal, and will have to be resumed after unmounting the cleansed')
            log('file system (or removing the subvolume).')
            log('')
            log('If this is acceptable, you can proceed with the migration. Alternatively, you')
            log('can unmount the relevant file systems, rerun the script (including rerunning')
            log('--analyze!) and migrate them manually afterwards.')

    def migrate(self, pretend, hardlink=False):
        try:
            # create the lib.new directories
            for prefix in self.prefixes:
                lib = os.path.join(prefix, b'lib')
                lib32 = os.path.join(prefix, b'lib32')
                lib_new = os.path.join(prefix, b'lib.new')

                if pretend:
                    out('mkdir {}', lib_new)
                else:
                    os.mkdir(lib_new)

                if not pretend:
                    log('[{}] & {} -> {} ...', lib32, lib, lib_new)
                link_flag = b'--link' if hardlink else b'--reflink=auto'
                cmd = [b'cp', b'-a', link_flag, b'--']
                if os.path.isdir(lib32):
                    cmd.append(os.path.join(lib32, b'.'))
                # include all appropriate pure&mixed lib stuff
                for p in self.includes[prefix]:
                    assert not p.endswith(b'/')
                    cmd.append(os.path.join(lib, p))
                cmd.append(lib_new + b'/')

                if len(cmd) > 5:
                    if pretend:
                        out('{}', b' '.join(cmd))
                    else:
                        p = subprocess.Popen(cmd)
                        if p.wait() != 0:
                            log('Non-successful return from cp: {}', p.returncode)
                            raise SystemExit(1)

                if self.excludes[prefix]:
                    if not pretend:
                        log('Remove extraneous files from {} ...', lib_new)
                    # remove excluded stuff
                    for p in self.excludes[prefix]:
                        fp = os.path.join(lib_new, p)
                        if pretend:
                            out('rm {}', fp)
                            out('rmdir -p --ignore-fail-on-non-empty {}',
                                          os.path.dirname(fp))
                        else:
                            os.unlink(fp)

                            try:
                                os.removedirs(os.path.dirname(fp))
                            except OSError as e:
                                if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
                                    raise
        except:
            log('')
            log('An error occurred while creating the "lib.new" directories. Please look')
            log('at the backtrace following this message for details. The partially')
            log('created "lib.new" directories were left in case they were useful')
            log('for determining the cause of the error.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --force-rollback action to reset your system.')
            log('')
            raise

        try:
            for prefix in self.prefixes:
                lib = os.path.join(prefix, b'lib')
                lib_tmp = os.path.join(prefix, b'lib.tmp')
                if pretend:
                    out('ln -s -f -T lib.new {}', lib)
                else:
                    log('Updating: {} -> lib.new ...', lib)
                    os.symlink(b'lib.new', lib_tmp)
                    os.rename(lib_tmp, lib)
        except:
            log('')
            log('An error occurred while updating the "lib" symlinks. Please look')
            log('at the backtrace following this message for details. The "lib.new"')
            log('directories are complete now but the "lib" symlinks were not updated')
            log('completely.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --force-rollback action to reset your system.')
            log('')
            raise

    def rollback(self, pretend):
        try:
            # restore the old 'lib' symlink
            for prefix in self.prefixes:
                lib = os.path.join(prefix, b'lib')
                lib_tmp = os.path.join(prefix, b'lib.tmp')
                if pretend:
                    out('ln -s -f -T lib64 {}', lib)
                else:
                    log('Updating: {} -> lib64 ...', lib)
                    os.symlink(b'lib64', lib_tmp)
                    os.rename(lib_tmp, lib)
        except:
            log('')
            log('An error occurred while restoring the "lib" symlinks. Please look')
            log('at the backtrace following this message for details.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --force-rollback action to reset your system.')
            log('')
            raise

        # clean up lib.new
        rm_failed = False
        for prefix in self.prefixes:
            lib_new = os.path.join(prefix, b'lib.new')
            if pretend:
                out('rm -rf -- {}', lib_new)
            else:
                log('Removing: {} ...', lib_new)
                p = subprocess.Popen([b'rm', b'-rf', b'--', lib_new])
                if p.wait() != 0:
                    rm_failed = True

        if rm_failed:
            log('')
            log('An error occurred while cleaning up the "lib.new" directories.')
            log('This message should be preceded by error messages from the "rm"')
            log('utility.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --force-rollback action to reset your system.')

    def finish(self, pretend, resume):
        try:
            # replace the 'lib' symlink with the directory
            for prefix in self.prefixes:
                lib = os.path.join(prefix, b'lib')
                lib_new = os.path.join(prefix, b'lib.new')
                # if lib.new does not exist, it has probably been moved already
                if resume and not os.path.isdir(lib_new):
                    continue
                if pretend:
                    out('mv -f -T {} {}', lib_new, lib)
                else:
                    log('Renaming {} -> {} ...', lib_new, lib)
                    # when resuming, 'lib' may have already been unlinked
                    # (otherwise, it must exist at this point)
                    if not resume or os.path.islink(lib):
                        os.unlink(lib)
                    os.rename(lib_new, lib)
        except:
            log('')
            log('An error occurred while replacing the "lib" symlinks. Please look')
            log('at the backtrace following this message for details.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --resume-finish action.')
            log('')
            raise

        try:
            # replace 'lib32' with a symlink
            for prefix in self.prefixes:
                lib32 = os.path.join(prefix, b'lib32')
                if os.path.isdir(lib32):
                    # if resuming, it can be a symlink already
                    if resume and os.path.islink(lib32):
                        continue
                    if pretend:
                        out('rm -rf -- {}', lib32)
                        out('ln -s lib {}', lib32)
                    else:
                        log('Removing: {} ...', lib32)
                        if subprocess.Popen([b'rm', b'-rf', b'--', lib32]).wait() != 0:
                            if os.path.ismount(lib32):
                                log('')
                                log('Note: lib32 looks like a mount point. If all files inside it were removed')
                                log('successfully, you need to unmount it to let the program replace it with')
                                log('a symlink. You can remove or reuse the backing device afterwards.')
                            raise SystemExit(1)
                        else:
                            log('Updating: {} -> lib ...', lib32)
                            try:
                                os.symlink(b'lib', lib32)
                            except OSError:
                                log('Symlinking failed for {}, please symlink it manually to lib.')
        except:
            log('')
            log('An error occurred while replacing the "lib32" directories. Please look')
            log('at the backtrace following this message for details.')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --resume-finish action.')
            log('')
            raise

        # clean up extraneous files from 'lib64'
        rm_failed = False
        for prefix in self.prefixes:
            lib64 = os.path.join(prefix, b'lib64')
            if not pretend:
                out('Removing stale files from {} ...', lib64)
            for p in self.includes[prefix]:
                if not os.path.islink(os.path.join(lib64, p)):
                    for root, dirs, files in os.walk(os.path.join(lib64, p), topdown=False):
                        for f in dirs + files:
                            fp = os.path.join(root, f)
                            rp = os.path.relpath(fp, lib64)
                            if rp not in self.excludes[prefix]:
                                if pretend:
                                    out('rm -d {}', fp)
                                elif not nonfatal_remove(fp):
                                    rm_failed = True
                if pretend:
                    out('rm -d {}', os.path.join(lib64, p))
                elif not nonfatal_remove(os.path.join(lib64, p)):
                    rm_failed = True

        if rm_failed:
            log('')
            log('An error occurred while cleaning up the "lib64" directories.')
            log('This message should be preceded by more specific error messages')
            log('(interspersed with progress output).')
            log('')
            log('Once you determine the cause of the error and would like to retry,')
            log('please use the --resume-finish action.')
            raise SystemExit(1)

    def save_state(self):
        with open(os.path.expanduser(b'~/.symlink_lib_migrate.state'), 'wb') as f:
            pickle.dump((self.eroot, self.prefixes, self.excludes, self.includes, self.has_lib32), f)

    def load_state(self):
        try:
            with open(os.path.expanduser(b'~/.symlink_lib_migrate.state'), 'rb') as f:
                orig_eroot, self.prefixes, self.excludes, self.includes, self.has_lib32 = pickle.load(f)
        except (OSError, IOError) as e:
            if e.errno == errno.ENOENT:
                return False
            else:
                raise

        if self.eroot != orig_eroot:
            raise NotImplementedError('The same --root must be passed to each invocation')

        return True

    def clear_state(self):
        try:
            os.unlink(os.path.expanduser(b'~/.symlink_lib_migrate.state'))
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise


def argv_to_bytes(arg):
    if sys.hexversion >= 0x03000000:
        return os.fsencode(arg)
    else:
        return arg


def main():
    if sys.hexversion < 0x03000000:
        reload(sys)
        sys.setdefaultencoding('utf-8')
    os.umask(0o22)

    argp = argparse.ArgumentParser()
    argp.add_argument('-p', '--pretend', action='store_true',
                      help='Do not modify the system, only print what would happen')
    argp.add_argument('--root', default='/',
                      help='Run on specified alternate system root')
    g = argp.add_argument_group('basic actions')
    g.add_argument('--analyze', action='store_const', dest='action',
                   const='analyze', help='Analyze and store system state',
                   default='analyze')
    g.add_argument('--migrate', action='store_const', dest='action',
                   const='migrate', help='Perform the migration')
    g.add_argument('--rollback', action='store_const', dest='action',
                   const='rollback', help='Revert the migration (after --migrate)')
    g.add_argument('--finish', action='store_const', dest='action',
                   const='finish', help='Finish the migration (clean up)')
    g = argp.add_argument_group('recovery actions (NO SAFETY!)')
    g.add_argument('--force-rollback', action='store_const', dest='action',
                   const='rollback_force',
                   help='Force resetting "lib" symlink and removing "lib.new"')
    g.add_argument('--resume-finish', action='store_const', dest='action',
                   const='finish_resume',
                   help='Attempt resuming failed --finish action')
    g = argp.add_argument_group('expert options')
    g.add_argument('-P', '--prefix',
                   help='Migrate only the specified prefix directory')
    g.add_argument('-U', '--unprivileged', action='store_true',
                   help='Permit running as unprivileged user (for Prefix)')
    g.add_argument('--hardlink', action='store_true',
                   help='Use hard links instead of copying')
    args = argp.parse_args()

    is_root = os.geteuid() == 0 or args.unprivileged

    if not is_root:
        if args.action != 'analyze':
            argp.error('Requested action requires root privileges')
        else:
            log('[Running as unprivileged user, results will not be saved]')

    top_dir = argv_to_bytes(args.root)

    if args.prefix:
        usr_merge = False
        prefixes = [argv_to_bytes(args.prefix)]
    else:
        # helpful consts
        usr_dir = os.path.join(top_dir, b'usr')
        lib64_dir = os.path.join(top_dir, b'lib64')
        usr_lib64_dir = os.path.join(usr_dir, b'lib64')

        # if /lib64 and /usr/lib64 are the same directory, we're dealing
        # with /usr merge most likely
        if os.path.samefile(lib64_dir, usr_lib64_dir):
            usr_merge = True
            # use whichever prefix has the real directories
            if not os.path.islink(lib64_dir):
                prefixes = [top_dir]
            else:
                prefixes = [usr_dir]
        else:
            usr_merge = False
            prefixes = [top_dir, usr_dir]

        # add /usr/local too
        prefixes.append(os.path.join(usr_dir, b'local'))

    if args.action == 'analyze':
        for p in prefixes:
            verify_initial(p)

        m = MigrationState(top_dir)
        m.analyze(usr_merge, prefixes)

        # safety check
        assert m.prefixes == prefixes

        if is_root:
            m.save_state()
            log('')
            log('')
            log('The state has been saved and the migration is ready to proceed.')
            log('To initiate it, please run:')
            log('')
            log('\t{} --migrate', sys.argv[0])
            log('')
            log('Please do not perform any changes to the system at this point.')
            log('If you performed any changes, please rerun the analysis.')
        else:
            log('')
            log('')
            log('Everything looks good from here. However, you need to rerun')
            log('the process as root to confirm.')
    elif args.action == 'migrate':
        for p in prefixes:
            verify_initial(p)

        m = MigrationState(top_dir)
        if not m.load_state():
            log('State file could not be loaded. Did you run --analyze?')
            return 1
        if args.pretend:
            log('Those are the actions that would be performed:')
        m.migrate(pretend=args.pretend, hardlink=args.hardlink)
        if not args.pretend:
            log('')
            log('')
            log('Initial migration complete. Please now test whether your system works')
            log('correctly. It might be a good idea to try rebooting it. Once tested,')
            log('complete the migration and clean up backup files via calling:')
            log('')
            log('\t{} --finish', sys.argv[0])
            log('')
            log('If you wish to revert the changes, run:')
            log('')
            log('\t{} --rollback', sys.argv[0])
    elif args.action.startswith('rollback'):
        if not args.action.endswith('force'):
            for p in prefixes:
                verify_migrated(p)

        m = MigrationState(top_dir)
        if not m.load_state():
            log('State file could not be loaded. Did you run --analyze?')
            return 1
        if args.pretend:
            log('Those are the actions that would be performed:')
        m.rollback(pretend=args.pretend)
        if not args.pretend:
            m.clear_state()
            log('')
            log('')
            log('Rollback complete. Your system should now be as before the migration.')
            log('Please look into fixing your issues and try again.')
    elif args.action.startswith('finish'):
        if not args.action.endswith('resume'):
            for p in prefixes:
                verify_migrated(p)

        m = MigrationState(top_dir)
        if not m.load_state():
            log('State file could not be loaded. Did you run --analyze?')
            return 1
        if args.pretend:
            log('Those are the actions that would be performed:')
        m.finish(pretend=args.pretend, resume=args.action.endswith('resume'))
        if not args.pretend:
            m.clear_state()
            log('')
            log('')
            log('Migration complete. Please switch to the new profiles, or add')
            log('the following to your make.conf (or equivalent):')
            log('')
            log('\tSYMLINK_LIB=no')
            if m.has_lib32:
                log('\tLIBDIR_x86=lib')
            log('')
            log('Afterwards, please rebuild all installed GCC versions', end='')
            if m.has_lib32:
                log(' and all\npackages installing into lib32', end='')
            log(', e.g.:')
            log('')
            log('\temerge -1v {}', os.path.join(top_dir, b'usr/lib/gcc'), end='')
            if m.has_lib32:
                log(' {} {}',
                    os.path.join(top_dir, b'lib32'),
                    os.path.join(top_dir, b'usr/lib32'),
                    end='')
            log('')
            if m.has_lib32:
                log('')
                log('When the rebuilds are complete, the package manager should remove')
                log('the lib32 symlink. If it does not, do:')
                log('')
                log('\trm {} {}',
                    os.path.join(top_dir, b'lib32'),
                    os.path.join(top_dir, b'usr/lib32'))
    else:
        raise NotImplementedError()


if __name__ == '__main__':
    main()
