esp32-installer 1.0.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.
@@ -0,0 +1,43 @@
1
+ """
2
+ Install embedded C libraries into an ESP-IDF project.
3
+
4
+ Installed with pip, it is the esp32-installer command. It takes a library from
5
+ GitHub, from a downloaded zip, or from a folder, and makes it a component of
6
+ the project: its own CMakeLists.txt and Kconfig, the ESP-IDF components it
7
+ uses, and the settings it needs in sdkconfig.defaults.
8
+
9
+ The work that is the same for any project is the shared core, in core/, from
10
+ github.com/nimaltd/core-installer. What an ESP-IDF project is, is in esp32.py.
11
+
12
+ See https://github.com/nimaltd/esp32-installer
13
+ """
14
+
15
+ __version__ = "1.0.0"
16
+
17
+ # Before anything of the core is used: it asks the target for every answer
18
+ # that depends on the kind of project.
19
+ from .core import target as _target # noqa: E402
20
+ from .esp32 import Esp32 # noqa: E402
21
+
22
+ _target.use(Esp32(__version__))
23
+
24
+ from .core.installer import InstallError, Result, install_in_place, install_to # noqa: E402
25
+ from .core.manifest import Manifest, ManifestError, load # noqa: E402
26
+
27
+ __all__ = [
28
+ "InstallError",
29
+ "Manifest",
30
+ "ManifestError",
31
+ "Result",
32
+ "install_in_place",
33
+ "install_to",
34
+ "load",
35
+ "main",
36
+ ]
37
+
38
+
39
+ def main(argv=None):
40
+ """Run the command line interface. Imported lazily to keep start-up cheap."""
41
+ from .cli import main as _main
42
+
43
+ return _main(argv=argv)
@@ -0,0 +1,8 @@
1
+ """Lets the package be run directly: python -m esp32_installer owner/example"""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
esp32_installer/cli.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ The esp32-installer command: the core's command line, with the ESP-IDF target.
3
+
4
+ The target is chosen when the package is imported, before this runs.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .core.cli import main
10
+
11
+ __all__ = ["main"]
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(main())
@@ -0,0 +1,318 @@
1
+ """
2
+ The library as an ESP-IDF component: its CMakeLists.txt, and a Kconfig for its settings.
3
+
4
+ ESP-IDF builds a folder of the project's components folder when it holds a
5
+ CMakeLists.txt that calls idf_component_register(). A library written for any
6
+ project has none, so it is written here, from installer.yml, into the folder
7
+ the library was installed in. It is written again on every install and update,
8
+ so a source file the new version adds is built without anyone editing anything.
9
+
10
+ A library that ships a CMakeLists.txt of its own knows its build better than
11
+ this does, and is left alone.
12
+
13
+ The component is named after its folder, as ESP-IDF names every component:
14
+ components/ee24 is the component ee24, which other components list in their
15
+ REQUIRES. A folder named like one of ESP-IDF's own components takes its place
16
+ in the build, which the target warns about before the library goes in.
17
+ """
18
+
19
+ import re
20
+ from pathlib import Path
21
+
22
+ from .core import usercode
23
+ from .core.files import backup
24
+ from .core.target import ALREADY, CHANGED, MANUAL, Outcome
25
+
26
+ NAME = "ESP-IDF component"
27
+ CMAKELISTS = "CMakeLists.txt"
28
+ KCONFIG = "Kconfig"
29
+
30
+ # The first line of each file written here, so a file of the library's own is
31
+ # never mistaken for one this wrote.
32
+ MARK = "# Generated by esp32-installer"
33
+
34
+ # How far the lines inside idf_component_register() and Kconfig are indented,
35
+ # the way ESP-IDF's own components write them.
36
+ INDENT = " "
37
+
38
+
39
+ def symbol_of(name):
40
+ """The Kconfig prefix for a library name: EE24 for ee24, ONE_WIRE for one-wire."""
41
+ return re.sub(r"[^A-Za-z0-9]", "_", str(name)).upper()
42
+
43
+
44
+ def component_name(destination):
45
+ """The component's name, which is its folder's: ee24 for components/ee24."""
46
+ return Path(destination).name
47
+
48
+
49
+ def cmake_quote(text):
50
+ """
51
+ Text as one quoted CMake argument.
52
+
53
+ A file name lands in CMakeLists.txt as it stands, so a quote, a dollar or a
54
+ semicolon in it could otherwise end the argument, expand a variable or
55
+ split it into two.
56
+ """
57
+ escaped = str(text).replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace(";", "\\;")
58
+
59
+ return f'"{escaped}"'
60
+
61
+
62
+ def section(library):
63
+ """The name of the USER CODE section in the component's CMakeLists.txt: EXAMPLE_CMAKE."""
64
+ return f"{symbol_of(library.name)}_CMAKE"
65
+
66
+
67
+ def _label(library):
68
+ return f"{library.name} {library.version}".strip()
69
+
70
+
71
+ def _argument(keyword, values):
72
+ """One keyword of idf_component_register() and its values, a line each."""
73
+ return [f"{INDENT}{keyword}"] + [f"{INDENT}{INDENT}{value}" for value in values]
74
+
75
+
76
+ def cmakelists(library, requires):
77
+ """
78
+ The component's CMakeLists.txt: the library's files, and the components it uses.
79
+
80
+ requires is every component the library needs, ESP-IDF's own and the
81
+ libraries it depends on, by the names their folders give them.
82
+ """
83
+ lines = [
84
+ f"{MARK} for {_label(library)}.",
85
+ "#",
86
+ "# ESP-IDF builds this folder as a component, since it sits in the project's",
87
+ "# components folder or in EXTRA_COMPONENT_DIRS. Written again on every install",
88
+ "# and update, so a change made here does not last, except between the USER",
89
+ "# CODE lines at the end. The files come from the library's installer.yml.",
90
+ "",
91
+ "idf_component_register(",
92
+ ]
93
+
94
+ if library.build_sources:
95
+ lines += _argument("SRCS", [cmake_quote(name) for name in library.build_sources])
96
+
97
+ lines += _argument("INCLUDE_DIRS", [cmake_quote(folder) for folder in library.include_dirs])
98
+
99
+ if requires:
100
+ lines += _argument("REQUIRES", requires)
101
+
102
+ lines.append(")")
103
+
104
+ # For every component that uses the library, not only its own files: a
105
+ # define can change what the library's header declares, and every file
106
+ # including it has to agree. A component of headers only is an interface
107
+ # library, and takes nothing but INTERFACE.
108
+ if library.defines:
109
+ scope = "PUBLIC" if library.build_sources else "INTERFACE"
110
+ lines.append("")
111
+ lines.append(f"target_compile_definitions(${{COMPONENT_LIB}} {scope}")
112
+ lines.extend(f"{INDENT}{define}" for define in library.defines)
113
+ lines.append(")")
114
+
115
+ if library.esp32.cmake:
116
+ lines.append("")
117
+ lines.append(f"# From {library.name}'s installer.yml.")
118
+ lines.extend(library.esp32.cmake)
119
+
120
+ # The user's own lines, which every install carries over. Empty to start
121
+ # with, so anything in it on an uninstall is the user's.
122
+ scope = "PRIVATE" if library.build_sources else "INTERFACE"
123
+ lines += [
124
+ "",
125
+ f"# Lines of your own for {library.name}, such as",
126
+ f"# target_compile_options(${{COMPONENT_LIB}} {scope} -O2), go between these",
127
+ "# two, and every install keeps them.",
128
+ f"# USER CODE BEGIN {section(library)}",
129
+ f"# USER CODE END {section(library)}",
130
+ ]
131
+
132
+ return "\n".join(lines) + "\n"
133
+
134
+
135
+ def _help(lines):
136
+ """A Kconfig help block, each line indented under it."""
137
+ out = [f"{INDENT}help"]
138
+ out.extend(f"{INDENT}{INDENT}{line}" if line.strip() else "" for line in lines)
139
+
140
+ return out
141
+
142
+
143
+ def one_line(text):
144
+ """Text for a Kconfig prompt: one line, its quotes and backslashes escaped."""
145
+ flat = " ".join(str(text).split())
146
+
147
+ return flat.replace("\\", "\\\\").replace('"', '\\"')
148
+
149
+
150
+ def kconfig(library):
151
+ """
152
+ The component's Kconfig: a menu of the library's settings in menuconfig.
153
+
154
+ None when the library has no settings. ESP-IDF reads a Kconfig file at the
155
+ top of any component on its own, so nothing else has to name it.
156
+ """
157
+ settings = library.esp32.settings
158
+
159
+ if not settings:
160
+ return None
161
+
162
+ title = library.name + (f": {library.description}" if library.description else "")
163
+ lines = [
164
+ f"{MARK} for {_label(library)}. Written again on",
165
+ "# every install and update, so a change made here does not last.",
166
+ "",
167
+ f'menu "{one_line(title)}"',
168
+ ]
169
+
170
+ for setting in settings:
171
+ lines.append("")
172
+ lines.append(f"{INDENT}config {setting.name}")
173
+ lines.append(f'{INDENT}{INDENT}{setting.type} "{setting.prompt}"')
174
+ lines.append(f"{INDENT}{INDENT}default {setting.default}")
175
+
176
+ if setting.range:
177
+ lines.append(f"{INDENT}{INDENT}range {setting.format(setting.range[0])} "
178
+ f"{setting.format(setting.range[1])}")
179
+
180
+ if setting.help:
181
+ lines.extend(f"{INDENT}{line}" if line else "" for line in _help(setting.help))
182
+
183
+ lines.append("")
184
+ lines.append("endmenu")
185
+
186
+ return "\n".join(lines) + "\n"
187
+
188
+
189
+ def _ours(path):
190
+ """Whether a file is one this wrote, by its first line."""
191
+ try:
192
+ return path.read_text(encoding="utf-8").startswith(MARK)
193
+ except (OSError, UnicodeDecodeError):
194
+ return False
195
+
196
+
197
+ def ships_its_own(destination):
198
+ """Whether the library brought a CMakeLists.txt of its own."""
199
+ path = Path(destination) / CMAKELISTS
200
+
201
+ return path.is_file() and not _ours(path)
202
+
203
+
204
+ def write(library, destination, requires):
205
+ """Write the component's files, each only when it changed. Returns an Outcome."""
206
+ folder = Path(destination)
207
+ name = component_name(destination)
208
+
209
+ if ships_its_own(destination):
210
+ return Outcome(NAME, ALREADY, f"{library.name} has its own CMakeLists.txt, used as it is.")
211
+
212
+ wanted = {CMAKELISTS: cmakelists(library, requires), KCONFIG: kconfig(library)}
213
+ written = []
214
+ removed = []
215
+ steps = []
216
+ saved = None
217
+
218
+ try:
219
+ for file_name, text in wanted.items():
220
+ path = folder / file_name
221
+
222
+ # A Kconfig an earlier version wrote, for settings this one no
223
+ # longer has. One of the library's own is never touched.
224
+ if text is None:
225
+ if path.is_file() and _ours(path):
226
+ path.unlink()
227
+ removed.append(file_name)
228
+
229
+ continue
230
+
231
+ data = text.encode("utf-8")
232
+ old = path.read_bytes() if path.is_file() else None
233
+
234
+ # What the user wrote between the USER CODE lines of the last copy
235
+ # goes into the new one. Markers that no longer pair leave nothing
236
+ # to carry, so the copy is put aside first, whole.
237
+ if old is not None and file_name == CMAKELISTS:
238
+ merged = usercode.merge(data, old)
239
+
240
+ if merged.broken or merged.lost:
241
+ saved = backup(path)
242
+ steps.append(f"Your lines in {file_name} could not be carried over: they are in {saved.name}.")
243
+ else:
244
+ data = merged.data
245
+
246
+ if merged.kept:
247
+ steps.append(f"Kept your USER CODE {', '.join(merged.kept)} in {file_name}.")
248
+
249
+ # Left alone when nothing changed, so the build does not see a
250
+ # newer file and configure again for nothing.
251
+ if old == data:
252
+ continue
253
+
254
+ path.write_bytes(data)
255
+ written.append(file_name)
256
+ except OSError as error:
257
+ return Outcome(NAME, MANUAL, f"Could not write {folder}: {error}")
258
+
259
+ if not written and not removed:
260
+ return Outcome(NAME, ALREADY, f"The component {name} is up to date.", steps)
261
+
262
+ done = [f"wrote {', '.join(written)}"] if written else []
263
+ done += [f"removed its {', '.join(removed)}, as {library.name} has no settings now"] if removed else []
264
+
265
+ return Outcome(NAME, CHANGED, f"The component {name}: {'; '.join(done)}.", steps, saved)
266
+
267
+
268
+ def _holds_yours(path):
269
+ """Whether a CMakeLists.txt write() made has anything between its USER CODE lines."""
270
+ try:
271
+ lines, sections = usercode.parse(path.read_bytes())
272
+ except usercode.MarkerError:
273
+ return True
274
+
275
+ return any(b"".join(lines[first:last]).strip() for first, last in sections.values())
276
+
277
+
278
+ def remove(destination):
279
+ """
280
+ Delete the files write() made. A library's own CMakeLists.txt is not touched.
281
+
282
+ Returns the copy put aside of a CMakeLists.txt that had the user's lines
283
+ in it, or None.
284
+ """
285
+ folder = Path(destination)
286
+ saved = None
287
+
288
+ for file_name in (CMAKELISTS, KCONFIG):
289
+ path = folder / file_name
290
+
291
+ try:
292
+ if not (path.is_file() and _ours(path)):
293
+ continue
294
+
295
+ if file_name == CMAKELISTS and _holds_yours(path):
296
+ saved = backup(path)
297
+
298
+ path.unlink()
299
+ except OSError:
300
+ continue
301
+
302
+ return saved
303
+
304
+
305
+ # A symbol the Kconfig defines: config NAME, or menuconfig NAME.
306
+ _DEFINED = re.compile(r"^[ \t]*(?:menu)?config[ \t]+([A-Za-z0-9_]+)", re.MULTILINE)
307
+
308
+
309
+ def symbols(destination):
310
+ """Every symbol the Kconfig write() made defines, read before it is removed."""
311
+ path = Path(destination) / KCONFIG
312
+
313
+ try:
314
+ text = path.read_text(encoding="utf-8")
315
+ except (OSError, UnicodeDecodeError):
316
+ return []
317
+
318
+ return _DEFINED.findall(text) if text.startswith(MARK) else []
@@ -0,0 +1,12 @@
1
+ """
2
+ The shared core of the installers: stm32-installer, zephyr-installer.
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 installer through target.use().
8
+
9
+ Kept in its own repository, github.com/nimaltd/core-installer, and built into
10
+ each installer's wheel, so an installer still installs from one file with no
11
+ internet.
12
+ """