Skip to content

Commit 66811f4

Browse files
vecchiot-awstedinski
authored andcommitted
Add configuration toml flag parsing for cargo rmc. (rust-lang#355)
1 parent a3c1825 commit 66811f4

File tree

16 files changed

+387
-85
lines changed

16 files changed

+387
-85
lines changed

scripts/cargo-rmc

+159-28
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,24 @@ import rmc
99
import rmc_flags
1010
import os
1111
import pathlib
12+
import toml
1213

1314
MY_PATH = pathlib.Path(__file__).parent.parent.absolute()
1415
RMC_C_LIB = MY_PATH / "library" / "rmc" / "rmc_lib.c"
1516
EXIT_CODE_SUCCESS = 0
1617
CBMC_VERIFICATION_FAILURE_EXIT_CODE = 10
1718

18-
1919
def main():
20-
# Remove "rmc" from arg if invoked `cargo rmc ...`
21-
if len(sys.argv) >= 2 and sys.argv[1] == "rmc":
22-
del sys.argv[1]
23-
24-
parser = argparse.ArgumentParser(prog="cargo rmc", description="Verify a Rust crate. For more information, see https://github.com/model-checking/rmc.")
25-
26-
crate_group = parser.add_argument_group("Crate", "You can pass in the rust crate positionally or with the --crate flag.")
27-
crate_group.add_argument("crate", help="crate to verify", nargs="?")
28-
crate_group.add_argument("--crate", help="crate to verify", dest="crate_flag", metavar="CRATE")
29-
30-
exclude_flags = []
31-
rmc_flags.add_flags(parser, {"default-target": "target"}, exclude_flags=exclude_flags)
32-
args = parser.parse_args()
33-
34-
if args.crate:
35-
rmc.ensure(args.crate_flag is None, "Please provide a single crate to verify.")
36-
else:
37-
rmc.ensure(args.crate_flag is not None, "Please provide a crate to verify.")
38-
args.crate = args.crate_flag
39-
40-
if args.quiet:
41-
args.verbose = False
20+
args = parse_args()
4221

4322
rmc.ensure_dependencies_in_path()
4423

45-
# Add some CBMC flags by default unless `--no-default-checks` is being used
46-
if not args.no_default_checks:
47-
rmc.add_selected_default_cbmc_flags(args)
48-
4924
rmc.cargo_build(args.crate, args.target_dir,
5025
args.verbose, args.debug, args.mangler, args.dry_run, [])
5126

5227
pattern = os.path.join(args.target_dir, "debug", "deps", "*.json")
5328
jsons = glob.glob(pattern)
54-
rmc.ensure(len(jsons) == 1, "Unexpected number of json outputs.")
29+
rmc.ensure(len(jsons) == 1, f"Unexpected number of json outputs: {len(jsons)}")
5530

5631
cbmc_filename = os.path.join(args.target_dir, "cbmc.out")
5732
c_filename = os.path.join(args.target_dir, "cbmc.c")
@@ -89,6 +64,162 @@ def main():
8964

9065
return retcode
9166

67+
def parse_args():
68+
# Create parser
69+
def create_parser():
70+
parser = argparse.ArgumentParser(prog="cargo rmc", description="Verify a Rust crate. For more information, see https://github.com/model-checking/rmc.")
71+
72+
crate_group = parser.add_argument_group("Crate", "You can pass in the rust crate positionally or with the --crate flag.")
73+
crate_group.add_argument("crate", help="crate to verify", nargs="?")
74+
crate_group.add_argument("--crate", help="crate to verify", dest="crate_flag", metavar="CRATE")
75+
76+
config_group = parser.add_argument_group("Config", "You can configure cargo rmc with your Cargo.toml file.")
77+
config_group.add_argument("--config-toml", help="Where to read configuration from; defaults to crate's Cargo.toml")
78+
config_group.add_argument("--no-config-toml", action="store_true", help="Do not use any configuration toml")
79+
80+
exclude_flags = []
81+
rmc_flags.add_flags(parser, {"default-target": "target"}, exclude_flags=exclude_flags)
82+
83+
return parser
84+
85+
# Determine what crate to analyze from flags
86+
def get_crate_from_args(args):
87+
validate(args)
88+
return args.crate or args.crate_flag or "."
89+
90+
# Combine the command line parameter to get a config file;
91+
# return None if no_config_toml is set
92+
def get_config_toml(no_config_toml, config_toml, args):
93+
crate = get_crate_from_args(args)
94+
if no_config_toml:
95+
return None
96+
elif config_toml is not None:
97+
# If config_toml is explicitly given, ensure it exists
98+
rmc.ensure(pathlib.Path(config_toml).is_file(), f"Invalid path to config file: {config_toml}")
99+
return config_toml
100+
else:
101+
# If no config flags are set, look for config toml in default location (<crate>/Cargo.toml)
102+
cargo_toml = pathlib.Path(crate).joinpath("Cargo.toml")
103+
rmc.ensure(cargo_toml.is_file(), f"Cannot find configuration toml at expected location: {cargo_toml}")
104+
return cargo_toml
105+
106+
# Combine args set by config toml and provided on command line
107+
def get_combined_args(parser, config_toml):
108+
# Extract flags from config_toml
109+
toml_flags = extract_flags_from_toml(config_toml)
110+
111+
# Load args from toml flags
112+
combined_args = argparse.Namespace()
113+
parser.parse_args(toml_flags, namespace=combined_args)
114+
115+
# Set relative paths to be rooted at config toml's directory rather than cwd
116+
def fix_paths(value):
117+
if type(value) == list:
118+
return list(map(fix_paths, value))
119+
elif isinstance(value, pathlib.PurePath):
120+
return pathlib.Path(config_toml).parent.joinpath(value)
121+
else:
122+
return value
123+
124+
ca_dict = combined_args.__dict__
125+
for key in ca_dict:
126+
ca_dict[key] = fix_paths(ca_dict[key])
127+
128+
# Load args from command line using toml args as default
129+
parser.parse_args(sys.argv[1:], namespace=combined_args)
130+
131+
return combined_args
132+
133+
# Check for conflicting flags
134+
def validate(args):
135+
rmc.ensure(not (args.crate and args.crate_flag), "Please provide a single crate to verify.")
136+
rmc.ensure(not (args.no_config_toml and args.config_toml), "Incompatible flags: --config-toml, --no-config-toml")
137+
138+
# Fix up args before returning
139+
def post_process(args):
140+
# Combine positional and flag argument for input
141+
args.crate = get_crate_from_args(args)
142+
143+
# --quiet overrides --verbose
144+
if args.quiet:
145+
args.verbose = False
146+
147+
# Add some CBMC flags by default unless `--no-default-checks` is being used
148+
if args.default_checks:
149+
rmc.add_selected_default_cbmc_flags(args)
150+
151+
# Remove "rmc" from arg if invoked `cargo rmc ...`
152+
if len(sys.argv) >= 2 and sys.argv[1] == "rmc":
153+
del sys.argv[1]
154+
155+
# Parse command line args to find crate and check for config flags
156+
parser = create_parser()
157+
cl_args = parser.parse_args()
158+
validate(cl_args)
159+
160+
# Try to find the config_toml based on command line arguments
161+
config_toml = get_config_toml(cl_args.no_config_toml, cl_args.config_toml, cl_args)
162+
163+
if config_toml is not None:
164+
# If config_toml is found, combine configuration with command line arguments
165+
combined_args = get_combined_args(parser, config_toml)
166+
validate(combined_args)
167+
post_process(combined_args)
168+
return combined_args
169+
else:
170+
# If config_toml is missing, just use the parsed command line arguments
171+
post_process(cl_args)
172+
return cl_args
173+
174+
# Extract a list of args based on given config toml
175+
def extract_flags_from_toml(path):
176+
# Load the flag data from toml
177+
data = toml.load(path)
178+
179+
# Extract the rmc flags from the toml, if any present
180+
if ("rmc" not in data) or ("flags" not in data["rmc"]):
181+
# If no flags are present, just return none
182+
return []
183+
rmc_data = data["rmc"]
184+
flag_data = rmc_data["flags"]
185+
186+
# Extract nested flags
187+
flags = dict()
188+
def find_flags(map):
189+
for key in map:
190+
if type(map[key]) == dict:
191+
find_flags(map[key])
192+
else:
193+
flags[key] = map[key]
194+
find_flags(flag_data)
195+
196+
# Add config toml flags to flag list
197+
success = True
198+
flag_list = []
199+
def add_flag(flag, value):
200+
if type(value) == bool:
201+
if value:
202+
flag_list.append(f"--{flag}")
203+
else:
204+
if flag.startswith("no-"):
205+
flag_list.append(f"--{flag[3:]}")
206+
else:
207+
flag_list.append(f"--no-{flag}")
208+
elif type(value) == list:
209+
flag_list.append(f"--{flag}")
210+
assert all(map(lambda arg: type(arg) == str, value)), f"ERROR: Invalid config: {flag} = {value}"
211+
flag_list.extend(value)
212+
elif type(value) == str:
213+
flag_list.append(f"--{flag}")
214+
flag_list.append(value)
215+
else:
216+
print(f"ERROR: Invalid config: {flag} = {value}")
217+
success = False
218+
for flag in flags:
219+
add_flag(flag, flags[flag])
220+
221+
rmc.ensure(success)
222+
return flag_list
92223

93224
if __name__ == "__main__":
94225
sys.exit(main())

scripts/rmc

+49-29
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,10 @@ RMC_C_LIB = MY_PATH / "library" / "rmc" / "rmc_lib.c"
1414
EXIT_CODE_SUCCESS = 0
1515
CBMC_VERIFICATION_FAILURE_EXIT_CODE = 10
1616

17-
1817
def main():
19-
parser = argparse.ArgumentParser(description="Verify a single Rust file. For more information, see https://github.com/model-checking/rmc.")
20-
21-
input_group = parser.add_argument_group("Input", "You can pass in the rust file positionally or with the --input flag.")
22-
input_group.add_argument("input", help="Rust file to verify", nargs="?")
23-
input_group.add_argument("--input", help="Rust file to verify", dest="input_flag", metavar="INPUT")
24-
25-
exclude_flags = [
26-
# In the future we hope to make this configurable in the command line.
27-
# For now, however, changing this from "main" breaks rmc.
28-
# Issue: https://github.com/model-checking/rmc/issues/169
29-
"--function"
30-
]
31-
rmc_flags.add_flags(parser, {"default-target": "."}, exclude_flags=exclude_flags)
32-
args = parser.parse_args()
33-
args.function = "main"
34-
35-
if args.input:
36-
rmc.ensure(args.input_flag is None, "Please provide a single file to verify.")
37-
else:
38-
rmc.ensure(args.input_flag is not None, "Please provide a file to verify.")
39-
args.input = args.input_flag
40-
41-
if args.quiet:
42-
args.verbose = False
18+
args = parse_args()
4319

4420
rmc.ensure_dependencies_in_path()
45-
46-
# Add some CBMC flags by default unless `--no-default-checks` is being used
47-
if not args.no_default_checks:
48-
rmc.add_selected_default_cbmc_flags(args)
4921

5022
base, ext = os.path.splitext(args.input)
5123
rmc.ensure(ext == ".rs", "Expecting .rs input file.")
@@ -91,6 +63,54 @@ def main():
9163

9264
return retcode
9365

66+
def parse_args():
67+
# Create parser
68+
def create_parser():
69+
parser = argparse.ArgumentParser(description="Verify a single Rust file. For more information, see https://github.com/model-checking/rmc.")
70+
71+
input_group = parser.add_argument_group("Input", "You can pass in the rust file positionally or with the --input flag.")
72+
input_group.add_argument("input", help="Rust file to verify", nargs="?")
73+
input_group.add_argument("--input", help="Rust file to verify", dest="input_flag", metavar="INPUT")
74+
75+
exclude_flags = [
76+
# In the future we hope to make this configurable in the command line.
77+
# For now, however, changing this from "main" breaks rmc.
78+
# Issue: https://github.com/model-checking/rmc/issues/169
79+
"--function"
80+
]
81+
rmc_flags.add_flags(parser, {"default-target": "."}, exclude_flags=exclude_flags)
82+
83+
return parser
84+
85+
# Check for conflicting flags
86+
def validate(args):
87+
rmc.ensure(not (args.input and args.input_flag), "Please provide a single file to verify.")
88+
rmc.ensure(args.input or args.input_flag, "Please provide a file to verify.")
89+
90+
# Fix up args before returning
91+
def post_process(args):
92+
# Combine positional and flag argument for input
93+
args.input = args.input or args.input_flag
94+
95+
# --quiet overrides --verbose
96+
if args.quiet:
97+
args.verbose = False
98+
99+
# In the future we hope to make this configurable in the command line.
100+
# For now, however, changing this from "main" breaks rmc.
101+
# Issue: https://github.com/model-checking/rmc/issues/169
102+
args.function = "main"
103+
104+
# Add some CBMC flags by default unless `--no-default-checks` is being used
105+
if args.default_checks:
106+
rmc.add_selected_default_cbmc_flags(args)
107+
108+
parser = create_parser()
109+
args = parser.parse_args()
110+
validate(args)
111+
post_process(args)
112+
113+
return args
94114

95115
if __name__ == "__main__":
96116
sys.exit(main())

scripts/rmc.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ def ensure_dependencies_in_path():
5151
ensure(is_exe(program), f"Could not find {program} in PATH")
5252

5353
# Assert a condition holds, or produce a user error message.
54-
def ensure(condition, message, retcode=1):
54+
def ensure(condition, message=None, retcode=1):
5555
if not condition:
56-
print(f"ERROR: {message}")
56+
if message:
57+
print(f"ERROR: {message}")
5758
sys.exit(retcode)
5859

5960
# Deletes a file; used by atexit.register to remove temporary files on exit
@@ -76,11 +77,11 @@ def add_set_cbmc_flags(args, flags):
7677

7778
# Add sets of selected default CBMC flags
7879
def add_selected_default_cbmc_flags(args):
79-
if not args.no_memory_safety_checks:
80+
if args.memory_safety_checks:
8081
add_set_cbmc_flags(args, MEMORY_SAFETY_CHECKS)
81-
if not args.no_overflow_checks:
82+
if args.overflow_checks:
8283
add_set_cbmc_flags(args, OVERFLOW_CHECKS)
83-
if not args.no_unwinding_checks:
84+
if args.unwinding_checks:
8485
add_set_cbmc_flags(args, UNWINDING_CHECKS)
8586

8687
# Updates environment to use gotoc backend debugging
@@ -153,6 +154,8 @@ def compile_single_rust_file(input_filename, output_filename, verbose=False, deb
153154

154155
# Generates a symbol table (and some other artifacts) from a rust crate
155156
def cargo_build(crate, target_dir="target", verbose=False, debug=False, mangler="v0", dry_run=False, symbol_table_passes=[]):
157+
ensure(os.path.isdir(crate), f"Invalid path to crate: {crate}")
158+
156159
rustflags = [
157160
"-Z", "codegen-backend=gotoc",
158161
"-Z", f"symbol-mangling-version={mangler}",

0 commit comments

Comments
 (0)