All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC][PATCH 1/2] distutils3-legacy: fallback for missing setup.py
@ 2021-11-24 20:15 Konrad Weihmann
  2021-11-24 21:02 ` [OE-core] " Tim Orling
  0 siblings, 1 reply; 3+ messages in thread
From: Konrad Weihmann @ 2021-11-24 20:15 UTC (permalink / raw)
  To: openembedded-core; +Cc: Konrad Weihmann

add a bbclass to disutils3 that generates a fallback setup.py in case
there is no setup.py available in the source dir, but a setup.cfg.

Use the mapping provided by
https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
to translate the most essential items to legacy setuptools.setup
dictitonary.

Signed-off-by: Konrad Weihmann <kweihmann@outlook.com>
---
 meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
 meta/classes/distutils3.bbclass        |   1 +
 2 files changed, 113 insertions(+)
 create mode 100644 meta/classes/distutils3-legacy.bbclass

diff --git a/meta/classes/distutils3-legacy.bbclass b/meta/classes/distutils3-legacy.bbclass
new file mode 100644
index 0000000000..266d30138f
--- /dev/null
+++ b/meta/classes/distutils3-legacy.bbclass
@@ -0,0 +1,112 @@
+# Helper to create a trimmed down setup.py from information found in
+# setup.cfg, in case there is no setup.py shipped with the sources
+
+# this functionality can be safely removed once the pypa community
+# comes up with a safe replacement for the functionality found in distutils3.bbclass
+
+def distutils_legacy_package_name(d):
+    # use pypi name or fall back to BPN
+    return d.getVar("PYPI_PACKAGE") or d.getVar('BPN').replace('python-', '').replace('python3-', '')
+
+DISTUTILS_LEGACY_VERSION ?= "${PV}"
+DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
+
+python do_create_setup_py_legacy() {
+    import os 
+
+    if os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py")):
+        return
+
+    from configparser import ConfigParser, NoOptionError, NoSectionError, ParsingError
+    import re
+
+    config = ConfigParser()
+    try:
+        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.cfg"))
+    except FileNotFoundError:
+        return
+
+    def _strip(x):
+        return re.sub(r"\s|\t|\n", "", x)
+
+    def get_section(section):
+        try:
+            return dict(config.items(section=section))
+        except (NoSectionError, ParsingError):
+            return None
+
+    def get_option(section, option):
+        try:
+            return config.get(section=section, option=option)
+        except (NoOptionError, NoSectionError, ParsingError):
+            return None
+
+    def extract_bool(section, option, default):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        return bool(_strip(_option))
+
+    def extract_str(section, option, default):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        return _strip(_option)
+
+    def extract_dict_vallist(section, default, delim=""):
+        _section = get_section(section)
+        if _section is None:
+            return default
+        return {_strip(k): [_strip(x) for x in re.split(delim, v)] if delim else [ _strip(v) ] for k, v in _section.items()}
+
+    def extract_dict(section, default):
+        _section = get_section(section)
+        if _section is None:
+            return default
+        return {_strip(k): _strip(v) for k, v in _section.items()}
+
+    def extract_list(section, option, default, delim):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        bb.warn("%s:%s -> %s" % (section, option, _option))
+        _listitems = re.split(delim, _option) if delim else [_option]
+        return [_strip(x) for x in _listitems]
+
+    def quote(x):
+        return '"%s"' % x
+
+    _pkginfo = {
+        "entry_points": extract_dict_vallist("options.entry_points", {}),
+        "include_package_data": extract_bool("options", "include_package_data", False),
+        "name": quote(extract_str("options", "name", d.getVar("DISTUTILS_LEGACY_NAME"))),
+        "package_data": extract_dict_vallist("options.package_data", {}, r"\s+|,"),
+        "packages": extract_list("options", "packages", [], ""),
+        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
+        "zip_safe": extract_bool("options", "zip_safe", False),
+        "install_requires": extract_list("options", "install_requires", [], r"\t+|\n+"),
+        "python_requires": quote(extract_str("options", "python_requires", ">0.0")),
+        "package_dir": extract_dict("package_dir", {}),
+        "py_modules": extract_list("options", "py_modules", [], r"\s+|,"),
+    }
+
+    # In case packages is using :find module
+    # we need to look for top level directories containing a __init__.py
+    if _pkginfo["packages"] == ["find:"]:
+        # top level search dir can be adjusted by options.packages.find option
+        _path = extract_str("options.packages.find", "where", "")
+        _pkginfo["packages"] = set(x.name for x in os.scandir(os.path.join(
+            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if os.path.isdir(x) and os.path.exists(os.path.join(x, "__init__.py")))
+
+    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py"), "w") as o:
+        o.write("import setuptools\n")
+        o.write("setuptools.setup(\n")
+        for k, v in _pkginfo.items():
+            o.write("%s = %s,\n" % (str(k), str(v)))
+        o.write(")")
+
+    
+}
+
+do_create_setup_py_legacy[doc] = "Create a fallback version of legacy setup.py if not existing"
+addtask do_create_setup_py_legacy before do_configure after do_patch do_prepare_recipe_sysroot
diff --git a/meta/classes/distutils3.bbclass b/meta/classes/distutils3.bbclass
index be645d37bd..f26f0d5184 100644
--- a/meta/classes/distutils3.bbclass
+++ b/meta/classes/distutils3.bbclass
@@ -1,4 +1,5 @@
 inherit distutils3-base
+inherit distutils3-legacy
 
 B = "${WORKDIR}/build"
 distutils_do_configure[cleandirs] = "${B}"
-- 
2.25.1



^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [OE-core] [RFC][PATCH 1/2] distutils3-legacy: fallback for missing setup.py
  2021-11-24 20:15 [RFC][PATCH 1/2] distutils3-legacy: fallback for missing setup.py Konrad Weihmann
@ 2021-11-24 21:02 ` Tim Orling
  2021-11-25 10:14   ` Konrad Weihmann
  0 siblings, 1 reply; 3+ messages in thread
From: Tim Orling @ 2021-11-24 21:02 UTC (permalink / raw)
  To: Konrad Weihmann; +Cc: openembedded-core

[-- Attachment #1: Type: text/plain, Size: 7252 bytes --]

On Wed, Nov 24, 2021 at 12:15 PM Konrad Weihmann <kweihmann@outlook.com>
wrote:

> add a bbclass to disutils3 that generates a fallback setup.py in case
> there is no setup.py available in the source dir, but a setup.cfg.
>

I’ll check this out later, but I do want to highlight that we will be
deprecating distutils bbclasses (and moving them to meta-python for
continuity).

https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610


We can refactor this series into the refactored setuptools3.bbclass (sans
distutils). I will do this if we merge these changes.

https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610

I’d like to hear what concerns others have.


> Use the mapping provided by
> https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
> to translate the most essential items to legacy setuptools.setup
> dictitonary.
>
> Signed-off-by: Konrad Weihmann <kweihmann@outlook.com>
> ---
>  meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
>  meta/classes/distutils3.bbclass        |   1 +
>  2 files changed, 113 insertions(+)
>  create mode 100644 meta/classes/distutils3-legacy.bbclass
>
> diff --git a/meta/classes/distutils3-legacy.bbclass
> b/meta/classes/distutils3-legacy.bbclass
> new file mode 100644
> index 0000000000..266d30138f
> --- /dev/null
> +++ b/meta/classes/distutils3-legacy.bbclass
> @@ -0,0 +1,112 @@
> +# Helper to create a trimmed down setup.py from information found in
> +# setup.cfg, in case there is no setup.py shipped with the sources
> +
> +# this functionality can be safely removed once the pypa community
> +# comes up with a safe replacement for the functionality found in
> distutils3.bbclass
> +
> +def distutils_legacy_package_name(d):
> +    # use pypi name or fall back to BPN
> +    return d.getVar("PYPI_PACKAGE") or d.getVar('BPN').replace('python-',
> '').replace('python3-', '')
> +
> +DISTUTILS_LEGACY_VERSION ?= "${PV}"
> +DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
> +
> +python do_create_setup_py_legacy() {
> +    import os
> +
> +    if os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
> "setup.py")):
> +        return
> +
> +    from configparser import ConfigParser, NoOptionError, NoSectionError,
> ParsingError
> +    import re
> +
> +    config = ConfigParser()
> +    try:
> +        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
> "setup.cfg"))
> +    except FileNotFoundError:
> +        return
> +
> +    def _strip(x):
> +        return re.sub(r"\s|\t|\n", "", x)
> +
> +    def get_section(section):
> +        try:
> +            return dict(config.items(section=section))
> +        except (NoSectionError, ParsingError):
> +            return None
> +
> +    def get_option(section, option):
> +        try:
> +            return config.get(section=section, option=option)
> +        except (NoOptionError, NoSectionError, ParsingError):
> +            return None
> +
> +    def extract_bool(section, option, default):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        return bool(_strip(_option))
> +
> +    def extract_str(section, option, default):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        return _strip(_option)
> +
> +    def extract_dict_vallist(section, default, delim=""):
> +        _section = get_section(section)
> +        if _section is None:
> +            return default
> +        return {_strip(k): [_strip(x) for x in re.split(delim, v)] if
> delim else [ _strip(v) ] for k, v in _section.items()}
> +
> +    def extract_dict(section, default):
> +        _section = get_section(section)
> +        if _section is None:
> +            return default
> +        return {_strip(k): _strip(v) for k, v in _section.items()}
> +
> +    def extract_list(section, option, default, delim):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        bb.warn("%s:%s -> %s" % (section, option, _option))
> +        _listitems = re.split(delim, _option) if delim else [_option]
> +        return [_strip(x) for x in _listitems]
> +
> +    def quote(x):
> +        return '"%s"' % x
> +
> +    _pkginfo = {
> +        "entry_points": extract_dict_vallist("options.entry_points", {}),
> +        "include_package_data": extract_bool("options",
> "include_package_data", False),
> +        "name": quote(extract_str("options", "name",
> d.getVar("DISTUTILS_LEGACY_NAME"))),
> +        "package_data": extract_dict_vallist("options.package_data", {},
> r"\s+|,"),
> +        "packages": extract_list("options", "packages", [], ""),
> +        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
> +        "zip_safe": extract_bool("options", "zip_safe", False),
> +        "install_requires": extract_list("options", "install_requires",
> [], r"\t+|\n+"),
> +        "python_requires": quote(extract_str("options",
> "python_requires", ">0.0")),
> +        "package_dir": extract_dict("package_dir", {}),
> +        "py_modules": extract_list("options", "py_modules", [], r"\s+|,"),
> +    }
> +
> +    # In case packages is using :find module
> +    # we need to look for top level directories containing a __init__.py
> +    if _pkginfo["packages"] == ["find:"]:
> +        # top level search dir can be adjusted by options.packages.find
> option
> +        _path = extract_str("options.packages.find", "where", "")
> +        _pkginfo["packages"] = set(x.name for x in
> os.scandir(os.path.join(
> +            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if os.path.isdir(x)
> and os.path.exists(os.path.join(x, "__init__.py")))
> +
> +    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py"),
> "w") as o:
> +        o.write("import setuptools\n")
> +        o.write("setuptools.setup(\n")
> +        for k, v in _pkginfo.items():
> +            o.write("%s = %s,\n" % (str(k), str(v)))
> +        o.write(")")
> +
> +
> +}
> +
> +do_create_setup_py_legacy[doc] = "Create a fallback version of legacy
> setup.py if not existing"
> +addtask do_create_setup_py_legacy before do_configure after do_patch
> do_prepare_recipe_sysroot
> diff --git a/meta/classes/distutils3.bbclass
> b/meta/classes/distutils3.bbclass
> index be645d37bd..f26f0d5184 100644
> --- a/meta/classes/distutils3.bbclass
> +++ b/meta/classes/distutils3.bbclass
> @@ -1,4 +1,5 @@
>  inherit distutils3-base
> +inherit distutils3-legacy
>
>  B = "${WORKDIR}/build"
>  distutils_do_configure[cleandirs] = "${B}"
> --
> 2.25.1
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#158741):
> https://lists.openembedded.org/g/openembedded-core/message/158741
> Mute This Topic: https://lists.openembedded.org/mt/87289428/924729
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [
> ticotimo@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
>

[-- Attachment #2: Type: text/html, Size: 10259 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [OE-core] [RFC][PATCH 1/2] distutils3-legacy: fallback for missing setup.py
  2021-11-24 21:02 ` [OE-core] " Tim Orling
