mirror of https://github.com/pyodide/pyodide.git
246 lines
8.6 KiB
Python
246 lines
8.6 KiB
Python
import argparse
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
import typer # type: ignore[import]
|
|
from unearth.evaluator import TargetPython
|
|
from unearth.finder import PackageFinder
|
|
|
|
from .. import buildall, common
|
|
from ..out_of_tree import build
|
|
from ..out_of_tree.utils import initialize_pyodide_root
|
|
|
|
app = typer.Typer()
|
|
|
|
|
|
def _fetch_pypi_package(package_spec, destdir):
|
|
PYMAJOR = common.get_make_flag("PYMAJOR")
|
|
PYMINOR = common.get_make_flag("PYMINOR")
|
|
tp = TargetPython(
|
|
py_ver=(int(PYMAJOR), int(PYMINOR)),
|
|
platforms=[common.platform()],
|
|
abis=[f"cp{PYMAJOR}{PYMINOR}"],
|
|
)
|
|
pf = PackageFinder(index_urls=["https://pypi.org/simple/"], target_python=tp)
|
|
match = pf.find_best_match(package_spec)
|
|
if match.best is None:
|
|
if len(match.candidates) != 0:
|
|
error = f"""Can't find version matching {package_spec}
|
|
versions found:
|
|
"""
|
|
for c in match.candidates:
|
|
error += " " + str(c.version) + "\t"
|
|
raise RuntimeError(error)
|
|
else:
|
|
raise RuntimeError(f"Can't find package: {package_spec}")
|
|
with tempfile.TemporaryDirectory() as download_dir:
|
|
return pf.download_and_unpack(
|
|
link=match.best.link, location=destdir, download_dir=download_dir
|
|
)
|
|
|
|
|
|
def pypi(
|
|
package: str,
|
|
exports: str = typer.Option(
|
|
"requested",
|
|
help="Which symbols should be exported when linking .so files?",
|
|
),
|
|
ctx: typer.Context = typer.Context,
|
|
) -> None:
|
|
"""Fetch a wheel from pypi, or build from source if none available."""
|
|
initialize_pyodide_root()
|
|
common.check_emscripten_version()
|
|
backend_flags = ctx.args
|
|
curdir = Path.cwd()
|
|
(curdir / "dist").mkdir(exist_ok=True)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
temppath = Path(tmpdir)
|
|
|
|
# get package from pypi
|
|
package_path = _fetch_pypi_package(package, temppath)
|
|
if not package_path.is_dir():
|
|
# a pure-python wheel has been downloaded - just copy to dist folder
|
|
shutil.copy(str(package_path), str(curdir / "dist"))
|
|
print(f"Successfully fetched: {package_path.name}")
|
|
return
|
|
|
|
# sdist - needs building
|
|
os.chdir(tmpdir)
|
|
build.run(exports, backend_flags)
|
|
for src in (temppath / "dist").iterdir():
|
|
print(f"Built {str(src.name)}")
|
|
shutil.copy(str(src), str(curdir / "dist"))
|
|
|
|
|
|
def url(
|
|
package_url: str,
|
|
exports: str = typer.Option(
|
|
"requested",
|
|
help="Which symbols should be exported when linking .so files?",
|
|
),
|
|
ctx: typer.Context = typer.Context,
|
|
) -> None:
|
|
"""Fetch a wheel or build sdist from url."""
|
|
initialize_pyodide_root()
|
|
common.check_emscripten_version()
|
|
backend_flags = ctx.args
|
|
curdir = Path.cwd()
|
|
(curdir / "dist").mkdir(exist_ok=True)
|
|
|
|
with requests.get(package_url, stream=True) as response:
|
|
parsed_url = urlparse(response.url)
|
|
filename = os.path.basename(parsed_url.path)
|
|
name_base, ext = os.path.splitext(filename)
|
|
if ext == ".gz" and name_base.rfind(".") != -1:
|
|
ext = name_base[name_base.rfind(".") :] + ext
|
|
if ext.lower() == ".whl":
|
|
# just copy wheel into dist and return
|
|
out_path = f"dist/{filename}"
|
|
with open(out_path, "b") as f:
|
|
for chunk in response.iter_content(chunk_size=1048576):
|
|
f.write(chunk)
|
|
return
|
|
else:
|
|
tf = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
|
for chunk in response.iter_content(chunk_size=1048576):
|
|
tf.write(chunk)
|
|
tf.close()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
temppath = Path(tmpdir)
|
|
shutil.unpack_archive(tf.name, tmpdir)
|
|
folder_list = list(temppath.iterdir())
|
|
if len(folder_list) == 1 and folder_list[0].is_dir():
|
|
# unzipped into subfolder
|
|
os.chdir(folder_list[0])
|
|
else:
|
|
# unzipped here
|
|
os.chdir(tmpdir)
|
|
print(os.listdir(tmpdir))
|
|
build.run(exports, backend_flags)
|
|
for src in (temppath / "dist").iterdir():
|
|
print(f"Built {str(src.name)}")
|
|
shutil.copy(str(src), str(curdir / "dist"))
|
|
os.unlink(tf.name)
|
|
|
|
|
|
def source(
|
|
source_location: "Optional[str]" = typer.Argument(None),
|
|
exports: str = typer.Option(
|
|
"requested",
|
|
help="Which symbols should be exported when linking .so files?",
|
|
),
|
|
ctx: typer.Context = typer.Context,
|
|
) -> None:
|
|
"""Use pypa/build to build a Python package from source"""
|
|
initialize_pyodide_root()
|
|
common.check_emscripten_version()
|
|
backend_flags = [source_location] + ctx.args
|
|
build.run(exports, backend_flags)
|
|
|
|
|
|
@app.command() # type: ignore[misc]
|
|
def recipe(
|
|
packages: list[str] = typer.Argument(
|
|
..., help="Packages to build, or * for all packages in recipe directory"
|
|
),
|
|
output: str = typer.Option(
|
|
None,
|
|
help="Path to output built packages and repodata.json. "
|
|
"If not specified, the default is `PYODIDE_ROOT/dist`.",
|
|
),
|
|
cflags: str = typer.Option(
|
|
None, help="Extra compiling flags. Default: SIDE_MODULE_CFLAGS"
|
|
),
|
|
cxxflags: str = typer.Option(
|
|
None, help="Extra compiling flags. Default: SIDE_MODULE_CXXFLAGS"
|
|
),
|
|
ldflags: str = typer.Option(
|
|
None, help="Extra linking flags. Default: SIDE_MODULE_LDFLAGS"
|
|
),
|
|
target_install_dir: str = typer.Option(
|
|
None,
|
|
help="The path to the target Python installation. Default: TARGETINSTALLDIR",
|
|
),
|
|
host_install_dir: str = typer.Option(
|
|
None,
|
|
help="Directory for installing built host packages. Default: HOSTINSTALLDIR",
|
|
),
|
|
log_dir: str = typer.Option(None, help="Directory to place log files"),
|
|
force_rebuild: bool = typer.Option(
|
|
False,
|
|
help="Force rebuild of all packages regardless of whether they appear to have been updated",
|
|
),
|
|
n_jobs: int = typer.Option(4, help="Number of packages to build in parallel"),
|
|
root: str = typer.Option(
|
|
None, help="The root directory of the Pyodide.", envvar="PYODIDE_ROOT"
|
|
),
|
|
recipe_dir: str = typer.Option(
|
|
None,
|
|
help="The directory containing the recipe of packages. "
|
|
"If not specified, the default is `packages` in the root directory.",
|
|
),
|
|
ctx: typer.Context = typer.Context,
|
|
) -> None:
|
|
"""Build packages using yaml recipes and create repodata.json"""
|
|
pyodide_root = common.search_pyodide_root(Path.cwd()) if not root else Path(root)
|
|
recipe_dir_ = pyodide_root / "packages" if not recipe_dir else Path(recipe_dir)
|
|
output_dir = pyodide_root / "dist" if not output else Path(output)
|
|
|
|
# Note: to make minimal changes to the existing pyodide-build entrypoint,
|
|
# keep arguments of buildall unghanged.
|
|
# TODO: refactor this when we remove pyodide-build entrypoint.
|
|
args = argparse.Namespace(**ctx.params)
|
|
args.dir = args.recipe_dir
|
|
|
|
if len(args.packages) == 1 and "," in args.packages[0]:
|
|
# Handle packages passed with old comma separated syntax.
|
|
# This is to support `PYODIDE_PACKAGES="pkg1,pkg2,..." make` syntax.
|
|
args.only = args.packages[0].replace(" ", "")
|
|
else:
|
|
args.only = ",".join(args.packages)
|
|
|
|
args = buildall.set_default_args(args)
|
|
|
|
buildall.build_packages(recipe_dir_, output_dir, args)
|
|
|
|
|
|
# simple 'pyodide build' command
|
|
@app.command() # type: ignore[misc]
|
|
def main(
|
|
source_location: "Optional[str]" = typer.Argument(
|
|
"",
|
|
help="Build source, can be source folder, pypi version specification, or url to a source dist archive or wheel file. If this is blank, it will build the current directory.",
|
|
),
|
|
exports: str = typer.Option(
|
|
"requested",
|
|
help="Which symbols should be exported when linking .so files?",
|
|
),
|
|
ctx: typer.Context = typer.Context,
|
|
) -> None:
|
|
"""Use pypa/build to build a Python package from source, pypi or url."""
|
|
if not source_location:
|
|
# build the current folder
|
|
source(".", exports, ctx)
|
|
elif source_location.find("://") != -1:
|
|
url(source_location, exports, ctx)
|
|
elif Path(source_location).is_dir():
|
|
# a folder, build it
|
|
source(source_location, exports, ctx)
|
|
else:
|
|
# try fetch from pypi
|
|
pypi(source_location, exports, ctx)
|
|
|
|
|
|
main.typer_kwargs = {
|
|
"context_settings": {
|
|
"ignore_unknown_options": True,
|
|
"allow_extra_args": True,
|
|
},
|
|
}
|