lib-installer 3.0.1__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.
- lib_installer/__init__.py +51 -0
- lib_installer/__main__.py +8 -0
- lib_installer/arduino/__init__.py +1 -0
- lib_installer/arduino/arduino.py +334 -0
- lib_installer/arduino/properties.py +168 -0
- lib_installer/arduino/sketchbook.py +133 -0
- lib_installer/cli.py +38 -0
- lib_installer/core/__init__.py +8 -0
- lib_installer/core/blocks.py +373 -0
- lib_installer/core/cli.py +1432 -0
- lib_installer/core/console.py +132 -0
- lib_installer/core/download.py +781 -0
- lib_installer/core/files.py +35 -0
- lib_installer/core/installer.py +1222 -0
- lib_installer/core/manifest.py +1487 -0
- lib_installer/core/steps.py +222 -0
- lib_installer/core/target.py +261 -0
- lib_installer/core/updates.py +120 -0
- lib_installer/core/usercode.py +193 -0
- lib_installer/core/yamlreader.py +417 -0
- lib_installer/esp32/__init__.py +1 -0
- lib_installer/esp32/component.py +354 -0
- lib_installer/esp32/esp32.py +491 -0
- lib_installer/esp32/project.py +768 -0
- lib_installer/pio/__init__.py +1 -0
- lib_installer/pio/ini.py +304 -0
- lib_installer/pio/library_json.py +161 -0
- lib_installer/pio/pio.py +208 -0
- lib_installer/stm32/__init__.py +1 -0
- lib_installer/stm32/checks.py +204 -0
- lib_installer/stm32/ide/__init__.py +110 -0
- lib_installer/stm32/ide/base.py +269 -0
- lib_installer/stm32/ide/cmake.py +299 -0
- lib_installer/stm32/ide/cubeide.py +344 -0
- lib_installer/stm32/ide/iar.py +509 -0
- lib_installer/stm32/ide/keil.py +419 -0
- lib_installer/stm32/ide/makefile.py +376 -0
- lib_installer/stm32/stm32.py +101 -0
- lib_installer/zephyr/__init__.py +1 -0
- lib_installer/zephyr/app.py +648 -0
- lib_installer/zephyr/module.py +375 -0
- lib_installer/zephyr/zephyr.py +620 -0
- lib_installer-3.0.1.dist-info/METADATA +583 -0
- lib_installer-3.0.1.dist-info/RECORD +48 -0
- lib_installer-3.0.1.dist-info/WHEEL +5 -0
- lib_installer-3.0.1.dist-info/entry_points.txt +2 -0
- lib_installer-3.0.1.dist-info/licenses/LICENSE +201 -0
- lib_installer-3.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Where Arduino keeps its libraries, and whether a folder is a sketch.
|
|
3
|
+
|
|
4
|
+
The sketchbook is the folder the Arduino IDE and arduino-cli keep sketches in,
|
|
5
|
+
and its libraries folder is where Library Manager installs to and where both
|
|
6
|
+
look for a library a sketch includes. Where it is depends on the machine, and
|
|
7
|
+
on what the user set, so it is read the way Arduino reads it, first answer
|
|
8
|
+
wins:
|
|
9
|
+
|
|
10
|
+
--sketchbook given on the command line
|
|
11
|
+
ARDUINO_DIRECTORIES_USER the variable arduino-cli reads
|
|
12
|
+
~/.arduinoIDE/arduino-cli.yaml Arduino IDE 2's settings, directories.user
|
|
13
|
+
Arduino15/arduino-cli.yaml arduino-cli's own settings, the same key
|
|
14
|
+
Arduino15/preferences.txt Arduino IDE 1's settings, sketchbook.path
|
|
15
|
+
the default Documents/Arduino on Windows and macOS,
|
|
16
|
+
~/Arduino on Linux
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# The two keys that hold the sketchbook, in Arduino IDE 2 and arduino-cli's
|
|
25
|
+
# YAML, and in Arduino IDE 1's preferences.txt.
|
|
26
|
+
_DIRECTORIES = re.compile(r"^directories:[ \t]*\r?$", re.MULTILINE)
|
|
27
|
+
_USER = re.compile(r"^([ \t]+)user:[ \t]*(.*?)[ \t]*\r?$", re.MULTILINE)
|
|
28
|
+
_PREFERENCE = re.compile(r"^sketchbook\.path=(.*?)[ \t]*\r?$", re.MULTILINE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def arduino15():
|
|
32
|
+
"""The folder Arduino keeps its own data and settings in."""
|
|
33
|
+
if sys.platform == "win32":
|
|
34
|
+
return Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local") / "Arduino15"
|
|
35
|
+
|
|
36
|
+
if sys.platform == "darwin":
|
|
37
|
+
return Path.home() / "Library" / "Arduino15"
|
|
38
|
+
|
|
39
|
+
return Path.home() / ".arduino15"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def documents():
|
|
43
|
+
"""
|
|
44
|
+
The user's Documents folder. On Windows it may have been moved, to
|
|
45
|
+
OneDrive for one, which the registry says.
|
|
46
|
+
"""
|
|
47
|
+
if sys.platform == "win32":
|
|
48
|
+
try:
|
|
49
|
+
import winreg
|
|
50
|
+
|
|
51
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
|
52
|
+
r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders") as key:
|
|
53
|
+
value, _kind = winreg.QueryValueEx(key, "Personal")
|
|
54
|
+
|
|
55
|
+
if value:
|
|
56
|
+
return Path(os.path.expandvars(value))
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
return Path.home() / "Documents"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def default_sketchbook():
|
|
64
|
+
"""Where Arduino puts the sketchbook when nothing says otherwise."""
|
|
65
|
+
if sys.platform.startswith("linux"):
|
|
66
|
+
return Path.home() / "Arduino"
|
|
67
|
+
|
|
68
|
+
return documents() / "Arduino"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _unquote(value):
|
|
72
|
+
"""A YAML scalar without its quotes."""
|
|
73
|
+
value = value.strip()
|
|
74
|
+
|
|
75
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
|
76
|
+
value = value[1:-1]
|
|
77
|
+
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def user_directory(text):
|
|
82
|
+
"""directories.user from the text of an arduino-cli.yaml, or None."""
|
|
83
|
+
found = _DIRECTORIES.search(text)
|
|
84
|
+
|
|
85
|
+
if found is None:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
# The keys indented under directories:, up to the next line at the top.
|
|
89
|
+
block = text[found.end():]
|
|
90
|
+
top = re.search(r"^\S", block, re.MULTILINE)
|
|
91
|
+
block = block[:top.start()] if top else block
|
|
92
|
+
user = _USER.search(block)
|
|
93
|
+
|
|
94
|
+
return _unquote(user.group(2)) if user and _unquote(user.group(2)) else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _read(path):
|
|
98
|
+
try:
|
|
99
|
+
return Path(path).read_text(encoding="utf-8")
|
|
100
|
+
except (OSError, UnicodeDecodeError):
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sketchbook(given=None):
|
|
105
|
+
"""The sketchbook folder, and where that answer came from: (Path, how)."""
|
|
106
|
+
if given:
|
|
107
|
+
return Path(given).expanduser().resolve(), "--sketchbook"
|
|
108
|
+
|
|
109
|
+
variable = os.environ.get("ARDUINO_DIRECTORIES_USER")
|
|
110
|
+
|
|
111
|
+
if variable:
|
|
112
|
+
return Path(variable).expanduser().resolve(), "ARDUINO_DIRECTORIES_USER"
|
|
113
|
+
|
|
114
|
+
for config, how in ((Path.home() / ".arduinoIDE" / "arduino-cli.yaml", "Arduino IDE's settings"),
|
|
115
|
+
(arduino15() / "arduino-cli.yaml", "arduino-cli's settings")):
|
|
116
|
+
found = user_directory(_read(config))
|
|
117
|
+
|
|
118
|
+
if found:
|
|
119
|
+
return Path(found).expanduser().resolve(), how
|
|
120
|
+
|
|
121
|
+
found = _PREFERENCE.search(_read(arduino15() / "preferences.txt"))
|
|
122
|
+
|
|
123
|
+
if found and found.group(1).strip():
|
|
124
|
+
return Path(found.group(1).strip()).expanduser().resolve(), "Arduino IDE 1's preferences"
|
|
125
|
+
|
|
126
|
+
return default_sketchbook().resolve(), "the default"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_sketch(folder):
|
|
130
|
+
"""Whether a folder is a sketch: one with a .ino named after it, as Arduino needs."""
|
|
131
|
+
folder = Path(folder)
|
|
132
|
+
|
|
133
|
+
return (folder / f"{folder.name}.ino").is_file() or (folder / f"{folder.name}.pde").is_file()
|
lib_installer/cli.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The lib-installer command: the core's command line, with every platform.
|
|
3
|
+
|
|
4
|
+
The core chooses the platform of each run from the library's installer.yml, or
|
|
5
|
+
from --platform, and every platform's own options, such as --ide and --conf,
|
|
6
|
+
are on the one command line.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .arduino.arduino import Arduino
|
|
13
|
+
from .core import cli
|
|
14
|
+
from .esp32.esp32 import Esp32
|
|
15
|
+
from .pio.pio import PlatformIO
|
|
16
|
+
from .stm32.stm32 import Stm32
|
|
17
|
+
from .zephyr.zephyr import Zephyr
|
|
18
|
+
|
|
19
|
+
# Each platform by the name platform: in installer.yml and --platform give it,
|
|
20
|
+
# in the order the help lists them.
|
|
21
|
+
PLATFORMS = {
|
|
22
|
+
"stm32": lambda: Stm32(__version__),
|
|
23
|
+
"zephyr": lambda: Zephyr(__version__),
|
|
24
|
+
"esp32": lambda: Esp32(__version__),
|
|
25
|
+
"arduino": lambda: Arduino(__version__),
|
|
26
|
+
"pio": lambda: PlatformIO(__version__),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
__all__ = ["PLATFORMS", "main"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv=None, library_root=None):
|
|
33
|
+
"""Run the command. Returns a process exit code."""
|
|
34
|
+
return cli.main(argv=argv, library_root=library_root, platforms=PLATFORMS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
sys.exit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The core of lib-installer, the same for every platform.
|
|
3
|
+
|
|
4
|
+
It fetches a library from GitHub, a zip or a folder, reads its installer.yml,
|
|
5
|
+
installs what it needs first, copies its files and keeps what the user wrote
|
|
6
|
+
between USER CODE markers. What a project looks like, and how a library is
|
|
7
|
+
registered with it, comes from the platform through target.use().
|
|
8
|
+
"""
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Marked blocks in a project's own files, the same on every platform.
|
|
3
|
+
|
|
4
|
+
The installer adds a library's lines to a file the project owns, such as a
|
|
5
|
+
CMakeLists.txt or platformio.ini, between two marker lines that name it:
|
|
6
|
+
|
|
7
|
+
# >>> lib-installer: gcore >>>
|
|
8
|
+
...
|
|
9
|
+
# <<< lib-installer: gcore <<<
|
|
10
|
+
|
|
11
|
+
and finds them again by those markers on the next install, to write them anew
|
|
12
|
+
or take them out. A label after the library's name keeps several blocks of one
|
|
13
|
+
library apart, "gcore/conf" and "gcore/report", and the sign is the file's own
|
|
14
|
+
comment sign: # in CMake and Kconfig, ; in platformio.ini.
|
|
15
|
+
|
|
16
|
+
Three rules hold in every file:
|
|
17
|
+
|
|
18
|
+
- A file with only one marker of a block is never changed: whatever is written
|
|
19
|
+
could land in the wrong place. The lines to add by hand are given instead.
|
|
20
|
+
- The file is backed up before any change, and its line endings are kept.
|
|
21
|
+
- Lines outside the blocks are never touched.
|
|
22
|
+
|
|
23
|
+
What each block held when it was written is kept in the record, as a
|
|
24
|
+
fingerprint, so the next install can tell the user edited inside one.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import re
|
|
29
|
+
|
|
30
|
+
from .files import backup
|
|
31
|
+
|
|
32
|
+
# A label, as a manifest writes it: like an option's name.
|
|
33
|
+
LABEL = re.compile(r"^[a-z][a-z0-9_-]*$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Marker:
|
|
37
|
+
"""The two lines around one block, and the name the record keeps it by."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, installer, library, label=None, sign="#"):
|
|
40
|
+
self.name = f"{library}/{label}" if label else str(library)
|
|
41
|
+
self.open = f"{sign} >>> {installer}: {self.name} >>>"
|
|
42
|
+
self.close = f"{sign} <<< {installer}: {self.name} <<<"
|
|
43
|
+
|
|
44
|
+
def __repr__(self):
|
|
45
|
+
return f"Marker({self.name})"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def span(text, marker):
|
|
49
|
+
"""
|
|
50
|
+
Where a block is in text: (start, end), from its first marker's first
|
|
51
|
+
character to its last marker's last; None when there is none; False when
|
|
52
|
+
only one of its markers is there, or they stand the wrong way round.
|
|
53
|
+
"""
|
|
54
|
+
found = spans(text, marker)
|
|
55
|
+
|
|
56
|
+
if found is False:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
return found[0] if found else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def spans(text, marker):
|
|
63
|
+
"""
|
|
64
|
+
Every block of this marker in text, for a file that holds one in several
|
|
65
|
+
places, as platformio.ini does in each environment. [] for none, False when
|
|
66
|
+
a marker is left without its partner.
|
|
67
|
+
"""
|
|
68
|
+
found = []
|
|
69
|
+
at = 0
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
start = text.find(marker.open, at)
|
|
73
|
+
stray = text.find(marker.close, at)
|
|
74
|
+
|
|
75
|
+
if start < 0:
|
|
76
|
+
# A closing marker with no opening one before it.
|
|
77
|
+
return False if stray >= 0 else found
|
|
78
|
+
|
|
79
|
+
if 0 <= stray < start:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
end = text.find(marker.close, start + len(marker.open))
|
|
83
|
+
|
|
84
|
+
if end < 0:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
nested = text.find(marker.open, start + len(marker.open))
|
|
88
|
+
|
|
89
|
+
if 0 <= nested < end:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
found.append((start, end + len(marker.close)))
|
|
93
|
+
at = end + len(marker.close)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def line_start(text, at):
|
|
97
|
+
"""Where the line holding position at begins."""
|
|
98
|
+
return text.rfind("\n", 0, at) + 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def body(text, block_span):
|
|
102
|
+
"""The lines between a block's two markers, as text."""
|
|
103
|
+
start, end = block_span
|
|
104
|
+
first = text.find("\n", start)
|
|
105
|
+
last = line_start(text, end)
|
|
106
|
+
|
|
107
|
+
return text[first + 1:last] if 0 <= first < last else ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def fingerprint(text):
|
|
111
|
+
"""
|
|
112
|
+
What a block holds, as a short hash. Line endings and the indentation the
|
|
113
|
+
block was written with do not count, since an editor changes those freely.
|
|
114
|
+
"""
|
|
115
|
+
lines = [line.strip() for line in text.replace("\r\n", "\n").split("\n") if line.strip()]
|
|
116
|
+
|
|
117
|
+
return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()[:16]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def outside(text, marker):
|
|
121
|
+
"""The text with every block of this marker taken out, for reading what is the user's."""
|
|
122
|
+
found = spans(text, marker)
|
|
123
|
+
|
|
124
|
+
if not found:
|
|
125
|
+
return text
|
|
126
|
+
|
|
127
|
+
out = []
|
|
128
|
+
at = 0
|
|
129
|
+
|
|
130
|
+
for start, end in found:
|
|
131
|
+
out.append(text[at:start])
|
|
132
|
+
at = end
|
|
133
|
+
|
|
134
|
+
out.append(text[at:])
|
|
135
|
+
|
|
136
|
+
return "".join(out)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cut(text, block_span, newline):
|
|
140
|
+
"""
|
|
141
|
+
The text with a block taken out, with the line it stood on and the one
|
|
142
|
+
blank line that came with it: below it, or above it when it ends the file.
|
|
143
|
+
"""
|
|
144
|
+
start, end = block_span
|
|
145
|
+
start = line_start(text, start)
|
|
146
|
+
|
|
147
|
+
if text.startswith(newline, end):
|
|
148
|
+
end += len(newline)
|
|
149
|
+
|
|
150
|
+
if text.startswith(newline, end):
|
|
151
|
+
end += len(newline)
|
|
152
|
+
elif end == len(text) and text[:start].endswith(newline + newline):
|
|
153
|
+
start -= len(newline)
|
|
154
|
+
|
|
155
|
+
return text[:start] + text[end:]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def line_ending(text):
|
|
159
|
+
"""The line ending a file uses: CRLF when it has any, otherwise LF."""
|
|
160
|
+
return "\r\n" if "\r\n" in text else "\n"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def lines(marker, content, indent="", newline="\n"):
|
|
164
|
+
"""A block as text: its markers around content, each line indented as given."""
|
|
165
|
+
rows = [marker.open] + list(content) + [marker.close]
|
|
166
|
+
|
|
167
|
+
return newline.join(indent + row if row else row for row in rows) + newline
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def save(path, text, updated, write):
|
|
171
|
+
"""
|
|
172
|
+
Write updated to path when it differs from text, backing the file up
|
|
173
|
+
first. Returns (changed, backup): backup is None for a file that did not
|
|
174
|
+
exist. write(path, text) is the installer's own, which keeps the bytes and
|
|
175
|
+
line endings it reads.
|
|
176
|
+
"""
|
|
177
|
+
if updated == text:
|
|
178
|
+
return False, None
|
|
179
|
+
|
|
180
|
+
saved = backup(path) if path.exists() else None
|
|
181
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
write(path, updated)
|
|
183
|
+
|
|
184
|
+
return True, saved
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def edited(recorded, file_name, marker, text):
|
|
188
|
+
"""
|
|
189
|
+
Whether the user changed what is inside a block since it was written, by
|
|
190
|
+
the fingerprint the record keeps. False when the record does not know it.
|
|
191
|
+
"""
|
|
192
|
+
found = span(text, marker)
|
|
193
|
+
|
|
194
|
+
if not found or not isinstance(recorded, dict):
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
known = recorded.get(file_name)
|
|
198
|
+
|
|
199
|
+
if not isinstance(known, dict) or marker.name not in known:
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
return fingerprint(body(text, found)) != known[marker.name]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def fingerprints(text, markers):
|
|
206
|
+
"""{marker name: fingerprint} for each of these blocks found in text."""
|
|
207
|
+
found = {}
|
|
208
|
+
|
|
209
|
+
for marker in markers:
|
|
210
|
+
place = span(text, marker)
|
|
211
|
+
|
|
212
|
+
if place:
|
|
213
|
+
found[marker.name] = fingerprint(body(text, place))
|
|
214
|
+
|
|
215
|
+
return found
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def labels(text, installer, library, sign="#"):
|
|
219
|
+
"""The labels of this library's labelled blocks in text, in the order they stand."""
|
|
220
|
+
pattern = re.compile(re.escape(f"{sign} >>> {installer}: {library}/") + r"([a-z][a-z0-9_-]*) >>>")
|
|
221
|
+
|
|
222
|
+
return list(dict.fromkeys(pattern.findall(text)))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def place(text, marker, content, newline, anchor=None):
|
|
226
|
+
"""
|
|
227
|
+
text with a block of content in its place: taken out from wherever it is,
|
|
228
|
+
and put back above the line anchor(text) gives the start of, with a blank
|
|
229
|
+
line after it, or, with no anchor or none found, at the end of the file
|
|
230
|
+
after a blank line. Done that way, a block already in its place comes back
|
|
231
|
+
exactly as it was. False when the block is half written.
|
|
232
|
+
"""
|
|
233
|
+
found = span(text, marker)
|
|
234
|
+
|
|
235
|
+
if found is False:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
rest = cut(text, found, newline) if found else text
|
|
239
|
+
block = newline.join([marker.open] + list(content) + [marker.close])
|
|
240
|
+
at = anchor(rest) if anchor is not None else None
|
|
241
|
+
|
|
242
|
+
if at is not None:
|
|
243
|
+
return rest[:at] + block + newline + newline + rest[at:]
|
|
244
|
+
|
|
245
|
+
head = rest.rstrip("\r\n")
|
|
246
|
+
|
|
247
|
+
return (head + newline + newline if head else "") + block + newline
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Group:
|
|
251
|
+
"""
|
|
252
|
+
Lines a library adds to a file of the project's, with a label and a place,
|
|
253
|
+
such as a group of CMake lines for a Zephyr application's
|
|
254
|
+
CMakeLists.txt.
|
|
255
|
+
|
|
256
|
+
legacy is a plain list of lines from a manifest written before groups:
|
|
257
|
+
written as it always was, so an update changes nothing.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, label, where, lines, legacy=False):
|
|
261
|
+
self.label = label
|
|
262
|
+
self.where = where
|
|
263
|
+
self.lines = lines
|
|
264
|
+
self.legacy = legacy
|
|
265
|
+
|
|
266
|
+
def __repr__(self):
|
|
267
|
+
return f"Group({self.label} in {self.where})"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _group_lines(value, key, indented=True):
|
|
271
|
+
"""
|
|
272
|
+
The lines of a group: text, one line each, blank ones dropped. Each keeps
|
|
273
|
+
the space it starts with, for the body of an if() or a foreach(), unless
|
|
274
|
+
indented is off, for the plain lists from before groups, written as they
|
|
275
|
+
always were.
|
|
276
|
+
"""
|
|
277
|
+
from .manifest import ManifestError
|
|
278
|
+
|
|
279
|
+
if not isinstance(value, list) or not all(isinstance(line, str) for line in value):
|
|
280
|
+
raise ManifestError(f"{key} is a list of lines, each in quotes when it holds a colon.")
|
|
281
|
+
|
|
282
|
+
if any("\n" in line or "\r" in line for line in value):
|
|
283
|
+
raise ManifestError(f"{key} has a line that runs onto another. Give each line on its own.")
|
|
284
|
+
|
|
285
|
+
return [line.rstrip() if indented else line.strip() for line in value if line.strip()]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _placed_groups(value, key, places, prefix, rooted):
|
|
289
|
+
"""The groups of a mapping of places to their lines, one group a place, in the manifest's order."""
|
|
290
|
+
from . import target
|
|
291
|
+
from .manifest import ManifestError
|
|
292
|
+
|
|
293
|
+
if not rooted:
|
|
294
|
+
raise ManifestError(
|
|
295
|
+
f"{key} gives its lines under the places they go, which is new in 2.1.0: ask for it under "
|
|
296
|
+
f"requires, as {target.current.requires_key}: 2.1.0, or give a list of groups."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
found = []
|
|
300
|
+
|
|
301
|
+
for where, lines in value.items():
|
|
302
|
+
where = str(where)
|
|
303
|
+
|
|
304
|
+
if where not in places:
|
|
305
|
+
raise ManifestError(f"{key} has {where!r}. Give the lines under one of: {', '.join(places)}.")
|
|
306
|
+
|
|
307
|
+
written = _group_lines(lines or [], f"{key}.{where}")
|
|
308
|
+
|
|
309
|
+
if written:
|
|
310
|
+
found.append(Group(f"{prefix}-{where}" if prefix else where, where, written))
|
|
311
|
+
|
|
312
|
+
return found
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def groups(value, key, places, default, legacy=True, prefix=None, rooted=False):
|
|
316
|
+
"""
|
|
317
|
+
The groups of a manifest key such as zephyr.cmake: a list of groups, each
|
|
318
|
+
with a label, where it goes, one of places, default when it does not say,
|
|
319
|
+
and its lines. A plain list of lines, the form from before groups, is one
|
|
320
|
+
group labelled after the key's last part, in default, unless legacy is off.
|
|
321
|
+
|
|
322
|
+
From 2.1.0, rooted, it may be a mapping of places to their lines instead,
|
|
323
|
+
with no label and no where: each place is one group, labelled after it, or
|
|
324
|
+
for an option after the option and it, prefix-place, so an option's group
|
|
325
|
+
never takes the name of the library's.
|
|
326
|
+
"""
|
|
327
|
+
from .manifest import ManifestError
|
|
328
|
+
|
|
329
|
+
if value is None or value == [] or value == {}:
|
|
330
|
+
return []
|
|
331
|
+
|
|
332
|
+
if isinstance(value, dict):
|
|
333
|
+
return _placed_groups(value, key, places, prefix, rooted)
|
|
334
|
+
|
|
335
|
+
if not isinstance(value, list):
|
|
336
|
+
raise ManifestError(f"{key} is a list of groups, each with a label, a where and its lines.")
|
|
337
|
+
|
|
338
|
+
if all(isinstance(item, str) for item in value):
|
|
339
|
+
if not legacy:
|
|
340
|
+
raise ManifestError(f"{key} is a list of groups, each with a label, a where and its lines.")
|
|
341
|
+
|
|
342
|
+
lines = _group_lines(value, key, indented=False)
|
|
343
|
+
|
|
344
|
+
return [Group(key.rsplit(".", 1)[-1], default, lines, legacy=True)] if lines else []
|
|
345
|
+
|
|
346
|
+
if not all(isinstance(item, dict) for item in value):
|
|
347
|
+
raise ManifestError(f"{key} mixes lines and groups. Put every line inside a group.")
|
|
348
|
+
|
|
349
|
+
found = []
|
|
350
|
+
|
|
351
|
+
for item in value:
|
|
352
|
+
label = str(item.get("label") or "")
|
|
353
|
+
|
|
354
|
+
if not LABEL.match(label):
|
|
355
|
+
raise ManifestError(
|
|
356
|
+
f"{key} has a group labelled {label!r}. Label a group in lower case letters, digits, "
|
|
357
|
+
"_ and -, starting with a letter."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
where = str(item.get("where") or default)
|
|
361
|
+
|
|
362
|
+
if where not in places:
|
|
363
|
+
raise ManifestError(f"{key}.{label} goes where {where!r}. Use one of: {', '.join(places)}.")
|
|
364
|
+
|
|
365
|
+
found.append(Group(label, where, _group_lines(item.get("lines") or [], f"{key}.{label}.lines")))
|
|
366
|
+
|
|
367
|
+
names = [group.label for group in found]
|
|
368
|
+
twice = sorted({name for name in names if names.count(name) > 1})
|
|
369
|
+
|
|
370
|
+
if twice:
|
|
371
|
+
raise ManifestError(f"{key} has two groups labelled {', '.join(twice)}. Give each its own label.")
|
|
372
|
+
|
|
373
|
+
return found
|