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.
Files changed (48) hide show
  1. lib_installer/__init__.py +51 -0
  2. lib_installer/__main__.py +8 -0
  3. lib_installer/arduino/__init__.py +1 -0
  4. lib_installer/arduino/arduino.py +334 -0
  5. lib_installer/arduino/properties.py +168 -0
  6. lib_installer/arduino/sketchbook.py +133 -0
  7. lib_installer/cli.py +38 -0
  8. lib_installer/core/__init__.py +8 -0
  9. lib_installer/core/blocks.py +373 -0
  10. lib_installer/core/cli.py +1432 -0
  11. lib_installer/core/console.py +132 -0
  12. lib_installer/core/download.py +781 -0
  13. lib_installer/core/files.py +35 -0
  14. lib_installer/core/installer.py +1222 -0
  15. lib_installer/core/manifest.py +1487 -0
  16. lib_installer/core/steps.py +222 -0
  17. lib_installer/core/target.py +261 -0
  18. lib_installer/core/updates.py +120 -0
  19. lib_installer/core/usercode.py +193 -0
  20. lib_installer/core/yamlreader.py +417 -0
  21. lib_installer/esp32/__init__.py +1 -0
  22. lib_installer/esp32/component.py +354 -0
  23. lib_installer/esp32/esp32.py +491 -0
  24. lib_installer/esp32/project.py +768 -0
  25. lib_installer/pio/__init__.py +1 -0
  26. lib_installer/pio/ini.py +304 -0
  27. lib_installer/pio/library_json.py +161 -0
  28. lib_installer/pio/pio.py +208 -0
  29. lib_installer/stm32/__init__.py +1 -0
  30. lib_installer/stm32/checks.py +204 -0
  31. lib_installer/stm32/ide/__init__.py +110 -0
  32. lib_installer/stm32/ide/base.py +269 -0
  33. lib_installer/stm32/ide/cmake.py +299 -0
  34. lib_installer/stm32/ide/cubeide.py +344 -0
  35. lib_installer/stm32/ide/iar.py +509 -0
  36. lib_installer/stm32/ide/keil.py +419 -0
  37. lib_installer/stm32/ide/makefile.py +376 -0
  38. lib_installer/stm32/stm32.py +101 -0
  39. lib_installer/zephyr/__init__.py +1 -0
  40. lib_installer/zephyr/app.py +648 -0
  41. lib_installer/zephyr/module.py +375 -0
  42. lib_installer/zephyr/zephyr.py +620 -0
  43. lib_installer-3.0.1.dist-info/METADATA +583 -0
  44. lib_installer-3.0.1.dist-info/RECORD +48 -0
  45. lib_installer-3.0.1.dist-info/WHEEL +5 -0
  46. lib_installer-3.0.1.dist-info/entry_points.txt +2 -0
  47. lib_installer-3.0.1.dist-info/licenses/LICENSE +201 -0
  48. 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