We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
pyodide-build shouldn't display tracebacks for "user error" / expected failures
## 🐛 Bug

pyodide-build has been updated to display rich tracebacks. This is useful only if the error was actually unexpected. In many places `pyodide-build` exits by raising an error which should just print the message and set a nonzero status code but not display some giant traceback message.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pyodide-build/pyodide_build/cli/skeleton.py`
Content:
```
1 # Create or update a package recipe skeleton,
2 # inspired from `conda skeleton` command.
3 
4 from pathlib import Path
5 
6 import typer
7 
8 from .. import common, mkpkg
9 
10 app = typer.Typer()
11 
12 
13 @app.callback(no_args_is_help=True)  # type: ignore[misc]
14 def callback() -> None:
15     """Add a new package build recipe or update an existing recipe"""
16     return
17 
18 
19 @app.command("pypi")  # type: ignore[misc]
20 def new_recipe_pypi(
21     name: str,
22     update: bool = typer.Option(
23         False,
24         "--update",
25         "-u",
26         help="Update an existing recipe instead of creating a new one",
27     ),
28     update_patched: bool = typer.Option(
29         False,
30         "--update-patched",
31         help="Force update the package even if it contains patches.",
32     ),
33     version: str = typer.Option(
34         None,
35         help="The version of the package, if not specified, latest version will be used.",
36     ),
37     source_format: str = typer.Option(
38         None,
39         help="Which source format is preferred. Options are wheel or sdist. "
40         "If not specified, then either a wheel or an sdist will be used. ",
41     ),
42     root: str = typer.Option(
43         None, help="The root directory of the Pyodide.", envvar="PYODIDE_ROOT"
44     ),
45     recipe_dir: str = typer.Option(
46         None,
47         help="The directory containing the recipe of packages."
48         "If not specified, the default is `packages` in the root directory.",
49     ),
50 ) -> None:
51     """
52     Create a new package from PyPI.
53     """
54     pyodide_root = common.search_pyodide_root(Path.cwd()) if not root else Path(root)
55     recipe_dir_ = pyodide_root / "packages" if not recipe_dir else Path(recipe_dir)
56 
57     if update or update_patched:
58         mkpkg.update_package(
59             recipe_dir_,
60             name,
61             version,
62             source_fmt=source_format,  # type: ignore[arg-type]
63             update_patched=update_patched,
64         )
65     else:
66         mkpkg.make_package(recipe_dir_, name, version, source_fmt=source_format)  # type: ignore[arg-type]
67
```

Path: `pyodide-build/pyodide_build/mkpkg.py`
Content:
```
1 #!/usr/bin/env python3
2 
3 import contextlib
4 import json
5 import shutil
6 import subprocess
7 import tempfile
8 import urllib.error
9 import urllib.request
10 import warnings
11 from collections.abc import Iterator
12 from pathlib import Path
13 from typing import Any, Literal, TypedDict
14 from urllib import request
15 
16 from packaging.version import Version
17 from ruamel.yaml import YAML
18 
19 from .common import parse_top_level_import_name
20 from .logger import logger
21 
22 
23 class URLDict(TypedDict):
24     comment_text: str
25     digests: dict[str, Any]
26     downloads: int
27     filename: str
28     has_sig: bool
29     md5_digest: str
30     packagetype: str
31     python_version: str
32     requires_python: str
33     size: int
34     upload_time: str
35     upload_time_iso_8601: str
36     url: str
37     yanked: bool
38     yanked_reason: str | None
39 
40 
41 class MetadataDict(TypedDict):
42     info: dict[str, Any]
43     last_serial: int
44     releases: dict[str, list[dict[str, Any]]]
45     urls: list[URLDict]
46     vulnerabilities: list[Any]
47 
48 
49 class MkpkgFailedException(Exception):
50     pass
51 
52 
53 SDIST_EXTENSIONS = tuple(
54     extension
55     for (name, extensions, description) in shutil.get_unpack_formats()
56     for extension in extensions
57 )
58 
59 
60 def _find_sdist(pypi_metadata: MetadataDict) -> URLDict | None:
61     """Get sdist file path from the metadata"""
62     # The first one we can use. Usually a .tar.gz
63     for entry in pypi_metadata["urls"]:
64         if entry["packagetype"] == "sdist" and entry["filename"].endswith(
65             SDIST_EXTENSIONS
66         ):
67             return entry
68     return None
69 
70 
71 def _find_wheel(pypi_metadata: MetadataDict, native: bool = False) -> URLDict | None:
72     """Get wheel file path from the metadata"""
73     predicate = lambda filename: filename.endswith(
74         ".whl" if native else "py3-none-any.whl"
75     )
76 
77     for entry in pypi_metadata["urls"]:
78         if entry["packagetype"] == "bdist_wheel" and predicate(entry["filename"]):
79             return entry
80     return None
81 
82 
83 def _find_dist(
84     pypi_metadata: MetadataDict, source_types: list[Literal["wheel", "sdist"]]
85 ) -> URLDict:
86     """Find a wheel or sdist, as appropriate.
87 
88     source_types controls which types (wheel and/or sdist) are accepted and also
89     the priority order.
90     E.g., ["wheel", "sdist"] means accept either wheel or sdist but prefer wheel.
91     ["sdist", "wheel"] means accept either wheel or sdist but prefer sdist.
92     """
93     result = None
94     for source in source_types:
95         if source == "wheel":
96             result = _find_wheel(pypi_metadata)
97         if source == "sdist":
98             result = _find_sdist(pypi_metadata)
99         if result:
100             return result
101 
102     types_str = " or ".join(source_types)
103     name = pypi_metadata["info"].get("name")
104     url = pypi_metadata["info"].get("package_url")
105     raise MkpkgFailedException(f"No {types_str} found for package {name} ({url})")
106 
107 
108 def _get_metadata(package: str, version: str | None = None) -> MetadataDict:
109     """Download metadata for a package from PyPI"""
110     version = ("/" + version) if version is not None else ""
111     url = f"https://pypi.org/pypi/{package}{version}/json"
112 
113     try:
114         with urllib.request.urlopen(url) as fd:
115             pypi_metadata = json.load(fd)
116     except urllib.error.HTTPError as e:
117         raise MkpkgFailedException(
118             f"Failed to load metadata for {package}{version} from "
119             f"https://pypi.org/pypi/{package}{version}/json: {e}"
120         ) from e
121 
122     return pypi_metadata
123 
124 
125 @contextlib.contextmanager
126 def _download_wheel(pypi_metadata: URLDict) -> Iterator[Path]:
127     response = request.urlopen(pypi_metadata["url"])
128     whlname = Path(response.geturl()).name
129 
130     with tempfile.TemporaryDirectory() as tmpdirname:
131         whlpath = Path(tmpdirname, whlname)
132         whlpath.write_bytes(response.read())
133         yield whlpath
134 
135 
136 def run_prettier(meta_path: str | Path) -> None:
137     subprocess.run(["npx", "prettier", "-w", meta_path])
138 
139 
140 def make_package(
141     packages_dir: Path,
142     package: str,
143     version: str | None = None,
144     source_fmt: Literal["wheel", "sdist"] | None = None,
145 ) -> None:
146     """
147     Creates a template that will work for most pure Python packages,
148     but will have to be edited for more complex things.
149     """
150     logger.info(f"Creating meta.yaml package for {package}")
151 
152     yaml = YAML()
153 
154     pypi_metadata = _get_metadata(package, version)
155 
156     if source_fmt:
157         sources = [source_fmt]
158     else:
159         # Prefer wheel unless sdist is specifically requested.
160         sources = ["wheel", "sdist"]
161     dist_metadata = _find_dist(pypi_metadata, sources)
162 
163     native_wheel_metadata = _find_wheel(pypi_metadata, native=True)
164 
165     top_level = None
166     if native_wheel_metadata is not None:
167         with _download_wheel(native_wheel_metadata) as native_wheel_path:
168             top_level = parse_top_level_import_name(native_wheel_path)
169 
170     url = dist_metadata["url"]
171     sha256 = dist_metadata["digests"]["sha256"]
172     version = pypi_metadata["info"]["version"]
173 
174     homepage = pypi_metadata["info"]["home_page"]
175     summary = pypi_metadata["info"]["summary"]
176     license = pypi_metadata["info"]["license"]
177     pypi = "https://pypi.org/project/" + package
178 
179     yaml_content = {
180         "package": {
181             "name": package,
182             "version": version,
183             "top-level": top_level or ["PUT_TOP_LEVEL_IMPORT_NAMES_HERE"],
184         },
185         "source": {"url": url, "sha256": sha256},
186         "about": {
187             "home": homepage,
188             "PyPI": pypi,
189             "summary": summary,
190             "license": license,
191         },
192     }
193 
194     package_dir = packages_dir / package
195     package_dir.mkdir(parents=True, exist_ok=True)
196 
197     meta_path = package_dir / "meta.yaml"
198     if meta_path.exists():
199         raise MkpkgFailedException(f"The package {package} already exists")
200 
201     yaml.representer.ignore_aliases = lambda *_: True
202     yaml.dump(yaml_content, meta_path)
203     try:
204         run_prettier(meta_path)
205     except FileNotFoundError:
206         warnings.warn("'npx' executable missing, output has not been prettified.")
207 
208     logger.success(f"Output written to {meta_path}")
209 
210 
211 def update_package(
212     root: Path,
213     package: str,
214     version: str | None = None,
215     update_patched: bool = True,
216     source_fmt: Literal["wheel", "sdist"] | None = None,
217 ) -> None:
218     yaml = YAML()
219 
220     meta_path = root / package / "meta.yaml"
221     if not meta_path.exists():
222         logger.error(f"{meta_path} does not exist")
223         exit(1)
224 
225     yaml_content = yaml.load(meta_path.read_bytes())
226 
227     if "url" not in yaml_content["source"]:
228         raise MkpkgFailedException(f"Skipping: {package} is a local package!")
229 
230     build_info = yaml_content.get("build", {})
231     if build_info.get("library", False) or build_info.get("sharedlibrary", False):
232         raise MkpkgFailedException(f"Skipping: {package} is a library!")
233 
234     if yaml_content["source"]["url"].endswith("whl"):
235         old_fmt = "wheel"
236     else:
237         old_fmt = "sdist"
238 
239     pypi_metadata = _get_metadata(package, version)
240     pypi_ver = Version(pypi_metadata["info"]["version"])
241     local_ver = Version(yaml_content["package"]["version"])
242     already_up_to_date = pypi_ver <= local_ver and (
243         source_fmt is None or source_fmt == old_fmt
244     )
245     if already_up_to_date:
246         logger.success(
247             f"{package} already up to date. Local: {local_ver} PyPI: {pypi_ver}"
248         )
249         return
250 
251     logger.info(f"{package} is out of date: {local_ver} <= {pypi_ver}.")
252 
253     if yaml_content["source"].get("patches"):
254         if update_patched:
255             logger.warning(
256                 f"Pyodide applies patches to {package}. Update the "
257                 "patches (if needed) to avoid build failing."
258             )
259         else:
260             raise MkpkgFailedException(
261                 f"Pyodide applies patches to {package}. Skipping update."
262             )
263 
264     if source_fmt:
265         # require the type requested
266         sources = [source_fmt]
267     elif old_fmt == "wheel":
268         # prefer wheel to sdist
269         sources = ["wheel", "sdist"]
270     else:
271         # prefer sdist to wheel
272         sources = ["sdist", "wheel"]
273 
274     dist_metadata = _find_dist(pypi_metadata, sources)
275 
276     yaml_content["source"]["url"] = dist_metadata["url"]
277     yaml_content["source"].pop("md5", None)
278     yaml_content["source"]["sha256"] = dist_metadata["digests"]["sha256"]
279     yaml_content["package"]["version"] = pypi_metadata["info"]["version"]
280 
281     yaml.dump(yaml_content, meta_path)
282     run_prettier(meta_path)
283 
284     logger.success(f"Updated {package} from {local_ver} to {pypi_ver}.")
285
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pyodide-build/pyodide_build/cli/skeleton.py b/pyodide-build/pyodide_build/cli/skeleton.py
--- a/pyodide-build/pyodide_build/cli/skeleton.py
+++ b/pyodide-build/pyodide_build/cli/skeleton.py
@@ -1,11 +1,13 @@
 # Create or update a package recipe skeleton,
 # inspired from `conda skeleton` command.
 
