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