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.
- esp32_installer/__init__.py +43 -0
- esp32_installer/__main__.py +8 -0
- esp32_installer/cli.py +14 -0
- esp32_installer/component.py +318 -0
- esp32_installer/core/__init__.py +12 -0
- esp32_installer/core/cli.py +1002 -0
- esp32_installer/core/console.py +132 -0
- esp32_installer/core/download.py +531 -0
- esp32_installer/core/files.py +35 -0
- esp32_installer/core/installer.py +865 -0
- esp32_installer/core/manifest.py +804 -0
- esp32_installer/core/target.py +143 -0
- esp32_installer/core/usercode.py +193 -0
- esp32_installer/core/yamlreader.py +402 -0
- esp32_installer/esp32.py +450 -0
- esp32_installer/project.py +669 -0
- esp32_installer-1.0.0.dist-info/METADATA +507 -0
- esp32_installer-1.0.0.dist-info/RECORD +22 -0
- esp32_installer-1.0.0.dist-info/WHEEL +5 -0
- esp32_installer-1.0.0.dist-info/entry_points.txt +2 -0
- esp32_installer-1.0.0.dist-info/licenses/LICENSE +201 -0
- esp32_installer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|
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
|
+
"""
|