@ 2021-11-25 10:14   ` Konrad Weihmann
  0 siblings, 0 replies; 3+ messages in thread
From: Konrad Weihmann @ 2021-11-25 10:14 UTC (permalink / raw)
  To: Tim Orling; +Cc: openembedded-core



On 24.11.21 22:02, Tim Orling wrote:
> 
> 
> On Wed, Nov 24, 2021 at 12:15 PM Konrad Weihmann <kweihmann@outlook.com 
> <mailto:kweihmann@outlook.com>> wrote:
> 
>     add a bbclass to disutils3 that generates a fallback setup.py in case
>     there is no setup.py available in the source dir, but a setup.cfg.
> 
> 
> I’ll check this out later, but I do want to highlight that we will be 
> deprecating distutils bbclasses (and moving them to meta-python for 
> continuity).
> 
> https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610 
> <https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610>

Not sure if we should support it at all after the rework is done.
Most of the python modules that haven't made at least the move to 
setup.cfg will stop working with py3.12 anyway - putting some pressure 
on the respective upstream to migrate.
So what is the benefit in moving distutils-support to meta-python (which 
btw bundles poky/meta to openembedded-layer - a move that is discouraged 
by a couple of projects I came across so far)?

I mean the patches currently in the works likely will affect kirkstone+ 
releases only, right? so current already released branches will remain 
unaffected by this switch - or is there a plan to even backport this 
change to other releases?

> 
> 
> We can refactor this series into the refactored setuptools3.bbclass 
> (sans distutils). I will do this if we merge these changes.
> 
> https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610 
> <https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610>

Fine for me - I would be also be fine, if we could use my patch for an 
intermediate period, till the removal of distutils is done.

> 
> I’d like to hear what concerns others have.
> 
> 
>     Use the mapping provided by
>     https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
>     <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>
>     to translate the most essential items to legacy setuptools.setup
>     dictitonary.
> 
>     Signed-off-by: Konrad Weihmann <kweihmann@outlook.com
>     <mailto:kweihmann@outlook.com>>
>     ---
>       meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
>       meta/classes/distutils3.bbclass        |   1 +
>       2 files changed, 113 insertions(+)
>       create mode 100644 meta/classes/distutils3-legacy.bbclass
> 
>     diff --git a/meta/classes/distutils3-legacy.bbclass
>     b/meta/classes/distutils3-legacy.bbclass
>     new file mode 100644
>     index 0000000000..266d30138f
>     --- /dev/null
>     +++ b/meta/classes/distutils3-legacy.bbclass
>     @@ -0,0 +1,112 @@
>     +# Helper to create a trimmed down setup.py from information found in
>     +# setup.cfg, in case there is no setup.py shipped with the sources
>     +
>     +# this functionality can be safely removed once the pypa community
>     +# comes up with a safe replacement for the functionality found in
>     distutils3.bbclass
>     +
>     +def distutils_legacy_package_name(d):
>     +    # use pypi name or fall back to BPN
>     +    return d.getVar("PYPI_PACKAGE") or
>     d.getVar('BPN').replace('python-', '').replace('python3-', '')
>     +
>     +DISTUTILS_LEGACY_VERSION ?= "${PV}"
>     +DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
>     +
>     +python do_create_setup_py_legacy() {
>     +    import os
>     +
>     +    if
>     os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.py")):
>     +        return
>     +
>     +    from configparser import ConfigParser, NoOptionError,
>     NoSectionError, ParsingError
>     +    import re
>     +
>     +    config = ConfigParser()
>     +    try:
>     +        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.cfg"))
>     +    except FileNotFoundError:
>     +        return
>     +
>     +    def _strip(x):
>     +        return re.sub(r"\s|\t|\n", "", x)
>     +
>     +    def get_section(section):
>     +        try:
>     +            return dict(config.items(section=section))
>     +        except (NoSectionError, ParsingError):
>     +            return None
>     +
>     +    def get_option(section, option):
>     +        try:
>     +            return config.get(section=section, option=option)
>     +        except (NoOptionError, NoSectionError, ParsingError):
>     +            return None
>     +
>     +    def extract_bool(section, option, default):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        return bool(_strip(_option))
>     +
>     +    def extract_str(section, option, default):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        return _strip(_option)
>     +
>     +    def extract_dict_vallist(section, default, delim=""):
>     +        _section = get_section(section)
>     +        if _section is None:
>     +            return default
>     +        return {_strip(k): [_strip(x) for x in re.split(delim, v)]
>     if delim else [ _strip(v) ] for k, v in _section.items()}
>     +
>     +    def extract_dict(section, default):
>     +        _section = get_section(section)
>     +        if _section is None:
>     +            return default
>     +        return {_strip(k): _strip(v) for k, v in _section.items()}
>     +
>     +    def extract_list(section, option, default, delim):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        bb.warn("%s:%s -> %s" % (section, option, _option))
>     +        _listitems = re.split(delim, _option) if delim else [_option]
>     +        return [_strip(x) for x in _listitems]
>     +
>     +    def quote(x):
>     +        return '"%s"' % x
>     +
>     +    _pkginfo = {
>     +        "entry_points":
>     extract_dict_vallist("options.entry_points", {}),
>     +        "include_package_data": extract_bool("options",
>     "include_package_data", False),
>     +        "name": quote(extract_str("options", "name",
>     d.getVar("DISTUTILS_LEGACY_NAME"))),
>     +        "package_data":
>     extract_dict_vallist("options.package_data", {}, r"\s+|,"),
>     +        "packages": extract_list("options", "packages", [], ""),
>     +        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
>     +        "zip_safe": extract_bool("options", "zip_safe", False),
>     +        "install_requires": extract_list("options",
>     "install_requires", [], r"\t+|\n+"),
>     +        "python_requires": quote(extract_str("options",
>     "python_requires", ">0.0")),
>     +        "package_dir": extract_dict("package_dir", {}),
>     +        "py_modules": extract_list("options", "py_modules", [],
>     r"\s+|,"),
>     +    }
>     +
>     +    # In case packages is using :find module
>     +    # we need to look for top level directories containing a
>     __init__.py
>     +    if _pkginfo["packages"] == ["find:"]:
>     +        # top level search dir can be adjusted by
>     options.packages.find option
>     +        _path = extract_str("options.packages.find", "where", "")
>     +        _pkginfo["packages"] = set(x.name <http://x.name> for x in
>     os.scandir(os.path.join(
>     +            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if
>     os.path.isdir(x) and os.path.exists(os.path.join(x, "__init__.py")))
>     +
>     +    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.py"), "w") as o:
>     +        o.write("import setuptools\n")
>     +        o.write("setuptools.setup(\n")
>     +        for k, v in _pkginfo.items():
>     +            o.write("%s = %s,\n" % (str(k), str(v)))
>     +        o.write(")")
>     +
>     +
>     +}
>     +
>     +do_create_setup_py_legacy[doc] = "Create a fallback version of
>     legacy setup.py if not existing"
>     +addtask do_create_setup_py_legacy before do_configure after
>     do_patch do_prepare_recipe_sysroot
>     diff --git a/meta/classes/distutils3.bbclass
>     b/meta/classes/distutils3.bbclass
>     index be645d37bd..f26f0d5184 100644
>     --- a/meta/classes/distutils3.bbclass
>     +++ b/meta/classes/distutils3.bbclass
>     @@ -1,4 +1,5 @@
>       inherit distutils3-base
>     +inherit distutils3-legacy
> 
>       B = "${WORKDIR}/build"
>       distutils_do_configure[cleandirs] = "${B}"
>     -- 
>     2.25.1
> 
> 
>     -=-=-=-=-=-=-=-=-=-=-=-
>     Links: You receive all messages sent to this group.
>     View/Reply Online (#158741):
>     https://lists.openembedded.org/g/openembedded-core/message/158741
>     <https://lists.openembedded.org/g/openembedded-core/message/158741>
>     Mute This Topic: https://lists.openembedded.org/mt/87289428/924729
>     <https://lists.openembedded.org/mt/87289428/924729>
>     Group Owner: openembedded-core+owner@lists.openembedded.org
>     <mailto:openembedded-core%2Bowner@lists.openembedded.org>
>     Unsubscribe:
>     https://lists.openembedded.org/g/openembedded-core/unsub
>     <https://lists.openembedded.org/g/openembedded-core/unsub>
>     [ticotimo@gmail.com <mailto:ticotimo@gmail.com>]
>     -=-=-=-=-=-=-=-=-=-=-=-
> 


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2021-11-25 10:14 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-11-24 20:15 [RFC][PATCH 1/2] distutils3-legacy: fallback for missing setup.py Konrad Weihmann
2021-11-24 21:02 ` [OE-core] " Tim Orling
2021-11-25 10:14   ` Konrad Weihmann

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.