+import sys
 from pathlib import Path
 
 import typer
 
 from .. import common, mkpkg
+from ..logger import logger
 
 app = typer.Typer()
 
@@ -55,12 +57,21 @@
     recipe_dir_ = pyodide_root / "packages" if not recipe_dir else Path(recipe_dir)
 
     if update or update_patched:
-        mkpkg.update_package(
-            recipe_dir_,
-            name,
-            version,
-            source_fmt=source_format,  # type: ignore[arg-type]
-            update_patched=update_patched,
-        )
+        try:
+            mkpkg.update_package(
+                recipe_dir_,
+                name,
+                version,
+                source_fmt=source_format,  # type: ignore[arg-type]
+                update_patched=update_patched,
+            )
+        except mkpkg.MkpkgFailedException as e:
+            logger.error(f"{name} update failed: {e}")
+            sys.exit(1)
+        except mkpkg.MkpkgSkipped as e:
+            logger.warn(f"{name} update skipped: {e}")
+        except Exception:
+            print(name)
+            raise
     else:
         mkpkg.make_package(recipe_dir_, name, version, source_fmt=source_format)  # type: ignore[arg-type]
diff --git a/pyodide-build/pyodide_build/mkpkg.py b/pyodide-build/pyodide_build/mkpkg.py
--- a/pyodide-build/pyodide_build/mkpkg.py
+++ b/pyodide-build/pyodide_build/mkpkg.py
@@ -46,6 +46,10 @@
     vulnerabilities: list[Any]
 
 
