fix doubly shell quoting args to spack spec
(#29282)
* add test to verify fix works * fix spec cflags/variants parsing test (breaking change) * fix `spack spec` arg quoting issue * add error report for deprecated cflags coalescing * use .group(n) vs subscript regex group extraction for 3.5 compat * add random test for untested functionality to pass codecov * fix new test failure since rebase
This commit is contained in:
parent
bf2b30a5f5
commit
0c7fd9bd8c
@ -8,7 +8,10 @@
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
import ruamel.yaml as yaml
|
||||
import six
|
||||
@ -147,6 +150,58 @@ def get_command(cmd_name):
|
||||
return getattr(get_module(cmd_name), pname)
|
||||
|
||||
|
||||
class _UnquotedFlags(object):
|
||||
"""Use a heuristic in `.extract()` to detect whether the user is trying to set
|
||||
multiple flags like the docker ENV attribute allows (e.g. 'cflags=-Os -pipe').
|
||||
|
||||
If the heuristic finds a match (which can be checked with `__bool__()`), a warning
|
||||
message explaining how to quote multiple flags correctly can be generated with
|
||||
`.report()`.
|
||||
"""
|
||||
|
||||
flags_arg_pattern = re.compile(
|
||||
r'^({0})=([^\'"].*)$'.format(
|
||||
'|'.join(spack.spec.FlagMap.valid_compiler_flags()),
|
||||
))
|
||||
|
||||
def __init__(self, all_unquoted_flag_pairs):
|
||||
# type: (List[Tuple[re.Match, str]]) -> None
|
||||
self._flag_pairs = all_unquoted_flag_pairs
|
||||
|
||||
def __bool__(self):
|
||||
# type: () -> bool
|
||||
return bool(self._flag_pairs)
|
||||
|
||||
@classmethod
|
||||
def extract(cls, sargs):
|
||||
# type: (str) -> _UnquotedFlags
|
||||
all_unquoted_flag_pairs = [] # type: List[Tuple[re.Match, str]]
|
||||
prev_flags_arg = None
|
||||
for arg in shlex.split(sargs):
|
||||
if prev_flags_arg is not None:
|
||||
all_unquoted_flag_pairs.append((prev_flags_arg, arg))
|
||||
prev_flags_arg = cls.flags_arg_pattern.match(arg)
|
||||
return cls(all_unquoted_flag_pairs)
|
||||
|
||||
def report(self):
|
||||
# type: () -> str
|
||||
single_errors = [
|
||||
'({0}) {1} {2} => {3}'.format(
|
||||
i + 1, match.group(0), next_arg,
|
||||
'{0}="{1} {2}"'.format(match.group(1), match.group(2), next_arg),
|
||||
)
|
||||
for i, (match, next_arg) in enumerate(self._flag_pairs)
|
||||
]
|
||||
return dedent("""\
|
||||
Some compiler or linker flags were provided without quoting their arguments,
|
||||
which now causes spack to try to parse the *next* argument as a spec component
|
||||
such as a variant instead of an additional compiler or linker flag. If the
|
||||
intent was to set multiple flags, try quoting them together as described below.
|
||||
|
||||
Possible flag quotation errors (with the correctly-quoted version after the =>):
|
||||
{0}""").format('\n'.join(single_errors))
|
||||
|
||||
|
||||
def parse_specs(args, **kwargs):
|
||||
"""Convenience function for parsing arguments from specs. Handles common
|
||||
exceptions and dies if there are errors.
|
||||
@ -157,15 +212,28 @@ def parse_specs(args, **kwargs):
|
||||
|
||||
sargs = args
|
||||
if not isinstance(args, six.string_types):
|
||||
sargs = ' '.join(spack.util.string.quote(args))
|
||||
specs = spack.spec.parse(sargs)
|
||||
for spec in specs:
|
||||
if concretize:
|
||||
spec.concretize(tests=tests) # implies normalize
|
||||
elif normalize:
|
||||
spec.normalize(tests=tests)
|
||||
sargs = ' '.join(args)
|
||||
unquoted_flags = _UnquotedFlags.extract(sargs)
|
||||
|
||||
return specs
|
||||
try:
|
||||
specs = spack.spec.parse(sargs)
|
||||
for spec in specs:
|
||||
if concretize:
|
||||
spec.concretize(tests=tests) # implies normalize
|
||||
elif normalize:
|
||||
spec.normalize(tests=tests)
|
||||
return specs
|
||||
|
||||
except spack.error.SpecError as e:
|
||||
|
||||
msg = e.message
|
||||
if e.long_message:
|
||||
msg += e.long_message
|
||||
if unquoted_flags:
|
||||
msg += '\n\n'
|
||||
msg += unquoted_flags.report()
|
||||
|
||||
raise spack.error.SpackError(msg)
|
||||
|
||||
|
||||
def matching_spec_from_env(spec):
|
||||
|
@ -80,7 +80,8 @@ def spec(parser, args):
|
||||
# Use command line specified specs, otherwise try to use environment specs.
|
||||
if args.specs:
|
||||
input_specs = spack.cmd.parse_specs(args.specs)
|
||||
specs = [(s, s.concretized()) for s in input_specs]
|
||||
concretized_specs = spack.cmd.parse_specs(args.specs, concretize=True)
|
||||
specs = list(zip(input_specs, concretized_specs))
|
||||
else:
|
||||
env = ev.active_environment()
|
||||
if env:
|
||||
|
@ -45,21 +45,22 @@ def test_negative_integers_not_allowed_for_parallel_jobs(job_parser):
|
||||
assert 'expected a positive integer' in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('specs,expected_variants,unexpected_variants', [
|
||||
(['coreutils', 'cflags=-O3 -g'], [], ['g']),
|
||||
(['coreutils', 'cflags=-O3', '-g'], ['g'], []),
|
||||
@pytest.mark.parametrize('specs,cflags,negated_variants', [
|
||||
(['coreutils cflags="-O3 -g"'], ['-O3', '-g'], []),
|
||||
(['coreutils', 'cflags=-O3 -g'], ['-O3'], ['g']),
|
||||
(['coreutils', 'cflags=-O3', '-g'], ['-O3'], ['g']),
|
||||
])
|
||||
@pytest.mark.regression('12951')
|
||||
def test_parse_spec_flags_with_spaces(
|
||||
specs, expected_variants, unexpected_variants
|
||||
):
|
||||
def test_parse_spec_flags_with_spaces(specs, cflags, negated_variants):
|
||||
spec_list = spack.cmd.parse_specs(specs)
|
||||
assert len(spec_list) == 1
|
||||
|
||||
s = spec_list.pop()
|
||||
|
||||
assert all(x not in s.variants for x in unexpected_variants)
|
||||
assert all(x in s.variants for x in expected_variants)
|
||||
assert s.compiler_flags['cflags'] == cflags
|
||||
assert list(s.variants.keys()) == negated_variants
|
||||
for v in negated_variants:
|
||||
assert '~{0}'.format(v) in s
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('config')
|
||||
|
@ -4,10 +4,12 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import re
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
import spack.environment as ev
|
||||
import spack.error
|
||||
import spack.spec
|
||||
import spack.store
|
||||
from spack.main import SpackCommand, SpackCommandError
|
||||
@ -55,6 +57,54 @@ def test_spec_concretizer_args(mutable_config, mutable_database):
|
||||
assert h in output
|
||||
|
||||
|
||||
def test_spec_parse_dependency_variant_value():
|
||||
"""Verify that we can provide multiple key=value variants to multiple separate
|
||||
packages within a spec string."""
|
||||
output = spec('multivalue-variant fee=barbaz ^ a foobar=baz')
|
||||
|
||||
assert 'fee=barbaz' in output
|
||||
assert 'foobar=baz' in output
|
||||
|
||||
|
||||
def test_spec_parse_cflags_quoting():
|
||||
"""Verify that compiler flags can be provided to a spec from the command line."""
|
||||
output = spec('--yaml', 'gcc cflags="-Os -pipe" cxxflags="-flto -Os"')
|
||||
gh_flagged = spack.spec.Spec.from_yaml(output)
|
||||
|
||||
assert ['-Os', '-pipe'] == gh_flagged.compiler_flags['cflags']
|
||||
assert ['-flto', '-Os'] == gh_flagged.compiler_flags['cxxflags']
|
||||
|
||||
|
||||
def test_spec_parse_unquoted_flags_report():
|
||||
"""Verify that a useful error message is produced if unquoted compiler flags are
|
||||
provided."""
|
||||
# This should fail during parsing, since /usr/include is interpreted as a spec hash.
|
||||
with pytest.raises(spack.error.SpackError) as cm:
|
||||
# We don't try to figure out how many following args were intended to be part of
|
||||
# cflags, we just explain how to fix it for the immediate next arg.
|
||||
spec('gcc cflags=-Os -pipe -other-arg-that-gets-ignored cflags=-I /usr/include')
|
||||
# Verify that the generated error message is nicely formatted.
|
||||
assert str(cm.value) == dedent('''\
|
||||
No installed spec matches the hash: 'usr'
|
||||
|
||||
Some compiler or linker flags were provided without quoting their arguments,
|
||||
which now causes spack to try to parse the *next* argument as a spec component
|
||||
such as a variant instead of an additional compiler or linker flag. If the
|
||||
intent was to set multiple flags, try quoting them together as described below.
|
||||
|
||||
Possible flag quotation errors (with the correctly-quoted version after the =>):
|
||||
(1) cflags=-Os -pipe => cflags="-Os -pipe"
|
||||
(2) cflags=-I /usr/include => cflags="-I /usr/include"''')
|
||||
|
||||
# Verify that the same unquoted cflags report is generated in the error message even
|
||||
# if it fails during concretization, not just during parsing.
|
||||
with pytest.raises(spack.error.SpackError) as cm:
|
||||
spec('gcc cflags=-Os -pipe')
|
||||
cm = str(cm.value)
|
||||
assert cm.startswith('trying to set variant "pipe" in package "gcc", but the package has no such variant [happened during concretization of gcc cflags="-Os" ~pipe]') # noqa: E501
|
||||
assert cm.endswith('(1) cflags=-Os -pipe => cflags="-Os -pipe"')
|
||||
|
||||
|
||||
def test_spec_yaml():
|
||||
output = spec('--yaml', 'mpileaks')
|
||||
|
||||
@ -125,14 +175,14 @@ def test_spec_returncode():
|
||||
|
||||
|
||||
def test_spec_parse_error():
|
||||
with pytest.raises(spack.spec.SpecParseError) as e:
|
||||
with pytest.raises(spack.error.SpackError) as e:
|
||||
spec("1.15:")
|
||||
|
||||
# make sure the error is formatted properly
|
||||
error_msg = """\
|
||||
1.15:
|
||||
^"""
|
||||
assert error_msg in e.value.long_message
|
||||
assert error_msg in str(e.value)
|
||||
|
||||
|
||||
def test_env_aware_spec(mutable_mock_env_path):
|
||||
|
@ -15,6 +15,7 @@
|
||||
SpecFormatSigilError,
|
||||
SpecFormatStringError,
|
||||
UnconstrainableDependencySpecError,
|
||||
UnsupportedCompilerError,
|
||||
)
|
||||
from spack.variant import (
|
||||
InvalidVariantValueError,
|
||||
@ -1320,3 +1321,8 @@ def test_concretize_partial_old_dag_hash_spec(mock_packages, config):
|
||||
|
||||
# make sure package hash is NOT recomputed
|
||||
assert not getattr(spec["dt-diamond-bottom"], '_package_hash', None)
|
||||
|
||||
|
||||
def test_unsupported_compiler():
|
||||
with pytest.raises(UnsupportedCompilerError):
|
||||
Spec('gcc%fake-compiler').validate_or_raise()
|
||||
|
Loading…
Reference in New Issue
Block a user