Skip to content

Commit 9b5f4d7

Browse files
jimmodpgeorge
authored andcommitted
tools/makepyproject.py: Add tool to generate PyPI package.
This tool makes a buildable package (including pyproject.toml) from supported micropython-lib packages, suitable for publishing to PyPI and using from CPython. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent afc9d0a commit 9b5f4d7

File tree

2 files changed

+224
-9
lines changed

2 files changed

+224
-9
lines changed

tools/build.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@
112112

113113
# mip (or other tools) should request /package/{mpy_version}/{package_name}/{version}.json.
114114

115-
import argparse
116115
import glob
117116
import hashlib
118117
import json
@@ -132,7 +131,7 @@
132131

133132

134133
# Create all directories in the path (such that the file can be created).
135-
def _ensure_path_exists(file_path):
134+
def ensure_path_exists(file_path):
136135
path = os.path.dirname(file_path)
137136
if not os.path.isdir(path):
138137
os.makedirs(path)
@@ -155,7 +154,7 @@ def _identical_files(path_a, path_b):
155154
# Helper to write the object as json to the specified path, creating any
156155
# directories as required.
157156
def _write_json(obj, path, minify=False):
158-
_ensure_path_exists(path)
157+
ensure_path_exists(path)
159158
with open(path, "w") as f:
160159
json.dump(
161160
obj, f, indent=(None if minify else 2), separators=((",", ":") if minify else None)
@@ -173,7 +172,7 @@ def _write_package_json(
173172

174173

175174
# Format s with bold red.
176-
def _error_color(s):
175+
def error_color(s):
177176
return _COLOR_ERROR_ON + s + _COLOR_ERROR_OFF
178177

179178

@@ -191,7 +190,7 @@ def _write_hashed_file(package_name, src, target_path, out_file_dir, hash_prefix
191190
# that it's actually the same file.
192191
if not _identical_files(src.name, output_file_path):
193192
print(
194-
_error_color("Hash collision processing:"),
193+
error_color("Hash collision processing:"),
195194
package_name,
196195
file=sys.stderr,
197196
)
@@ -204,7 +203,7 @@ def _write_hashed_file(package_name, src, target_path, out_file_dir, hash_prefix
204203
sys.exit(1)
205204
else:
206205
# Create new file.
207-
_ensure_path_exists(output_file_path)
206+
ensure_path_exists(output_file_path)
208207
shutil.copyfile(src.name, output_file_path)
209208

210209
return short_file_hash
@@ -235,7 +234,7 @@ def _compile_as_mpy(
235234
)
236235
except mpy_cross.CrossCompileError as e:
237236
print(
238-
_error_color("Error:"),
237+
error_color("Error:"),
239238
"Unable to compile",
240239
target_path,
241240
"in package",
@@ -329,7 +328,7 @@ def build(output_path, hash_prefix_len, mpy_cross_path):
329328

330329
# Append this package to the index.
331330
if not manifest.metadata().version:
332-
print(_error_color("Warning:"), package_name, "doesn't have a version.")
331+
print(error_color("Warning:"), package_name, "doesn't have a version.")
333332

334333
# Try to find this package in the previous index.json.
335334
for p in index_json["packages"]:
@@ -360,11 +359,12 @@ def build(output_path, hash_prefix_len, mpy_cross_path):
360359
for result in manifest.files():
361360
# This isn't allowed in micropython-lib anyway.
362361
if result.file_type != manifestfile.FILE_TYPE_LOCAL:
363-
print("Non-local file not supported.", file=sys.stderr)
362+
print(error_color("Error:"), "Non-local file not supported.", file=sys.stderr)
364363
sys.exit(1)
365364

366365
if not result.target_path.endswith(".py"):
367366
print(
367+
error_color("Error:"),
368368
"Target path isn't a .py file:",
369369
result.target_path,
370370
file=sys.stderr,

tools/makepyproject.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
#!/usr/bin/env python3
2+
#
3+
# This file is part of the MicroPython project, http://micropython.org/
4+
#
5+
# The MIT License (MIT)
6+
#
7+
# Copyright (c) 2023 Jim Mussared
8+
#
9+
# Permission is hereby granted, free of charge, to any person obtaining a copy
10+
# of this software and associated documentation files (the "Software"), to deal
11+
# in the Software without restriction, including without limitation the rights
12+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
# copies of the Software, and to permit persons to whom the Software is
14+
# furnished to do so, subject to the following conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be included in
17+
# all copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
# THE SOFTWARE.
26+
27+
# This script makes a CPython-compatible package from a micropython-lib package
28+
# with a pyproject.toml that can be built (via hatch) and deployed to PyPI.
29+
# Requires that the project sets the pypi_publish= kwarg in its metadata().
30+
31+
# Usage:
32+
# ./tools/makepyproject.py --output /tmp/foo micropython/foo
33+
# python -m build /tmp/foo
34+
# python -m twine upload /tmp/foo/dist/*.whl
35+
36+
from email.utils import parseaddr
37+
import os
38+
import re
39+
import shutil
40+
import sys
41+
42+
from build import error_color, ensure_path_exists
43+
44+
45+
DEFAULT_AUTHOR = "micropython-lib <contact@micropython.org>"
46+
DEFAULT_LICENSE = "MIT"
47+
48+
49+
def quoted_escape(s):
50+
return s.replace('"', '\\"')
51+
52+
53+
def build(manifest_path, output_path):
54+
import manifestfile
55+
56+
if not manifest_path.endswith(".py"):
57+
# Allow specifying either the directory or the manifest file explicitly.
58+
manifest_path = os.path.join(manifest_path, "manifest.py")
59+
60+
print("Generating pyproject for {} in {}...".format(manifest_path, output_path))
61+
62+
toml_path = os.path.join(output_path, "pyproject.toml")
63+
ensure_path_exists(toml_path)
64+
65+
path_vars = {
66+
"MPY_LIB_DIR": os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
67+
}
68+
69+
# .../foo/manifest.py -> foo
70+
package_name = os.path.basename(os.path.dirname(manifest_path))
71+
72+
# Compile the manifest.
73+
manifest = manifestfile.ManifestFile(manifestfile.MODE_PYPROJECT, path_vars)
74+
manifest.execute(manifest_path)
75+
76+
# If a package doesn't have a pypi name, then assume it isn't intended to
77+
# be publishable.
78+
if not manifest.metadata().pypi_publish:
79+
print(error_color("Error:"), package_name, "doesn't have a pypi_publish name.")
80+
sys.exit(1)
81+
82+
# These should be in all packages eventually.
83+
if not manifest.metadata().version:
84+
print(error_color("Error:"), package_name, "doesn't have a version.")
85+
sys.exit(1)
86+
if not manifest.metadata().description:
87+
print(error_color("Error:"), package_name, "doesn't have a description.")
88+
sys.exit(1)
89+
90+
# This is the root path of all .py files that are copied. We ensure that
91+
# they all match.
92+
top_level_package = None
93+
94+
for result in manifest.files():
95+
# This isn't allowed in micropython-lib anyway.
96+
if result.file_type != manifestfile.FILE_TYPE_LOCAL:
97+
print(error_color("Error:"), "Non-local file not supported.", file=sys.stderr)
98+
sys.exit(1)
99+
100+
# "foo/bar/baz.py" --> "foo"
101+
# "baz.py" --> ""
102+
result_package = os.path.split(result.target_path)[0]
103+
104+
if not result_package:
105+
# This is a standalone .py file.
106+
print(
107+
error_color("Error:"),
108+
"Unsupported single-file module: {}".format(result.target_path),
109+
file=sys.stderr,
110+
)
111+
sys.exit(1)
112+
if top_level_package and result_package != top_level_package:
113+
# This likely suggests that something needs to use require(..., pypi="...").
114+
print(
115+
error_color("Error:"),
116+
"More than one top-level package: {}, {}.".format(
117+
result_package, top_level_package
118+
),
119+
file=sys.stderr,
120+
)
121+
sys.exit(1)
122+
top_level_package = result_package
123+
124+
# Tag each file with the package metadata and copy the .py directly.
125+
with manifestfile.tagged_py_file(result.full_path, result.metadata) as tagged_path:
126+
dest_path = os.path.join(output_path, result.target_path)
127+
ensure_path_exists(dest_path)
128+
shutil.copyfile(tagged_path, dest_path)
129+
130+
# Copy README.md if it exists
131+
readme_path = os.path.join(os.path.dirname(manifest_path), "README.md")
132+
readme_toml = ""
133+
if os.path.exists(readme_path):
134+
shutil.copyfile(readme_path, os.path.join(output_path, "README.md"))
135+
readme_toml = 'readme = "README.md"'
136+
137+
# Apply default author and license, otherwise use the package metadata.
138+
license_toml = 'license = {{ text = "{}" }}'.format(
139+
quoted_escape(manifest.metadata().license or DEFAULT_LICENSE)
140+
)
141+
author_name, author_email = parseaddr(manifest.metadata().author or DEFAULT_AUTHOR)
142+
author_toml = 'authors = [ {{ name = "{}", email = "{}"}} ]'.format(
143+
quoted_escape(author_name), quoted_escape(author_email)
144+
)
145+
146+
# Write pyproject.toml.
147+
with open(toml_path, "w") as toml_file:
148+
print("# Generated by makepyproject.py", file=toml_file)
149+
150+
print(
151+
"""
152+
[build-system]
153+
requires = [
154+
"hatchling"
155+
]
156+
build-backend = "hatchling.build"
157+
""",
158+
file=toml_file,
159+
)
160+
161+
print(
162+
"""
163+
[project]
164+
name = "{}"
165+
description = "{}"
166+
{}
167+
{}
168+
version = "{}"
169+
dependencies = [{}]
170+
urls = {{ Homepage = "https://github.com/micropython/micropython-lib" }}
171+
{}
172+
""".format(
173+
quoted_escape(manifest.metadata().pypi_publish),
174+
quoted_escape(manifest.metadata().description),
175+
author_toml,
176+
license_toml,
177+
quoted_escape(manifest.metadata().version),
178+
", ".join('"{}"'.format(quoted_escape(r)) for r in manifest.pypi_dependencies()),
179+
readme_toml,
180+
),
181+
file=toml_file,
182+
)
183+
184+
print(
185+
"""
186+
[tool.hatch.build]
187+
packages = ["{}"]
188+
""".format(
189+
top_level_package
190+
),
191+
file=toml_file,
192+
)
193+
194+
print("Done.")
195+
196+
197+
def main():
198+
import argparse
199+
200+
cmd_parser = argparse.ArgumentParser(
201+
description="Generate a project that can be pushed to PyPI."
202+
)
203+
cmd_parser.add_argument("--output", required=True, help="output directory")
204+
cmd_parser.add_argument("--micropython", default=None, help="path to micropython repo")
205+
cmd_parser.add_argument("manifest", help="input package path")
206+
args = cmd_parser.parse_args()
207+
208+
if args.micropython:
209+
sys.path.append(os.path.join(args.micropython, "tools")) # for manifestfile
210+
211+
build(args.manifest, args.output)
212+
213+
214+
if __name__ == "__main__":
215+
main()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy