Repository: miurahr/aqtinstall (python)

Issue:
Allow `list-qt` to print a list of archives for use with the `--archives` option
Let's say you want to install Qt 5.15.0 for linux desktop, but only install the 7z archives for {qtbase, qtdeclarative, icu}. You would use this command:

```
aqt install-qt linux desktop 5.15.0 --archives qtbase qtdeclarative icu
```

This feature is documented  at https://aqtinstall.readthedocs.io/en/latest/cli.html#cmdoption-list-tool-archives, and there's an example under https://aqtinstall.readthedocs.io/en/latest/cli.html#command-examples; it's the third example in the list. 

I think it would be useful to add an option that lists archives to `list-qt`, but I cannot do it without an idea of what the interface would look like. I would like to use this issue to figure out what that interface would look like.

_Originally posted by @ddalcino in https://github.com/miurahr/aqtinstall/issues/382#issuecomment-911653563_

Pull request description:
Add `list-qt --archives` feature
Fix #394

Patch:
diff --git a/aqt/helper.py b/aqt/helper.py
index b723552..2bc6407 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -28,13 +28,13 @@ import sys
 import xml.etree.ElementTree as ElementTree
 from logging import getLogger
 from logging.handlers import QueueListener
-from typing import Callable, Dict, Iterable, List, Optional
+from typing import Callable, Dict, List
 from urllib.parse import urlparse
 
 import requests
 import requests.adapters
 
-from aqt.exceptions import ArchiveConnectionError, ArchiveDownloadError
+from aqt.exceptions import ArchiveConnectionError, ArchiveDownloadError, ArchiveListError
 
 
 def _get_meta(url: str):
@@ -171,34 +171,26 @@ class MyQueueListener(QueueListener):
 def xml_to_modules(
     xml_text: str,
     predicate: Callable[[ElementTree.Element], bool],
-    keys_to_keep: Optional[Iterable[str]] = None,
 ) -> Dict[str, Dict[str, str]]:
     """Converts an XML document to a dict of `PackageUpdate` dicts, indexed by `Name` attribute.
     Only report elements that satisfy `predicate(element)`.
-    Only report keys in the list `keys_to_keep`.
+    Reports all keys available in the PackageUpdate tag as strings.
 
     :param xml_text: The entire contents of an xml file
     :param predicate: A function that decides which elements to keep or discard
-    :param keys_to_keep: A list of which tags in the element should be kept.
-                        If the list is empty, then no tags will be kept.
-                        If the list is None, then all tags will be kept.
     """
     try:
         parsed_xml = ElementTree.fromstring(xml_text)
-    except ElementTree.ParseError:
-        return {}
+    except ElementTree.ParseError as perror:
+        raise ArchiveListError(f"Downloaded metadata is corrupted. {perror}") from perror
     packages = {}
     for packageupdate in parsed_xml.iter("PackageUpdate"):
         if predicate and not predicate(packageupdate):
             continue
         name = packageupdate.find("Name").text
         packages[name] = {}
-        if keys_to_keep is None:
-            for child in packageupdate:
-                packages[name][child.tag] = child.text
-        else:
-            for key in keys_to_keep:
-                packages[name][key] = getattr(packageupdate.find(key), "text", None)
+        for child in packageupdate:
+            packages[name][child.tag] = child.text
     return packages
 
 
diff --git a/aqt/installer.py b/aqt/installer.py
index 1c0cbc6..e7a60c3 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -442,7 +442,7 @@ class Cli:
         else:
             modules_ver, modules_query = None, None
 
-        for version_str in (modules_ver, args.extensions, args.arch):
+        for version_str in (modules_ver, args.extensions, args.arch, args.archives[0] if args.archives else None):
             Cli._validate_version_str(version_str, allow_latest=True, allow_empty=True)
 
         spec = None
@@ -464,6 +464,7 @@ class Cli:
             modules_query=modules_query,
             extensions_ver=args.extensions,
             architectures_ver=args.arch,
+            archives_query=args.archives,
         )
         show_list(meta)
 
@@ -601,18 +602,18 @@ class Cli:
             "list-qt",
             formatter_class=argparse.RawDescriptionHelpFormatter,
             epilog="Examples:\n"
