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 +3 -0
- brkas/__main__.py +6 -0
- brkas/buildroot.py +328 -0
- brkas/cli.py +264 -0
- brkas/config.py +206 -0
- brkas/container/Dockerfile +27 -0
- brkas/container/brkas.sh +16 -0
- brkas/container.py +190 -0
- brkas/repos.py +253 -0
- brkas/schema.json +114 -0
- brkas-0.1.0.dist-info/METADATA +327 -0
- brkas-0.1.0.dist-info/RECORD +16 -0
- brkas-0.1.0.dist-info/WHEEL +5 -0
- brkas-0.1.0.dist-info/entry_points.txt +3 -0
- brkas-0.1.0.dist-info/licenses/COPYING +343 -0
- brkas-0.1.0.dist-info/top_level.txt +1 -0
brkas/__init__.py
ADDED
brkas/__main__.py
ADDED
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())
|