Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 30 Mar 2020 01:38:40 +0000 (UTC)
From:      Muhammad Moinur Rahman <bofh@FreeBSD.org>
To:        ports-committers@freebsd.org, svn-ports-all@freebsd.org, svn-ports-head@freebsd.org
Subject:   svn commit: r529851 - in head/devel/py-molecule: . files
Message-ID:  <202003300138.02U1ce9M079355@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: bofh
Date: Mon Mar 30 01:38:40 2020
New Revision: 529851
URL: https://svnweb.freebsd.org/changeset/ports/529851

Log:
  devel/py-molecule: Update version 2.20.2=>2.22
  
  - Add OPTIONS AZURE, DOCKER, DOCS, GCE, EC2
  - Chane PY requirements upto 3.7
  - Take MAINTAINERSHIP
  
  Approved by:	matthew (private mail)

Added:
  head/devel/py-molecule/files/
  head/devel/py-molecule/files/patch-setup.py   (contents, props changed)
  head/devel/py-molecule/pkg-message   (contents, props changed)
Modified:
  head/devel/py-molecule/Makefile
  head/devel/py-molecule/distinfo

Modified: head/devel/py-molecule/Makefile
==============================================================================
--- head/devel/py-molecule/Makefile	Mon Mar 30 00:29:56 2020	(r529850)
+++ head/devel/py-molecule/Makefile	Mon Mar 30 01:38:40 2020	(r529851)
@@ -1,46 +1,69 @@
 # $FreeBSD$
 
 PORTNAME=	molecule
-PORTVERSION=	2.20.2
+PORTVERSION=	2.22
 CATEGORIES=	devel python
 MASTER_SITES=	CHEESESHOP
 PKGNAMEPREFIX=	${PYTHON_PKGNAMEPREFIX}
 
-MAINTAINER=	matthew@FreeBSD.org
+MAINTAINER=	bofh@FreeBSD.org
 COMMENT=	Aid for the development and testing of Ansible roles
 
 LICENSE=	MIT
 LICENSE_FILE=	${WRKSRC}/LICENSE
 
-BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}pbr>0:devel/py-pbr@${PY_FLAVOR}
-RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}ansible-lint>=4.0.2:sysutils/py-ansible-lint@${PY_FLAVOR} \
+BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}setuptools_scm_git_archive>=1.0:devel/py-setuptools_scm_git_archive@${PY_FLAVOR}
+RUN_DEPENDS=	\
+		${PYTHON_PKGNAMEPREFIX}ansible>=2.5:sysutils/ansible@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}ansible-lint>=4.0.2:sysutils/py-ansible-lint@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}Cerberus>=1.3.1:devel/py-cerberus@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}Jinja2>=2.10.1:devel/py-Jinja2@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}anyconfig>=0.9.7:devel/py-anyconfig@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}Cerberus>=1.2:devel/py-cerberus@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}click>=6.7:devel/py-click@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}click-completion>=0.3.1:devel/py-click-completion@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}colorama>=0.3.9:devel/py-colorama@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}cookiecutter>=1.6.0:devel/py-cookiecutter@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}flake8>=3.6.0:devel/py-flake8@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}paramiko>=2.5.0:security/py-paramiko@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pexpect>=4.6.0:misc/py-pexpect@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pre-commit>=1.17.0:devel/py-pre-commit@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}psutil>=5.4.6:sysutils/py-psutil@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}python-gilt>=1.2.1:devel/py-python-gilt@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}Jinja2>=2.10:devel/py-Jinja2@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}pbr>0:devel/py-pbr@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}yaml>=3.13:devel/py-yaml@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}sh>=1.12.14:devel/py-sh@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}six>=1.11.0:devel/py-six@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}tabulate>=0.8.2:devel/py-tabulate@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}testinfra>=1.19.0:devel/py-testinfra@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}tabulate>=0.8.3:devel/py-tabulate@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}testinfra>=3.0.6:devel/py-testinfra@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}tree-format>=0.1.2:devel/py-tree-format@${PY_FLAVOR} \
-		${PY_TYPING} \
-		${PYTHON_PKGNAMEPREFIX}yamllint>=1.11.1:devel/py-yamllint@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}pexpect>=4.6.0:misc/py-pexpect@${PY_FLAVOR}
-TEST_DEPENDS=	${PYTHON_PKGNAMEPREFIX}pytest>=3.0.7:devel/py-pytest@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}pytest-helpers-namespace>0:devel/py-pytest-helpers-namespace@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}pytest-mock>0:devel/py-pytest-mock@${PY_FLAVOR}
+		${PYTHON_PKGNAMEPREFIX}yaml>=5.1:devel/py-yaml@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}yamllint>=1.15.0:devel/py-yamllint@${PY_FLAVOR}
+# TEST requires pytest >=4.6.3. Will change once updated
+TEST_DEPENDS=	\
+		${PYTHON_PKGNAMEPREFIX}flake8>=3.6.0:devel/py-flake8@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}mock>=3.0.5:devel/py-mock@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest>=4.5.0:devel/py-pytest@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest-cov>=2.7.1:devel/py-pytest-cov@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest-helpers-namespace>=2019.1.8:devel/py-pytest-helpers-namespace@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest-mock>=1.10.4:devel/py-pytest-mock@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest-verbose-parametrize>=1.7.0:devel/py-pytest-verbose-parametrize@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}pytest-xdist>=1.29.0:devel/py-pytest-xdist@${PY_FLAVOR}
 