+class MkpkgSkipped(Exception):
+    pass
+
+
 class MkpkgFailedException(Exception):
     pass
 
@@ -224,12 +228,13 @@
 
     yaml_content = yaml.load(meta_path.read_bytes())
 
-    if "url" not in yaml_content["source"]:
-        raise MkpkgFailedException(f"Skipping: {package} is a local package!")
-
     build_info = yaml_content.get("build", {})
-    if build_info.get("library", False) or build_info.get("sharedlibrary", False):
-        raise MkpkgFailedException(f"Skipping: {package} is a library!")
+    ty = build_info.get("type", None)
+    if ty in ["static_library", "shared_library", "cpython_module"]:
+        raise MkpkgSkipped(f"{package} is a {ty.replace('_', ' ')}!")
+
+    if "url" not in yaml_content["source"]:
+        raise MkpkgSkipped(f"{package} is a local package!")
 
     if yaml_content["source"]["url"].endswith("whl"):
         old_fmt = "wheel"

{"golden_diff": "diff --git a/pyodide-build/pyodide_build/cli/skeleton.py b/pyodide-build/pyodide_build/cli/skeleton.py\n--- a/pyodide-build/pyodide_build/cli/skeleton.py\n+++ b/pyodide-build/pyodide_build/cli/skeleton.py\n@@ -1,11 +1,13 @@\n # Create or update a package recipe skeleton,\n # inspired from `conda skeleton` command.\n \n+import sys\n from pathlib import Path\n \n import typer\n \n from .. import common, mkpkg\n+from ..logger import logger\n \n app = typer.Typer()\n \n@@ -55,12 +57,21 @@\n     recipe_dir_ = pyodide_root / \"packages\" if not recipe_dir else Path(recipe_dir)\n \n     if update or update_patched:\n-        mkpkg.update_package(\n-            recipe_dir_,\n-            name,\n-            version,\n-            source_fmt=source_format,  # type: ignore[arg-type]\n-            update_patched=update_patched,\n-        )\n+        try:\n+            mkpkg.update_package(\n+                recipe_dir_,\n+                name,\n+                version,\n+                source_fmt=source_format,  # type: ignore[arg-type]\n+                update_patched=update_patched,\n+            )\n+        except mkpkg.MkpkgFailedException as e:\n+            logger.error(f\"{name} update failed: {e}\")\n+            sys.exit(1)\n+        except mkpkg.MkpkgSkipped as e:\n+            logger.warn(f\"{name} update skipped: {e}\")\n+        except Exception:\n+            print(name)\n+            raise\n     else:\n         mkpkg.make_package(recipe_dir_, name, version, source_fmt=source_format)  # type: ignore[arg-type]\ndiff --git a/pyodide-build/pyodide_build/mkpkg.py b/pyodide-build/pyodide_build/mkpkg.py\n--- a/pyodide-build/pyodide_build/mkpkg.py\n+++ b/pyodide-build/pyodide_build/mkpkg.py\n@@ -46,6 +46,10 @@\n     vulnerabilities: list[Any]\n \n \n+class MkpkgSkipped(Exception):\n+    pass\n+\n+\n class MkpkgFailedException(Exception):\n     pass\n \n@@ -224,12 +228,13 @@\n \n     yaml_content = yaml.load(meta_path.read_bytes())\n \n-    if \"url\" not in yaml_content[\"source\"]:\n-        raise MkpkgFailedException(f\"Skipping: {package} is a local package!\")\n-\n     build_info = yaml_content.get(\"build\", {})\n-    if build_info.get(\"library\", False) or build_info.get(\"sharedlibrary\", False):\n-        raise MkpkgFailedException(f\"Skipping: {package} is a library!\")\n+    ty = build_info.get(\"type\", None)\n+    if ty in [\"static_library\", \"shared_library\", \"cpython_module\"]:\n+        raise MkpkgSkipped(f\"{package} is a {ty.replace('_', ' ')}!\")\n+\n+    if \"url\" not in yaml_content[\"source\"]:\n+        raise MkpkgSkipped(f\"{package} is a local package!\")\n \n     if yaml_content[\"source\"][\"url\"].endswith(\"whl\"):\n         old_fmt = \"wheel\"\n", "issue": "pyodide-build shouldn't display tracebacks for \"user error\" / expected failures\n## \ud83d\udc1b Bug\r\n\r\npyodide-build has been updated to display rich tracebacks. This is useful only if the error was actually unexpected. In many places `pyodide-build` exits by raising an error which should just print the message and set a nonzero status code but not display some giant traceback message.\n", "before_files": [{"content": "# Create or update a package recipe skeleton,\n# inspired from `conda skeleton` command.\n\nfrom pathlib import Path\n\nimport typer\n\nfrom .. import common, mkpkg\n\napp = typer.Typer()\n\n\n@app.callback(no_args_is_help=True)  # type: ignore[misc]\ndef callback() -> None:\n    \"\"\"Add a new package build recipe or update an existing recipe\"\"\"\n    return\n\n\n@app.command(\"pypi\")  # type: ignore[misc]\ndef new_recipe_pypi(\n    name: str,\n    update: bool = typer.Option(\n        False,\n        \"--update\",\n        \"-u\",\n        help=\"Update an existing recipe instead of creating a new one\",\n    ),\n    update_patched: bool = typer.Option(\n        False,\n        \"--update-patched\",\n        help=\"Force update the package even if it contains patches.\",\n    ),\n    version: str = typer.Option(\n        None,\n        help=\"The version of the package, if not specified, latest version will be used.\",\n    ),\n    source_format: str = typer.Option(\n        None,\n        help=\"Which source format is preferred. Options are wheel or sdist. \"\n        \"If not specified, then either a wheel or an sdist will be used. \",\n    ),\n    root: str = typer.Option(\n        None, help=\"The root directory of the Pyodide.\", envvar=\"PYODIDE_ROOT\"\n    ),\n    recipe_dir: str = typer.Option(\n        None,\n        help=\"The directory containing the recipe of packages.\"\n        \"If not specified, the default is `packages` in the root directory.\",\n    ),\n) -> None:\n    \"\"\"\n    Create a new package from PyPI.\n    \"\"\"\n    pyodide_root = common.search_pyodide_root(Path.cwd()) if not root else Path(root)\n    recipe_dir_ = pyodide_root / \"packages\" if not recipe_dir else Path(recipe_dir)\n\n    if update or update_patched:\n        mkpkg.update_package(\n            recipe_dir_,\n            name,\n            version,\n            source_fmt=source_format,  # type: ignore[arg-type]\n            update_patched=update_patched,\n        )\n    else:\n        mkpkg.make_package(recipe_dir_, name, version, source_fmt=source_format)  # type: ignore[arg-type]\n", "path": "pyodide-build/pyodide_build/cli/skeleton.py"}, {"content": "#!/usr/bin/env python3\n\nimport contextlib\nimport json\nimport shutil\nimport subprocess\nimport tempfile\nimport urllib.error\nimport urllib.request\nimport warnings\nfrom collections.abc import Iterator\nfrom pathlib import Path\nfrom typing import Any, Literal, TypedDict\nfrom urllib import request\n\nfrom packaging.version import Version\nfrom ruamel.yaml import YAML\n\nfrom .common import parse_top_level_import_name\nfrom .logger import logger\n\n\nclass URLDict(TypedDict):\n    comment_text: str\n    digests: dict[str, Any]\n    downloads: int\n    filename: str\n    has_sig: bool\n    md5_digest: str\n    packagetype: str\n    python_version: str\n    requires_python: str\n    size: int\n    upload_time: str\n    upload_time_iso_8601: str\n    url: str\n    yanked: bool\n    yanked_reason: str | None\n\n\nclass MetadataDict(TypedDict):\n    info: dict[str, Any]\n    last_serial: int\n    releases: dict[str, list[dict[str, Any]]]\n    urls: list[URLDict]\n    vulnerabilities: list[Any]\n\n\nclass MkpkgFailedException(Exception):\n    pass\n\n\nSDIST_EXTENSIONS = tuple(\n    extension\n    for (name, extensions, description) in shutil.get_unpack_formats()\n    for extension in extensions\n)\n\n\ndef _find_sdist(pypi_metadata: MetadataDict) -> URLDict | None:\n    \"\"\"Get sdist file path from the metadata\"\"\"\n    # The first one we can use. Usually a .tar.gz\n    for entry in pypi_metadata[\"urls\"]:\n        if entry[\"packagetype\"] == \"sdist\" and entry[\"filename\"].endswith(\n            SDIST_EXTENSIONS\n        ):\n            return entry\n    return None\n\n\ndef _find_wheel(pypi_metadata: MetadataDict, native: bool = False) -> URLDict | None:\n    \"\"\"Get wheel file path from the metadata\"\"\"\n    predicate = lambda filename: filename.endswith(\n        \".whl\" if native else \"py3-none-any.whl\"\n    )\n\n    for entry in pypi_metadata[\"urls\"]:\n        if entry[\"packagetype\"] == \"bdist_wheel\" and predicate(entry[\"filename\"]):\n            return entry\n    return None\n\n\ndef _find_dist(\n    pypi_metadata: MetadataDict, source_types: list[Literal[\"wheel\", \"sdist\"]]\n) -> URLDict:\n    \"\"\"Find a wheel or sdist, as appropriate.\n\n    source_types controls which types (wheel and/or sdist) are accepted and also\n    the priority order.\n    E.g., [\"wheel\", \"sdist\"] means accept either wheel or sdist but prefer wheel.\n    [\"sdist\", \"wheel\"] means accept either wheel or sdist but prefer sdist.\n    \"\"\"\n    result = None\n    for source in source_types:\n        if source == \"wheel\":\n            result = _find_wheel(pypi_metadata)\n        if source == \"sdist\":\n            result = _find_sdist(pypi_metadata)\n        if result:\n            return result\n\n    types_str = \" or \".join(source_types)\n    name = pypi_metadata[\"info\"].get(\"name\")\n    url = pypi_metadata[\"info\"].get(\"package_url\")\n    raise MkpkgFailedException(f\"No {types_str} found for package {name} ({url})\")\n\n\ndef _get_metadata(package: str, version: str | None = None) -> MetadataDict:\n    \"\"\"Download metadata for a package from PyPI\"\"\"\n    version = (\"/\" + version) if version is not None else \"\"\n    url = f\"https://pypi.org/pypi/{package}{version}/json\"\n\n    try:\n        with urllib.request.urlopen(url) as fd:\n            pypi_metadata = json.load(fd)\n    except urllib.error.HTTPError as e:\n        raise MkpkgFailedException(\n            f\"Failed to load metadata for {package}{version} from \"\n            f\"https://pypi.org/pypi/{package}{version}/json: {e}\"\n        ) from e\n\n    return pypi_metadata\n\n\n@contextlib.contextmanager\ndef _download_wheel(pypi_metadata: URLDict) -> Iterator[Path]:\n    response = request.urlopen(pypi_metadata[\"url\"])\n    whlname = Path(response.geturl()).name\n\n    with tempfile.TemporaryDirectory() as tmpdirname:\n        whlpath = Path(tmpdirname, whlname)\n        whlpath.write_bytes(response.read())\n        yield whlpath\n\n\ndef run_prettier(meta_path: str | Path) -> None:\n    subprocess.run([\"npx\", \"prettier\", \"-w\", meta_path])\n\n\ndef make_package(\n    packages_dir: Path,\n    package: str,\n    version: str | None = None,\n    source_fmt: Literal[\"wheel\", \"sdist\"] | None = None,\n) -> None:\n    \"\"\"\n    Creates a template that will work for most pure Python packages,\n    but will have to be edited for more complex things.\n    \"\"\"\n    logger.info(f\"Creating meta.yaml package for {package}\")\n\n    yaml = YAML()\n\n    pypi_metadata = _get_metadata(package, version)\n\n    if source_fmt:\n        sources = [source_fmt]\n    else:\n        # Prefer wheel unless sdist is specifically requested.\n        sources = [\"wheel\", \"sdist\"]\n    dist_metadata = _find_dist(pypi_metadata, sources)\n\n    native_wheel_metadata = _find_wheel(pypi_metadata, native=True)\n\n    top_level = None\n    if native_wheel_metadata is not None:\n        with _download_wheel(native_wheel_metadata) as native_wheel_path:\n            top_level = parse_top_level_import_name(native_wheel_path)\n\n    url = dist_metadata[\"url\"]\n    sha256 = dist_metadata[\"digests\"][\"sha256\"]\n    version = pypi_metadata[\"info\"][\"version\"]\n\n    homepage = pypi_metadata[\"info\"][\"home_page\"]\n    summary = pypi_metadata[\"info\"][\"summary\"]\n    license = pypi_metadata[\"info\"][\"license\"]\n    pypi = \"https://pypi.org/project/\" + package\n\n    yaml_content = {\n        \"package\": {\n            \"name\": package,\n            \"version\": version,\n            \"top-level\": top_level or [\"PUT_TOP_LEVEL_IMPORT_NAMES_HERE\"],\n        },\n        \"source\": {\"url\": url, \"sha256\": sha256},\n        \"about\": {\n            \"home\": homepage,\n            \"PyPI\": pypi,\n            \"summary\": summary,\n            \"license\": license,\n        },\n    }\n\n    package_dir = packages_dir / package\n    package_dir.mkdir(parents=True, exist_ok=True)\n\n    meta_path = package_dir / \"meta.yaml\"\n    if meta_path.exists():\n        raise MkpkgFailedException(f\"The package {package} already exists\")\n\n    yaml.representer.ignore_aliases = lambda *_: True\n    yaml.dump(yaml_content, meta_path)\n    try:\n        run_prettier(meta_path)\n    except FileNotFoundError:\n        warnings.warn(\"'npx' executable missing, output has not been prettified.\")\n\n    logger.success(f\"Output written to {meta_path}\")\n\n\ndef update_package(\n    root: Path,\n    package: str,\n    version: str | None = None,\n    update_patched: bool = True,\n    source_fmt: Literal[\"wheel\", \"sdist\"] | None = None,\n) -> None:\n    yaml = YAML()\n\n    meta_path = root / package / \"meta.yaml\"\n    if not meta_path.exists():\n        logger.error(f\"{meta_path} does not exist\")\n        exit(1)\n\n    yaml_content = yaml.load(meta_path.read_bytes())\n\n    if \"url\" not in yaml_content[\"source\"]:\n        raise MkpkgFailedException(f\"Skipping: {package} is a local package!\")\n\n    build_info = yaml_content.get(\"build\", {})\n    if build_info.get(\"library\", False) or build_info.get(\"sharedlibrary\", False):\n        raise MkpkgFailedException(f\"Skipping: {package} is a library!\")\n\n    if yaml_content[\"source\"][\"url\"].endswith(\"whl\"):\n        old_fmt = \"wheel\"\n    else:\n        old_fmt = \"sdist\"\n\n    pypi_metadata = _get_metadata(package, version)\n    pypi_ver = Version(pypi_metadata[\"info\"][\"version\"])\n    local_ver = Version(yaml_content[\"package\"][\"version\"])\n    already_up_to_date = pypi_ver <= local_ver and (\n        source_fmt is None or source_fmt == old_fmt\n    )\n    if already_up_to_date:\n        logger.success(\n            f\"{package} already up to date. Local: {local_ver} PyPI: {pypi_ver}\"\n        )\n        return\n\n    logger.info(f\"{package} is out of date: {local_ver} <= {pypi_ver}.\")\n\n    if yaml_content[\"source\"].get(\"patches\"):\n        if update_patched:\n            logger.warning(\n                f\"Pyodide applies patches to {package}. Update the \"\n                \"patches (if needed) to avoid build failing.\"\n            )\n        else:\n            raise MkpkgFailedException(\n                f\"Pyodide applies patches to {package}. Skipping update.\"\n            )\n\n    if source_fmt:\n        # require the type requested\n        sources = [source_fmt]\n    elif old_fmt == \"wheel\":\n        # prefer wheel to sdist\n        sources = [\"wheel\", \"sdist\"]\n    else:\n        # prefer sdist to wheel\n        sources = [\"sdist\", \"wheel\"]\n\n    dist_metadata = _find_dist(pypi_metadata, sources)\n\n    yaml_content[\"source\"][\"url\"] = dist_metadata[\"url\"]\n    yaml_content[\"source\"].pop(\"md5\", None)\n    yaml_content[\"source\"][\"sha256\"] = dist_metadata[\"digests\"][\"sha256\"]\n    yaml_content[\"package\"][\"version\"] = pypi_metadata[\"info\"][\"version\"]\n\n    yaml.dump(yaml_content, meta_path)\n    run_prettier(meta_path)\n\n    logger.success(f\"Updated {package} from {local_ver} to {pypi_ver}.\")\n", "path": "pyodide-build/pyodide_build/mkpkg.py"}], "after_files": [{"content": "# Create or update a package recipe skeleton,\n# inspired from `conda skeleton` command.\n\nimport sys\nfrom pathlib import Path\n\nimport typer\n\nfrom .. import common, mkpkg\nfrom ..logger import logger\n\napp = typer.Typer()\n\n\n@app.callback(no_args_is_help=True)  # type: ignore[misc]\ndef callback() -> None:\n    \"\"\"Add a new package build recipe or update an existing recipe\"\"\"\n    return\n\n\n@app.command(\"pypi\")  # type: ignore[misc]\ndef new_recipe_pypi(\n    name: str,\n    update: bool = typer.Option(\n        False,\n        \"--update\",\n        \"-u\",\n        help=\"Update an existing recipe instead of creating a new one\",\n    ),\n    update_patched: bool = typer.Option(\n        False,\n        \"--update-patched\",\n        help=\"Force update the package even if it contains patches.\",\n    ),\n    version: str = typer.Option(\n        None,\n        help=\"The version of the package, if not specified, latest version will be used.\",\n    ),\n    source_format: str = typer.Option(\n        None,\n        help=\"Which source format is preferred. Options are wheel or sdist. \"\n        \"If not specified, then either a wheel or an sdist will be used. \",\n    ),\n    root: str = typer.Option(\n        None, help=\"The root directory of the Pyodide.\", envvar=\"PYODIDE_ROOT\"\n    ),\n    recipe_dir: str = typer.Option(\n        None,\n        help=\"The directory containing the recipe of packages.\"\n        \"If not specified, the default is `packages` in the root directory.\",\n    ),\n) -> None:\n    \"\"\"\n    Create a new package from PyPI.\n    \"\"\"\n    pyodide_root = common.search_pyodide_root(Path.cwd()) if not root else Path(root)\n    recipe_dir_ = pyodide_root / \"packages\" if not recipe_dir else Path(recipe_dir)\n\n    if update or update_patched:\n        try:\n            mkpkg.update_package(\n                recipe_dir_,\n                name,\n                version,\n                source_fmt=source_format,  # type: ignore[arg-type]\n                update_patched=update_patched,\n            )\n        except mkpkg.MkpkgFailedException as e:\n            logger.error(f\"{name} update failed: {e}\")\n            sys.exit(1)\n        except mkpkg.MkpkgSkipped as e:\n            logger.warn(f\"{name} update skipped: {e}\")\n        except Exception:\n            print(name)\n            raise\n    else:\n        mkpkg.make_package(recipe_dir_, name, version, source_fmt=source_format)  # type: ignore[arg-type]\n", "path": "pyodide-build/pyodide_build/cli/skeleton.py"}, {"content": "#!/usr/bin/env python3\n\nimport contextlib\nimport json\nimport shutil\nimport subprocess\nimport tempfile\nimport urllib.error\nimport urllib.request\nimport warnings\nfrom collections.abc import Iterator\nfrom pathlib import Path\nfrom typing import Any, Literal, TypedDict\nfrom urllib import request\n\nfrom packaging.version import Version\nfrom ruamel.yaml import YAML\n\nfrom .common import parse_top_level_import_name\nfrom .logger import logger\n\n\nclass URLDict(TypedDict):\n    comment_text: str\n    digests: dict[str, Any]\n    downloads: int\n    filename: str\n    has_sig: bool\n    md5_digest: str\n    packagetype: str\n    python_version: str\n    requires_python: str\n    size: int\n    upload_time: str\n    upload_time_iso_8601: str\n    url: str\n    yanked: bool\n    yanked_reason: str | None\n\n\nclass MetadataDict(TypedDict):\n    info: dict[str, Any]\n    last_serial: int\n    releases: dict[str, list[dict[str, Any]]]\n    urls: list[URLDict]\n    vulnerabilities: list[Any]\n\n\nclass MkpkgSkipped(Exception):\n    pass\n\n\nclass MkpkgFailedException(Exception):\n    pass\n\n\nSDIST_EXTENSIONS = tuple(\n    extension\n    for (name, extensions, description) in shutil.get_unpack_formats()\n    for extension in extensions\n)\n\n\ndef _find_sdist(pypi_metadata: MetadataDict) -> URLDict | None:\n    \"\"\"Get sdist file path from the metadata\"\"\"\n    # The first one we can use. Usually a .tar.gz\n    for entry in pypi_metadata[\"urls\"]:\n        if entry[\"packagetype\"] == \"sdist\" and entry[\"filename\"].endswith(\n            SDIST_EXTENSIONS\n        ):\n            return entry\n    return None\n\n\ndef _find_wheel(pypi_metadata: MetadataDict, native: bool = False) -> URLDict | None:\n    \"\"\"Get wheel file path from the metadata\"\"\"\n    predicate = lambda filename: filename.endswith(\n        \".whl\" if native else \"py3-none-any.whl\"\n    )\n\n    for entry in pypi_metadata[\"urls\"]:\n        if entry[\"packagetype\"] == \"bdist_wheel\" and predicate(entry[\"filename\"]):\n            return entry\n    return None\n\n\ndef _find_dist(\n    pypi_metadata: MetadataDict, source_types: list[Literal[\"wheel\", \"sdist\"]]\n) -> URLDict:\n    \"\"\"Find a wheel or sdist, as appropriate.\n\n    source_types controls which types (wheel and/or sdist) are accepted and also\n    the priority order.\n    E.g., [\"wheel\", \"sdist\"] means accept either wheel or sdist but prefer wheel.\n    [\"sdist\", \"wheel\"] means accept either wheel or sdist but prefer sdist.\n    \"\"\"\n    result = None\n    for source in source_types:\n        if source == \"wheel\":\n            result = _find_wheel(pypi_metadata)\n        if source == \"sdist\":\n            result = _find_sdist(pypi_metadata)\n        if result:\n            return result\n\n    types_str = \" or \".join(source_types)\n    name = pypi_metadata[\"info\"].get(\"name\")\n    url = pypi_metadata[\"info\"].get(\"package_url\")\n    raise MkpkgFailedException(f\"No {types_str} found for package {name} ({url})\")\n\n\ndef _get_metadata(package: str, version: str | None = None) -> MetadataDict:\n    \"\"\"Download metadata for a package from PyPI\"\"\"\n    version = (\"/\" + version) if version is not None else \"\"\n    url = f\"https://pypi.org/pypi/{package}{version}/json\"\n\n    try:\n        with urllib.request.urlopen(url) as fd:\n            pypi_metadata = json.load(fd)\n    except urllib.error.HTTPError as e:\n        raise MkpkgFailedException(\n            f\"Failed to load metadata for {package}{version} from \"\n            f\"https://pypi.org/pypi/{package}{version}/json: {e}\"\n        ) from e\n\n    return pypi_metadata\n\n\n@contextlib.contextmanager\ndef _download_wheel(pypi_metadata: URLDict) -> Iterator[Path]:\n    response = request.urlopen(pypi_metadata[\"url\"])\n    whlname = Path(response.geturl()).name\n\n    with tempfile.TemporaryDirectory() as tmpdirname:\n        whlpath = Path(tmpdirname, whlname)\n        whlpath.write_bytes(response.read())\n        yield whlpath\n\n\ndef run_prettier(meta_path: str | Path) -> None:\n    subprocess.run([\"npx\", \"prettier\", \"-w\", meta_path])\n\n\ndef make_package(\n    packages_dir: Path,\n    package: str,\n    version: str | None = None,\n    source_fmt: Literal[\"wheel\", \"sdist\"] | None = None,\n) -> None:\n    \"\"\"\n    Creates a template that will work for most pure Python packages,\n    but will have to be edited for more complex things.\n    \"\"\"\n    logger.info(f\"Creating meta.yaml package for {package}\")\n\n    yaml = YAML()\n\n    pypi_metadata = _get_metadata(package, version)\n\n    if source_fmt:\n        sources = [source_fmt]\n    else:\n        # Prefer wheel unless sdist is specifically requested.\n        sources = [\"wheel\", \"sdist\"]\n    dist_metadata = _find_dist(pypi_metadata, sources)\n\n    native_wheel_metadata = _find_wheel(pypi_metadata, native=True)\n\n    top_level = None\n    if native_wheel_metadata is not None:\n        with _download_wheel(native_wheel_metadata) as native_wheel_path:\n            top_level = parse_top_level_import_name(native_wheel_path)\n\n    url = dist_metadata[\"url\"]\n    sha256 = dist_metadata[\"digests\"][\"sha256\"]\n    version = pypi_metadata[\"info\"][\"version\"]\n\n    homepage = pypi_metadata[\"info\"][\"home_page\"]\n    summary = pypi_metadata[\"info\"][\"summary\"]\n    license = pypi_metadata[\"info\"][\"license\"]\n    pypi = \"https://pypi.org/project/\" + package\n\n    yaml_content = {\n        \"package\": {\n            \"name\": package,\n            \"version\": version,\n            \"top-level\": top_level or [\"PUT_TOP_LEVEL_IMPORT_NAMES_HERE\"],\n        },\n        \"source\": {\"url\": url, \"sha256\": sha256},\n        \"about\": {\n            \"home\": homepage,\n            \"PyPI\": pypi,\n            \"summary\": summary,\n            \"license\": license,\n        },\n    }\n\n    package_dir = packages_dir / package\n    package_dir.mkdir(parents=True, exist_ok=True)\n\n    meta_path = package_dir / \"meta.yaml\"\n    if meta_path.exists():\n        raise MkpkgFailedException(f\"The package {package} already exists\")\n\n    yaml.representer.ignore_aliases = lambda *_: True\n    yaml.dump(yaml_content, meta_path)\n    try:\n        run_prettier(meta_path)\n    except FileNotFoundError:\n        warnings.warn(\"'npx' executable missing, output has not been prettified.\")\n\n    logger.success(f\"Output written to {meta_path}\")\n\n\ndef update_package(\n    root: Path,\n    package: str,\n    version: str | None = None,\n    update_patched: bool = True,\n    source_fmt: Literal[\"wheel\", \"sdist\"] | None = None,\n) -> None:\n    yaml = YAML()\n\n    meta_path = root / package / \"meta.yaml\"\n    if not meta_path.exists():\n        logger.error(f\"{meta_path} does not exist\")\n        exit(1)\n\n    yaml_content = yaml.load(meta_path.read_bytes())\n\n    build_info = yaml_content.get(\"build\", {})\n    ty = build_info.get(\"type\", None)\n    if ty in [\"static_library\", \"shared_library\", \"cpython_module\"]:\n        raise MkpkgSkipped(f\"{package} is a {ty.replace('_', ' ')}!\")\n\n    if \"url\" not in yaml_content[\"source\"]:\n        raise MkpkgSkipped(f\"{package} is a local package!\")\n\n    if yaml_content[\"source\"][\"url\"].endswith(\"whl\"):\n        old_fmt = \"wheel\"\n    else:\n        old_fmt = \"sdist\"\n\n    pypi_metadata = _get_metadata(package, version)\n    pypi_ver = Version(pypi_metadata[\"info\"][\"version\"])\n    local_ver = Version(yaml_content[\"package\"][\"version\"])\n    already_up_to_date = pypi_ver <= local_ver and (\n        source_fmt is None or source_fmt == old_fmt\n    )\n    if already_up_to_date:\n        logger.success(\n            f\"{package} already up to date. Local: {local_ver} PyPI: {pypi_ver}\"\n        )\n        return\n\n    logger.info(f\"{package} is out of date: {local_ver} <= {pypi_ver}.\")\n\n    if yaml_content[\"source\"].get(\"patches\"):\n        if update_patched:\n            logger.warning(\n                f\"Pyodide applies patches to {package}. Update the \"\n                \"patches (if needed) to avoid build failing.\"\n            )\n        else:\n            raise MkpkgFailedException(\n                f\"Pyodide applies patches to {package}. Skipping update.\"\n            )\n\n    if source_fmt:\n        # require the type requested\n        sources = [source_fmt]\n    elif old_fmt == \"wheel\":\n        # prefer wheel to sdist\n        sources = [\"wheel\", \"sdist\"]\n    else:\n        # prefer sdist to wheel\n        sources = [\"sdist\", \"wheel\"]\n\n    dist_metadata = _find_dist(pypi_metadata, sources)\n\n    yaml_content[\"source\"][\"url\"] = dist_metadata[\"url\"]\n    yaml_content[\"source\"].pop(\"md5\", None)\n    yaml_content[\"source\"][\"sha256\"] = dist_metadata[\"digests\"][\"sha256\"]\n    yaml_content[\"package\"][\"version\"] = pypi_metadata[\"info\"][\"version\"]\n\n    yaml.dump(yaml_content, meta_path)\n    run_prettier(meta_path)\n\n    logger.success(f\"Updated {package} from {local_ver} to {pypi_ver}.\")\n", "path": "pyodide-build/pyodide_build/mkpkg.py"}]}