Skip to content

Commit d8719e4

Browse files
committed
Get package name from settings.toml, allow arbitrary files in packages
This is more dependable, and when we know the package name we can glob inside it to get all files such as bin or ttf files. This will allow e.g., 5x8.bin & ov5640_autofocus.bin within bundles. the behavior of bundlefly and circup when encountering .bin files needs to be checked. Tested by building modified pycamera bundle and the autofocus.bin file appears in the generated zip files: ``` pycamera-py-ec67bde/lib/adafruit_pycamera/ov5640_autofocus.bin 4077 4096 pycamera-8.x-mpy-ec67bde/lib/adafruit_pycamera/ov5640_autofocus.bin 4077 4096 pycamera-9.x-mpy-ec67bde/lib/adafruit_pycamera/ov5640_autofocus.bin 4077 4096 ``` There's at least one library in the bundle that has incorrect metadata and that leads to an error: adafruit/Adafruit_CircuitPython_Colorsys#29
1 parent 34c259d commit d8719e4

File tree

2 files changed

+122
-139
lines changed

2 files changed

+122
-139
lines changed

circuitpython_build_tools/build.py

+121-139
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,29 @@
3636
import subprocess
3737
import tempfile
3838

39+
if sys.version_info >= (3, 11):
40+
from tomllib import loads as load_toml
41+
else:
42+
from tomli import loads as load_toml
43+
44+
def load_settings_toml(lib_path: pathlib.Path):
45+
try:
46+
return load_toml((lib_path / "pyproject.toml") .read_text(encoding="utf-8"))
47+
except FileNotFoundError:
48+
print(f"No settings.toml in {lib_path}")
49+
return {}
50+
51+
def get_nested(doc, *args, default=None):
52+
for a in args:
53+
if doc is None: return default
54+
try:
55+
doc = doc[a]
56+
except (KeyError, IndexError) as e:
57+
return default
58+
return doc
59+
3960
IGNORE_PY = ["setup.py", "conf.py", "__init__.py"]
40-
GLOB_PATTERNS = ["*.py", "font5x8.bin"]
61+
GLOB_PATTERNS = ["*.py", "*.bin"]
4162
S3_MPY_PREFIX = "https://adafruit-circuit-python.s3.amazonaws.com/bin/mpy-cross"
4263

4364
def version_string(path=None, *, valid_semver=False):
@@ -131,17 +152,13 @@ def mpy_cross(mpy_cross_filename, circuitpython_tag, quiet=False):
131152
shutil.copy("build_deps/circuitpython/mpy-cross/mpy-cross", mpy_cross_filename)
132153

133154
def _munge_to_temp(original_path, temp_file, library_version):
134-
with open(original_path, "rb") as original_file:
155+
with open(original_path, "r", encoding="utf-8") as original_file:
135156
for line in original_file:
136-
if original_path.endswith(".bin"):
137-
# this is solely for adafruit_framebuf/examples/font5x8.bin
138-
temp_file.write(line)
139-
else:
140-
line = line.decode("utf-8").strip("\n")
141-
if line.startswith("__version__"):
142-
line = line.replace("0.0.0-auto.0", library_version)
143-
line = line.replace("0.0.0+auto.0", library_version)
144-
temp_file.write(line.encode("utf-8") + b"\r\n")
157+
line = line.strip("\n")
158+
if line.startswith("__version__"):
159+
line = line.replace("0.0.0-auto.0", library_version)
160+
line = line.replace("0.0.0+auto.0", library_version)
161+
print(line, file=temp_file)
145162
temp_file.flush()
146163

147164
def get_package_info(library_path, package_folder_prefix):
@@ -154,61 +171,46 @@ def get_package_info(library_path, package_folder_prefix):
154171
for pattern in GLOB_PATTERNS:
155172
glob_search.extend(list(lib_path.rglob(pattern)))
156173