-USES=		python
+USES=		python:-3.7
 USE_PYTHON=	autoplist concurrent distutils
 
 NO_ARCH=	yes
 
+OPTIONS_DEFINE=	AZURE DOCKER DOCS EC2 GCE
+AZURE_DESC=	For testing on Microsoft Azure Platform
+DOCKER_DESC=	For testing on Docker Platform(Remote)
+EC2_DESC=	For testing on Amazon EC2 Platform
+GCE_DESC=	For testing on Google Compute Engine Platform
+DOCS_BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}alabaster>0:textproc/py-alabaster@${PY_FLAVOR} \
+			${PYTHON_PKGNAMEPREFIX}sphinx>0:textproc/py-sphinx@${PY_FLAVOR}
+AZURE_RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}azure-cli>0:sysutils/py-azure-cli@${PY_FLAVOR}
+DOCKER_RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}docker>=2.0.0:sysutils/py-docker@${PY_FLAVOR}
+EC2_RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}boto>0:devel/py-boto@${PY_FLAVOR} \
+			${PYTHON_PKGNAMEPREFIX}boto3>0:www/py-boto3@${PY_FLAVOR}
+GCE_RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}libcloud>0:net/py-libcloud@${PY_FLAVOR}
 # Note: we're only running the unit tests here.  Upstream acknowledges
 # that functional tests are still a work in progress.
 #
@@ -52,8 +75,8 @@ TEST_ENV=	LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
 
 .include <bsd.port.pre.mk>
 
-.if ${PYTHON_REL} < 3500
-RUN_DEPENDS+=	${PYTHON_PKGNAMEPREFIX}functools32>=0:devel/py-functools32@${PY_FLAVOR}
+.if ${PYTHON_REL} < 3200
+RUN_DEPENDS+=	${PYTHON_PKGNAMEPREFIX}pathlib2>=0:devel/py-pathlib2@${PY_FLAVOR}
 .endif
 
 do-test:

Modified: head/devel/py-molecule/distinfo
==============================================================================
--- head/devel/py-molecule/distinfo	Mon Mar 30 00:29:56 2020	(r529850)
+++ head/devel/py-molecule/distinfo	Mon Mar 30 01:38:40 2020	(r529851)
@@ -1,3 +1,3 @@
-TIMESTAMP = 1563826185
-SHA256 (molecule-2.20.2.tar.gz) = 9dc29b9ef172b26532752784687faca2e868c84e2d90f0b4f018d81d76a8b30a
-SIZE (molecule-2.20.2.tar.gz) = 251623
+TIMESTAMP = 1585511014
+SHA256 (molecule-2.22.tar.gz) = d9d7621167041ae2a8eb19f1f8dc23c071cdab2cd3ca80655e2c8796b4c00e09
+SIZE (molecule-2.22.tar.gz) = 281356

