#!/usr/bin/env python
# Copyright (c) Facebook, Inc. and its affiliates.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
'''

Extends FBCodeBuilder to produce Docker context directories.

In order to get the largest iteration-time savings from Docker's build
caching, you will want to:
 - Use fine-grained steps as appropriate (e.g. separate make & make install),
 - Start your action sequence with the lowest-risk steps, and with the steps
   that change the least often, and
 - Put the steps that you are debugging towards the very end.

'''
import logging
import os
import shutil
import tempfile

from fbcode_builder import FBCodeBuilder
from shell_quoting import (
    raw_shell, shell_comment, shell_join, ShellQuoted, path_join
)
from utils import recursively_flatten_list, run_command


class DockerFBCodeBuilder(FBCodeBuilder):

    def _user(self):
        return self.option('user', 'root')

    def _change_user(self):
        return ShellQuoted('USER {u}').format(u=self._user())

    def setup(self):
        # Please add RPM-based OSes here as appropriate.
        #
        # To allow exercising non-root installs -- we change users after the
        # system packages are installed.  TODO: For users not defined in the
        # image, we should probably `useradd`.
        return self.step(
            "Setup",
            [
                # Docker's FROM does not understand shell quoting.
                ShellQuoted("FROM {}".format(self.option("os_image"))),
                # /bin/sh syntax is a pain
                ShellQuoted('SHELL ["/bin/bash", "-c"]'),
            ]
            + self.install_debian_deps()
            + [self._change_user()]
            + [self.workdir(self.option("prefix"))]
            + self.create_python_venv()
            + self.python_venv()
            + self.rust_toolchain(),
        )

    def python_venv(self):
        # To both avoid calling venv activate on each RUN command AND to ensure
        # it is present when the resulting container is run add to PATH
        actions = []
        if self.option("PYTHON_VENV", "OFF") == "ON":
            actions = ShellQuoted('ENV PATH={p}:$PATH').format(
                p=path_join(self.option('prefix'), "venv", "bin"))
        return(actions)

    def step(self, name, actions):
        assert '\n' not in name, 'Name {0} would span > 1 line'.format(name)
        b = ShellQuoted('')
        return [ShellQuoted('### {0} ###'.format(name)), b] + actions + [b]

    def run(self, shell_cmd):
        return ShellQuoted('RUN {cmd}').format(cmd=shell_cmd)

    def set_env(self, key, value):
        return ShellQuoted("ENV {key}={val}").format(key=key, val=value)

    def workdir(self, dir):
        return [
            # As late as Docker 1.12.5, this results in `build` being owned
            # by root:root -- the explicit `mkdir` works around the bug:
            #   USER nobody
            #   WORKDIR build
            ShellQuoted('USER root'),
            ShellQuoted('RUN mkdir -p {d} && chown {u} {d}').format(
                d=dir, u=self._user()
            ),
            self._change_user(),
            ShellQuoted('WORKDIR {dir}').format(dir=dir),
        ]

    def comment(self, comment):
        # This should not be a command since we don't want comment changes
        # to invalidate the Docker build cache.
        return shell_comment(comment)

    def copy_local_repo(self, repo_dir, dest_name):
        fd, archive_path = tempfile.mkstemp(
            prefix='local_repo_{0}_'.format(dest_name),
            suffix='.tgz',
            dir=os.path.abspath(self.option('docker_context_dir')),
        )
        os.close(fd)
        run_command('tar', 'czf', archive_path, '.', cwd=repo_dir)
        return [
            ShellQuoted('ADD {archive} {dest_name}').format(
                archive=os.path.basename(archive_path), dest_name=dest_name
            ),
            # Docker permissions make very little sense... see also workdir()
            ShellQuoted('USER root'),
            ShellQuoted('RUN chown -R {u} {d}').format(
                d=dest_name, u=self._user()
            ),
            self._change_user(),
        ]

    def _render_impl(self, steps):
        return raw_shell(shell_join('\n', recursively_flatten_list(steps)))

    def debian_ccache_setup_steps(self):
        source_ccache_tgz = self.option('ccache_tgz', '')
        if not source_ccache_tgz:
            logging.info('Docker ccache not enabled')
            return []

        dest_ccache_tgz = os.path.join(
            self.option('docker_context_dir'), 'ccache.tgz'
        )

        try:
            try:
                os.link(source_ccache_tgz, dest_ccache_tgz)
            except OSError:
                logging.exception(
                    'Hard-linking {s} to {d} failed, falling back to copy'
                    .format(s=source_ccache_tgz, d=dest_ccache_tgz)
                )
                shutil.copyfile(source_ccache_tgz, dest_ccache_tgz)
        except Exception:
            logging.exception(
                'Failed to copy or link {s} to {d}, aborting'
                .format(s=source_ccache_tgz, d=dest_ccache_tgz)
            )
            raise

        return [
            # Separate layer so that in development we avoid re-downloads.
            self.run(ShellQuoted('apt-get install -yq ccache')),
            ShellQuoted('ADD ccache.tgz /'),
            ShellQuoted(
                # Set CCACHE_DIR before the `ccache` invocations below.
                'ENV CCACHE_DIR=/ccache '
                # No clang support for now, so it's easiest to hardcode gcc.
                'CC="ccache gcc" CXX="ccache g++" '
                # Always log for ease of debugging. For real FB projects,
                # this log is several megabytes, so dumping it to stdout
                # would likely exceed the Travis log limit of 4MB.
                #
                # On a local machine, `docker cp` will get you the data.  To
                # get the data out from Travis, I would compress and dump
                # uuencoded bytes to the log -- for Bistro this was about
                # 600kb or 8000 lines:
                #
                #   apt-get install sharutils
                #   bzip2 -9 < /tmp/ccache.log | uuencode -m ccache.log.bz2
                'CCACHE_LOGFILE=/tmp/ccache.log'
            ),
            self.run(ShellQuoted(
                # Future: Skipping this part made this Docker step instant,
                # saving ~1min of build time.  It's unclear if it is the
                # chown or the du, but probably the chown -- since a large
                # part of the cost is incurred at image save time.
                #
                # ccache.tgz may be empty, or may have the wrong
                # permissions.
                'mkdir -p /ccache && time chown -R nobody /ccache && '
                'time du -sh /ccache && '
                # Reset stats so `docker_build_with_ccache.sh` can print
                # useful values at the end of the run.
                'echo === Prev run stats === && ccache -s && ccache -z && '
                # Record the current time to let travis_build.sh figure out
                # the number of bytes in the cache that are actually used --
                # this is crucial for tuning the maximum cache size.
                'date +%s > /FBCODE_BUILDER_CCACHE_START_TIME && '
                # The build running as `nobody` should be able to write here
                'chown nobody /tmp/ccache.log'
            )),
        ]