157-
package_info["is_package"] = False
158-
for file in glob_search:
159-
if file.parts[parent_idx] != "examples":
160-
if len(file.parts) > parent_idx + 1:
161-
for prefix in package_folder_prefix:
162-
if file.parts[parent_idx].startswith(prefix):
163-
package_info["is_package"] = True
164-
if package_info["is_package"]:
165-
package_files.append(file)
166-
else:
167-
if file.name in IGNORE_PY:
168-
#print("Ignoring:", file.resolve())
169-
continue
170-
if file.parent == lib_path:
171-
py_files.append(file)
172-
173-
if package_files:
174-
package_info["module_name"] = package_files[0].relative_to(library_path).parent.name
175-
elif py_files:
176-
package_info["module_name"] = py_files[0].relative_to(library_path).name[:-3]
177-
else:
178-
package_info["module_name"] = None
179-
180-
try:
181-
package_info["version"] = version_string(library_path, valid_semver=True)
182-
except ValueError as e:
183-
package_info["version"] = version_string(library_path)
184-
185-
return package_info
186-
187-
def library(library_path, output_directory, package_folder_prefix,
188-
mpy_cross=None, example_bundle=False):
189-
py_files = []
190-
package_files = []
191-
example_files = []
192-
total_size = 512
193-
194-
lib_path = pathlib.Path(library_path)
195-
parent_idx = len(lib_path.parts)
196-
glob_search = []
197-
for pattern in GLOB_PATTERNS:
198-
glob_search.extend(list(lib_path.rglob(pattern)))
199-
200-
for file in glob_search:
201-
if file.parts[parent_idx] == "examples":
202-
example_files.append(file)
203-
else:
204-
if not example_bundle:
205-
is_package = False
174+
settings_toml = load_settings_toml(lib_path)
175+
py_modules = get_nested(settings_toml, "tool", "setuptools", "py-modules", default=[])
176+
packages = get_nested(settings_toml, "tool", "setuptools", "packages", default=[])
177+
178+
example_files = [sub_path for sub_path in (lib_path / "examples").rglob("*")
179+
if sub_path.is_file()]
180+
181+
if packages and py_modules:
182+
raise ValueError("Cannot specify both tool.setuptools.py-modules and .packages")
183+
184+
elif packages:
185+
if len(packages) > 1:
186+
raise ValueError("Only a single package is supported")
187+
package_name = packages[0]
188+
print(f"Using package name from settings.toml: {package_name}")
189+
package_info["is_package"] = True
190+
package_info["module_name"] = package_name
191+
package_files = [sub_path for sub_path in (lib_path / package_name).rglob("*")
192+
if sub_path.is_file()]
193+
194+
elif py_modules:
195+
if len(py_modules) > 1:
196+
raise ValueError("Only a single module is supported")
197+
print("Using module name from settings.toml")
198+
py_module = py_modules[0]
199+
package_name = py_module
200+
package_info["is_package"] = False
201+
package_info["module_name"] = py_module
202+
py_files = [lib_path / f"{py_module}.py"]
203+
204+
if not packages and not py_modules:
205+
print("Using legacy autodetection")
206+
package_info["is_package"] = False
207+
for file in glob_search:
208+
if file.parts[parent_idx] != "examples":
206209
if len(file.parts) > parent_idx + 1:
207210
for prefix in package_folder_prefix:
208211
if file.parts[parent_idx].startswith(prefix):
209-
is_package = True
210-
211-
if is_package:
212+
package_info["is_package"] = True
213+
if package_info["is_package"]:
212214
package_files.append(file)
213215
else:
214216
if file.name in IGNORE_PY:
@@ -217,91 +219,78 @@ def library(library_path, output_directory, package_folder_prefix,
217219
if file.parent == lib_path:
218220
py_files.append(file)
219221

222+
if package_files:
223+
package_info["module_name"] = package_files[0].relative_to(library_path).parent.name
224+
elif py_files:
225+
package_info["module_name"] = py_files[0].relative_to(library_path).name[:-3]
226+
else:
227+
package_info["module_name"] = None
228+
220229
if len(py_files) > 1:
221230
raise ValueError("Multiple top level py files not allowed. Please put "
222231
"them in a package or combine them into a single file.")
223232

224-
if package_files:
225-
module_name = package_files[0].relative_to(library_path).parent.name
226-
elif py_files:
227-
module_name = py_files[0].relative_to(library_path).name[:-3]
228-
else:
229-
module_name = None
233+
package_info["package_files"] = package_files
234+
package_info["py_files"] = py_files
235+
package_info["example_files"] = example_files
236+
237+
try:
238+
package_info["version"] = version_string(library_path, valid_semver=True)
239+
except ValueError as e:
240+
print(library_path + " has version that doesn't follow SemVer (semver.org)")
241+
print(e)
242+
package_info["version"] = version_string(library_path)
243+
244+
return package_info
245+
246+
def library(library_path, output_directory, package_folder_prefix,
247+
mpy_cross=None, example_bundle=False):
248+
lib_path = pathlib.Path(library_path)
249+
package_info = get_package_info(library_path, package_folder_prefix)
250+
py_package_files = package_info["package_files"] + package_info["py_files"]
251+
example_files = package_info["example_files"]
252+
module_name = package_info["module_name"]
230253

231254
for fn in example_files:
232255
base_dir = os.path.join(output_directory.replace("/lib", "/"),
233256
fn.relative_to(library_path).parent)
234257
if not os.path.isdir(base_dir):
235258
os.makedirs(base_dir)
236-
total_size += 512
237259

238-
for fn in package_files:
260+
for fn in py_package_files:
239261
base_dir = os.path.join(output_directory,
240262
fn.relative_to(library_path).parent)
241263
if not os.path.isdir(base_dir):
242264
os.makedirs(base_dir)
243-
total_size += 512
244-
245-
new_extension = ".py"
246-
if mpy_cross:
247-
new_extension = ".mpy"
248-
249-
try:
250-
library_version = version_string(library_path, valid_semver=True)
251-
except ValueError as e:
252-
print(library_path + " has version that doesn't follow SemVer (semver.org)")
253-
print(e)
254-
library_version = version_string(library_path)
255265

256-
for filename in py_files:
257-
full_path = os.path.join(library_path, filename)
258-
output_file = os.path.join(
259-
output_directory,
260-
filename.relative_to(library_path).with_suffix(new_extension)
261-
)
262-
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
263-
_munge_to_temp(full_path, temp_file, library_version)
264-
temp_filename = temp_file.name
265-
# Windows: close the temp file before it can be read or copied by name
266-
if mpy_cross:
267-
mpy_success = subprocess.call([
268-
mpy_cross,
269-
"-o", output_file,
270-
"-s", str(filename.relative_to(library_path)),
271-
temp_filename
272-
])
273-
if mpy_success != 0:
274-
raise RuntimeError("mpy-cross failed on", full_path)
275-
else:
276-
shutil.copyfile(temp_filename, output_file)
277-
os.remove(temp_filename)
266+
library_version = package_info['version']
278267

279-
for filename in package_files:
280-
full_path = os.path.join(library_path, filename)
281-
output_file = ""
282-
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
283-
_munge_to_temp(full_path, temp_file, library_version)
284-
temp_filename = temp_file.name
285-
# Windows: close the temp file before it can be read or copied by name
286-
if not mpy_cross or os.stat(full_path).st_size == 0:
287-
output_file = os.path.join(output_directory,
288-
filename.relative_to(library_path))
289-
shutil.copyfile(temp_filename, output_file)
290-
else:
291-
output_file = os.path.join(
292-
output_directory,
293-
filename.relative_to(library_path).with_suffix(new_extension)
294-
)
295-
296-
mpy_success = subprocess.call([
297-
mpy_cross,
298-
"-o", output_file,
299-
"-s", str(filename.relative_to(library_path)),
300-
temp_filename
301-
])
302-
if mpy_success != 0:
303-
raise RuntimeError("mpy-cross failed on", full_path)
304-
os.remove(temp_filename)
268+
if not example_bundle:
269+
for filename in py_package_files:
270+
full_path = os.path.join(library_path, filename)
271+
output_file = output_directory / filename.relative_to(library_path)
272+
if filename.suffix == ".py":
273+
with tempfile.NamedTemporaryFile(delete=False, mode="w+") as temp_file:
274+
temp_file_name = temp_file.name
275+
try:
276+
_munge_to_temp(full_path, temp_file, library_version)
277+
temp_file.close()
278+
if mpy_cross:
279+
output_file = output_file.with_suffix(".mpy")
280+
mpy_success = subprocess.call([
281+
mpy_cross,
282+
"-o", output_file,
283+
"-s", str(filename.relative_to(library_path)),
284+
temp_file.name
285+
])
286+
if mpy_success != 0:
287+
raise RuntimeError("mpy-cross failed on", full_path)
288+
else:
289+
shutil.copyfile(full_path, output_file)
290+
finally:
291+
os.remove(temp_file_name)
292+
else:
293+
shutil.copyfile(full_path, output_file)
305294

306295
requirements_files = lib_path.glob("requirements.txt*")
307296
requirements_files = [f for f in requirements_files if f.stat().st_size > 0]
@@ -314,11 +303,9 @@ def library(library_path, output_directory, package_folder_prefix,
314303
requirements_dir = pathlib.Path(output_directory).parent / "requirements"
315304
if not os.path.isdir(requirements_dir):
316305
os.makedirs(requirements_dir, exist_ok=True)
317-
total_size += 512
318306
requirements_subdir = f"{requirements_dir}/{module_name}"
319307
if not os.path.isdir(requirements_subdir):
320308
os.makedirs(requirements_subdir, exist_ok=True)
321-
total_size += 512
322309
for filename in requirements_files:
323310
full_path = os.path.join(library_path, filename)
324311
output_file = os.path.join(requirements_subdir, filename.name)
@@ -328,9 +315,4 @@ def library(library_path, output_directory, package_folder_prefix,
328315
full_path = os.path.join(library_path, filename)
329316
output_file = os.path.join(output_directory.replace("/lib", "/"),
330317
filename.relative_to(library_path))
331-
temp_filename = ""
332-
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
333-
_munge_to_temp(full_path, temp_file, library_version)
334-
temp_filename = temp_file.name
335-
shutil.copyfile(temp_filename, output_file)
336-
os.remove(temp_filename)
318+
shutil.copyfile(full_path, output_file)

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ Click
22
requests
33
semver
44
wheel
5+
tomli; python_version < "3.11"

0 commit comments

Comments
 (0)