diff options
| -rwxr-xr-x | bitbake/bin/bitbake-setup | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/bitbake/bin/bitbake-setup b/bitbake/bin/bitbake-setup new file mode 100755 index 0000000000..739474003f --- /dev/null +++ b/bitbake/bin/bitbake-setup | |||
| @@ -0,0 +1,809 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | |||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | |||
| 7 | import logging | ||
| 8 | import os | ||
| 9 | import sys | ||
| 10 | import argparse | ||
| 11 | import warnings | ||
| 12 | import json | ||
| 13 | import shutil | ||
| 14 | import time | ||
| 15 | import stat | ||
| 16 | import tempfile | ||
| 17 | import configparser | ||
| 18 | import datetime | ||
| 19 | |||
| 20 | default_registry = 'git://github.com/kanavin/bitbake-setup-configurations.git;protocol=https;branch=main;rev=main' | ||
| 21 | |||
| 22 | bindir = os.path.abspath(os.path.dirname(__file__)) | ||
| 23 | sys.path[0:0] = [os.path.join(os.path.dirname(bindir), 'lib')] | ||
| 24 | |||
| 25 | import bb.msg | ||
| 26 | import bb.process | ||
| 27 | |||
| 28 | logger = bb.msg.logger_create('bitbake-setup', sys.stdout) | ||
| 29 | |||
| 30 | def cache_dir(top_dir): | ||
| 31 | return os.path.join(top_dir, '.bitbake-setup-cache') | ||
| 32 | |||
| 33 | def init_bb_cache(settings, args): | ||
| 34 | dldir = settings["default"]["dl-dir"] | ||
| 35 | bb_cachedir = os.path.join(cache_dir(args.top_dir), 'bitbake-cache') | ||
| 36 | |||
| 37 | d = bb.data.init() | ||
| 38 | d.setVar("DL_DIR", dldir) | ||
| 39 | d.setVar("BB_CACHEDIR", bb_cachedir) | ||
| 40 | d.setVar("__BBSRCREV_SEEN", "1") | ||
| 41 | if args.no_network: | ||
| 42 | d.setVar("BB_SRCREV_POLICY", "cache") | ||
| 43 | bb.fetch.fetcher_init(d) | ||
| 44 | return d | ||
| 45 | |||
| 46 | def save_bb_cache(): | ||
| 47 | bb.fetch2.fetcher_parse_save() | ||
| 48 | bb.fetch2.fetcher_parse_done() | ||
| 49 | |||
| 50 | def get_config_name(config): | ||
| 51 | suffix = '.conf.json' | ||
| 52 | config_file = os.path.basename(config) | ||
| 53 | if config_file.endswith(suffix): | ||
| 54 | return config_file[:-len(suffix)] | ||
| 55 | else: | ||
| 56 | raise Exception("Config file {} does not end with {}, please rename the file.".format(config, suffix)) | ||
| 57 | |||
| 58 | def write_config(config, config_dir): | ||
| 59 | with open(os.path.join(config_dir, "config-upstream.json"),'w') as s: | ||
| 60 | json.dump(config, s, sort_keys=True, indent=4) | ||
| 61 | |||
| 62 | def commit_config(config_dir): | ||
| 63 | bb.process.run("git -C {} add .".format(config_dir)) | ||
| 64 | bb.process.run("git -C {} commit --no-verify -a -m 'Configuration at {}'".format(config_dir, time.asctime())) | ||
| 65 | |||
| 66 | def _write_layer_list(dest, repodirs): | ||
| 67 | layers = [] | ||
| 68 | for r in repodirs: | ||
| 69 | for root, dirs, files in os.walk(os.path.join(dest,r)): | ||
| 70 | if os.path.basename(root) == 'conf' and 'layer.conf' in files: | ||
| 71 | layers.append(os.path.relpath(os.path.dirname(root), dest)) | ||
| 72 | layers_f = os.path.join(dest, ".oe-layers.json") | ||
| 73 | with open(layers_f, 'w') as f: | ||
| 74 | json.dump({"version":"1.0","layers":layers}, f, sort_keys=True, indent=4) | ||
| 75 | |||
| 76 | def checkout_layers(layers, layerdir, d): | ||
| 77 | repodirs = [] | ||
| 78 | oesetupbuild = None | ||
| 79 | print("Fetching layer/tool repositories into {}".format(layerdir)) | ||
| 80 | for r_name in layers: | ||
| 81 | r_data = layers[r_name] | ||
| 82 | repodir = r_data["path"] | ||
| 83 | repodirs.append(repodir) | ||
| 84 | |||
| 85 | r_remote = r_data['git-remote'] | ||
| 86 | rev = r_remote['rev'] | ||
| 87 | remotes = r_remote['remotes'] | ||
| 88 | |||
| 89 | for remote in remotes: | ||
| 90 | type,host,path,user,pswd,params = bb.fetch.decodeurl(remotes[remote]["uri"]) | ||
| 91 | fetchuri = bb.fetch.encodeurl(('git',host,path,user,pswd,params)) | ||
| 92 | print(" {}".format(r_name)) | ||
| 93 | fetcher = bb.fetch.Fetch(["{};protocol={};rev={};nobranch=1;destsuffix={}".format(fetchuri,type,rev,repodir)], d) | ||
| 94 | do_fetch(fetcher, layerdir) | ||
| 95 | |||
| 96 | if os.path.exists(os.path.join(layerdir, repodir, 'scripts/oe-setup-build')): | ||
| 97 | oesetupbuild = os.path.join(layerdir, repodir, 'scripts/oe-setup-build') | ||
| 98 | oeinitbuildenv = os.path.join(layerdir, repodir, 'oe-init-build-env') | ||
| 99 | |||
| 100 | print(" ") | ||
| 101 | _write_layer_list(layerdir, repodirs) | ||
| 102 | |||
| 103 | if oesetupbuild: | ||
| 104 | links = {'setup-build': oesetupbuild, 'oe-scripts': os.path.dirname(oesetupbuild), 'init-build-env': oeinitbuildenv} | ||
| 105 | for l,t in links.items(): | ||
| 106 | symlink = os.path.join(layerdir, l) | ||
| 107 | if os.path.lexists(symlink): | ||
| 108 | os.remove(symlink) | ||
| 109 | os.symlink(os.path.relpath(t,layerdir),symlink) | ||
| 110 | |||
| 111 | def setup_bitbake_build(bitbake_config, layerdir, builddir): | ||
| 112 | def _setup_build_conf(layers, build_conf_dir): | ||
| 113 | os.makedirs(build_conf_dir) | ||
| 114 | layers_s = "\n".join([" {} \\".format(os.path.join(layerdir,l)) for l in layers]) | ||
| 115 | bblayers_conf = """BBLAYERS ?= " \\ | ||
| 116 | {} | ||
| 117 | " | ||
| 118 | """.format(layers_s) | ||
| 119 | with open(os.path.join(build_conf_dir, "bblayers.conf"), 'w') as f: | ||
| 120 | f.write(bblayers_conf) | ||
| 121 | |||
| 122 | local_conf = """# | ||
| 123 | # This file is intended for local configuration tweaks. | ||
| 124 | # | ||
| 125 | # If you would like to publish and share changes made to this file, | ||
| 126 | # it is recommended to put them into a distro config, or to create | ||
| 127 | # layer fragments from changes made here. | ||
| 128 | # | ||
| 129 | """ | ||
| 130 | with open(os.path.join(build_conf_dir, "local.conf"), 'w') as f: | ||
| 131 | f.write(local_conf) | ||
| 132 | |||
| 133 | with open(os.path.join(build_conf_dir, "templateconf.cfg"), 'w') as f: | ||
| 134 | f.write("") | ||
| 135 | |||
| 136 | with open(os.path.join(build_conf_dir, "conf-summary.txt"), 'w') as f: | ||
| 137 | f.write(bitbake_config["description"] + "\n") | ||
| 138 | |||
| 139 | with open(os.path.join(build_conf_dir, "conf-notes.txt"), 'w') as f: | ||
| 140 | f.write("") | ||
| 141 | |||
| 142 | def _make_init_build_env(builddir, initbuildenv): | ||
| 143 | cmd = ". {} {}".format(initbuildenv, builddir) | ||
| 144 | initbuild_in_builddir = os.path.join(builddir, 'init-build-env') | ||
| 145 | with open(initbuild_in_builddir, 'w') as f: | ||
| 146 | f.write(cmd) | ||
| 147 | |||
| 148 | bitbake_builddir = os.path.join(builddir, "build") | ||
| 149 | print("Setting up bitbake configuration in\n {}\n".format(bitbake_builddir)) | ||
| 150 | |||
| 151 | template = bitbake_config.get("oe-template") | ||
| 152 | layers = bitbake_config.get("bb-layers") | ||
| 153 | if not template and not layers: | ||
| 154 | print("Bitbake configuration does not contain a reference to an OpenEmbedded build template via 'oe-template' or a list of layers via 'bb-layers'; please use oe-setup-build, oe-init-build-env or another mechanism manually to complete the setup.") | ||
| 155 | return | ||
| 156 | oesetupbuild = os.path.join(layerdir, 'setup-build') | ||
| 157 | if template and not os.path.exists(oesetupbuild): | ||
| 158 | raise Exception("Cannot complete setting up a bitbake build directory from OpenEmbedded template '{}' as oe-setup-build was not found in any layers; please use oe-init-build-env manually.".format(template)) | ||
| 159 | |||
| 160 | bitbake_confdir = os.path.join(bitbake_builddir, 'conf') | ||
| 161 | backup_bitbake_confdir = bitbake_confdir + "-backup.{}".format(time.strftime("%Y%m%d%H%M%S")) | ||
| 162 | if os.path.exists(bitbake_confdir): | ||
| 163 | os.rename(bitbake_confdir, backup_bitbake_confdir) | ||
| 164 | |||
| 165 | if layers: | ||
| 166 | _setup_build_conf(layers, bitbake_confdir) | ||
| 167 | |||
| 168 | if template: | ||
| 169 | bb.process.run("{} setup -c {} -b {} --no-shell".format(oesetupbuild, template, bitbake_builddir)) | ||
| 170 | else: | ||
| 171 | initbuildenv = os.path.join(layerdir, 'init-build-env') | ||
| 172 | if not os.path.exists(initbuildenv): | ||
| 173 | print("Could not find oe-init-build-env in any of the layers; please use another mechanism to initialize the bitbake environment") | ||
| 174 | return | ||
| 175 | _make_init_build_env(bitbake_builddir, os.path.realpath(initbuildenv)) | ||
| 176 | |||
| 177 | siteconf_symlink = os.path.join(bitbake_confdir, "site.conf") | ||
| 178 | siteconf = os.path.normpath(os.path.join(builddir, '..', "site.conf")) | ||
| 179 | if os.path.lexists(siteconf_symlink): | ||
| 180 | os.remove(symlink) | ||
| 181 | os.symlink(os.path.relpath(siteconf, bitbake_confdir) ,siteconf_symlink) | ||
| 182 | |||
| 183 | |||
| 184 | init_script = os.path.join(bitbake_builddir, "init-build-env") | ||
| 185 | shell = "bash" | ||
| 186 | fragments = bitbake_config.get("oe-fragments", []) + sorted(bitbake_config.get("oe-fragment-choices",{}).values()) | ||
| 187 | if fragments: | ||
| 188 | bb.process.run("{} -c '. {} && bitbake-config-build enable-fragment {}'".format(shell, init_script, " ".join(fragments))) | ||
| 189 | |||
| 190 | if os.path.exists(backup_bitbake_confdir): | ||
| 191 | bitbake_config_diff = get_diff(backup_bitbake_confdir, bitbake_confdir) | ||
| 192 | if bitbake_config_diff: | ||
| 193 | print("Existing bitbake configuration directory renamed to {}".format(backup_bitbake_confdir)) | ||
| 194 | print("The bitbake configuration has changed:") | ||
| 195 | print(bitbake_config_diff) | ||
| 196 | else: | ||
| 197 | shutil.rmtree(backup_bitbake_confdir) | ||
| 198 | |||
| 199 | print("This bitbake configuration provides:\n {}\n".format(bitbake_config["description"])) | ||
| 200 | |||
| 201 | readme = """{}\n\nAdditional information is in {} and {}\n | ||
| 202 | Source the environment using '. {}' to run builds from the command line. | ||
| 203 | The bitbake configuration files (local.conf, bblayers.conf and more) can be found in {}/conf | ||
| 204 | """.format( | ||
| 205 | bitbake_config["description"], | ||
| 206 | os.path.join(bitbake_builddir,'conf/conf-summary.txt'), | ||
| 207 | os.path.join(bitbake_builddir,'conf/conf-notes.txt'), | ||
| 208 | init_script, | ||
| 209 | bitbake_builddir | ||
| 210 | ) | ||
| 211 | readme_file = os.path.join(bitbake_builddir, "README") | ||
| 212 | with open(readme_file, 'w') as f: | ||
| 213 | f.write(readme) | ||
| 214 | print("Usage instructions and additional information are in\n {}\n".format(readme_file)) | ||
| 215 | print("The bitbake configuration files (local.conf, bblayers.conf and more) can be found in\n {}/conf\n".format(bitbake_builddir)) | ||
| 216 | print("To run builds, source the environment using\n source {}".format(init_script)) | ||
| 217 | |||
| 218 | def get_registry_config(registry_path, id): | ||
| 219 | for root, dirs, files in os.walk(registry_path): | ||
| 220 | for f in files: | ||
| 221 | if f.endswith('.conf.json') and id == get_config_name(f): | ||
| 222 | return os.path.join(root, f) | ||
| 223 | raise Exception("Unable to find {} in available configurations; use 'list' sub-command to see what is available".format(id)) | ||
| 224 | |||
| 225 | def update_build(config, confdir, builddir, layerdir, d): | ||
| 226 | layer_config = config["data"]["sources"] | ||
| 227 | layer_overrides = config["source-overrides"]["sources"] | ||
| 228 | for k,v in layer_overrides.items(): | ||
| 229 | if k in layer_config: | ||
| 230 | layer_config[k]["git-remote"] = v["git-remote"] | ||
| 231 | checkout_layers(layer_config, layerdir, d) | ||
| 232 | bitbake_config = config["bitbake-config"] | ||
| 233 | setup_bitbake_build(bitbake_config, layerdir, builddir) | ||
| 234 | |||
| 235 | def int_input(allowed_values): | ||
| 236 | n = None | ||
| 237 | while n is None: | ||
| 238 | try: | ||
| 239 | n = int(input()) | ||
| 240 | except ValueError: | ||
| 241 | print('Not a valid number, please try again:') | ||
| 242 | continue | ||
| 243 | if n not in allowed_values: | ||
| 244 | print('Number {} not one of {}, please try again:'.format(n, allowed_values)) | ||
| 245 | n = None | ||
| 246 | return n | ||
| 247 | |||
| 248 | def flatten_bitbake_configs(configs): | ||
| 249 | def merge_configs(c1,c2): | ||
| 250 | c_merged = {} | ||
| 251 | for k,v in c2.items(): | ||
| 252 | if k not in c1.keys(): | ||
| 253 | c_merged[k] = v | ||
| 254 | for k,v in c1.items(): | ||
| 255 | if k not in c2.keys(): | ||
| 256 | c_merged[k] = v | ||
| 257 | else: | ||
| 258 | c_merged[k] = c1[k] + c2[k] | ||
| 259 | del c_merged['configurations'] | ||
| 260 | return c_merged | ||
| 261 | |||
| 262 | flattened_configs = [] | ||
| 263 | for c in configs: | ||
| 264 | if 'configurations' not in c: | ||
| 265 | flattened_configs.append(c) | ||
| 266 | else: | ||
| 267 | for sub_c in flatten_bitbake_configs(c['configurations']): | ||
| 268 | flattened_configs.append(merge_configs(c, sub_c)) | ||
| 269 | return flattened_configs | ||
| 270 | |||
| 271 | def choose_bitbake_config(configs, parameters, non_interactive): | ||
| 272 | flattened_configs = flatten_bitbake_configs(configs) | ||
| 273 | configs_dict = {i["name"]:i for i in flattened_configs} | ||
| 274 | |||
| 275 | if parameters: | ||
| 276 | config_id = parameters[0] | ||
| 277 | if config_id not in configs_dict: | ||
| 278 | raise Exception("Bitbake configuration {} not found; replace with one of {}".format(config_id, configs_dict)) | ||
| 279 | return configs_dict[config_id] | ||
| 280 | |||
| 281 | enumerated_configs = list(enumerate(flattened_configs)) | ||
| 282 | if len(enumerated_configs) == 1: | ||
| 283 | only_config = flattened_configs[0] | ||
| 284 | print("\nSelecting the only available bitbake configuration {}".format(only_config["name"])) | ||
| 285 | return only_config | ||
| 286 | |||
| 287 | if non_interactive: | ||
| 288 | raise Exception("Unable to choose from bitbake configurations in non-interactive mode: {}".format(configs_dict)) | ||
| 289 | |||
| 290 | print("\nAvailable bitbake configurations:") | ||
| 291 | for n, config_data in enumerated_configs: | ||
| 292 | print("{}. {}\t{}".format(n, config_data["name"], config_data["description"])) | ||
| 293 | print("\nPlease select one of the above bitbake configurations by its number:") | ||
| 294 | config_n = int_input([i[0] for i in enumerated_configs]) | ||
| 295 | return flattened_configs[config_n] | ||
| 296 | |||
| 297 | def choose_config(configs, non_interactive): | ||
| 298 | not_expired_configs = [k for k in configs.keys() if not has_expired(configs[k].get("expires", None))] | ||
| 299 | config_list = list(enumerate(not_expired_configs)) | ||
| 300 | if len(config_list) == 1: | ||
| 301 | only_config = config_list[0][1] | ||
| 302 | print("\nSelecting the only available configuration {}\n".format(only_config)) | ||
| 303 | return only_config | ||
| 304 | |||
| 305 | if non_interactive: | ||
| 306 | raise Exception("Unable to choose from configurations in non-interactive mode: {}".format(not_expired_configs)) | ||
| 307 | |||
| 308 | print("\nAvailable configurations:") | ||
| 309 | for n, config_name in config_list: | ||
| 310 | config_data = configs[config_name] | ||
| 311 | expiry_date = config_data.get("expires", None) | ||
| 312 | config_desc = config_data["description"] | ||
| 313 | if expiry_date: | ||
| 314 | print("{}. {}\t{} (supported until {})".format(n, config_name, config_desc, expiry_date)) | ||
| 315 | else: | ||
| 316 | print("{}. {}\t{}".format(n, config_name, config_desc)) | ||
| 317 | print("\nPlease select one of the above configurations by its number:") | ||
| 318 | config_n = int_input([i[0] for i in config_list]) | ||
| 319 | return config_list[config_n][1] | ||
| 320 | |||
| 321 | def choose_fragments(possibilities, parameters, non_interactive): | ||
| 322 | choices = {} | ||
| 323 | for k,v in possibilities.items(): | ||
| 324 | choice = [o for o in v["options"] if o in parameters] | ||
| 325 | if len(choice) > 1: | ||
| 326 | raise Exception("Options specified on command line do not allow a single selection from possibilities {}, please remove one or more from {}".format(v["options"], parameters)) | ||
| 327 | if len(choice) == 1: | ||
| 328 | choices[k] = choice[0] | ||
| 329 | continue | ||
| 330 | |||
| 331 | if non_interactive: | ||
| 332 | raise Exception("Unable to choose from options in non-interactive mode: {}".format(v["options"])) | ||
| 333 | |||
| 334 | print("\n" + v["description"] + ":") | ||
| 335 | options_enumerated = list(enumerate(v["options"])) | ||
| 336 | for n,o in options_enumerated: | ||
| 337 | print("{}. {}".format(n, o)) | ||
| 338 | print("\nPlease select one of the above options by its number:") | ||
| 339 | option_n = int_input([i[0] for i in options_enumerated]) | ||
| 340 | choices[k] = options_enumerated[option_n][1] | ||
| 341 | return choices | ||
| 342 | |||
| 343 | def obtain_config(settings, args, source_overrides, d): | ||
| 344 | if args.config: | ||
| 345 | config_id = args.config[0] | ||
| 346 | config_parameters = args.config[1:] | ||
| 347 | if os.path.exists(config_id): | ||
| 348 | print("Reading configuration from local file\n {}".format(config_id)) | ||
| 349 | upstream_config = {'type':'local', | ||
| 350 | 'path':os.path.abspath(config_id), | ||
| 351 | 'name':get_config_name(config_id), | ||
| 352 | 'data':json.load(open(config_id)) | ||
| 353 | } | ||
| 354 | elif config_id.startswith("http://") or config_id.startswith("https://"): | ||
| 355 | print("Reading configuration from network URI\n {}".format(config_id)) | ||
| 356 | import urllib.request | ||
| 357 | with urllib.request.urlopen(config_id) as f: | ||
| 358 | upstream_config = {'type':'network','uri':config_id,'name':get_config_name(config_id),'data':json.load(f)} | ||
| 359 | else: | ||
| 360 | print("Looking up config {} in configuration registry".format(config_id)) | ||
| 361 | registry_path = update_registry(settings["default"]["registry"], cache_dir(args.top_dir), d) | ||
| 362 | registry_configs = list_registry(registry_path, with_expired=True) | ||
| 363 | if config_id not in registry_configs: | ||
| 364 | raise Exception("Config {} not found in configuration registry, re-run 'init' without parameters to choose from available configurations.".format(config_id)) | ||
| 365 | upstream_config = {'type':'registry','registry':settings["default"]["registry"],'name':config_id,'data':json.load(open(get_registry_config(registry_path,config_id)))} | ||
| 366 | expiry_date = upstream_config['data'].get("expires", None) | ||
| 367 | if has_expired(expiry_date): | ||
| 368 | print("This configuration is no longer supported after {}. Please consider changing to a supported configuration.".format(expiry_date)) | ||
| 369 | else: | ||
| 370 | registry_path = update_registry(settings["default"]["registry"], cache_dir(args.top_dir), d) | ||
| 371 | registry_configs = list_registry(registry_path, with_expired=True) | ||
| 372 | config_id = choose_config(registry_configs, args.non_interactive) | ||
| 373 | config_parameters = [] | ||
| 374 | upstream_config = {'type':'registry','registry':settings["default"]["registry"],'name':config_id,'data':json.load(open(get_registry_config(registry_path,config_id)))} | ||
| 375 | |||
| 376 | upstream_config['bitbake-config'] = choose_bitbake_config(upstream_config['data']['bitbake-setup']['configurations'], config_parameters, args.non_interactive) | ||
| 377 | upstream_config['bitbake-config']['oe-fragment-choices'] = choose_fragments(upstream_config['bitbake-config'].get('oe-fragments-one-of',{}), config_parameters[1:], args.non_interactive) | ||
| 378 | upstream_config['non-interactive-cmdline-options'] = [config_id, upstream_config['bitbake-config']['name']] + sorted(upstream_config['bitbake-config']['oe-fragment-choices'].values()) | ||
| 379 | upstream_config['source-overrides'] = source_overrides | ||
| 380 | return upstream_config | ||
| 381 | |||
| 382 | def init_config(settings, args, d): | ||
| 383 | stdout = sys.stdout | ||
| 384 | def handle_task_progress(event, d): | ||
| 385 | rate = event.rate if event.rate else '' | ||
| 386 | progress = event.progress if event.progress > 0 else 0 | ||
| 387 | print("{}% {} ".format(progress, rate), file=stdout, end='\r') | ||
| 388 | |||
| 389 | source_overrides = json.load(open(args.source_overrides)) if args.source_overrides else {'sources':{}} | ||
| 390 | upstream_config = obtain_config(settings, args, source_overrides, d) | ||
| 391 | print("\nRun 'bitbake-setup init --non-interactive {}' to select this configuration non-interactively.\n".format(" ".join(upstream_config['non-interactive-cmdline-options']))) | ||
| 392 | |||
| 393 | builddir = os.path.join(os.path.abspath(args.top_dir), args.build_dir_name or "{}-{}".format(upstream_config['name']," ".join(upstream_config['non-interactive-cmdline-options'][1:]).replace(" ","-").replace("/","_"))) | ||
| 394 | if os.path.exists(builddir): | ||
| 395 | print("Build already initialized in {}\nUse 'bitbake-setup status' to check if it needs to be updated or 'bitbake-setup update' to perform the update.".format(builddir)) | ||
| 396 | return | ||
| 397 | |||
| 398 | print("Initializing a build in\n {}".format(builddir)) | ||
| 399 | if not args.non_interactive: | ||
| 400 | y_or_n = input('Continue? y/n: ') | ||
| 401 | if y_or_n != 'y': | ||
| 402 | exit() | ||
| 403 | print() | ||
| 404 | |||
| 405 | os.makedirs(builddir) | ||
| 406 | |||
| 407 | confdir = os.path.join(builddir, "config") | ||
| 408 | layerdir = os.path.join(builddir, "layers") | ||
| 409 | |||
| 410 | os.makedirs(confdir) | ||
| 411 | os.makedirs(layerdir) | ||
| 412 | |||
| 413 | bb.process.run("git -C {} init -b main".format(confdir)) | ||
| 414 | # Make sure commiting doesn't fail if no default git user is configured on the machine | ||
| 415 | bb.process.run("git -C {} config user.name bitbake-setup".format(confdir)) | ||
| 416 | bb.process.run("git -C {} config user.email bitbake-setup@not.set".format(confdir)) | ||
| 417 | bb.process.run("git -C {} commit --no-verify --allow-empty -m 'Initial commit'".format(confdir)) | ||
| 418 | |||
| 419 | bb.event.register("bb.build.TaskProgress", handle_task_progress, data=d) | ||
| 420 | |||
| 421 | write_config(upstream_config, confdir) | ||
| 422 | commit_config(confdir) | ||
| 423 | update_build(upstream_config, confdir, builddir, layerdir, d) | ||
| 424 | |||
| 425 | bb.event.remove("bb.build.TaskProgress", None) | ||
| 426 | |||
| 427 | def get_diff(file1, file2): | ||
| 428 | try: | ||
| 429 | bb.process.run('diff -uNr {} {}'.format(file1, file2)) | ||
| 430 | except bb.process.ExecutionError as e: | ||
| 431 | if e.exitcode == 1: | ||
| 432 | return e.stdout | ||
| 433 | else: | ||
| 434 | raise e | ||
| 435 | return None | ||
| 436 | |||
| 437 | def are_layers_changed(layers, layerdir, d): | ||
| 438 | changed = False | ||
| 439 | for r_name in layers: | ||
| 440 | r_data = layers[r_name] | ||
| 441 | repodir = r_data["path"] | ||
| 442 | |||
| 443 | r_remote = r_data['git-remote'] | ||
| 444 | rev = r_remote['rev'] | ||
| 445 | remotes = r_remote['remotes'] | ||
| 446 | |||
| 447 | for remote in remotes: | ||
| 448 | type,host,path,user,pswd,params = bb.fetch.decodeurl(remotes[remote]["uri"]) | ||
| 449 | fetchuri = bb.fetch.encodeurl(('git',host,path,user,pswd,params)) | ||
| 450 | fetcher = bb.fetch.FetchData("{};protocol={};rev={};nobranch=1;destsuffix={}".format(fetchuri,type,rev,repodir), d) | ||
| 451 | upstream_revision = fetcher.method.latest_revision(fetcher, d, 'default') | ||
| 452 | rev_parse_result = bb.process.run('git -C {} rev-parse HEAD'.format(os.path.join(layerdir, repodir))) | ||
| 453 | local_revision = rev_parse_result[0].strip() | ||
| 454 | if upstream_revision != local_revision: | ||
| 455 | changed = True | ||
| 456 | print('Layer repository {} checked out into {} updated revision {} from {} to {}'.format(remotes[remote]["uri"], os.path.join(layerdir, repodir), rev, local_revision, upstream_revision)) | ||
| 457 | |||
| 458 | return changed | ||
| 459 | |||
| 460 | def build_status(settings, args, d, update=False): | ||
| 461 | builddir = args.build_dir | ||
| 462 | |||
| 463 | confdir = os.path.join(builddir, "config") | ||
| 464 | layerdir = os.path.join(builddir, "layers") | ||
| 465 | |||
| 466 | current_upstream_config = json.load(open(os.path.join(confdir, "config-upstream.json"))) | ||
| 467 | |||
| 468 | args.config = current_upstream_config['non-interactive-cmdline-options'] | ||
| 469 | args.non_interactive = True | ||
| 470 | source_overrides = current_upstream_config["source-overrides"] | ||
| 471 | new_upstream_config = obtain_config(settings, args, source_overrides, d) | ||
| 472 | |||
| 473 | write_config(new_upstream_config, confdir) | ||
| 474 | config_diff = bb.process.run('git -C {} diff'.format(confdir))[0] | ||
| 475 | |||
| 476 | if config_diff: | ||
| 477 | print('\nConfiguration in {} has changed:\n{}'.format(builddir, config_diff)) | ||
| 478 | if update: | ||
| 479 | commit_config(confdir) | ||
| 480 | update_build(new_upstream_config, confdir, builddir, layerdir, d) | ||
| 481 | else: | ||
| 482 | bb.process.run('git -C {} restore config-upstream.json'.format(confdir)) | ||
| 483 | return | ||
| 484 | |||
| 485 | if are_layers_changed(current_upstream_config["data"]["sources"], layerdir, d): | ||
| 486 | if update: | ||
| 487 | update_build(current_upstream_config, confdir, builddir, layerdir, d) | ||
| 488 | return | ||
| 489 | |||
| 490 | print("\nConfiguration in {} has not changed.".format(builddir)) | ||
| 491 | |||
| 492 | def build_update(settings, args, d): | ||
| 493 | build_status(settings, args, d, update=True) | ||
| 494 | |||
| 495 | def do_fetch(fetcher, dir): | ||
| 496 | # git fetcher simply dumps git output to stdout; in bitbake context that is redirected to temp/log.do_fetch | ||
| 497 | # and we need to set up smth similar here | ||
| 498 | fetchlogdir = os.path.join(dir, 'logs') | ||
| 499 | os.makedirs(fetchlogdir, exist_ok=True) | ||
| 500 | fetchlog = os.path.join(fetchlogdir, 'fetch_log.{}'.format(datetime.datetime.now().strftime("%Y%m%d%H%M%S"))) | ||
| 501 | with open(fetchlog, 'a') as f: | ||
| 502 | oldstdout = sys.stdout | ||
| 503 | sys.stdout = f | ||
| 504 | fetcher.download() | ||
| 505 | fetcher.unpack(dir) | ||
| 506 | sys.stdout = oldstdout | ||
| 507 | |||
| 508 | def update_registry(registry, cachedir, d): | ||
| 509 | registrydir = 'configurations' | ||
| 510 | full_registrydir = os.path.join(cachedir, registrydir) | ||
| 511 | print("Fetching configuration registry\n {}\ninto\n {}".format(registry, full_registrydir)) | ||
| 512 | fetcher = bb.fetch.Fetch(["{};destsuffix={}".format(registry, registrydir)], d) | ||
| 513 | do_fetch(fetcher, cachedir) | ||
| 514 | return full_registrydir | ||
| 515 | |||
| 516 | def has_expired(expiry_date): | ||
| 517 | if expiry_date: | ||
| 518 | return datetime.datetime.now() > datetime.datetime.fromisoformat(expiry_date) | ||
| 519 | return False | ||
| 520 | |||
| 521 | def list_registry(registry_path, with_expired): | ||
| 522 | json_data = {} | ||
| 523 | |||
| 524 | for root, dirs, files in os.walk(registry_path): | ||
| 525 | for f in files: | ||
| 526 | if f.endswith('.conf.json'): | ||
| 527 | config_name = get_config_name(f) | ||
| 528 | config_data = json.load(open(os.path.join(root, f))) | ||
| 529 | config_desc = config_data["description"] | ||
| 530 | expiry_date = config_data.get("expires", None) | ||
| 531 | if expiry_date: | ||
| 532 | if with_expired or not has_expired(expiry_date): | ||
| 533 | json_data[config_name] = {"description": config_desc, "expires": expiry_date} | ||
| 534 | else: | ||
| 535 | json_data[config_name] = {"description": config_desc} | ||
| 536 | return json_data | ||
| 537 | |||
| 538 | def list_configs(settings, args, d): | ||
| 539 | registry_path = update_registry(settings["default"]["registry"], cache_dir(args.top_dir), d) | ||
| 540 | json_data = list_registry(registry_path, args.with_expired) | ||
| 541 | print("\nAvailable configurations:") | ||
| 542 | for config_name, config_data in json_data.items(): | ||
| 543 | expiry_date = config_data.get("expires", None) | ||
| 544 | config_desc = config_data["description"] | ||
| 545 | if expiry_date: | ||
| 546 | if args.with_expired or not has_expired(expiry_date): | ||
| 547 | print("{}\t{} (supported until {})".format(config_name, config_desc, expiry_date)) | ||
| 548 | else: | ||
| 549 | print("{}\t{}".format(config_name, config_desc)) | ||
| 550 | print("\nRun 'init' with one of the above configuration identifiers to set up a build.") | ||
| 551 | |||
| 552 | if args.write_json: | ||
| 553 | with open(args.write_json, 'w') as f: | ||
| 554 | json.dump(json_data, f, sort_keys=True, indent=4) | ||
| 555 | print("Available configurations written into {}".format(args.write_json)) | ||
| 556 | |||
| 557 | def default_settings_path(top_dir): | ||
| 558 | return os.path.join(top_dir, 'bitbake-setup.conf') | ||
| 559 | |||
| 560 | def write_settings(top_dir, force_replace, non_interactive=True): | ||
| 561 | settings_path = default_settings_path(top_dir) | ||
| 562 | if not os.path.exists(settings_path) or force_replace: | ||
| 563 | |||
| 564 | settings = configparser.ConfigParser() | ||
| 565 | settings['default'] = { | ||
| 566 | 'registry':default_registry, | ||
| 567 | 'dl-dir':os.path.join(top_dir, '.bitbake-setup-downloads'), | ||
| 568 | } | ||
| 569 | os.makedirs(os.path.dirname(settings_path), exist_ok=True) | ||
| 570 | |||
| 571 | siteconfpath = os.path.join(top_dir, 'site.conf') | ||
| 572 | print('Configuration registry set to\n {}\n'.format(settings['default']['registry'])) | ||
| 573 | print('Bitbake-setup download cache (DL_DIR) set to\n {}\n'.format(settings['default']['dl-dir'])) | ||
| 574 | print('A new settings file will be created in\n {}\n'.format(settings_path)) | ||
| 575 | print('A common site.conf file will be created, please edit or replace before running builds\n {}\n'.format(siteconfpath)) | ||
| 576 | if not non_interactive: | ||
| 577 | y_or_n = input('Bitbake-setup will be configured with the above settings in {}, y/n: '.format(top_dir)) | ||
| 578 | if y_or_n != 'y': | ||
| 579 | print("\nYou can run 'bitbake-setup install-settings' to edit them before setting up builds") | ||
| 580 | exit() | ||
| 581 | print() | ||
| 582 | |||
| 583 | if os.path.exists(settings_path): | ||
| 584 | backup_conf = settings_path + "-backup.{}".format(time.strftime("%Y%m%d%H%M%S")) | ||
| 585 | os.rename(settings_path, backup_conf) | ||
| 586 | print("Previous settings are in {}".format(backup_conf)) | ||
| 587 | with open(settings_path, 'w') as settingsfile: | ||
| 588 | settings.write(settingsfile) | ||
| 589 | |||
| 590 | if os.path.exists(siteconfpath): | ||
| 591 | backup_siteconf = siteconfpath + "-backup.{}".format(time.strftime("%Y%m%d%H%M%S")) | ||
| 592 | os.rename(siteconfpath, backup_siteconf) | ||
| 593 | print("Previous settings are in {}".format(backup_siteconf)) | ||
| 594 | with open(siteconfpath, 'w') as siteconffile: | ||
| 595 | siteconffile.write('# This file is intended for build host-specific bitbake settings\n') | ||
| 596 | |||
| 597 | def load_settings(top_dir, non_interactive): | ||
| 598 | # This creates a new settings file if it does not yet exist | ||
| 599 | write_settings(top_dir, force_replace=False, non_interactive=non_interactive) | ||
| 600 | |||
| 601 | settings_path = default_settings_path(top_dir) | ||
| 602 | settings = configparser.ConfigParser() | ||
| 603 | print('Loading settings from\n {}\n'.format(settings_path)) | ||
| 604 | settings.read([settings_path]) | ||
| 605 | return settings | ||
| 606 | |||
| 607 | def global_settings_path(args): | ||
| 608 | return args.global_settings if args.global_settings else os.path.join(os.path.expanduser('~'), '.config', 'bitbake-setup', 'config') | ||
| 609 | |||
| 610 | def write_global_settings(settings_path, force_replace, non_interactive=True): | ||
| 611 | if not os.path.exists(settings_path) or force_replace: | ||
| 612 | |||
| 613 | settings = configparser.ConfigParser() | ||
| 614 | settings['default'] = { | ||
| 615 | 'top-dir-prefix':os.path.expanduser('~'), | ||
| 616 | 'top-dir-name':'bitbake-builds' | ||
| 617 | } | ||
| 618 | os.makedirs(os.path.dirname(settings_path), exist_ok=True) | ||
| 619 | print('Configuring global settings in\n {}\n'.format(settings_path)) | ||
| 620 | print('Top directory prefix (where all top level directories are created) set to\n {}\n'.format(settings['default']['top-dir-prefix'])) | ||
| 621 | print('Top directory name (this is added to the top directory prefix to form a top directory where builds are set up) set to\n {}\n'.format(settings['default']['top-dir-name'])) | ||
| 622 | if not non_interactive: | ||
| 623 | y_or_n = input('Write out the global settings as specified above (y/n)? ') | ||
| 624 | if y_or_n != 'y': | ||
| 625 | print("\nYou can run 'bitbake-setup install-global-settings' to edit them before setting up builds") | ||
| 626 | exit() | ||
| 627 | print() | ||
| 628 | |||
| 629 | if os.path.exists(settings_path): | ||
| 630 | backup_conf = settings_path + "-backup.{}".format(time.strftime("%Y%m%d%H%M%S")) | ||
| 631 | os.rename(settings_path, backup_conf) | ||
| 632 | print("Previous global settings are in {}".format(backup_conf)) | ||
| 633 | with open(settings_path, 'w') as settingsfile: | ||
| 634 | settings.write(settingsfile) | ||
| 635 | |||
| 636 | def load_global_settings(settings_path, non_interactive): | ||
| 637 | # This creates a new settings file if it does not yet exist | ||
| 638 | write_global_settings(settings_path, force_replace=False, non_interactive=non_interactive) | ||
| 639 | |||
| 640 | settings = configparser.ConfigParser() | ||
| 641 | print('Loading global settings from\n {}\n'.format(settings_path)) | ||
| 642 | settings.read([settings_path]) | ||
| 643 | return settings | ||
| 644 | |||
| 645 | def change_settings(top_dir, new_settings): | ||
| 646 | settings = load_settings(top_dir, non_interactive=True) | ||
| 647 | for section, section_settings in new_settings.items(): | ||
| 648 | for setting, value in section_settings.items(): | ||
| 649 | settings[section][setting] = value | ||
| 650 | print("Setting '{}' in section '{}' is changed to '{}'".format(setting, section, value)) | ||
| 651 | |||
| 652 | settings_path = default_settings_path(top_dir) | ||
| 653 | with open(settings_path, 'w') as settingsfile: | ||
| 654 | settings.write(settingsfile) | ||
| 655 | print("New settings written to {}".format(settings_path)) | ||
| 656 | return settings | ||
| 657 | |||
| 658 | def change_global_settings(settings_path, new_settings): | ||
| 659 | settings = load_global_settings(settings_path, non_interactive=True) | ||
| 660 | for section, section_settings in new_settings.items(): | ||
| 661 | for setting, value in section_settings.items(): | ||
| 662 | settings[section][setting] = value | ||
| 663 | print("Setting '{}' in section '{}' is changed to '{}'".format(setting, section, value)) | ||
| 664 | |||
| 665 | with open(settings_path, 'w') as settingsfile: | ||
| 666 | settings.write(settingsfile) | ||
| 667 | print("New global settings written to {}".format(settings_path)) | ||
| 668 | return settings | ||
| 669 | |||
| 670 | def get_build_dir_via_bbpath(): | ||
| 671 | bbpath = os.environ.get('BBPATH') | ||
| 672 | if bbpath: | ||
| 673 | bitbake_dir = os.path.normpath(bbpath.split(':')[0]) | ||
| 674 | if os.path.exists(os.path.join(bitbake_dir,'init-build-env')): | ||
| 675 | build_dir = os.path.dirname(bitbake_dir) | ||
| 676 | return build_dir | ||
| 677 | return None | ||
| 678 | |||
| 679 | def get_top_dir(args, global_settings): | ||
| 680 | build_dir_via_bbpath = get_build_dir_via_bbpath() | ||
| 681 | if build_dir_via_bbpath: | ||
| 682 | top_dir = os.path.dirname(build_dir_via_bbpath) | ||
| 683 | if os.path.exists(default_settings_path(top_dir)): | ||
| 684 | return top_dir | ||
| 685 | |||
| 686 | if hasattr(args, 'build_dir'): | ||
| 687 | # commands without --top-dir-prefix/name arguments (status, update) still need to know where | ||
| 688 | # the top dir is, but it should be auto-deduced as parent of args.build_dir | ||
| 689 | top_dir = os.path.dirname(os.path.normpath(args.build_dir)) | ||
| 690 | return top_dir | ||
| 691 | |||
| 692 | top_dir_prefix = args.top_dir_prefix if args.top_dir_prefix else global_settings['default']['top-dir-prefix'] | ||
| 693 | top_dir_name = args.top_dir_name if args.top_dir_name else global_settings['default']['top-dir-name'] | ||
| 694 | return os.path.join(top_dir_prefix, top_dir_name) | ||
| 695 | |||
| 696 | def main(): | ||
| 697 | def add_top_dir_arg(parser): | ||
| 698 | parser.add_argument('--top-dir-prefix', help='Top level directory prefix. This is where all top level directories are created.') | ||
| 699 | parser.add_argument('--top-dir-name', help='Top level directory name. Together with the top directory prefix this forms a top directory where builds are set up and downloaded configurations and layers are cached for reproducibility and offline builds.') | ||
| 700 | |||
| 701 | def add_build_dir_arg(parser): | ||
| 702 | build_dir = get_build_dir_via_bbpath() | ||
| 703 | if build_dir: | ||
| 704 | parser.add_argument('--build-dir', default=build_dir, help="Path to the build, default is %(default)s via BBPATH") | ||
| 705 | else: | ||
| 706 | parser.add_argument('--build-dir', required=True, help="Path to the build") | ||
| 707 | |||
| 708 | parser = argparse.ArgumentParser( | ||
| 709 | description="BitBake setup utility. Run with 'init' argument to get started.", | ||
| 710 | epilog="Use %(prog)s <subcommand> --help to get help on a specific command" | ||
| 711 | ) | ||
| 712 | parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true') | ||
| 713 | parser.add_argument('-q', '--quiet', help='Print only errors', action='store_true') | ||
| 714 | parser.add_argument('--color', choices=['auto', 'always', 'never'], default='auto', help='Colorize output (where %(metavar)s is %(choices)s)', metavar='COLOR') | ||
| 715 | parser.add_argument('--no-network', action='store_true', help='Do not check whether configuration repositories and layer repositories have been updated; use only the local cache.') | ||
| 716 | parser.add_argument('--global-settings', action='store', help='Path to the global settings file where defaults for top directory prefix and name can be specified') | ||
| 717 | |||
| 718 | subparsers = parser.add_subparsers() | ||
| 719 | |||
| 720 | parser_list = subparsers.add_parser('list', help='List available configurations') | ||
| 721 | add_top_dir_arg(parser_list) | ||
| 722 | parser_list.add_argument('--with-expired', action='store_true', help='List also configurations that are no longer supported due to reaching their end-of-life dates.') | ||
| 723 | parser_list.add_argument('--write-json', action='store', help='Write available configurations into a json file so they can be programmatically processed.') | ||
| 724 | parser_list.set_defaults(func=list_configs) | ||
| 725 | |||
| 726 | parser_init = subparsers.add_parser('init', help='Select a configuration and initialize a build from it') | ||
| 727 | add_top_dir_arg(parser_init) | ||
| 728 | parser_init.add_argument('config', nargs='*', help="path/URL/id to a configuration file (use 'list' command to get available ids), followed by configuration options. Bitbake-setup will ask to choose from available choices if command line doesn't completely specify them.") | ||
| 729 | parser_init.add_argument('--non-interactive', action='store_true', help='Do not ask to interactively choose from available options; if bitbake-setup cannot make a decision it will stop with a failure.') | ||
| 730 | parser_init.add_argument('--source-overrides', action='store', help='Override sources information (repositories/revisions) with values from a local json file.') | ||
| 731 | parser_init.add_argument('--build-dir-name', action='store', help='A custom build directory name under the top directory.') | ||
| 732 | parser_init.set_defaults(func=init_config) | ||
| 733 | |||
| 734 | parser_status = subparsers.add_parser('status', help='Check if the build needs to be synchronized with configuration') | ||
| 735 | add_build_dir_arg(parser_status) | ||
| 736 | parser_status.set_defaults(func=build_status) | ||
| 737 | |||
| 738 | parser_update = subparsers.add_parser('update', help='Update a build to be in sync with configuration') | ||
| 739 | add_build_dir_arg(parser_update) | ||
| 740 | parser_update.set_defaults(func=build_update) | ||
| 741 | |||
| 742 | parser_install_settings = subparsers.add_parser('install-settings', help='Write a settings file with default values into the top level directory (contains the location of build configuration registry, downloads directory and other settings specific to a top directory)') | ||
| 743 | add_top_dir_arg(parser_install_settings) | ||
| 744 | parser_install_settings.set_defaults(func=write_settings) | ||
| 745 | |||
| 746 | parser_install_global_settings = subparsers.add_parser('install-global-settings', help='Write a global settings file with default values (contains the default prefix and name of the top directory)') | ||
| 747 | parser_install_global_settings.set_defaults(func=write_global_settings) | ||
| 748 | |||
| 749 | parser_change_setting = subparsers.add_parser('change-setting', help='Change a setting in the settings file') | ||
| 750 | add_top_dir_arg(parser_change_setting) | ||
| 751 | parser_change_setting.add_argument('section', help="Section in a settings file, typically 'default'") | ||
| 752 | parser_change_setting.add_argument('key', help="Name of the setting") | ||
| 753 | parser_change_setting.add_argument('value', help="Value of the setting") | ||
| 754 | parser_change_setting.set_defaults(func=change_settings) | ||
| 755 | |||
| 756 | parser_change_global_setting = subparsers.add_parser('change-global-setting', help='Change a setting in the global settings file') | ||
| 757 | parser_change_global_setting.add_argument('section', help="Section in a global settings file, typically 'default'") | ||
| 758 | parser_change_global_setting.add_argument('key', help="Name of the setting") | ||
| 759 | parser_change_global_setting.add_argument('value', help="Value of the setting") | ||
| 760 | parser_change_global_setting.set_defaults(func=change_global_settings) | ||
| 761 | |||
| 762 | args = parser.parse_args() | ||
| 763 | |||
| 764 | logging.basicConfig(stream=sys.stdout) | ||
| 765 | if args.debug: | ||
| 766 | logger.setLevel(logging.DEBUG) | ||
| 767 | elif args.quiet: | ||
| 768 | logger.setLevel(logging.ERROR) | ||
| 769 | |||
| 770 | # Need to re-run logger_create with color argument | ||
| 771 | # (will be the same logger since it has the same name) | ||
| 772 | bb.msg.logger_create('bitbake-setup', output=sys.stdout, | ||
| 773 | color=args.color, | ||
| 774 | level=logger.getEffectiveLevel()) | ||
| 775 | |||
| 776 | if 'func' in args: | ||
| 777 | if args.func == write_global_settings: | ||
| 778 | write_global_settings(global_settings_path(args), force_replace=True) | ||
| 779 | return | ||
| 780 | elif args.func == change_global_settings: | ||
| 781 | change_global_settings(global_settings_path(args), {args.section:{args.key:args.value}}) | ||
| 782 | return | ||
| 783 | |||
| 784 | if hasattr(args, 'build_dir'): | ||
| 785 | if not os.path.exists(os.path.join(args.build_dir,'build', 'init-build-env')): | ||
| 786 | print("Not a valid build directory: build/init-build-env does not exist in {}".format(args.build_dir)) | ||
| 787 | return | ||
| 788 | |||
| 789 | if not hasattr(args, 'non_interactive'): | ||
| 790 | args.non_interactive = True | ||
| 791 | |||
| 792 | global_settings = load_global_settings(global_settings_path(args), args.non_interactive) | ||
| 793 | args.top_dir = get_top_dir(args, global_settings) | ||
| 794 | |||
| 795 | print('Bitbake-setup is using {} as top directory (can be changed with --top-dir-prefix/name arguments or by setting them in {}).\n'.format(args.top_dir, global_settings_path(args))) | ||
| 796 | if args.func == write_settings: | ||
| 797 | write_settings(args.top_dir, force_replace=True) | ||
| 798 | elif args.func == change_settings: | ||
| 799 | change_settings(args.top_dir, {args.section:{args.key:args.value}}) | ||
| 800 | else: | ||
| 801 | settings = load_settings(args.top_dir, args.non_interactive) | ||
| 802 | d = init_bb_cache(settings, args) | ||
| 803 | args.func(settings, args, d) | ||
| 804 | save_bb_cache() | ||
| 805 | else: | ||
| 806 | from argparse import Namespace | ||
| 807 | parser.print_help() | ||
| 808 | |||
| 809 | main() | ||