-            "$ aqt list-qt mac                                            # print all targets for Mac OS\n"
-            "$ aqt list-qt mac desktop                                    # print all versions of Qt 5\n"
-            "$ aqt list-qt mac desktop --extension wasm                   # print all wasm versions of Qt 5\n"
-            '$ aqt list-qt mac desktop --spec "5.9"                       # print all versions of Qt 5.9\n'
-            '$ aqt list-qt mac desktop --spec "5.9" --latest-version      # print latest Qt 5.9\n'
-            "$ aqt list-qt mac desktop --modules 5.12.0 clang_64          # print modules for 5.12.0\n"
-            "$ aqt list-qt mac desktop --spec 5.9 --modules latest  # print modules for latest 5.9\n"
-            "$ aqt list-qt mac desktop --extensions 5.9.0                 # print choices for --extension flag\n"
-            "$ aqt list-qt mac desktop --arch 5.9.9                       "
-            "# print architectures for 5.9.9/mac/desktop\n"
-            "$ aqt list-qt mac desktop --arch latest                      "
-            "# print architectures for the latest Qt 5\n",
+            "$ aqt list-qt mac                                                # print all targets for Mac OS\n"
+            "$ aqt list-qt mac desktop                                        # print all versions of Qt 5\n"
+            "$ aqt list-qt mac desktop --extension wasm                       # print all wasm versions of Qt 5\n"
+            '$ aqt list-qt mac desktop --spec "5.9"                           # print all versions of Qt 5.9\n'
+            '$ aqt list-qt mac desktop --spec "5.9" --latest-version          # print latest Qt 5.9\n'
+            "$ aqt list-qt mac desktop --modules 5.12.0 clang_64              # print modules for 5.12.0\n"
+            "$ aqt list-qt mac desktop --spec 5.9 --modules latest clang_64   # print modules for latest 5.9\n"
+            "$ aqt list-qt mac desktop --extensions 5.9.0                     # print choices for --extension flag\n"
+            "$ aqt list-qt mac desktop --arch 5.9.9                           # print architectures for 5.9.9/mac/desktop\n"
+            "$ aqt list-qt mac desktop --arch latest                          # print architectures for the latest Qt 5\n"
+            "$ aqt list-qt mac desktop --archives 5.9.0 clang_64              # list archives in base Qt installation\n"
+            "$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info  # list archives in debug_info module\n",
         )
         list_parser.add_argument("host", choices=["linux", "mac", "windows"], help="host os name")
         list_parser.add_argument(
@@ -665,6 +666,17 @@ class Cli:
             action="store_true",
             help="print only the newest version available",
         )
+        output_modifier_exclusive_group.add_argument(
+            "--archives",
+            type=str,
+            nargs="+",
+            help="print the archives available for Qt base or modules. "
+            "If two arguments are provided, the first two arguments must be 'VERSION | latest' and "
+            "'ARCHITECTURE', and this command will print all archives associated with the base Qt package. "
+            "If more than two arguments are provided, the remaining arguments will be interpreted as modules, "
+            "and this command will print all archives associated with those modules. "
+            "At least two arguments are required.",
+        )
         list_parser.set_defaults(func=self.run_list_qt)
 
     def _make_list_tool_parser(self, subparsers: argparse._SubParsersAction):
diff --git a/aqt/metadata.py b/aqt/metadata.py
index bca3bbe..c74b035 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -34,7 +34,7 @@ from semantic_version import SimpleSpec as SemanticSimpleSpec
 from semantic_version import Version as SemanticVersion
 from texttable import Texttable
 
-from aqt.exceptions import ArchiveConnectionError, ArchiveDownloadError, CliInputError, EmptyMetadata
+from aqt.exceptions import ArchiveConnectionError, ArchiveDownloadError, ArchiveListError, CliInputError, EmptyMetadata
 from aqt.helper import Settings, getUrl, xml_to_modules
 
 
@@ -365,6 +365,7 @@ class MetadataFactory:
         modules_query: Optional[Tuple[str, str]] = None,
         extensions_ver: Optional[str] = None,
         architectures_ver: Optional[str] = None,
+        archives_query: Optional[List[str]] = None,
         tool_name: Optional[str] = None,
         is_long_listing: bool = False,
     ):
@@ -378,6 +379,7 @@ class MetadataFactory:
         :param modules_query:       [Version of Qt, architecture] for which to list modules
         :param extensions_ver:      Version of Qt for which to list extensions
         :param architectures_ver:   Version of Qt for which to list architectures
