• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import argparse
18import os
19import subprocess
20import sys
21
22import api_assembly
23import api_domain
24import api_export
25import final_packaging
26import inner_tree
27import tree_analysis
28import interrogate
29import lunch
30import ninja_runner
31import nsjail
32import utils
33
34EXIT_STATUS_OK = 0
35EXIT_STATUS_ERROR = 1
36
37API_DOMAIN_SYSTEM = "system"
38API_DOMAIN_VENDOR = "vendor"
39API_DOMAIN_MODULE = "module"
40
41
42class Orchestrator():
43
44    def __init__(self, argv):
45        """Initialize the object."""
46        self.argv = argv
47        self.opts = self._get_parser().parse_args(argv[1:])
48        self._validate_options()
49
50        # Choose the out directory, set up error handling, etc.
51        self.context = utils.Context(utils.choose_out_dir(),
52                                     utils.Errors(sys.stderr))
53
54        # Read the lunch config file
55        config_file, config, variant = lunch.load_current_config()
56        sys.stdout.write(lunch.make_config_header(config_file, config,
57                                                  variant))
58
59        self.config_file = config_file
60        self.config = config
61        self.variant = variant
62
63        # Construct the trees and domains dicts.
64        self.inner_trees = self.process_config(self.config)
65
66    def _get_parser(self):
67        """Return the argument parser."""
68        p = argparse.ArgumentParser()
69        p.add_argument("--shell",
70                       action="store_true",
71                       help="invoke a shell instead of building")
72        # Mount source tree read-write.
73        p.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
74
75        p.add_argument('targets',
76                       metavar="TARGET",
77                       nargs="*",
78                       help="ninja targets")
79
80        return p
81
82    def _validate_options(self):
83        """Validate the options provided by the user."""
84        # Debug includes shell.
85        if self.opts.debug:
86            self.opts.shell = True
87
88    def process_config(self, lunch_config: dict) -> inner_tree.InnerTrees:
89        """Process the config file.
90
91        Args:
92          lunch_config: JSON encoded config from the mcombo file.
93
94        Returns:
95          The inner_trees definition for this build.
96        """
97
98        trees = {}
99        domains = {}
100        context = self.context
101
102        def add(domain_name, tree_root, product):
103            tree_key = inner_tree.InnerTreeKey(tree_root, product)
104            if tree_key in trees:
105                tree = trees[tree_key]
106            else:
107                tree = inner_tree.InnerTree(context, tree_root, product,
108                                            self.variant)
109                trees[tree_key] = tree
110            domain = api_domain.ApiDomain(domain_name, tree, product)
111            domains[domain_name] = domain
112            tree.domains[domain_name] = domain
113
114        system_entry = lunch_config.get("system")
115        if system_entry:
116            add(API_DOMAIN_SYSTEM, system_entry["inner-tree"],
117                system_entry["product"])
118
119        vendor_entry = lunch_config.get("vendor")
120        if vendor_entry:
121            add(API_DOMAIN_VENDOR, vendor_entry["inner-tree"],
122                vendor_entry["product"])
123
124        for name, entry in lunch_config.get("modules", {}).items():
125            add(name, entry["inner-tree"], None)
126
127        return inner_tree.InnerTrees(trees, domains)
128
129    def _create_nsjail_config(self):
130        """Create the nsjail config."""
131        # The filesystem layout for the nsjail has binaries, libraries, and such
132        # where we expect them to be.  Outside of that, we mount the workspace
133        # (which is presumably the current working directory thanks to lunch),
134        # and those are the only files present in the tree.
135        #
136        # The orchestrator needs to have the outer tree as the top of the tree,
137        # with all of the inner tree nsjail configs merged with it, so that we
138        # can do one ninja run this step.  The source workspace is mounted
139        # read-only, with the out_dir mounted read-write.
140        root = os.path.abspath('.')
141        jail_cfg = nsjail.Nsjail(root)
142        jail_cfg.add_mountpt(src=root,
143                             dst=root,
144                             is_bind=True,
145                             rw=False,
146                             mandatory=True)
147        # Add the outer-tree outdir (which may be unrelated to the workspace
148        # root). The inner-trees will additionally mount their outdir under as
149        # {inner_tree}/out, so that all access to the out_dir in a subninja
150        # stays within the inner-tree's workspace.
151        out = self.context.out
152        jail_cfg.add_mountpt(src=out.root(abspath=True),
153                             dst=out.root(base=out.Base.OUTER, abspath=True),
154                             is_bind=True,
155                             rw=True,
156                             mandatory=True)
157
158        for tree in self.inner_trees.trees.values():
159            jail_cfg.add_nsjail(tree.meld_config)
160
161        return jail_cfg
162
163    def _build(self):
164        """Orchestrate the build."""
165
166        context = self.context
167        inner_trees = self.inner_trees
168        jail_cfg = self._create_nsjail_config()
169
170        # 1. Interrogate the trees
171        description = inner_trees.for_each_tree(interrogate.interrogate_tree)
172        # TODO: Do something when bazel_only is True.  Provided now as an
173        # example of how we can query the interrogation results.
174        _bazel_only = len(inner_trees.keys()) == 1 and all(
175            x.get("single_bazel_optimization_available")
176            for x in description.values())
177
178        # 2a. API Export
179        inner_trees.for_each_tree(api_export.export_apis_from_tree)
180
181        # 2b. API Surface Assembly
182        api_assembly.assemble_apis(context, inner_trees)
183
184        # 3a. Inner tree analysis
185        tree_analysis.analyze_trees(context, inner_trees)
186
187        # 3b. Final Packaging Rules
188        final_packaging.final_packaging(context, inner_trees)
189
190        # 4. Build Execution
191        # TODO: determine the targets from the lunch command and mcombo files.
192        # For now, use a default that is consistent with having the build work.
193        targets = self.opts.targets or ["vendor/nothing"]
194        print("Running ninja...")
195        # Disable network access in the combined ninja execution
196        jail_cfg.add_option(name="clone_newnet", value="true")
197        ninja_runner.run_ninja(context, jail_cfg, targets)
198
199        # Success!
200        return EXIT_STATUS_OK
201
202    def _shell(self):
203        """Launch a shell."""
204
205        jail_cfg = self._create_nsjail_config()
206        jail_cfg.add_envar(name='TERM')  # Pass TERM to the nsjail environment.
207
208        if self.opts.debug:
209            home = os.environ.get('HOME')
210            jail_cfg.make_cwd_writable()
211            if home:
212                print(f"setting HOME={home}")
213                jail_cfg.add_envar(name='HOME', value=home)
214
215        nsjail_bin = self.context.tools.nsjail
216        # Write the nsjail config
217        nsjail_config_file = self.context.out.nsjail_config_file() + "-shell"
218        jail_cfg.generate_config(nsjail_config_file)
219
220        # Construct the command
221        cmd = [nsjail_bin, "--config", nsjail_config_file, "--", "/bin/bash"]
222
223        # Run the shell, and ignore errors from the interactive shell.
224        print(f"Running: {' '.join(cmd)}")
225        subprocess.run(cmd, shell=False, check=False)
226        return EXIT_STATUS_OK
227
228    def Run(self):
229        """Orchestrate the build."""
230
231        if self.opts.shell:
232            return self._shell()
233
234        return self._build()
235
236
237if __name__ == "__main__":
238    try:
239        orch = Orchestrator(sys.argv)
240    except lunch.ConfigException as e:
241        print(f"{e}", file=sys.stderr)
242        sys.exit(EXIT_STATUS_ERROR)
243
244    sys.exit(orch.Run())
245
246# vim: sts=4:ts=4:sw=4
247