brkas 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
brkas/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ """brkas: kas-like project files for Buildroot."""
3
+ __version__ = "0.1.0"
brkas/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
brkas/buildroot.py ADDED
@@ -0,0 +1,328 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ """Drives Buildroot for a project: configure, build, menuconfig, shell."""
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import time
11
+
12
+ from .config import ConfigError, expand
13
+ from .repos import RepoError, log, run
14
+
15
+ CONFIG_LINE = re.compile(r"^(BR2_[A-Za-z0-9_]+)=(.*)$")
16
+ ENVIRONMENT_SETUP = "BR2_PACKAGE_HOST_ENVIRONMENT_SETUP"
17
+ SPINNER = "|/-\\"
18
+ UNSET_LINE = re.compile(r"^# (BR2_[A-Za-z0-9_]+) is not set$")
19
+
20
+
21
+ def render(symbol, value):
22
+ """One .config line for a value of the config: section."""
23
+ if value is True or value == "y":
24
+ return f"{symbol}=y"
25
+ if value is False or value == "n":
26
+ return f"# {symbol} is not set"
27
+ if value == "m" or isinstance(value, int):
28
+ return f"{symbol}={value}"
29
+ text = str(value)
30
+ if len(text) >= 2 and text[0] == text[-1] == '"':
31
+ return f"{symbol}={text}"
32
+ return f'{symbol}="' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
33
+
34
+
35
+ def parse_config(text):
36
+ """{symbol: value} of a .config or fragment, 'n' for unset symbols."""
37
+ values = {}
38
+ for line in text.splitlines():
39
+ line = line.strip()
40
+ m = CONFIG_LINE.match(line)
41
+ if m:
42
+ values[m.group(1)] = m.group(2)
43
+ continue
44
+ m = UNSET_LINE.match(line)
45
+ if m:
46
+ values[m.group(1)] = "n"
47
+ return values
48
+
49
+
50
+ def read_config(path):
51
+ try:
52
+ with open(path) as f:
53
+ return parse_config(f.read())
54
+ except OSError:
55
+ return {}
56
+
57
+
58
+ def config_diff(before, after):
59
+ """Fragment lines that turn before into after."""
60
+ lines = []
61
+ for symbol in sorted(set(before) | set(after)):
62
+ old, new = before.get(symbol, "n"), after.get(symbol, "n")
63
+ if old != new:
64
+ lines.append(f"# {symbol} is not set" if new == "n" else f"{symbol}={new}")
65
+ return lines
66
+
67
+
68
+ def symbol_of(line):
69
+ m = CONFIG_LINE.match(line) or UNSET_LINE.match(line)
70
+ return m.group(1) if m else None
71
+
72
+
73
+ def save_fragment(path, lines):
74
+ """Writes lines in a fragment file: a symbol already in it takes its
75
+ new value, other lines are kept."""
76
+ old = []
77
+ if os.path.exists(path):
78
+ with open(path) as f:
79
+ old = [line.rstrip("\n") for line in f]
80
+ names = {symbol_of(line) for line in lines}
81
+ kept = [line for line in old if symbol_of(line) not in names or symbol_of(line) is None]
82
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
83
+ with open(path, "w") as f:
84
+ f.write("\n".join(kept + lines) + "\n")
85
+
86
+
87
+ class Builder:
88
+ def __init__(self, project, repos, work_dir, out_dir=None, shell=False):
89
+ self.project = project
90
+ self.repos = repos
91
+ build = project.data.get("build") or {}
92
+ root = build.get("dir") or "build"
93
+ if not os.path.isabs(root):
94
+ root = os.path.join(work_dir, root)
95
+ self.out = out_dir or os.path.join(root, project.name)
96
+ self.br = repos["buildroot"].tree
97
+ self.externals = [repos[name].tree for name in project.externals()]
98
+ # brkas shell needs host/environment-setup: the shell enables its
99
+ # package, which then stays on so that a build does not drop it
100
+ self.environment_setup_on = shell or \
101
+ read_config(os.path.join(self.out, ".config")).get(ENVIRONMENT_SETUP) == "y"
102
+
103
+ def env(self):
104
+ env = dict(os.environ)
105
+ for key, value in (self.project.data.get("env") or {}).items():
106
+ if value is None:
107
+ env.pop(key, None)
108
+ else:
109
+ env[key] = str(value)
110
+ dl_dir = (self.project.data.get("build") or {}).get("dl_dir")
111
+ if dl_dir:
112
+ env["BR2_DL_DIR"] = expand(dl_dir)
113
+ return env
114
+
115
+ def make_cmd(self, *targets):
116
+ cmd = ["make", "-C", self.br, f"O={self.out}"]
117
+ cmd.append("BR2_EXTERNAL=" + ":".join(self.externals))
118
+ return cmd + list(targets)
119
+
120
+ def make(self, *targets, interactive=True):
121
+ cmd = self.make_cmd(*targets)
122
+ if interactive:
123
+ if subprocess.run(cmd, env=self.env()).returncode:
124
+ raise RepoError(f"make {' '.join(targets)} failed")
125
+ return None
126
+ p = subprocess.run(cmd, env=self.env(), text=True, capture_output=True)
127
+ if p.returncode:
128
+ raise RepoError(f"make {' '.join(targets)} failed:\n{(p.stdout + p.stderr).strip()[-4000:]}")
129
+ return p.stdout
130
+
131
+ def jobs(self):
132
+ """-j with every available core, unless the project sets it in
133
+ MAKEFLAGS; Buildroot only runs packages in parallel with
134
+ BR2_PER_PACKAGE_DIRECTORIES, and ignores it otherwise."""
135
+ if "-j" in self.env().get("MAKEFLAGS", ""):
136
+ return []
137
+ return [f"-j{len(os.sched_getaffinity(0))}"]
138
+
139
+ def make_quietly(self, target, what):
140
+ """make target with its output in a log file, and a spinner on a
141
+ terminal meanwhile; the end of the log is shown if it fails."""
142
+ path = os.path.join(self.out, f"brkas-{target}.log")
143
+ tty = sys.stderr.isatty()
144
+ log(f"{what}: make {target}, output in {path}")
145
+ start = time.monotonic()
146
+ with open(path, "w") as out:
147
+ p = subprocess.Popen(self.make_cmd(*self.jobs(), target), env=self.env(), stdout=out,
148
+ stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
149
+ try:
150
+ frame = 0
151
+ while p.poll() is None:
152
+ if tty:
153
+ elapsed = int(time.monotonic() - start)
154
+ sys.stderr.write(f"\rbrkas: {what} {SPINNER[frame % len(SPINNER)]} "
155
+ f"{elapsed // 60:02d}:{elapsed % 60:02d}")
156
+ sys.stderr.flush()
157
+ frame += 1
158
+ time.sleep(0.2)
159
+ except KeyboardInterrupt:
160
+ p.terminate()
161
+ p.wait()
162
+ raise
163
+ finally:
164
+ if tty:
165
+ sys.stderr.write("\r\033[K")
166
+ sys.stderr.flush()
167
+ elapsed = int(time.monotonic() - start)
168
+ if p.returncode:
169
+ with open(path, errors="replace") as f:
170
+ tail = f.readlines()[-30:]
171
+ sys.stderr.write("".join(tail))
172
+ raise RepoError(f"make {target} failed, the whole output is in {path}")
173
+ log(f"{what}: done in {elapsed // 60:02d}:{elapsed % 60:02d}")
174
+
175
+ # --- configuration -------------------------------------------------
176
+
177
+ def defconfig(self):
178
+ """('target', name) or ('file', path) or None."""
179
+ entry = self.project.data.get("defconfig")
180
+ if entry is None:
181
+ return None
182
+ if isinstance(entry, dict):
183
+ return ("file", self.project.locate(entry))
184
+ if os.path.isabs(entry):
185
+ return ("file", entry)
186
+ name = entry if entry.endswith("_defconfig") else entry + "_defconfig"
187
+ for tree in [self.br] + self.externals:
188
+ if os.path.exists(os.path.join(tree, "configs", name)):
189
+ return ("target", name)
190
+ raise ConfigError(f"defconfig {name} not found in Buildroot nor in the externals")
191
+
192
+ def fragment_files(self):
193
+ return [(ident, self.project.locate(entry) if isinstance(entry, dict) else entry)
194
+ for ident, entry in self.project.fragments()]
195
+
196
+ def inline_fragment(self):
197
+ lines = [ENVIRONMENT_SETUP + "=y"] if self.environment_setup_on else []
198
+ # config: comes last, it may still turn it off
199
+ return lines + [render(sym, value) for sym, value in self.project.config().items()]
200
+
201
+ def digest(self):
202
+ h = hashlib.sha256()
203
+ h.update(json.dumps([self.br] + self.externals).encode())
204
+ kind = self.defconfig()
205
+ h.update(json.dumps(kind).encode())
206
+ files = [p for _, p in self.fragment_files()] + ([kind[1]] if kind and kind[0] == "file" else [])
207
+ if kind and kind[0] == "target":
208
+ files += [os.path.join(t, "configs", kind[1]) for t in [self.br] + self.externals
209
+ if os.path.exists(os.path.join(t, "configs", kind[1]))]
210
+ for path in files:
211
+ with open(path, "rb") as f:
212
+ h.update(f.read())
213
+ h.update("\n".join(self.inline_fragment()).encode())
214
+ for repo in self.repos.values():
215
+ h.update(json.dumps(repo.state(), sort_keys=True).encode())
216
+ return h.hexdigest()
217
+
218
+ def configure(self, force=False):
219
+ stamp = os.path.join(self.out, ".brkas-config")
220
+ digest = self.digest()
221
+ if not force and os.path.exists(os.path.join(self.out, ".config")):
222
+ try:
223
+ with open(stamp) as f:
224
+ if f.read().strip() == digest:
225
+ return False
226
+ except OSError:
227
+ pass
228
+ os.makedirs(self.out, exist_ok=True)
229
+ kind = self.defconfig()
230
+ log(f"configuring {self.out}" + (f" from {os.path.basename(kind[1])}" if kind else ""))
231
+ if kind is None:
232
+ self.make("defconfig", interactive=False)
233
+ elif kind[0] == "target":
234
+ self.make(kind[1], interactive=False)
235
+ else:
236
+ self.make("defconfig", f"BR2_DEFCONFIG={kind[1]}", interactive=False)
237
+
238
+ fragments = [p for _, p in self.fragment_files()]
239
+ inline = self.inline_fragment()
240
+ if inline:
241
+ path = os.path.join(self.out, "brkas-config.fragment")
242
+ with open(path, "w") as f:
243
+ f.write("# config: of the brkas project\n" + "\n".join(inline) + "\n")
244
+ fragments.append(path)
245
+ if fragments:
246
+ run(["sh", os.path.join(self.br, "support/kconfig/merge_config.sh"), "-m",
247
+ "-O", self.out, os.path.join(self.out, ".config"), *fragments], cwd=self.out)
248
+ self.make("olddefconfig", interactive=False)
249
+ self.check(fragments)
250
+ with open(stamp, "w") as f:
251
+ f.write(digest + "\n")
252
+ return True
253
+
254
+ def check(self, fragments):
255
+ """Warns about values Kconfig did not keep (missing dependencies)."""
256
+ wanted = {}
257
+ for path in fragments:
258
+ with open(path) as f:
259
+ wanted.update(parse_config(f.read()))
260
+ got = read_config(os.path.join(self.out, ".config"))
261
+ for symbol, value in wanted.items():
262
+ if got.get(symbol, "n") != value:
263
+ log(f"warning: {symbol}={value} asked, the configuration has "
264
+ f"{symbol}={got.get(symbol, 'n')} (a dependency missing?)")
265
+
266
+ # --- commands ------------------------------------------------------
267
+
268
+ def build(self, targets):
269
+ self.configure()
270
+ targets = targets or (self.project.data.get("build") or {}).get("targets") or []
271
+ self.make(*targets)
272
+
273
+ def menuconfig(self, tool, save):
274
+ self.configure()
275
+ config = os.path.join(self.out, ".config")
276
+ before = read_config(config)
277
+ self.make(tool)
278
+ lines = config_diff(before, read_config(config))
279
+ if not lines:
280
+ log("no change")
281
+ return lines
282
+ if save:
283
+ save_fragment(save, lines)
284
+ log(f"{len(lines)} changes saved in {save}")
285
+ else:
286
+ print("\n".join(lines))
287
+ # The saved fragment is part of the project: no reconfigure next time
288
+ with open(os.path.join(self.out, ".brkas-config"), "w") as f:
289
+ f.write(self.digest() + "\n")
290
+ return lines
291
+
292
+ def sdk_built(self):
293
+ """make sdk ran: it writes host/share/buildroot/sdk-location."""
294
+ return os.path.exists(os.path.join(self.out, "host", "share", "buildroot", "sdk-location"))
295
+
296
+ def environment_setup(self, reconfigured=False):
297
+ """host/environment-setup, after 'make sdk' when the SDK is not
298
+ built yet, or the configuration changed since."""
299
+ config = read_config(os.path.join(self.out, ".config"))
300
+ if config.get(ENVIRONMENT_SETUP, "n") != "y":
301
+ log(f"{ENVIRONMENT_SETUP} is off: a shell without the cross-compilation environment")
302
+ return None
303
+ setup = os.path.join(self.out, "host", "environment-setup")
304
+ if reconfigured or not self.sdk_built() or not os.path.exists(setup):
305
+ self.make_quietly("sdk", "building the SDK")
306
+ return setup
307
+
308
+ def shell(self, command=None):
309
+ """bash in the output directory, with host/environment-setup sourced
310
+ (cross compiler, CC, CFLAGS, ARCH, CROSS_COMPILE...), building the
311
+ SDK first if needed."""
312
+ reconfigured = self.configure()
313
+ env = self.env()
314
+ env["BRKAS_OUTPUT"] = self.out
315
+ env["BRKAS_BUILDROOT"] = self.br
316
+ setup = self.environment_setup(reconfigured)
317
+ if command:
318
+ # its aliases (cmake, configure) too: they apply from the next line
319
+ source = f'shopt -s expand_aliases\n. "{setup}" >/dev/null\n' if setup else ""
320
+ return subprocess.run(["bash", "-c", source + command], cwd=self.out, env=env).returncode
321
+ with tempfile.NamedTemporaryFile("w", prefix="brkas-rc.", suffix=".sh") as rc:
322
+ rc.write('[ -f ~/.bashrc ] && . ~/.bashrc\n')
323
+ if setup:
324
+ rc.write(f'. "{setup}"\n')
325
+ rc.write('PS1="(brkas) $PS1"\n')
326
+ rc.flush()
327
+ log(f"shell in {self.out}: exit to leave")
328
+ return subprocess.run(["bash", "--rcfile", rc.name, "-i"], cwd=self.out, env=env).returncode
brkas/cli.py ADDED
@@ -0,0 +1,264 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ """brkas: set up and build Buildroot projects described in YAML files."""
3
+ import argparse
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+
9
+ import yaml
10
+
11
+ from . import __version__, config
12
+ from .buildroot import Builder
13
+ from .config import ConfigError
14
+ from .repos import Repo, RepoError, log
15
+
16
+
17
+ def make_repos(project, work_dir):
18
+ repos = {"buildroot": Repo("buildroot", project.buildroot(), work_dir)}
19
+ for name, spec in project.externals().items():
20
+ if name == "buildroot":
21
+ raise ConfigError("externals: 'buildroot' is reserved for Buildroot itself")
22
+ repos[name] = Repo(name, spec, work_dir)
23
+ return repos
24
+
25
+
26
+ def _patch_deps(repo):
27
+ return {e.get("repo") for e in (repo.spec.get("patches") or {}).values()
28
+ if isinstance(e, dict) and e.get("repo")} - {None}
29
+
30
+
31
+ def prepare(args, checkout=True):
32
+ """The project and its repos, checked out with the includes they hold."""
33
+ work = os.path.abspath(args.work_dir)
34
+ # lock --update makes a new lock: the old one must not pin the repos
35
+ locks = not (args.func is cmd_lock and args.update)
36
+ project = config.load(args.project, locks=locks)
37
+ for _ in range(8):
38
+ repos = make_repos(project, work)
39
+ project.repo_dirs = {name: repo.root for name, repo in repos.items()}
40
+ if checkout:
41
+ # repos providing patches to others go first
42
+ for repo in sorted(repos.values(), key=lambda r: bool(_patch_deps(r))):
43
+ repo.checkout(project.locate, update=args.update, force=args.force)
44
+ elif project.missing:
45
+ raise ConfigError("includes from repos not checked out yet: " + ", ".join(sorted(project.missing))
46
+ + " (run brkas checkout first)")
47
+ if not project.missing:
48
+ return project, repos
49
+ project = config.load(args.project, project.repo_dirs, locks)
50
+ if not checkout:
51
+ continue
52
+ raise ConfigError("includes keep pointing to new repos: " + ", ".join(sorted(project.missing)))
53
+
54
+
55
+ def builder(args, project, repos, shell=False):
56
+ return Builder(project, repos, os.path.abspath(args.work_dir), args.output, shell)
57
+
58
+
59
+ def lock_data(repos):
60
+ data = {"header": {"version": config.FORMAT_VERSION}}
61
+ pins = {}
62
+ for name, repo in repos.items():
63
+ pin = repo.lock()
64
+ if pin:
65
+ pins[name] = pin
66
+ if "buildroot" in pins:
67
+ data["buildroot"] = pins.pop("buildroot")
68
+ if pins:
69
+ data["externals"] = pins
70
+ return data
71
+
72
+
73
+ # --- commands ----------------------------------------------------------
74
+
75
+ def cmd_checkout(args):
76
+ project, repos = prepare(args)
77
+ for name, repo in repos.items():
78
+ log(f"{name}: {repo.describe()} -> {repo.tree}")
79
+ if not args.no_config:
80
+ builder(args, project, repos).configure(force=args.reconfigure)
81
+
82
+
83
+ def cmd_build(args):
84
+ project, repos = prepare(args)
85
+ b = builder(args, project, repos)
86
+ b.configure(force=args.reconfigure)
87
+ b.build(args.targets)
88
+
89
+
90
+ def cmd_shell(args):
91
+ project, repos = prepare(args)
92
+ return builder(args, project, repos, shell=True).shell(args.command)
93
+
94
+
95
+ def cmd_menuconfig(args):
96
+ project, repos = prepare(args)
97
+ builder(args, project, repos).menuconfig(args.tool, args.save)
98
+
99
+
100
+ CONFIG_TOOLS = {
101
+ "menuconfig": "change the configuration in menuconfig, save the changes as a fragment",
102
+ "xconfig": "the same, in xconfig (Qt)",
103
+ }
104
+
105
+
106
+ def cmd_dump(args):
107
+ project, repos = prepare(args, checkout=not args.no_checkout)
108
+ data = lock_data(repos) if args.lock else dict({"header": {"version": config.FORMAT_VERSION}}, **project.data)
109
+ yaml.safe_dump(data, sys.stdout, sort_keys=False, default_flow_style=False)
110
+
111
+
112
+ def cmd_lock(args):
113
+ project, repos = prepare(args)
114
+ path = config.lock_path(project.files[-1])
115
+ with open(path, "w") as f:
116
+ f.write("# Written by brkas lock: the exact sources of the project\n")
117
+ yaml.safe_dump(lock_data(repos), f, sort_keys=False, default_flow_style=False)
118
+ log(f"wrote {path}")
119
+
120
+
121
+ def cmd_for_all_repos(args):
122
+ project, repos = prepare(args, checkout=not args.no_checkout)
123
+ status = 0
124
+ for name, repo in repos.items():
125
+ env = dict(os.environ, BRKAS_REPO_NAME=name, BRKAS_REPO_DIR=repo.root,
126
+ BRKAS_REPO_URL=repo.spec.get("url", ""),
127
+ BRKAS_REPO_COMMIT=repo.state().get("base", ""))
128
+ log(f"{name}: {args.command}")
129
+ status |= subprocess.run(args.command, shell=True, cwd=repo.root, env=env).returncode
130
+ return status
131
+
132
+
133
+ def remove(path):
134
+ if os.path.isdir(path):
135
+ log(f"removing {path}")
136
+ shutil.rmtree(path)
137
+
138
+
139
+ def download_cache(project, work):
140
+ """Where Buildroot downloads for this project: build.dl_dir, else
141
+ $BR2_DL_DIR, else dl/ in the Buildroot tree."""
142
+ dl_dir = (project.data.get("build") or {}).get("dl_dir") or os.environ.get("BR2_DL_DIR")
143
+ if dl_dir:
144
+ return os.path.abspath(config.expand(dl_dir))
145
+ return os.path.join(make_repos(project, work)["buildroot"].tree, "dl")
146
+
147
+
148
+ def remove_cache(path):
149
+ """Empties the cache; the directory itself may stay, as when it is a
150
+ mount point in brkas-container."""
151
+ if not os.path.isdir(path):
152
+ return
153
+ log(f"removing {path}")
154
+ for entry in os.listdir(path):
155
+ entry = os.path.join(path, entry)
156
+ if os.path.isdir(entry) and not os.path.islink(entry):
157
+ shutil.rmtree(entry)
158
+ else:
159
+ os.unlink(entry)
160
+ try:
161
+ os.rmdir(path)
162
+ except OSError:
163
+ pass
164
+
165
+
166
+ def clean_work_dir(work):
167
+ """clean --all without a project: what brkas made in the work
168
+ directory, i.e. build/, downloads/ and the repos it checked out (those
169
+ with its state file)."""
170
+ for name in ("build", "downloads"):
171
+ remove(os.path.join(work, name))
172
+ for entry in sorted(os.listdir(work)) if os.path.isdir(work) else []:
173
+ path = os.path.join(work, entry)
174
+ if os.path.exists(os.path.join(path, ".git", "brkas.json")) or \
175
+ os.path.exists(os.path.join(path, ".brkas.json")):
176
+ remove(path)
177
+
178
+
179
+ def cmd_clean(args):
180
+ work = os.path.abspath(args.work_dir)
181
+ if not args.project:
182
+ if args.cache:
183
+ raise ConfigError("clean --cache needs a project: the cache is its build.dl_dir")
184
+ if not args.all or args.output:
185
+ raise ConfigError("clean needs a project, or --all to clean the whole work directory")
186
+ return clean_work_dir(work)
187
+ project = config.load(args.project)
188
+ if args.cache:
189
+ remove_cache(download_cache(project, work))
190
+ if not args.all:
191
+ return # only the cache: the output stays
192
+ build = project.data.get("build") or {}
193
+ root = build.get("dir") or "build"
194
+ out_dir = args.output or os.path.join(root if os.path.isabs(root) else os.path.join(work, root), project.name)
195
+ remove(out_dir)
196
+ if args.all:
197
+ for name, repo in make_repos(project, work).items():
198
+ if repo.kind != "local":
199
+ remove(repo.root)
200
+
201
+
202
+ # --- arguments ---------------------------------------------------------
203
+
204
+ def parser():
205
+ p = argparse.ArgumentParser(prog="brkas", description=__doc__)
206
+ p.add_argument("--version", action="version", version=f"brkas {__version__}")
207
+ p.add_argument("--work-dir", default=os.environ.get("BRKAS_WORK_DIR", "."),
208
+ help="where repos and build/ go (default: $BRKAS_WORK_DIR or .)")
209
+ sub = p.add_subparsers(dest="command", required=True, metavar="command")
210
+
211
+ def add(name, func, help, checkout=True, project="project file(s), a.yml or a.yml:b.yml"):
212
+ s = sub.add_parser(name, help=help, description=help)
213
+ if project:
214
+ s.add_argument("project", help=project)
215
+ s.set_defaults(func=func)
216
+ if checkout:
217
+ s.add_argument("--update", action="store_true", help="fetch the repos, to follow their branches")
218
+ s.add_argument("--force", action="store_true", help="discard local changes in the repos")
219
+ s.add_argument("-o", "--output", help="output directory (default: build/<name>)")
220
+ return s
221
+
222
+ s = add("checkout", cmd_checkout, "check out the repos and configure the output directory")
223
+ s.add_argument("--no-config", action="store_true", help="only check out the repos")
224
+ s.add_argument("--reconfigure", action="store_true", help="configure again from scratch")
225
+ s = add("build", cmd_build, "check out, configure and build")
226
+ s.add_argument("--reconfigure", action="store_true", help="configure again from scratch")
227
+ s.add_argument("targets", nargs="*", help="make targets (default: build.targets, else all)")
228
+ s = add("shell", cmd_shell, "bash in the output directory with the SDK environment, after make sdk if needed")
229
+ s.add_argument("-c", "--command", help="run this command instead of an interactive shell")
230
+ for tool, help in CONFIG_TOOLS.items():
231
+ s = add(tool, cmd_menuconfig, help)
232
+ s.set_defaults(tool=tool)
233
+ s.add_argument("--save", metavar="FRAGMENT", help="fragment file to write the changes to (default: print them)")
234
+ s = add("dump", cmd_dump, "print the merged project, or with --lock its exact sources")
235
+ s.add_argument("--lock", action="store_true", help="print the lock file contents")
236
+ s.add_argument("--no-checkout", action="store_true", help="use the repos as they are")
237
+ add("lock", cmd_lock, "pin the repos: write <last project file>.lock.yml")
238
+ s = add("for-all-repos", cmd_for_all_repos, "run a shell command in every repo")
239
+ s.add_argument("--no-checkout", action="store_true", help="use the repos as they are")
240
+ s.add_argument("command", help="command, with BRKAS_REPO_{NAME,DIR,URL,COMMIT} set")
241
+ s = add("clean", cmd_clean, "remove the output directory", checkout=False, project=None)
242
+ s.add_argument("project", nargs="?", help="project file(s); without it, --all cleans the whole "
243
+ "work directory")
244
+ s.add_argument("-o", "--output", help="output directory (default: build/<name>)")
245
+ s.add_argument("--all", action="store_true", help="also remove the repos brkas checked out")
246
+ s.add_argument("--cache", action="store_true",
247
+ help="remove the download cache of the project (build.dl_dir) and nothing else; "
248
+ "with --all, the cache as well")
249
+ return p
250
+
251
+
252
+ def main(argv=None):
253
+ args = parser().parse_args(argv)
254
+ try:
255
+ return args.func(args) or 0
256
+ except (ConfigError, RepoError) as e:
257
+ print(f"brkas: error: {e}", file=sys.stderr)
258
+ return 1
259
+ except KeyboardInterrupt:
260
+ return 130
261
+
262
+
263
+ if __name__ == "__main__":
264
+ sys.exit(main())