+        :param archives_query:      [Qt_Version, architecture, *module_names]: used to print list of archives
         :param tool_name:           Name of a tool, without architecture, ie "tools_qtcreator" or "tools_ifw"
         :param is_long_listing:     If true, long listing is used for tools output
         """
@@ -411,6 +413,12 @@ class MetadataFactory:
         elif architectures_ver:
             self.request_type = "architectures"
             self._action = lambda: self.fetch_arches(self._to_version(architectures_ver))
+        elif archives_query:
+            if len(archives_query) < 2:
+                raise CliInputError("The '--archives' flag requires a 'QT_VERSION' and an 'ARCHITECTURE' parameter.")
+            self.request_type = "archives for modules" if len(archives_query) > 2 else "archives for qt"
+            version, arch, modules = archives_query[0], archives_query[1], archives_query[2:]
+            self._action = lambda: self.fetch_archives(self._to_version(version), arch, modules)
         else:
             self.request_type = "versions"
             self._action = self.fetch_versions
@@ -652,6 +660,41 @@ class MetadataFactory:
                 modules.add(module)
         return sorted(modules)
 
+    def fetch_archives(self, version: Version, arch: str, modules: List[str]) -> List[str]:
+        qt_version_str = self._get_qt_version_str(version)
+        nonempty = MetadataFactory._has_nonempty_downloads
+
+        def all_modules(element: ElementTree.Element) -> bool:
+            _module, _arch = element.find("Name").text.split(".")[-2:]
+            return _arch == arch and _module != qt_version_str and nonempty(element)
+
+        def specify_modules(element: ElementTree.Element) -> bool:
+            _module, _arch = element.find("Name").text.split(".")[-2:]
+            return _arch == arch and _module in modules and nonempty(element)
+
+        def no_modules(element: ElementTree.Element) -> bool:
+            name: Optional[str] = element.find("Name").text
+            return name and name.endswith(f".{qt_version_str}.{arch}") and nonempty(element)
+
+        predicate = no_modules if not modules else all_modules if "all" in modules else specify_modules
+        try:
+            mod_metadata = self._fetch_module_metadata(self.archive_id.to_folder(qt_version_str), predicate=predicate)
+        except (AttributeError,) as e:
+            raise ArchiveListError(f"Downloaded metadata is corrupted. {e}") from e
+
+        # Did we find all requested modules?
+        if modules and "all" not in modules:
+            requested_set = set(modules)
+            actual_set = set([_name.split(".")[-2] for _name in mod_metadata.keys()])
+            not_found = sorted(requested_set.difference(actual_set))
+            if not_found:
+                raise CliInputError(
+                    f"The requested modules were not located: {not_found}", suggested_action=suggested_follow_up(self)
+                )
+
+        csv_lists = [mod["DownloadableArchives"] for mod in mod_metadata.values()]
+        return sorted(set([arc.split("-")[0] for csv in csv_lists for arc in csv.split(", ")]))
+
     def describe_filters(self) -> str:
         if self.spec is None:
             return str(self.archive_id)
@@ -663,6 +706,8 @@ def suggested_follow_up(meta: MetadataFactory) -> List[str]:
     msg = []
     list_cmd = "list-tool" if meta.archive_id.is_tools() else "list-qt"
     base_cmd = "aqt {0} {1.host} {1.target}".format(list_cmd, meta.archive_id)
+    versions_msg = f"Please use '{base_cmd}' to show versions of Qt available."
+    arches_msg = f"Please use '{base_cmd} --arch <QT_VERSION>' to show architectures available."
     if meta.archive_id.extension:
         msg.append(f"Please use '{base_cmd} --extensions <QT_VERSION>' to list valid extensions.")
 
@@ -677,6 +722,10 @@ def suggested_follow_up(meta: MetadataFactory) -> List[str]:
         msg.append(f"Please use '{base_cmd}' to show versions of Qt available.")
         if meta.request_type == "modules":
             msg.append(f"Please use '{base_cmd} --arch <QT_VERSION>' to list valid architectures.")
+    elif meta.request_type == "archives for modules":
+        msg.extend([versions_msg, arches_msg, f"Please use '{base_cmd} --modules <QT_VERSION>' to show modules available."])
+    elif meta.request_type == "archives for qt":
+        msg.extend([versions_msg, arches_msg])
 
     return msg
 
diff --git a/docs/cli.rst b/docs/cli.rst
index 7599e5d..87a3451 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -46,6 +46,7 @@ list-qt command
                 [--modules    (<Qt version> | latest) <architecture> |
                  --extensions (<Qt version> | latest) |
                  --arch       (<Qt version> | latest) |
+                 --archives   (<Qt version> | latest) architecture [modules...]
                  --latest-version]
                 <host> [<target>]
 
@@ -106,6 +107,25 @@ List available versions of Qt, targets, extensions, modules, and architectures.
     available for Qt 5.X.Y with a host/target/extension, or the latest version
     of Qt if ``latest`` is specified.
 
+.. _`list archives flag`:
+.. option:: --archives (<Qt version> | latest) architecture [modules...]
+
+    This flag requires a list of at least two arguments: 'Qt version' and 'architecture'.
+    The 'Qt version' argument can be in the format "5.X.Y" or the "latest" keyword.
+    You can use the ``--arch`` flag to see a list of acceptable values for the 'architecture' argument.
+    Any following arguments must be the names of modules available for the preceding version and architecture.
+    You can use the ``--modules`` flag to see a list of acceptable values.
+
+    If you do not add a list of modules to this flag, this command will print a
+    list of all the archives that make up the base Qt installation.
+
+    If you add a list of modules to this flag, this command will print a list
+    of all the archives that make up the specified modules.
+
+    The purpose of this command is to show you what arguments you can pass to the
+    :ref:`archives flag <install archives flag>` when using the ``install-*`` commands.
+    This flag allows you to avoid installing parts of Qt that you do not need.
+
 .. option:: --latest-version
 
     Print only the newest version available
@@ -236,6 +256,7 @@ are described here:
         aqt install-* <host> <target> <Qt version> <arch> -m all
 
 
+.. _install archives flag:
 .. option:: --archives <list of archives>
 
     [Advanced] Specify subset of archives to **limit** installed archives.
@@ -245,6 +266,9 @@ are described here:
 
     This option is applicable to all the ``install-*`` commands except for ``install-tool``.
 
+    You can print a list of all acceptable values to use with this command by
+    using ``aqt list-qt`` with the :ref:`archives flag <list archives flag>`.
+
 
 .. _qt installation command:
 
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 8a8bf5a..09a13ce 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -407,3 +407,211 @@ the installer may use the ``installscript.qs`` script to make additional changes
 to your system. If you need those changes to occur, it will be your responsibility
 to make those changes happen, because ``aqt`` is not capable of running this script.
 
+
+Installing a subset of Qt archives [Advanced]
+---------------------------------------------
+
+Introduction
+````````````
+
+You may have noticed that by default, ``aqt install-qt`` installs a lot of
+archives that you may or may not need, and a typical installation can take up
+more disk space than necessary. If you installed the module ``debug_info``, it
+may have installed more than 1 gigabyte of data. This section will help you to
+reduce the footprint of your Qt installation.
+
+.. note::
+
+    Be careful about using the ``--archives`` flag; it is marked `Advanced` for a reason!
+    It is very easy to misuse this command and end up with a Qt installation that
+    is missing the components that you need.
+    Don't use it unless you know what you are doing!
+
+
+Minimum Qt Installation
+```````````````````````
+
+Normally, when you run ``aqt install-qt``, the program will print a long list
+of archives that it is downloading, extracting, and installing,
+including ``qtbase``, ``qtmultimedia``, ``qt3d``, and ~25 more items.
+We can use the ``--archives`` flag to choose which of these archives we will
+actually install.
+
+.. note::
+    In this documentation, **"modules"**, **"archives"**, and **"the base Qt installation"**
+    refer to different things, and are defined here:
+
+    - **Archives**: In this context, an **archive** is a bundle of files compressed
+      with the 7zip algorithm.
+      It exists on a disk drive as a file with the extension ``.7z``.
+
+    - **Modules**: The Qt repository organizes groups of archives into modules.
+      A **module** contains one or more **archives**.
+
+    - **the base Qt installation**:
+      By definition, this is just another **module** that contains 20-30 **archives**.
+      This documentation refers to it as **the base Qt installation** instead of
+      a **module** for several reasons:
+
+        - The ``aqt install-qt`` installs this module by default.
+        - You cannot specify this module with ``aqt install-qt --modules``.
+        - The ``aqt list-qt --modules`` command is incapable of printing this module.
+        - ``aqt`` transforms the names of modules as they exist in the Qt repository
+          so that they are easier to read and write.
+          If the name of **the base Qt installation** were transformed using the
+          same rules, the name would be empty.
+
+          The fully-qualified name of the **base Qt installation** module is
+          usually something like ``qt.qt6.620.gcc_64``.
+          The fully-qualified name of the ``qtcharts`` module could be
+          something like ``qt.qt6.620.qtcharts.gcc_64``.
+          It would be difficult to read and write a list of 20 modules with the prefix
+          ``qt.qt6.620.`` and the suffix ``.gcc_64``, because these parts are
+          repetitive and not meaningful. Only the ``qtcharts`` part is useful.
+
+Let's say that we want to install Qt 5.15.2 for Linux desktop, using the gcc_64 architecture.
+The ``qtbase`` archive includes the bare minimum for a working Qt installation,
+and we can install it alone with the ``--archives`` flag:
+
+.. code-block:: console
+
+    $ aqt install-qt linux desktop 5.15.2 --archives qtbase
+
+This time, ``aqt install-qt`` will only install one archive, ``qtbase``, instead
+of the ~27 archives it installs by default.
+
+Installing More Than The Bare Minimum
+`````````````````````````````````````
+
+Let's say that the ``qtbase`` archive is missing some features that you need.
+Using the ``--archives qtbase`` flag causes ``aqt install-qt`` to omit roughly 27 archives.
+We can print a list of these archives with the ``aqt list-qt --archives`` command:
+
+.. code-block:: console
+
+    $ aqt list-qt linux desktop --archives 5.15.2 gcc_64
+    icu qt3d qtbase qtconnectivity qtdeclarative qtgamepad qtgraphicaleffects qtimageformats
+    qtlocation qtmultimedia qtquickcontrols qtquickcontrols2 qtremoteobjects qtscxml
+    qtsensors qtserialbus qtserialport qtspeech qtsvg qttools qttranslations qtwayland
+    qtwebchannel qtwebsockets qtwebview qtx11extras qtxmlpatterns
+
+Here, we have used the ``--archives`` flag with two arguments:
+the version of Qt we are interested in, and the architecture we are using.
+As a result, the command printed a list of archives that are part of the base
+(non-minimal) Qt installation.
+
+Let's say we need to use ``qtmultimedia``, ``qtdeclarative``, ``qtsvg``, and
+nothing else. Remember that the ``qtbase`` archive is required for a minimal
+working Qt installation. We can install these archives using this command:
+
+.. code-block:: console
+
+    $ aqt install-qt linux desktop 5.15.2 --archives qtbase qtmultimedia qtdeclarative qtsvg
+
+Installing Modules With Archives Specified
+``````````````````````````````````````````
+
+Now let's say we need to install the ``qtcharts`` and ``qtlottie`` modules.
+Let's see what archives are part of these modules:
+
+.. code-block:: console
+
+    $ aqt list-qt linux desktop --archives 5.15.2 gcc_64 qtcharts qtlottie
+    qtcharts qtlottie
+
+This time, the command only printed two archives.
+The ``qtcharts`` and ``qtlottie`` modules contain one archive per module.
+
+.. note::
+    This command printed all archives associated with the modules we provided it,
+    and it did not print any of the archives associated with the base Qt installation.
+    This happened because we added a list of modules to the ``--archives`` flag.
+
+    .. code-block:: bash
+
+        # Print archives from the base Qt installation
+        aqt list-qt linux desktop --archives 5.15.2 gcc_64
+
+        # Print archives from the specified modules, but not the base Qt installation
+        aqt list-qt linux desktop --archives 5.15.2 gcc_64 qtcharts qtlottie
+
+
+Now let's install Qt with these modules:
+
+.. code-block:: console
+
+    $ aqt install-qt linux desktop 5.15.2 --modules qtcharts qtlottie \
+                                          --archives qtbase qtcharts qtlottie
+
+This command will install the bare minimum Qt, along with the modules ``qtcharts`` and ``qtlottie``.
+
+Remember that when using the ``--archives`` flag, you must specify every archive you
+intend to install.
+
+.. code-block:: bash
+
+    # Ways to misuse the `--archives` flag:
+
+    # This command installs modules without a working Qt installation,
+    # because `--archives` is missing `qtbase`.
+    aqt install-qt linux desktop 5.15.2 --modules qtcharts qtlottie \
+                                        --archives qtcharts qtlottie
+
+    # This command skips installation of the specified modules,
+    # because `--archives` is missing `qtcharts` and `qtlottie`.
+    aqt install-qt linux desktop 5.15.2 --modules qtcharts qtlottie \
+                                        --archives qtbase
+
+    # This command installs qtbase, but neither `qtcharts` nor `qtlottie`,
+    # because `--modules` is missing those names.
+    aqt install-qt linux desktop 5.15.2 --archives qtbase qtcharts qtlottie
+
+.. note::
+
+    If you install modules that depend on archives other than ``qtbase``,
+    and you have not specified those archives after the ``--archives`` flag,
+    those modules will not work when you try to use them.
+
+Installing the ``debug_info`` module
+````````````````````````````````````
+
+Now let's say we need to install the ``debug_info`` module, which is particularly large.
+We do not want to install all of it, so we can use ``aqt list-qt --archives`` again
+to print which archives are part of the ``debug_info`` module:
+
+.. code-block:: console
+
+    $ aqt list-qt linux desktop --archives 5.15.2 gcc_64 debug_info
+    qt3d qtbase qtcharts qtconnectivity qtdatavis3d qtdeclarative qtgamepad qtgraphicaleffects
+    qtimageformats qtlocation qtlottie qtmultimedia qtnetworkauth qtpurchasing qtquick3d
+    qtquickcontrols qtquickcontrols2 qtquicktimeline qtremoteobjects qtscript qtscxml qtsensors
+    qtserialbus qtserialport qtspeech qtsvg qttools qtvirtualkeyboard qtwayland qtwebchannel
+    qtwebengine qtwebglplugin qtwebsockets qtwebview qtx11extras qtxmlpatterns
+
+This is a lot of archives.
+Note that there's a name collision between the ``debug_info`` archives and the
+archives in every other module/Qt base install:
+this is because there's a ``debug_info`` archive that corresponds to almost
+every other archive available.
+
+Let's install Qt with ``qtcharts`` and ``debug_info`` with some archives specified:
+
+.. code-block:: console
+
+    $ aqt install-qt linux desktop --modules qtcharts debug_info \
+                                   --archives qtcharts qtbase qtdeclarative
+
+Notice what we did here: We specified the ``qtcharts`` and ``debug_info`` modules,
+and we specified the ``qtbase``, ``qtcharts``, and ``qtdeclarative`` archives.
+This will install a total of 6 archives:
+
+- the 3 archives named ``qtbase``, ``qtcharts``, and ``qtdeclarative`` from the ``debug_info`` module,
+- the 1 archive ``qtcharts`` from the ``qtcharts`` module, and
+- the 2 archives ``qtbase`` and ``qtdeclarative`` from the base Qt installation.
+
+.. note::
+    At present, ``aqt install-qt`` is incapable of installing any archive from
+    the ``debug_info`` module without also installing the corresponding module
+    from the base Qt installation.
+    For instance, you cannot install the ``debug_info`` archive for ``qtbase``
+    without also installing the usual ``qtbase`` archive.


Test patch:
diff --git a/tests/test_list.py b/tests/test_list.py
index 6d5a5f2..1e1ca3b 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -9,7 +9,14 @@ from urllib.parse import urlparse
 
 import pytest
 
-from aqt.exceptions import AqtException, ArchiveConnectionError, ArchiveDownloadError, CliInputError, EmptyMetadata
+from aqt.exceptions import (
+    AqtException,
+    ArchiveConnectionError,
+    ArchiveDownloadError,
+    ArchiveListError,
+    CliInputError,
+    EmptyMetadata,
+)
 from aqt.helper import Settings
 from aqt.installer import Cli
 from aqt.metadata import (
@@ -199,6 +206,82 @@ def test_list_architectures_and_modules(monkeypatch, version: str, extension: st
     assert arches == expect["architectures"]
 
 
+@pytest.mark.parametrize(
+    "version, arch, modules_to_query, modules_failed_query",
+    (
+        ("5.14.0", "win32_mingw73", [], []),
+        ("5.14.0", "win32_mingw73", ["qtcharts"], []),
+        ("5.14.0", "win32_mingw73", ["all"], []),
+        ("5.14.0", "win32_mingw73", ["debug_info"], ["debug_info"]),
+        ("5.14.0", "win64_msvc2017_64", [], []),
+        ("5.14.0", "win64_msvc2017_64", ["debug_info"], []),
+    ),
+)
+def test_list_archives(
+    monkeypatch, capsys, version: str, arch: str, modules_to_query: List[str], modules_failed_query: List[str]
+):
+    archive_id = ArchiveId("qt", "windows", "desktop")
+    in_file = f"{archive_id.host}-{version.replace('.', '')}-update.xml"
+    expect_out_file = f"{archive_id.host}-{version.replace('.', '')}-expect.json"
+    _xml = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
+    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)
+    expect = json.loads((Path(__file__).parent / "data" / expect_out_file).read_text("utf-8"))
+
+    if not modules_to_query:
+        expected_qt_archives = expect["qt_base_pkgs_by_arch"][arch]["DownloadableArchives"]
+        expected = set([arc.split("-")[0] for arc in expected_qt_archives])
+    else:
+        expected_mod_metadata = expect["modules_metadata_by_arch"][arch]
+        if "all" not in modules_to_query:
+            expected_mod_metadata = filter(lambda mod: mod["Name"].split(".")[-2] in modules_to_query, expected_mod_metadata)
+        expected = set([arc.split("-")[0] for mod in expected_mod_metadata for arc in mod["DownloadableArchives"]])
+
+    archives_query = [version, arch, *modules_to_query]
+    cli_args = ["list-qt", "windows", "desktop", "--archives", *archives_query]
+    if not modules_failed_query:
+        meta = set(MetadataFactory(archive_id, archives_query=archives_query).getList())
+        assert meta == expected
+
+        cli = Cli()
+        assert 0 == cli.run(cli_args)
+        out, err = capsys.readouterr()
+        assert out.rstrip() == " ".join(sorted(expected))
+        return
+
+    expected_err_msg = f"The requested modules were not located: {sorted(modules_failed_query)}"
+    with pytest.raises(CliInputError) as err:
+        MetadataFactory(archive_id, archives_query=archives_query).getList()
+    assert err.type == CliInputError
+    assert format(err.value).startswith(expected_err_msg)
+
+    cli = Cli()
+    assert 1 == cli.run(cli_args)
+    out, err = capsys.readouterr()
+    assert expected_err_msg in err
+
+
+def test_list_archives_insufficient_args(capsys):
+    cli = Cli()
+    assert 1 == cli.run("list-qt mac desktop --archives 5.14.0".split())
+    out, err = capsys.readouterr()
+    assert err.strip() == "The '--archives' flag requires a 'QT_VERSION' and an 'ARCHITECTURE' parameter."
+
+
+def test_list_archives_bad_xml(monkeypatch):
+    archive_id = ArchiveId("qt", "windows", "desktop")
+    archives_query = ["5.15.2", "win32_mingw81", "qtcharts"]
+
+    xml_no_name = "<Updates><PackageUpdate><badname></badname></PackageUpdate></Updates>"
+    xml_empty_name = "<Updates><PackageUpdate><Name></Name></PackageUpdate></Updates>"
+    xml_broken = "<Updates></PackageUpdate><PackageUpdate></Updates><Name></Name>"
+
+    for _xml in (xml_no_name, xml_empty_name, xml_broken):
+        monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)
+        with pytest.raises(ArchiveListError) as e:
+            MetadataFactory(archive_id, archives_query=archives_query).getList()
+        assert e.type == ArchiveListError
+
+
 @pytest.mark.parametrize(
     "host, target, tool_name",
     [
@@ -449,6 +532,21 @@ wrong_arch_msg = "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to li
             MetadataFactory(mac_qt, extensions_ver="1.2.3"),
             [wrong_qt_version_msg],
         ),
+        (
+            MetadataFactory(mac_qt, archives_query=["1.2.3", "clang_64", "a module", "another module"]),
+            [
+                "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
+                "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to show architectures available.",
+                "Please use 'aqt list-qt mac desktop --modules <QT_VERSION>' to show modules available.",
+            ],
+        ),
+        (
+            MetadataFactory(mac_qt, archives_query=["1.2.3", "clang_64"]),
+            [
+                "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
+                "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to show architectures available.",
+            ],
+        ),
         (
             MetadataFactory(mac_wasm),
             [wrong_ext_msg],