summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Kanavin <alex@linutronix.de>2025-09-29 14:56:07 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-10-04 21:33:32 +0100
commit4e117d279a25a896abe2bc209591fd9b8b7287ba (patch)
tree1a7e7b146dd544821794225d476397e1f4b5e83c
parent2a00d80e2c414441253a96fd5fa93075758cd5f0 (diff)
downloadpoky-4e117d279a25a896abe2bc209591fd9b8b7287ba.tar.gz
bitbake: bitbake-setup: add the initial implementation
Preamble ======== The latest iteration of this patchset is available at https://github.com/kanavin/bitbake I recommend taking the patches from there to ensure that you are not trying out outdated code. For the rationale and design guidelines please see this message: https://lists.openembedded.org/g/openembedded-architecture/message/1913 Left out for now but will be done later: - official configuration repository - documentation Amble *scratch* HOWTO ===================== 1. If you don't know where to start, run 'bitbake-setup init'. Bitbake-setup will ask a few questions about available configuration choices and set up a build. Note: 'init' sub-command can also take a path or a URL with a configuration file directly. You can see how those files look like here: https://github.com/kanavin/bitbake-setup-configurations 2. You can then source the bitbake environment and run bitbake to perform builds as usual: $ . /home/alex/bitbake-builds/yocto-master-options-poky-distro_poky-machine_qemux86-64/build/init-build-env Also, subsequent status/update commands will not require a separate --build-dir argument telling bitbake-setup where the build is. 3. To check if the build configuration needs to be updated, run: === $ bin/bitbake-setup status ... Configuration in /home/alex/bitbake-builds/poky-alex/ has not changed. === If the configuration has changed, you will see the difference as a diff. ... - "rev": "akanavin/sstate-for-all" + "rev": "akanavin/bitbake-setup-testing" ... If the configuration has not changed, but layer revisions referred to it have (for example if the configuration specifies a tip of a branch), you will see that too: === ... Layer repository git://git.yoctoproject.org/poky-contrib checked out into /home/alex/builds/poky-alex/layers/poky updated revision akanavin/sstate-for-all from 6b842ba55f996b27c900e3de78ceac8cb3b1c492 to aeb73e29379fe6007a8adc8d94c1ac18a93e68de === 4. If the configuration has changed, you can bring it in sync with: $ bin/bitbake-setup update Note that it will also rename/preserve the existing build/conf directory, and print changes in bitbake configuration (diff of content of build/conf/) if that has changed. I can't at the moment think of anything more clever that is also not much more brittle or complex to implement, but open to suggestions. Terminology =========== - 'top directory' means the place under which bitbake-setup reads and writes everything. bitbake-setup makes a promise to not touch anything outside of that, unless otherwise directed to by entries in settings (currently there is one such setting for fetcher downloads for layers and config registries). Top directory can be selected by an environment variable, a command line option, or otherwise assumed to be ~/bitbake-builds/. If BBPATH is in environment (e.g. we are in a bitbake environment), then the top directory is deduced from that and doesn't need to be specified by hand. - 'settings' means bitbake-setup operational parameters that are global to all builds under a top directory. E.g. the location of configuration registry, or where the bitbake fetcher should place the downloads (DL_DIR setting). Settings are stored in a .conf file in ini format just under the top directory. - 'build' means a tree structure set up by 'bitbake-setup init', consisting of, at least, a layers checkout, and a bitbake build. It maps 1:1 to the json data it was constructed from, which is called 'build configuration'. Build configurations are constructed from generic configurations that may involve making one or more choices about available options in them. Generic configurations are files, URLs or are obtained from git repositories called 'config registries', in which case they can be listed with 'bitbake-setup list'. There can be multiple 'builds' under a top directory. Here are two example generic configurations that showcase this: https://github.com/kanavin/bitbake-setup-configurations/blob/main/yocto-master-options.conf.json https://github.com/kanavin/bitbake-setup-configurations/blob/main/yocto-master-nested-configs.conf.json - 'bitbake-setup status' will tell if a build is in sync with the generic configuration it was made from. 'bitbake-setup update' will bring a build in sync with a configuration if needed. - 'bitbake build' means a particular sub-tree inside a build that bitbake itself operates on, e.g. what is set in BBPATH/BUILDDIR by oe-init-build-env. conf/* in that tree is 'bitbake configuration'. Bitbake configurations are constructed from templates and fragments, with existing mechanisms provided by oe-core. The configuration file format is specified such that other mechanisms to set up a bitbake build can be added; there was a mention of ability to specify local.conf content and a set of layers directly in a configuration. I think that scales poorly compared to templates and fragments, but I made sure alternative ways to configure a bitbake build are possible to add in the future :) - 'source override' is a json file that can be used to modify revisions and origins of layers that need to be checkout into a build (e.g. when master branches need to be changed to master-next for purposes of testing). Such a file is specified with a command-line option to 'init' and an example can be seen here: https://github.com/kanavin/bitbake-setup-configurations/blob/main/yocto-master-next.override.json This commit includes fixes by Ryan Eatmon <reatmon@ti.com> https://github.com/kanavin/bitbake/pull/1 Gyorgy Sarvari <skandigraun@gmail.com> https://github.com/kanavin/bitbake/pull/2 Johannes Schneider <johannes.schneider@leica-geosystems.com> https://github.com/kanavin/bitbake/pull/3 https://github.com/kanavin/bitbake/pull/5 (Bitbake rev: bf3542076cdd751da9de8745aa3f351f5c6b0c5f) Signed-off-by: Alexander Kanavin <alex@linutronix.de> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rwxr-xr-xbitbake/bin/bitbake-setup809
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
7import logging
8import os
9import sys
10import argparse
11import warnings
12import json
13import shutil
14import time
15import stat
16import tempfile
17import configparser
18import datetime
19
20default_registry = 'git://github.com/kanavin/bitbake-setup-configurations.git;protocol=https;branch=main;rev=main'
21
22bindir = os.path.abspath(os.path.dirname(__file__))
23sys.path[0:0] = [os.path.join(os.path.dirname(bindir), 'lib')]
24
25import bb.msg
26import bb.process
27
28logger = bb.msg.logger_create('bitbake-setup', sys.stdout)
29
30def cache_dir(top_dir):
31 return os.path.join(top_dir, '.bitbake-setup-cache')
32
33def 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
46def save_bb_cache():
47 bb.fetch2.fetcher_parse_save()
48 bb.fetch2.fetcher_parse_done()
49
50def 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
58def 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
62def 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
66def _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
76def 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
111def 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
202Source the environment using '. {}' to run builds from the command line.
203The 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
218def 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
225def 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
235def 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
248def 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
271def 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
297def 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
321def 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
343def 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
382def 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
427def 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
437def 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
460def 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
492def build_update(settings, args, d):
493 build_status(settings, args, d, update=True)
494
495def 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
508def 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
516def has_expired(expiry_date):
517 if expiry_date:
518 return datetime.datetime.now() > datetime.datetime.fromisoformat(expiry_date)
519 return False
520
521def 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
538def 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
557def default_settings_path(top_dir):
558 return os.path.join(top_dir, 'bitbake-setup.conf')
559
560def 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
597def 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
607def 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
610def 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
636def 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
645def 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
658def 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
670def 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
679def 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
696def 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
809main()