Skip to content

Commit c3bfd67

Browse files
committed
qclib remove CLI command now supports --only-ref option
1 parent df9920f commit c3bfd67

5 files changed

Lines changed: 103 additions & 23 deletions

File tree

quantcrypt/internal/cli/annotations.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"CompileAlgos",
2020
"RemoveAlgos",
2121
"KeepAlgos",
22+
"OnlyRef",
2223
"WithOpt",
2324
"Version",
2425
"DryRun",
@@ -74,6 +75,14 @@
7475
])
7576
)]
7677

78+
OnlyRef = Annotated[bool, Option(
79+
"--only-ref", "-r", show_default=False, help=' '.join([
80+
"Can be used together with the --keep option to keep only the clean reference binaries",
81+
"of algorithms. Useful for when QuantCrypt is being used within virtualized environments",
82+
"which do not support specialized CPU instructions."
83+
])
84+
)]
85+
7786
WithOpt = Annotated[bool, Option(
7887
"--with-opt", "-o", show_default=False, help=' '.join([
7988
"Includes architecture-specific optimized variants to compilation targets.",

quantcrypt/internal/cli/commands/remove.py

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# SPDX-License-Identifier: MIT
1010
#
1111

12+
import json
1213
from typer import Typer
1314
from itertools import product
1415
from quantcrypt.internal import utils, constants as const
@@ -24,43 +25,101 @@
2425
)
2526

2627

28+
def remove_spec_variants(
29+
spec_variants: dict[const.AlgoSpec, list[const.PQAVariant]]
30+
) -> tuple[dict, dict]:
31+
removed_variants: dict[const.AlgoSpec, list[const.PQAVariant]] = dict()
32+
already_removed: dict[const.AlgoSpec, list[const.PQAVariant]] = dict()
33+
bin_path = utils.search_upwards("bin")
34+
bin_contents = list(bin_path.iterdir())
35+
36+
for spec, variants in spec_variants.items(): # type: const.AlgoSpec, list[const.PQAVariant]
37+
for variant in variants:
38+
did_remove = False
39+
40+
for item in bin_contents:
41+
if spec.module_name(variant) in item.name and item.exists():
42+
item.unlink()
43+
x = removed_variants.get(spec, list())
44+
x.append(variant)
45+
removed_variants[spec] = x
46+
did_remove = True
47+
48+
if not did_remove:
49+
y = already_removed.get(spec, list())
50+
y.append(variant)
51+
already_removed[spec] = y
52+
53+
return removed_variants, already_removed
54+
55+
56+
def report_spec_variants(
57+
spec_variants: dict[const.AlgoSpec, list[const.PQAVariant]]
58+
) -> None:
59+
armor_names = [s.armor_name() for s in spec_variants.keys()]
60+
longest_name_len = max(len(n) for n in armor_names) if armor_names else 0
61+
62+
for spec, variants in spec_variants.items():
63+
variants_fmt = json.dumps([v.value for v in variants])
64+
arna_fmt = spec.armor_name().rjust(longest_name_len)
65+
console.styled_print(f"{arna_fmt}: {variants_fmt}")
66+
67+
2768
@remove_app.callback()
2869
def command_remove(
2970
algorithms: ats.RemoveAlgos,
3071
keep_algos: ats.KeepAlgos = False,
72+
only_ref: ats.OnlyRef = False,
3173
dry_run: ats.DryRun = False,
3274
non_interactive: ats.NonInteractive = False
3375
) -> None:
76+
if only_ref and not keep_algos:
77+
console.raise_error("Cannot use --only-ref without --keep")
78+
79+
chosen_algos = const.SupportedAlgos.filter(algorithms)
80+
if len(chosen_algos) != len(algorithms):
81+
algo_names = [s.armor_name() for s in chosen_algos]
82+
bad_names = [a for a in algorithms if a.upper() not in algo_names]
83+
if bad_names: # pragma: no branch
84+
console.raise_error(
85+
f"Unknown algorithm name(s): {json.dumps(bad_names)}. " +
86+
"Please choose algorithm names from the following list:\n" +
87+
' | '.join(const.SupportedAlgos.armor_names())
88+
)
89+
3490
console.notify_dry_run(dry_run)
3591
console.styled_print("QuantCrypt is about to remove compiled PQA binaries from itself.")
3692

3793
if not non_interactive:
3894
console.ask_continue(exit_on_false=True)
3995

40-
algos = const.SupportedAlgos.filter(algorithms, invert=keep_algos)
4196
variants = const.PQAVariant.members()
97+
if only_ref:
98+
algorithms = [a.upper() for a in algorithms]
99+
algos = const.SupportedAlgos
100+
else:
101+
algos = const.SupportedAlgos.filter(algorithms, invert=keep_algos)
102+
103+
to_remove: dict[const.AlgoSpec, list[const.PQAVariant]] = dict()
104+
for spec, variant in product(algos, variants): # type: const.AlgoSpec, const.PQAVariant
105+
if only_ref and spec.armor_name() in algorithms and variant == const.PQAVariant.REF:
106+
continue
107+
variants = to_remove.get(spec, list())
108+
variants.append(variant)
109+
to_remove[spec] = variants
42110

43111
if dry_run:
44-
console.styled_print("QuantCrypt would have removed the following algorithms:")
45-
console.pretty_print(', '.join(s.armor_name() for s in algos))
112+
console.styled_print("\nQuantCrypt would have removed the following algorithms and their variants:")
113+
report_spec_variants(to_remove)
46114
return
47115

48-
print()
49-
bin_path = utils.search_upwards("bin")
50-
bin_contents = list(bin_path.iterdir())
51-
52-
for spec, variant in product(algos, variants): # type: const.AlgoSpec, const.PQAVariant
53-
module_name = spec.module_name(variant)
54-
did_remove = False
116+
removed_variants, already_removed = remove_spec_variants(to_remove)
55117

56-
for item in bin_contents:
57-
if spec.module_name(variant) in item.name and item.exists():
58-
item.unlink()
59-
did_remove = True
60-
console.styled_print(f"Successfully removed binaries - {module_name}")
118+
console.styled_print("\nSuccessfully removed binaries: ")
119+
report_spec_variants(removed_variants)
61120

62-
if not did_remove:
63-
console.styled_print(f"Binaries already removed - {module_name}")
121+
console.styled_print("\nAlready removed binaries: ")
122+
report_spec_variants(already_removed)
64123

65124
print()
66125
console.print_success()

quantcrypt/internal/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ def armor_names(self: list[AlgoSpec], pqa_type: PQAType | None = None) -> list[s
133133
]
134134

135135
def filter(self, armor_names: list[str], invert: bool = False) -> list[AlgoSpec]:
136+
armor_names = [n.upper() for n in armor_names]
136137
return [
137138
spec for spec, name in product(self, armor_names)
138-
if (not invert and spec.armor_name() == name.upper())
139+
if (not invert and spec.armor_name() == name)
139140
or (invert and spec.armor_name() not in armor_names)
140141
]
141142

tests/test_cli/test_remove.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# SPDX-License-Identifier: MIT
1010
#
1111

12+
from pathlib import Path
1213
from unittest.mock import patch
1314
from quantcrypt.internal import constants as const
1415
from .conftest import CLIMessages
@@ -18,11 +19,21 @@ def test_remove(cli_runner, alt_tmp_path) -> None:
1819
with patch('internal.cli.commands.remove.utils.search_upwards') as mock:
1920
mock.return_value = alt_tmp_path
2021

22+
cli_runner("remove", ["mlkem512"], "n\n", CLIMessages.CANCELLED)
23+
cli_runner("remove", ["-D", "mlkem512"], "y\n", CLIMessages.DRYRUN)
24+
cli_runner("remove", ["--only-ref", "mlkem512"], "", CLIMessages.ERROR)
25+
2126
for spec in const.SupportedAlgos: # type: const.AlgoSpec
22-
(alt_tmp_path / spec.module_name(const.PQAVariant.REF)).touch()
27+
(alt_tmp_path / spec.module_name(const.PQAVariant.REF)).touch(exist_ok=True)
2328

24-
cli_runner("remove", ["mlkem512"], "n\n", CLIMessages.CANCELLED)
2529
cli_runner("remove", ["mlkem512"], "y\n", CLIMessages.SUCCESS)
26-
cli_runner("remove", ["asdfg"], "y\n", CLIMessages.SUCCESS)
27-
cli_runner("remove", ["-D", "mlkem512"], "y\n", CLIMessages.DRYRUN)
30+
31+
spec = const.SupportedAlgos.filter(["mlkem512"])[0]
32+
for item in alt_tmp_path.iterdir(): # type: Path
33+
for variant in const.PQAVariant.members():
34+
assert spec.module_name(variant) not in item.name
35+
36+
cli_runner("remove", ["asdfg"], "y\n", CLIMessages.ERROR)
2837
cli_runner("remove", ["-N", "fastsphincs"], "", CLIMessages.SUCCESS)
38+
cli_runner("remove", ["--keep", "--only-ref", "mlkem512"], "y\n", CLIMessages.SUCCESS)
39+

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)