Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
__main__.py655 linesDownload Raw Back to build
1# SPDX-License-Identifier: MIT2 3from __future__ import annotations4 5 6__lazy_modules__ = [7    'argparse',8    'build._exceptions',9    'build.env',10    'functools',11    'json',12    'os',13    'platform',14    'pyproject_hooks',15    'shutil',16    'subprocess',17    'tempfile',18    'textwrap',19    'traceback',20    'typing',21    'warnings',22]23 24import argparse25import contextlib26import contextvars27import json28import os29import platform30import shutil31import subprocess32import sys33import tempfile34import textwrap35import traceback36import warnings37 38from functools import partial39from typing import Any, NoReturn, TextIO40 41import pyproject_hooks42 43import build44import build.env as _env45 46from build import ProjectBuilder, _ctx47from build._exceptions import BuildBackendException, BuildException, FailedProcessError48from build.env import DefaultIsolatedEnv49 50 51TYPE_CHECKING = False52 53if TYPE_CHECKING:54    from collections.abc import Iterator, Mapping, Sequence55 56    from build._types import ConfigSettings, Distribution, StrPath, SubprocessRunner57 58 59_COLORS = {60    'red': '\33[91m',61    'green': '\33[92m',62    'yellow': '\33[93m',63    'bold': '\33[1m',64    'dim': '\33[2m',65    'underline': '\33[4m',66    'reset': '\33[0m',67}68_NO_COLORS = dict.fromkeys(_COLORS, '')69 70 71_styles = contextvars.ContextVar('_styles', default=_COLORS)72 73 74def _init_colors() -> None:75    if 'NO_COLOR' in os.environ:76        if 'FORCE_COLOR' in os.environ:77            warnings.warn('Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color', stacklevel=2)78        _styles.set(_NO_COLORS)79    elif 'FORCE_COLOR' in os.environ or sys.stdout.isatty():80        return81    _styles.set(_NO_COLORS)82 83 84def _cprint(fmt: str = '', msg: str = '', file: TextIO | None = None) -> None:85    print(fmt.format(msg, **_styles.get()), file=file, flush=True)86 87 88def _showwarning(89    message: Warning | str,90    category: type[Warning],  # noqa: ARG00191    filename: str,  # noqa: ARG00192    lineno: int,  # noqa: ARG00193    file: TextIO | None = None,94    line: str | None = None,  # noqa: ARG00195) -> None:  # pragma: no cover96    _cprint('{yellow}WARNING{reset} {}', str(message), file)97 98 99def _make_logger() -> _ctx.Logger:100    max_terminal_width = shutil.get_terminal_size().columns - 2101    if max_terminal_width <= 0:  # pragma: no cover102        max_terminal_width = 78103 104    fill = partial(textwrap.fill, subsequent_indent='  ', width=max_terminal_width)105 106    def log(message: str, *, kind: tuple[str, ...] | None = None) -> None:107        if _ctx.verbosity >= -1:108            match kind:109                case ('step', *_):110                    (first, *rest) = message.splitlines()111                    _cprint('{bold}{}{reset}', fill(first, initial_indent='* '), file=sys.stderr)112                    for line in rest:113                        print(fill(line, initial_indent='  '), file=sys.stderr)  # noqa: T201114                case ('subprocess', 'cmd'):115                    for line in message.splitlines():116                        _cprint('{dim}{}{reset}', fill(line, initial_indent='> '), file=sys.stderr)117                case ('subprocess', 'stdout' | 'stderr'):118                    for line in message.splitlines():119                        _cprint('{dim}{}{reset}', fill(line, initial_indent='< '), file=sys.stderr)120                case _:121                    print(fill(message, initial_indent='  '), file=sys.stderr)  # noqa: T201122 123    return log124 125 126def _setup_cli(*, verbosity: int) -> None:127    warnings.showwarning = _showwarning128 129    if platform.system() == 'Windows':130        try:131            import colorama132 133            colorama.init()134        except ModuleNotFoundError:135            pass136 137    _init_colors()138 139    _ctx.LOGGER.set(_make_logger())140    _ctx.VERBOSITY.set(verbosity)141 142 143def _error(msg: str, code: int = 1) -> NoReturn:  # pragma: no cover144    """145    Print an error message and exit. Will color the output when writing to a TTY.146 147    :param msg: Error message148    :param code: Error code149    """150    _cprint('{red}ERROR{reset} {}', msg, file=sys.stderr)151    raise SystemExit(code)152 153 154def _format_dep_chain(dep_chain: tuple[str, ...]) -> str:155    return ' -> '.join(dep.partition(';')[0].strip() for dep in dep_chain)156 157 158@contextlib.contextmanager159def _bootstrap_build_env(160    isolation: bool,161    srcdir: StrPath,162    distribution: Distribution,163    config_settings: ConfigSettings | None,164    skip_dependency_check: bool,165    dependency_constraints_txt: os.PathLike[str] | None,166    installer: _env.Installer,167    runner: SubprocessRunner | None = None,168) -> Iterator[ProjectBuilder]:169    if isolation:170        with DefaultIsolatedEnv(installer=installer) as env:171            make_builder = partial(ProjectBuilder.from_isolated_env, env, srcdir)172            if runner:173                make_builder = partial(make_builder, runner=runner)174            builder = make_builder()175 176            install = env.install177            if dependency_constraints_txt:178                with open(dependency_constraints_txt, encoding='utf-8') as dependency_constraints_file:179                    install = partial(install, constraints=set(map(str.strip, dependency_constraints_file)))180 181            # first install the build dependencies182            install(builder.build_system_requires, _fresh=True)183            # then get the extra required dependencies from the backend (which was installed in the call above :P)184            install(builder.get_requires_for_build(distribution, config_settings))185 186            yield builder187 188    else:189        make_builder = partial(ProjectBuilder, srcdir)190        if runner:191            make_builder = partial(make_builder, runner=runner)192        builder = make_builder()193 194        if not skip_dependency_check:195            missing = builder.check_dependencies(distribution, config_settings)196            if missing:197                dependencies = ''.join('\n\t' + _format_dep_chain(deps) for deps in missing)198                _cprint()199                _error(f'Missing dependencies:{dependencies}')200 201        yield builder202 203 204def _build(205    isolation: bool,206    srcdir: StrPath,207    outdir: StrPath,208    distribution: Distribution,209    config_settings: ConfigSettings | None,210    skip_dependency_check: bool,211    dependency_constraints_txt: os.PathLike[str] | None,212    installer: _env.Installer,213) -> str:214    with _bootstrap_build_env(215        isolation,216        srcdir,217        distribution,218        config_settings,219        skip_dependency_check,220        dependency_constraints_txt,221        installer,222        pyproject_hooks.quiet_subprocess_runner if _ctx.verbosity < 0 else None,223    ) as builder:224        return builder.build(distribution, outdir, config_settings)225 226 227@contextlib.contextmanager228def _handle_build_error() -> Iterator[None]:229    try:230        yield231    except (BuildException, FailedProcessError) as e:232        _error(str(e))233    except BuildBackendException as e:234        if isinstance(e.exception, subprocess.CalledProcessError):235            _cprint()236            _error(str(e))237 238        if e.exc_info:239            tb_lines = traceback.format_exception(240                e.exc_info[0],241                e.exc_info[1],242                e.exc_info[2],243                limit=-1,244            )245            tb = ''.join(tb_lines)246        else:  # pragma: no cover247            tb = traceback.format_exc(-1)  # type: ignore[unreachable]248        _cprint('\n{dim}{}{reset}\n', tb.strip('\n'))249        _error(str(e))250    except Exception as e:  # pragma: no cover251        tb = traceback.format_exc().strip('\n')252        _cprint('\n{dim}{}{reset}\n', tb)253        _error(str(e))254 255 256def _natural_language_list(elements: Sequence[str]) -> str:257    if len(elements) == 0:258        msg = 'no elements'259        raise IndexError(msg)260    if len(elements) == 1:261        return elements[0]262    return '{} and {}'.format(263        ', '.join(elements[:-1]),264        elements[-1],265    )266 267 268def build_package(269    srcdir: StrPath,270    outdir: StrPath,271    distributions: Sequence[Distribution],272    config_settings: ConfigSettings | None = None,273    isolation: bool = True,274    skip_dependency_check: bool = False,275    dependency_constraints_txt: os.PathLike[str] | None = None,276    installer: _env.Installer = 'pip',277) -> list[str]:278    """279    Run the build process.280 281    :param srcdir: Source directory282    :param outdir: Output directory283    :param distribution: Distribution to build (sdist or wheel)284    :param config_settings: Configuration settings to be passed to the backend285    :param isolation: Isolate the build in a separate environment286    :param skip_dependency_check: Do not perform the dependency check287    """288    built: list[str] = []289    for distribution in distributions:290        out = _build(291            isolation,292            srcdir,293            outdir,294            distribution,295            config_settings,296            skip_dependency_check,297            dependency_constraints_txt,298            installer,299        )300        built.append(os.path.basename(out))301    return built302 303 304def build_package_via_sdist(305    srcdir: StrPath,306    outdir: StrPath,307    distributions: Sequence[Distribution],308    config_settings: ConfigSettings | None = None,309    isolation: bool = True,310    skip_dependency_check: bool = False,311    dependency_constraints_txt: os.PathLike[str] | None = None,312    installer: _env.Installer = 'pip',313) -> list[str]:314    """315    Build a sdist and then the specified distributions from it.316 317    :param srcdir: Source directory318    :param outdir: Output directory319    :param distribution: Distribution to build (only wheel)320    :param config_settings: Configuration settings to be passed to the backend321    :param isolation: Isolate the build in a separate environment322    :param skip_dependency_check: Do not perform the dependency check323    """324    from ._compat import tarfile325 326    if 'sdist' in distributions:327        msg = 'Only binary distributions are allowed but sdist was specified'328        raise ValueError(msg)329 330    sdist = _build(331        isolation, srcdir, outdir, 'sdist', config_settings, skip_dependency_check, dependency_constraints_txt, installer332    )333 334    sdist_name = os.path.basename(sdist)335    sdist_out = tempfile.mkdtemp(prefix='build-via-sdist-')336    built: list[str] = []337    if distributions:338        # extract sdist339        with tarfile.TarFile.open(sdist) as t:340            t.extractall(sdist_out)341            try:342                _ctx.log(f'Building {_natural_language_list(distributions)} from sdist', kind=('step',))343                srcdir = os.path.join(sdist_out, sdist_name[: -len('.tar.gz')])344                for distribution in distributions:345                    out = _build(346                        isolation,347                        srcdir,348                        outdir,349                        distribution,350                        config_settings,351                        skip_dependency_check,352                        dependency_constraints_txt,353                        installer,354                    )355                    built.append(os.path.basename(out))356            finally:357                shutil.rmtree(sdist_out, ignore_errors=True)358    return [sdist_name, *built]359 360 361def _build_metadata(362    srcdir: StrPath,363    outdir: StrPath,  # noqa: ARG001364    distributions: Sequence[Distribution],  # noqa: ARG001365    config_settings: ConfigSettings | None = None,366    isolation: bool = True,367    skip_dependency_check: bool = False,368    dependency_constraints_txt: os.PathLike[str] | None = None,369    installer: _env.Installer = 'pip',370) -> list[str]:371    import packaging.metadata372 373    def run_subprocess(cmd: Sequence[StrPath], cwd: str | None = None, extra_environ: Mapping[str, str] | None = None) -> None:374        env = os.environ.copy()375        if extra_environ:376            env.update(extra_environ)377        _ctx.run_subprocess(cmd, cwd, env)378 379    with (380        _bootstrap_build_env(381            isolation,382            srcdir,383            'wheel',384            config_settings,385            skip_dependency_check,386            dependency_constraints_txt,387            installer,388            runner=run_subprocess,389        ) as builder,390        tempfile.TemporaryDirectory() as tempdir,391        open(392            os.path.join(builder.metadata_path(tempdir), 'METADATA'),393            'rb',394        ) as metadata_file,395    ):396        valid_metadata, _ = packaging.metadata.parse_email(metadata_file.read())397    print(  # noqa: T201398        json.dumps(valid_metadata, ensure_ascii=False, indent=2),399    )400 401    return []402 403 404def main_parser() -> argparse.ArgumentParser:405    """406    Construct the main parser.407    """408 409    class NegativeCountAction(argparse.Action):410        def __init__(411            self,412            option_strings: Sequence[str],413            dest: str,414            default: Any = None,415            help: str | None = None,416        ) -> None:417            super().__init__(418                option_strings=option_strings,419                dest=dest,420                nargs=0,421                default=default,422                help=help,423            )424 425        def __call__(426            self,427            parser: argparse.ArgumentParser,  # noqa: ARG002428            namespace: object,429            values: str | Sequence[Any] | None,  # noqa: ARG002430            option_string: str | None = None,  # noqa: ARG002431        ) -> None:432            setattr(namespace, self.dest, getattr(namespace, self.dest, 0) - 1)433 434    make_parser = partial(435        argparse.ArgumentParser,436        add_help=False,437        description="""\438    A simple, correct Python build frontend.439 440    By default, a source distribution (sdist) is built from the project root441    and a binary distribution (wheel) is built from the sdist.442    If this is undesirable, you can pass `--sdist` and/or `--wheel`443    to build distributions independently of each other.444    """.rstrip(),445        # Prevent argparse from taking up the entire width of the terminal window446        # which impedes readability. Also keep the description formatted.447        formatter_class=argparse.RawDescriptionHelpFormatter,448    )449    if sys.version_info >= (3, 14):  # pragma: no branch450        make_parser = partial(make_parser, suggest_on_error=True)  # pragma: no cover451 452    parser = make_parser()453    parser.add_argument(454        'srcdir',455        type=str,456        nargs='?',457        default=os.getcwd(),458        help='source directory (defaults to the current working directory)',459    )460 461    global_group = parser.add_argument_group('global options')462    global_group.add_argument(463        '-h',464        '--help',465        action='help',466        default=argparse.SUPPRESS,467        help='show this help message and exit',468    )469    global_group.add_argument(470        '--version',471        '-V',472        action='version',473        version=f'build {build.__version__} ({",".join(build.__path__)})',474    )475    verbosity_exclusive_group = global_group.add_mutually_exclusive_group()476    verbosity_exclusive_group.add_argument(477        '--quiet',478        '-q',479        dest='verbosity',480        action=NegativeCountAction,481        default=0,482        help='reduce verbosity',483    )484    verbosity_exclusive_group.add_argument(485        '--verbose',486        '-v',487        dest='verbosity',488        action='count',489        default=0,490        help='increase verbosity',491    )492 493    build_group = parser.add_argument_group('build options')494    build_group.add_argument(495        '--outdir',496        '-o',497        help=f'output directory (defaults to {{srcdir}}{os.sep}dist).  Cannot be used together with ``--metadata``',498        metavar='PATH',499    )500    build_group.add_argument(501        '--sdist',502        '-s',503        dest='distributions',504        action='append_const',505        const='sdist',506        help='build a source distribution (disables the default behavior)',507    )508    build_group.add_argument(509        '--wheel',510        '-w',511        dest='distributions',512        action='append_const',513        const='wheel',514        help='build a wheel (disables the default behavior)',515    )516    build_group.add_argument(517        '--metadata',518        action='store_true',519        help="print out a wheel's metadata in JSON format. Cannot be used in conjunction with ``--sdist`` or ``--wheel``",520    )521    config_exclusive_group = build_group.add_mutually_exclusive_group()522    config_exclusive_group.add_argument(523        '--config-setting',524        '-C',525        dest='config_settings',526        action='append',527        help='settings to pass to the backend.  Multiple settings can be provided. '528        'Settings beginning with a hyphen will erroneously be interpreted as options to build if separated '529        'by a space; use ``--config-setting=--my-setting -C--my-other-setting`` instead',530        metavar='KEY[=VALUE]',531    )532    config_exclusive_group.add_argument(533        '--config-json',534        help='settings to pass to the backend as a JSON object. '535        'This is an alternative to ``--config-setting`` that allows complex nested structures. '536        'Cannot be used together with ``--config-setting``',537        metavar='JSON_STRING',538    )539 540    install_group = parser.add_argument_group('installation options')541    env_exclusive_group = install_group.add_mutually_exclusive_group()542    env_exclusive_group.add_argument(543        '--installer',544        choices=_env.INSTALLERS,545        help='Python package installer to use (defaults to pip)',546    )547    env_exclusive_group.add_argument(548        '--no-isolation',549        '-n',550        action='store_true',551        help='disable building the project in an isolated virtual environment. '552        'Build dependencies must be installed separately when this option is used',553    )554    install_group.add_argument(555        '--dependency-constraints-txt',556        help='constrain build dependencies using a constraints.txt when installing dependencies',557        metavar='PATH',558    )559    install_group.add_argument(560        '--skip-dependency-check',561        '-x',562        action='store_true',563        help='do not check that build dependencies are installed',564    )565 566    return parser567 568 569def _parse_config_settings(raw_config_settings: list[str]) -> dict[str, Any]:570    config_settings = dict[str, Any]()571 572    for arg in raw_config_settings:573        setting, _, value = arg.partition('=')574        if setting not in config_settings:575            config_settings[setting] = value576        else:577            if not isinstance(config_settings[setting], list):578                config_settings[setting] = [config_settings[setting]]579 580            config_settings[setting].append(value)581 582    return config_settings583 584 585def main(cli_args: Sequence[str], prog: str | None = None) -> None:586    """587    Parse the CLI arguments and invoke the build process.588 589    :param cli_args: CLI arguments590    :param prog: Program name to show in help text591    """592    parser = main_parser()593    if prog:594        parser.prog = prog595    args = parser.parse_args(cli_args)596 597    _setup_cli(verbosity=args.verbosity)598 599    config_settings = dict[str, Any]()600 601    # Handle --config-json602    if args.config_json:603        try:604            config_settings = json.loads(args.config_json)605            if not isinstance(config_settings, dict):606                _error('--config-json must contain a JSON object (dict), not a list or primitive value')607        except json.JSONDecodeError as e:608            _error(f'Invalid JSON in --config-json: {e}')609 610    # Handle --config-setting (original logic)611    elif args.config_settings:612        config_settings = _parse_config_settings(args.config_settings)613 614    # outdir is relative to srcdir only if omitted.615    outdir = os.path.join(args.srcdir, 'dist') if args.outdir is None else args.outdir616 617    if args.metadata and args.distributions:618        parser.error('--metadata: not allowed with --sdist or --wheel')619    elif args.metadata:620        build = partial(_build_metadata, distributions=['wheel'])621    elif args.distributions:622        build = partial(build_package, distributions=args.distributions)623    else:624        build = partial(build_package_via_sdist, distributions=['wheel'])625 626    with _handle_build_error():627        built = build(628            args.srcdir,629            outdir,630            config_settings=config_settings,631            isolation=not args.no_isolation,632            skip_dependency_check=args.skip_dependency_check,633            dependency_constraints_txt=args.dependency_constraints_txt,634            installer=args.installer,635        )636        if _ctx.verbosity >= -1 and built:637            artifact_list = _natural_language_list(638                ['{underline}{}{reset}{bold}{green}'.format(artifact, **_styles.get()) for artifact in built]639            )640            _cprint('{bold}{green}Successfully built {}{reset}', artifact_list)641 642 643def entrypoint() -> None:644    main(sys.argv[1:])645 646 647if __name__ == '__main__':  # pragma: no cover648    main(sys.argv[1:], 'python -m build')649 650 651__all__ = [652    'main',653    'main_parser',654]655 
codekingpro/portable-devtools · Team Ai