1#!/usr/bin/env python
2# Copyright (c) Facebook, Inc. and its affiliates.
3from __future__ import absolute_import
4from __future__ import division
5from __future__ import print_function
6from __future__ import unicode_literals
7
8"""
9shell_builder.py allows running the fbcode_builder logic
10on the host rather than in a container.
11
12It emits a bash script with set -exo pipefail configured such that
13any failing step will cause the script to exit with failure.
14
15== How to run it? ==
16
17cd build
18python fbcode_builder/shell_builder.py > ~/run.sh
19bash ~/run.sh
20"""
21
22import distutils.spawn
23import os
24
25from fbcode_builder import FBCodeBuilder
26from shell_quoting import raw_shell, shell_comment, shell_join, ShellQuoted
27from utils import recursively_flatten_list
28
29
30class ShellFBCodeBuilder(FBCodeBuilder):
31    def _render_impl(self, steps):
32        return raw_shell(shell_join("\n", recursively_flatten_list(steps)))
33
34    def set_env(self, key, value):
35        return ShellQuoted("export {key}={val}").format(key=key, val=value)
36
37    def workdir(self, dir):
38        return [
39            ShellQuoted("mkdir -p {d} && cd {d}").format(d=dir),
40        ]
41
42    def run(self, shell_cmd):
43        return ShellQuoted("{cmd}").format(cmd=shell_cmd)
44
45    def step(self, name, actions):
46        assert "\n" not in name, "Name {0} would span > 1 line".format(name)
47        b = ShellQuoted("")
48        return [ShellQuoted("### {0} ###".format(name)), b] + actions + [b]
49
50    def setup(self):
51        steps = (
52            [
53                ShellQuoted("set -exo pipefail"),
54            ]
55            + self.create_python_venv()
56            + self.python_venv()
57        )
58        if self.has_option("ccache_dir"):
59            ccache_dir = self.option("ccache_dir")
60            steps += [
61                ShellQuoted(
62                    # Set CCACHE_DIR before the `ccache` invocations below.
63                    "export CCACHE_DIR={ccache_dir} "
64                    'CC="ccache ${{CC:-gcc}}" CXX="ccache ${{CXX:-g++}}"'
65                ).format(ccache_dir=ccache_dir)
66            ]
67        return steps
68
69    def comment(self, comment):
70        return shell_comment(comment)
71
72    def copy_local_repo(self, dir, dest_name):
73        return [
74            ShellQuoted("cp -r {dir} {dest_name}").format(dir=dir, dest_name=dest_name),
75        ]
76
77
78def find_project_root():
79    here = os.path.dirname(os.path.realpath(__file__))
80    maybe_root = os.path.dirname(os.path.dirname(here))
81    if os.path.isdir(os.path.join(maybe_root, ".git")):
82        return maybe_root
83    raise RuntimeError(
84        "I expected shell_builder.py to be in the "
85        "build/fbcode_builder subdir of a git repo"
86    )
87
88
89def persistent_temp_dir(repo_root):
90    escaped = repo_root.replace("/", "sZs").replace("\\", "sZs").replace(":", "")
91    return os.path.join(os.path.expandvars("$HOME"), ".fbcode_builder-" + escaped)
92
93
94if __name__ == "__main__":
95    from utils import read_fbcode_builder_config, build_fbcode_builder_config
96
97    repo_root = find_project_root()
98    temp = persistent_temp_dir(repo_root)
99
100    config = read_fbcode_builder_config("fbcode_builder_config.py")
101    builder = ShellFBCodeBuilder(projects_dir=temp)
102
103    if distutils.spawn.find_executable("ccache"):
104        builder.add_option(
105            "ccache_dir", os.environ.get("CCACHE_DIR", os.path.join(temp, ".ccache"))
106        )
107    builder.add_option("prefix", os.path.join(temp, "installed"))
108    builder.add_option("make_parallelism", 4)
109    builder.add_option(
110        "{project}:local_repo_dir".format(project=config["github_project"]), repo_root
111    )
112    make_steps = build_fbcode_builder_config(config)
113    steps = make_steps(builder)
114    print(builder.render(steps))
115