diff --git a/cibuildwheel/bashlex_eval.py b/cibuildwheel/bashlex_eval.py index e4865d6d..c055a0a6 100644 --- a/cibuildwheel/bashlex_eval.py +++ b/cibuildwheel/bashlex_eval.py @@ -1,7 +1,7 @@ import shlex import subprocess -from typing import Dict, NamedTuple, Callable, Optional +from typing import Dict, NamedTuple, Callable, Optional, List, Sequence import bashlex # type: ignore @@ -47,7 +47,7 @@ def evaluate_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: elif node.kind == 'parameter': return evaluate_parameter_node(node, context=context) else: - raise ValueError(f'Unsupported bash construct: "{node.word}"') + raise ValueError(f'Unsupported bash construct: "{node.kind}"') def evaluate_word_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: @@ -74,7 +74,34 @@ def evaluate_word_node(node: bashlex.ast.node, context: NodeExecutionContext) -> def evaluate_command_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: - words = [evaluate_node(part, context=context) for part in node.parts] + if any(n.kind == 'operator' for n in node.parts): + return evaluate_nodes_as_compound_command(node.parts, context=context) + else: + return evaluate_nodes_as_simple_command(node.parts, context=context) + + +def evaluate_nodes_as_compound_command(nodes: Sequence[bashlex.ast.node], context: NodeExecutionContext) -> str: + # bashlex doesn't support any operators besides ';' inside command + # substitutions, so we only need to handle that case. We do so assuming + # that `set -o errexit` is on, because it's easier to code! + + result = '' + for node in nodes: + if node.kind == 'command': + result += evaluate_command_node(node, context=context) + elif node.kind == 'operator': + if node.op == ';': + pass + else: + raise ValueError(f'Unsupported bash operator: "{node.op}"') + else: + raise ValueError(f'Unsupported bash node in compound command: "{node.kind}"') + + return result + + +def evaluate_nodes_as_simple_command(nodes: List[bashlex.ast.node], context: NodeExecutionContext): + words = [evaluate_node(part, context=context) for part in nodes] command = ' '.join(words) return context.executor(command, context.environment) diff --git a/test/test_environment.py b/test/test_environment.py index 470a151f..7e5f8d10 100644 --- a/test/test_environment.py +++ b/test/test_environment.py @@ -44,7 +44,7 @@ def test(tmp_path): assert set(actual_wheels) == set(expected_wheels) -def test_overridden_path(tmp_path): +def test_overridden_path(tmp_path, capfd): project_dir = tmp_path / 'project' output_dir = tmp_path / 'output' @@ -55,7 +55,10 @@ def test_overridden_path(tmp_path): # mess up PATH, somehow with pytest.raises(subprocess.CalledProcessError): utils.cibuildwheel_run(project_dir, output_dir=output_dir, add_env={ - 'CIBW_ENVIRONMENT': '''SOMETHING="$(mkdir new_path && touch new_path/python)" PATH="$(realpath new_path):$PATH"''', + 'CIBW_ENVIRONMENT': '''SOMETHING="$(mkdir -p /new_path ; touch /new_path/python ; chmod +x /new_path/python)" PATH="/new_path:$PATH"''', 'CIBW_ENVIRONMENT_WINDOWS': '''SOMETHING="$(mkdir new_path && type nul > new_path/python.exe)" PATH="$CD\\new_path;$PATH"''', }) + assert len(os.listdir(output_dir)) == 0 + captured = capfd.readouterr() + assert "python available on PATH doesn't match our installed instance" in captured.err diff --git a/unit_test/environment_test.py b/unit_test/environment_test.py index b8b961a8..850233e3 100644 --- a/unit_test/environment_test.py +++ b/unit_test/environment_test.py @@ -94,3 +94,11 @@ def test_no_vars_pass_through(): environment_dict = environment_recipe.as_dictionary(prev_environment={'CIBUILDWHEEL': 'awesome'}) assert environment_dict == {'CIBUILDWHEEL': 'awesome'} + + +def test_operators_inside_eval(): + environment_recipe = parse_environment('SOMETHING="$(echo a ; echo b ; echo c)"') + + environment_dict = environment_recipe.as_dictionary({}) + + assert environment_dict == {'SOMETHING': 'a\nb\nc'}