diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py index 5b74207af77..966d30f3d06 100644 --- a/conan/cli/commands/list.py +++ b/conan/cli/commands/list.py @@ -82,6 +82,65 @@ def format_timestamps(item): print_serial(info) +def print_list_compact(results): + info = results["results"] + # Extract command single package name + new_info = {} + + for remote, remote_info in info.items(): + if not remote_info or "error" in remote_info: + new_info[remote] = {"warning": "There are no matching recipe references"} + continue + new_remote_info = {} + for ref, ref_info in remote_info.items(): + new_ref_info = {} + for rrev, rrev_info in ref_info.get("revisions", {}).items(): + new_rrev_info = {} + new_rrev = f"{ref}#{rrev}" + timestamp = rrev_info.get("timestamp") + if timestamp: + new_rrev += f" ({timestamp_to_str(timestamp)})" + # collect all options + common_options = {} + for pid, pid_info in rrev_info.get("packages", {}).items(): + options = pid_info.get("info", {}).get("options", {}) + common_options.update(options) + for pid, pid_info in rrev_info.get("packages", {}).items(): + options = pid_info.get("info", {}).get("options") + if options: # If a package has no options, like header-only, skip + common_options = {k: v for k, v in common_options.items() + if k in options and v == options[k]} + for pid, pid_info in rrev_info.get("packages", {}).items(): + options = pid_info.get("info", {}).get("options") + if options: + for k, v in options.items(): + if v != common_options.get(k): + common_options.pop(k, None) + # format options + for pid, pid_info in rrev_info.get("packages", {}).items(): + new_pid = f"{ref}#{rrev}:{pid}" + new_pid_info = {} + info = pid_info.get("info") + settings = info.get("settings") + if settings: # A bit of pretty order, first OS-ARCH + values = [settings.pop(s, None) + for s in ("os", "arch", "build_type", "compiler")] + values = [v for v in values if v is not None] + values.extend(settings.values()) + new_pid_info["settings"] = ", ".join(values) + options = info.get("options") + if options: + diff_options = {k: v for k, v in options.items() if k not in common_options} + options = ", ".join(f"{k}={v}" for k, v in diff_options.items()) + new_pid_info["options(diff)"] = options + new_rrev_info[new_pid] = new_pid_info + new_ref_info[new_rrev] = new_rrev_info + new_remote_info[ref] = new_ref_info + new_info[remote] = new_remote_info + + print_serial(new_info) + + def print_list_json(data): results = data["results"] myjson = json.dumps(results, indent=4) @@ -90,7 +149,8 @@ def print_list_json(data): @conan_command(group="Consumer", formatters={"text": print_list_text, "json": print_list_json, - "html": list_packages_html}) + "html": list_packages_html, + "compact": print_list_compact}) def list(conan_api: ConanAPI, parser, *args): """ List existing recipes, revisions, or packages in the cache (by default) or the remotes. diff --git a/conans/test/integration/command_v2/list_test.py b/conans/test/integration/command_v2/list_test.py index 25f5e1db362..1550a30fa6b 100644 --- a/conans/test/integration/command_v2/list_test.py +++ b/conans/test/integration/command_v2/list_test.py @@ -731,9 +731,8 @@ def test_list_html(self): c.run("create dep") c.run("create pkg -s os=Windows -s arch=x86") # Revision is needed explicitly! - c.run("list pkg/2.3.4#latest --format=html", redirect_stdout="table.html") - table = c.load("table.html") - assert "" in table + c.run("list pkg/2.3.4#latest --format=html") + assert "" in c.stdout # TODO: The actual good html is missing def test_list_html_custom(self): @@ -747,3 +746,48 @@ def test_list_html_custom(self): c.save({"list_packages.html": '{{ base_template_path }}'}, path=template_folder) c.run("list lib/0.1#latest --format=html") assert template_folder in c.stdout + + +class TestListCompact: + def test_list_compact(self): + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch") + .with_shared_option(False)}) + c.run("create . -s os=Windows -s arch=x86") + c.run("create . -s os=Linux -s arch=armv8") + c.run("create . -s os=Macos -s arch=armv8 -o shared=True") + c.run("list pkg:* --format=compact") + + expected = textwrap.dedent("""\ + pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:2a67a51fbf36a4ee345b2125dd2642be60ffd3ec + settings: Macos, armv8 + options(diff): shared=True + pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:2d46abc802bbffdf2af11591e3e452bc6149ea2b + settings: Linux, armv8 + options(diff): shared=False + pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:d2e97769569ac0a583d72c10a37d5ca26de7c9fa + settings: Windows, x86 + options(diff): shared=False + """) + assert textwrap.indent(expected, " ") in c.stdout + + def test_list_compact_no_settings_no_options(self): + c = TestClient() + c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch"), + "other/conanfile.py": GenConanfile("other", "1.0")}) + c.run("create pkg -s os=Windows -s arch=x86") + c.run("create other") + c.run("list *:* --format=compact") + expected_output = re.sub(r"\(.*\)", "(timestamp)", c.stdout) + expected = textwrap.dedent("""\ + Local Cache + other/1.0 + other/1.0#d3c8cc5e6d23ca8c6f0eaa6285c04cbd (timestamp) + other/1.0#d3c8cc5e6d23ca8c6f0eaa6285c04cbd:da39a3ee5e6b4b0d3255bfef95601890afd80709 + pkg/1.0 + pkg/1.0#d24b74828b7681f08d8f5ba0e7fd791e (timestamp) + pkg/1.0#d24b74828b7681f08d8f5ba0e7fd791e:c11e463c49652ba9c5adc62573ee49f966bd8417 + settings: Windows, x86 + """) + + assert expected == expected_output