micropython-lib/tools/makepyproject.py

214 wiersze
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
#
# This file is part of the MicroPython project, http://micropython.org/
#
# The MIT License (MIT)
#
# Copyright (c) 2023 Jim Mussared
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# This script makes a CPython-compatible package from a micropython-lib package
# with a pyproject.toml that can be built (via hatch) and deployed to PyPI.
# Requires that the project sets the pypi_publish= kwarg in its metadata().
# Usage:
# ./tools/makepyproject.py --output /tmp/foo micropython/foo
# python -m build /tmp/foo
# python -m twine upload /tmp/foo/dist/*.whl
from email.utils import parseaddr
import os
import re
import shutil
import sys
from build import error_color, ensure_path_exists
DEFAULT_AUTHOR = "micropython-lib <contact@micropython.org>"
DEFAULT_LICENSE = "MIT"
def quoted_escape(s):
return s.replace('"', '\\"')
def build(manifest_path, output_path):
import manifestfile
if not manifest_path.endswith(".py"):
# Allow specifying either the directory or the manifest file explicitly.
manifest_path = os.path.join(manifest_path, "manifest.py")
print("Generating pyproject for {} in {}...".format(manifest_path, output_path))
toml_path = os.path.join(output_path, "pyproject.toml")
ensure_path_exists(toml_path)
path_vars = {
"MPY_LIB_DIR": os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
}
# .../foo/manifest.py -> foo
package_name = os.path.basename(os.path.dirname(manifest_path))
# Compile the manifest.
manifest = manifestfile.ManifestFile(manifestfile.MODE_PYPROJECT, path_vars)
manifest.execute(manifest_path)
# If a package doesn't have a pypi name, then assume it isn't intended to
# be publishable.
if not manifest.metadata().pypi_publish:
print(error_color("Error:"), package_name, "doesn't have a pypi_publish name.")
sys.exit(1)
# These should be in all packages eventually.
if not manifest.metadata().version:
print(error_color("Error:"), package_name, "doesn't have a version.")
sys.exit(1)
if not manifest.metadata().description:
print(error_color("Error:"), package_name, "doesn't have a description.")
sys.exit(1)
# This is the root path of all .py files that are copied. We ensure that
# they all match.
top_level_package = None
for result in manifest.files():
# This isn't allowed in micropython-lib anyway.
if result.file_type != manifestfile.FILE_TYPE_LOCAL:
print(error_color("Error:"), "Non-local file not supported.", file=sys.stderr)
sys.exit(1)
# "foo/bar/baz.py" --> "foo"
# "baz.py" --> ""
result_package = os.path.split(result.target_path)[0]
if not result_package:
# This is a standalone .py file.
print(
error_color("Error:"),
"Unsupported single-file module: {}".format(result.target_path),
file=sys.stderr,
)
sys.exit(1)
if top_level_package and result_package != top_level_package:
# This likely suggests that something needs to use require(..., pypi="...").
print(
error_color("Error:"),
"More than one top-level package: {}, {}.".format(
result_package, top_level_package
),
file=sys.stderr,
)
sys.exit(1)
top_level_package = result_package
# Tag each file with the package metadata and copy the .py directly.
with manifestfile.tagged_py_file(result.full_path, result.metadata) as tagged_path:
dest_path = os.path.join(output_path, result.target_path)
ensure_path_exists(dest_path)
shutil.copyfile(tagged_path, dest_path)
# Copy README.md if it exists
readme_path = os.path.join(os.path.dirname(manifest_path), "README.md")
readme_toml = ""
if os.path.exists(readme_path):
shutil.copyfile(readme_path, os.path.join(output_path, "README.md"))
readme_toml = 'readme = "README.md"'
# Apply default author and license, otherwise use the package metadata.
license_toml = 'license = {{ text = "{}" }}'.format(
quoted_escape(manifest.metadata().license or DEFAULT_LICENSE)
)
author_name, author_email = parseaddr(manifest.metadata().author or DEFAULT_AUTHOR)
author_toml = 'authors = [ {{ name = "{}", email = "{}"}} ]'.format(
quoted_escape(author_name), quoted_escape(author_email)
)
# Write pyproject.toml.
with open(toml_path, "w") as toml_file:
print("# Generated by makepyproject.py", file=toml_file)
print(
"""
[build-system]
requires = [
"hatchling"
]
build-backend = "hatchling.build"
""",
file=toml_file,
)
print(
"""
[project]
name = "{}"
description = "{}"
{}
{}
version = "{}"
dependencies = [{}]
urls = {{ Homepage = "https://github.com/micropython/micropython-lib" }}
{}
""".format(
quoted_escape(manifest.metadata().pypi_publish),
quoted_escape(manifest.metadata().description),
author_toml,
license_toml,
quoted_escape(manifest.metadata().version),
", ".join('"{}"'.format(quoted_escape(r)) for r in manifest.pypi_dependencies()),
readme_toml,
),
file=toml_file,
)
print(
"""
[tool.hatch.build]
packages = ["{}"]
""".format(top_level_package),
file=toml_file,
)
print("Done.")
def main():
import argparse
cmd_parser = argparse.ArgumentParser(
description="Generate a project that can be pushed to PyPI."
)
cmd_parser.add_argument("--output", required=True, help="output directory")
cmd_parser.add_argument("--micropython", default=None, help="path to micropython repo")
cmd_parser.add_argument("manifest", help="input package path")
args = cmd_parser.parse_args()
if args.micropython:
sys.path.append(os.path.join(args.micropython, "tools")) # for manifestfile
build(args.manifest, args.output)
if __name__ == "__main__":
main()