Added: head/devel/py-molecule/files/patch-setup.py
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ head/devel/py-molecule/files/patch-setup.py	Mon Mar 30 01:38:40 2020	(r529851)
@@ -0,0 +1,334 @@
+--- setup.py.orig	2020-03-22 22:04:03 UTC
++++ setup.py
+@@ -23,328 +23,7 @@
+ 
+ import setuptools
+ 
+-HAS_DIST_INFO_CMD = False
+-try:
+-    import setuptools.command.dist_info
+-
+-    HAS_DIST_INFO_CMD = True
+-except ImportError:
+-    """Setuptools version is too old."""
+-
+-
+-ALL_STRING_TYPES = tuple(map(type, ('', b'', u'')))
+-MIN_NATIVE_SETUPTOOLS_VERSION = 34, 4
+-"""Minimal setuptools having good read_configuration implementation."""
+-
+-# Patch version can be a non integer value, like 'post20190705'
+-RUNTIME_SETUPTOOLS_VERSION = tuple(map(int, setuptools.__version__.split('.')[:2]))
+-"""Setuptools imported now."""
+-
+-READ_CONFIG_SHIM_NEEDED = RUNTIME_SETUPTOOLS_VERSION < MIN_NATIVE_SETUPTOOLS_VERSION
+-
+-
+-def str_if_nested_or_str(s):
+-    """Turn input into a native string if possible."""
+-    if isinstance(s, ALL_STRING_TYPES):
+-        return str(s)
+-    if isinstance(s, (list, tuple)):
+-        return type(s)(map(str_if_nested_or_str, s))
+-    if isinstance(s, (dict,)):
+-        return stringify_dict_contents(s)
+-    return s
+-
+-
+-def stringify_dict_contents(dct):
+-    """Turn dict keys and values into native strings."""
+-    return {str_if_nested_or_str(k): str_if_nested_or_str(v) for k, v in dct.items()}
+-
+-
+-if not READ_CONFIG_SHIM_NEEDED:
+-    from setuptools.config import read_configuration, ConfigOptionsHandler
+-    import setuptools.config
+-    import setuptools.dist
+-
+-    # Set default value for 'use_scm_version'
+-    setattr(setuptools.dist.Distribution, 'use_scm_version', False)
+-
+-    # Attach bool parser to 'use_scm_version' option
+-    class ShimConfigOptionsHandler(ConfigOptionsHandler):
+-        """Extension class for ConfigOptionsHandler."""
+-
+-        @property
+-        def parsers(self):
+-            """Return an option mapping with default data type parsers."""
+-            _orig_parsers = super(ShimConfigOptionsHandler, self).parsers
+-            return dict(use_scm_version=self._parse_bool, **_orig_parsers)
+-
+-        def parse_section_packages__find(self, section_options):
+-            find_kwargs = super(
+-                ShimConfigOptionsHandler, self
+-            ).parse_section_packages__find(section_options)
+-            return stringify_dict_contents(find_kwargs)
+-
+-    setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler
+-else:
+-    """This is a shim for setuptools<required."""
+-    import functools
+-    import io
+-    import json
+-    import sys
+-    import warnings
+-
+-    try:
+-        import setuptools.config
+-
+-        def filter_out_unknown_section(i):
+-            def chi(self, *args, **kwargs):
+-                i(self, *args, **kwargs)
+-                self.sections = {
+-                    s: v for s, v in self.sections.items() if s != 'packages.find'
+-                }
+-
+-            return chi
+-
+-        setuptools.config.ConfigHandler.__init__ = filter_out_unknown_section(
+-            setuptools.config.ConfigHandler.__init__
+-        )
+-    except ImportError:
+-        pass
+-
+-    def ignore_unknown_options(s):
+-        @functools.wraps(s)
+-        def sw(**attrs):
+-            try:
+-                ignore_warning_regex = (
+-                    r"Unknown distribution option: "
+-                    r"'(license_file|project_urls|python_requires)'"
+-                )
+-                warnings.filterwarnings(
+-                    'ignore',
+-                    message=ignore_warning_regex,
+-                    category=UserWarning,
+-                    module='distutils.dist',
+-                )
+-                return s(**attrs)
+-            finally:
+-                warnings.resetwarnings()
+-
+-        return sw
+-
+-    def parse_predicates(python_requires):
+-        import itertools
+-        import operator
+-
+-        sorted_operators_map = tuple(
+-            sorted(
+-                {
+-                    '>': operator.gt,
+-                    '<': operator.lt,
+-                    '>=': operator.ge,
+-                    '<=': operator.le,
+-                    '==': operator.eq,
+-                    '!=': operator.ne,
+-                    '': operator.eq,
+-                }.items(),
+-                key=lambda i: len(i[0]),
+-                reverse=True,
+-            )
+-        )
+-
+-        def is_decimal(s):
+-            return type(u'')(s).isdecimal()
+-
+-        conditions = map(str.strip, python_requires.split(','))
+-        for c in conditions:
+-            for op_sign, op_func in sorted_operators_map:
+-                if not c.startswith(op_sign):
+-                    continue
+-                raw_ver = itertools.takewhile(
+-                    is_decimal, c[len(op_sign) :].strip().split('.')
+-                )
+-                ver = tuple(map(int, raw_ver))
+-                yield op_func, ver
+-                break
+-
+-    def validate_required_python_or_fail(python_requires=None):
+-        if python_requires is None:
+-            return
+-
+-        python_version = sys.version_info
+-        preds = parse_predicates(python_requires)
+-        for op, v in preds:
+-            py_ver_slug = python_version[: max(len(v), 3)]
+-            condition_matches = op(py_ver_slug, v)
+-            if not condition_matches:
+-                raise RuntimeError(
+-                    "requires Python '{}' but the running Python is {}".format(
+-                        python_requires, '.'.join(map(str, python_version[:3]))
+-                    )
+-                )
+-
+-    def verify_required_python_runtime(s):
+-        @functools.wraps(s)
+-        def sw(**attrs):
+-            try:
+-                validate_required_python_or_fail(attrs.get('python_requires'))
+-            except RuntimeError as re:
+-                sys.exit('{} {!s}'.format(attrs['name'], re))
+-            return s(**attrs)
+-
+-        return sw
+-
+-    setuptools.setup = ignore_unknown_options(setuptools.setup)
+-    setuptools.setup = verify_required_python_runtime(setuptools.setup)
+-
+-    try:
+-        from configparser import ConfigParser, NoSectionError
+-    except ImportError:
+-        from ConfigParser import ConfigParser, NoSectionError
+-
+-        ConfigParser.read_file = ConfigParser.readfp
+-
+-    def maybe_read_files(d):
+-        """Read files if the string starts with `file:` marker."""
+-        FILE_FUNC_MARKER = 'file:'
+-
+-        d = d.strip()
+-        if not d.startswith(FILE_FUNC_MARKER):
+-            return d
+-        descs = []
+-        for fname in map(str.strip, str(d[len(FILE_FUNC_MARKER) :]).split(',')):
+-            with io.open(fname, encoding='utf-8') as f:
+-                descs.append(f.read())
+-        return ''.join(descs)
+-
+-    def cfg_val_to_list(v):
+-        """Turn config val to list and filter out empty lines."""
+-        return list(filter(bool, map(str.strip, str(v).strip().splitlines())))
+-
+-    def cfg_val_to_dict(v):
+-        """Turn config val to dict and filter out empty lines."""
+-        return dict(
+-            map(
+-                lambda l: list(map(str.strip, l.split('=', 1))),
+-                filter(bool, map(str.strip, str(v).strip().splitlines())),
+-            )
+-        )
+-
+-    def cfg_val_to_primitive(v):
+-        """Parse primitive config val to appropriate data type."""
+-        return json.loads(v.strip().lower())
+-
+-    def read_configuration(filepath):
+-        """Read metadata and options from setup.cfg located at filepath."""
+-        cfg = ConfigParser()
+-        with io.open(filepath, encoding='utf-8') as f:
+-            cfg.read_file(f)
+-
+-        md = dict(cfg.items('metadata'))
+-        for list_key in 'classifiers', 'keywords', 'project_urls':
+-            try:
+-                md[list_key] = cfg_val_to_list(md[list_key])
+-            except KeyError:
+-                pass
+-        try:
+-            md['long_description'] = maybe_read_files(md['long_description'])
+-        except KeyError:
+-            pass
+-        opt = dict(cfg.items('options'))
+-        for list_key in 'include_package_data', 'use_scm_version', 'zip_safe':
+-            try:
+-                opt[list_key] = cfg_val_to_primitive(opt[list_key])
+-            except KeyError:
+-                pass
+-        for list_key in 'scripts', 'install_requires', 'setup_requires':
+-            try:
+-                opt[list_key] = cfg_val_to_list(opt[list_key])
+-            except KeyError:
+-                pass
+-        try:
+-            opt['package_dir'] = cfg_val_to_dict(opt['package_dir'])
+-        except KeyError:
+-            pass
+-        try:
+-            opt_package_data = dict(cfg.items('options.package_data'))
+-            if not opt_package_data.get('', '').strip():
+-                opt_package_data[''] = opt_package_data['*']
+-                del opt_package_data['*']
+-        except (KeyError, NoSectionError):
+-            opt_package_data = {}
+-        try:
+-            opt_extras_require = dict(cfg.items('options.extras_require'))
+-            opt['extras_require'] = {}
+-            for k, v in opt_extras_require.items():
+-                opt['extras_require'][k] = cfg_val_to_list(v)
+-        except NoSectionError:
+-            pass
+-        opt['package_data'] = {}
+-        for k, v in opt_package_data.items():
+-            opt['package_data'][k] = cfg_val_to_list(v)
+-        try:
+-            opt_exclude_package_data = dict(cfg.items('options.exclude_package_data'))
+-            if (
+-                not opt_exclude_package_data.get('', '').strip()
+-                and '*' in opt_exclude_package_data
+-            ):
+-                opt_exclude_package_data[''] = opt_exclude_package_data['*']
+-                del opt_exclude_package_data['*']
+-        except NoSectionError:
+-            pass
+-        else:
+-            opt['exclude_package_data'] = {}
+-            for k, v in opt_exclude_package_data.items():
+-                opt['exclude_package_data'][k] = cfg_val_to_list(v)
+-        cur_pkgs = opt.get('packages', '').strip()
+-        if '\n' in cur_pkgs:
+-            opt['packages'] = cfg_val_to_list(opt['packages'])
+-        elif cur_pkgs.startswith('find:'):
+-            opt_packages_find = stringify_dict_contents(
+-                dict(cfg.items('options.packages.find'))
+-            )
+-            opt['packages'] = setuptools.find_packages(**opt_packages_find)
+-        return {'metadata': md, 'options': opt}
+-
+-
+-def cut_local_version_on_upload(version):
+-    """Generate a PEP440 local version if uploading to PyPI."""
+-    import os
+-    import setuptools_scm.version  # only present during setup time
+-
+-    IS_PYPI_UPLOAD = os.getenv('PYPI_UPLOAD') == 'true'  # set in tox.ini
+-    return (
+-        ''
+-        if IS_PYPI_UPLOAD
+-        else setuptools_scm.version.get_local_node_and_date(version)
++if __name__ == "__main__":
++    setuptools.setup(
++        use_scm_version=True, setup_requires=["setuptools_scm"],
+     )
+-
+-
+-if HAS_DIST_INFO_CMD:
+-
+-    class patched_dist_info(setuptools.command.dist_info.dist_info):
+-        def run(self):
+-            self.egg_base = str_if_nested_or_str(self.egg_base)
+-            return setuptools.command.dist_info.dist_info.run(self)
+-
+-
+-declarative_setup_params = read_configuration('setup.cfg')
+-"""Declarative metadata and options as read by setuptools."""
+-
+-
+-setup_params = {}
+-"""Explicit metadata for passing into setuptools.setup() call."""
+-
+-setup_params = dict(setup_params, **declarative_setup_params['metadata'])
+-setup_params = dict(setup_params, **declarative_setup_params['options'])
+-
+-if HAS_DIST_INFO_CMD:
+-    setup_params['cmdclass'] = {'dist_info': patched_dist_info}
+-
+-setup_params['use_scm_version'] = {'local_scheme': cut_local_version_on_upload}
+-
+-# Patch incorrectly decoded package_dir option
+-# ``egg_info`` demands native strings failing with unicode under Python 2
+-# Ref https://github.com/pypa/setuptools/issues/1136
+-setup_params = stringify_dict_contents(setup_params)
+-
+-
+-__name__ == '__main__' and setuptools.setup(**setup_params)

Added: head/devel/py-molecule/pkg-message
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ head/devel/py-molecule/pkg-message	Mon Mar 30 01:38:40 2020	(r529851)
@@ -0,0 +1,9 @@
+[
+{ type: install
+  message: <<EOM
+DOCKER option doesn't enable local DOCKER HOST as DOCKER is BROKEN in FreeBSD.
+This just installs DOCKER client to interact with a remote DOCKER HOST which has
+remote API enabled.
+EOM
+}
+]



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202003300138.02U1ce9M079355>