diff --git a/cibuildwheel/bashlex_eval.py b/cibuildwheel/bashlex_eval.py new file mode 100644 index 00000000..4437990b --- /dev/null +++ b/cibuildwheel/bashlex_eval.py @@ -0,0 +1,39 @@ +import subprocess, shlex + + +def evaluate_node(node, environment): + if node.kind == 'word': + return evaluate_word_node(node, environment=environment) + elif node.kind == 'commandsubstitution': + return evaluate_command_node(node.command, environment=environment) + elif node.kind == 'parameter': + return evaluate_parameter_node(node, environment=environment) + else: + raise ValueError('Unsupported bash construct: "%s"' % node.word) + + +def evaluate_word_node(node, environment): + letters = list(node.word) + + for part in node.parts: + part_start = part.pos[0] + part_end = part.pos[1] + + # Set all the characters in the part to None + for i in range(part_start, part_end): + letters[i] = None + + letters[part_start] = evaluate_node(part, environment=environment) + + # remove the None letters and concat + return ''.join(l for l in letters if l is not None) + + +def evaluate_command_node(node, environment): + words = [evaluate_node(part, environment=environment) for part in node.parts] + command = ' '.join(words) + return subprocess.check_output(shlex.split(command), env=environment) + + +def evaluate_parameter_node(node, environment): + return environment.get(node.value, '') diff --git a/cibuildwheel/environment.py b/cibuildwheel/environment.py new file mode 100644 index 00000000..6d6ed71f --- /dev/null +++ b/cibuildwheel/environment.py @@ -0,0 +1,73 @@ +import bashlex +from . import bashlex_eval + + +class EnvironmentParseError(Exception): + pass + + +def parse_environment(env_string): + env_items = split_env_items(env_string) + assignments = [EnvironmentAssignment(item) for item in env_items] + return ParsedEnvironment(assignments=assignments) + + +def split_env_items(env_string): + '''Splits space-separated variable assignments into a list of individual assignments. + + >>> split_env_items('VAR=abc') + ['VAR=abc'] + >>> split_env_items('VAR="a string" THING=3') + ['VAR="a string"', 'THING=3'] + >>> split_env_items('VAR="a string" THING=\\'single "quotes"\\'') + ['VAR="a string"', 'THING=\\'single "quotes"\\''] + >>> split_env_items('VAR="dont \\\\"forget\\\\" about backslashes"') + ['VAR="dont \\\\"forget\\\\" about backslashes"'] + ''' + return list(bashlex.split(env_string)) + + +class EnvironmentAssignment(object): + def __init__(self, assignment): + name, equals, value = assignment.partition('=') + if not equals: + raise EnvironmentParseError(assignment) + self.name = name + self.value = value + + if value: + command_node = bashlex.parsesingle(value) + + if len(command_node.parts) != 1: + raise ValueError('"%s" has too many parts' % value) + + self.value_word_node = command_node.parts[0] + else: + self.value_word_node = None + + def evaluated_value(self, environment): + '''Returns the value of this assignment, as evaluated in the environment''' + if self.value_word_node: + return bashlex_eval.evaluate_node(self.value_word_node, environment=environment) + else: + return '' + + def as_shell_assignment(self): + return 'export %s=%s' % (self.name, self.value) + + +class ParsedEnvironment(object): + def __init__(self, assignments): + self.assignments = assignments + + def as_dictionary(self, prev_environment): + environment = prev_environment.copy() + + for assignment in self.assignments: + value = assignment.evaluated_value(environment=environment) + environment[assignment.name] = value + + return environment + + def as_shell_commands(self): + return [a.as_shell_assignment() for a in self.assignments] diff --git a/setup.py b/setup.py index af684db1..1191f54b 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ except ImportError: setup( name='cibuildwheel', version='0.4.0', + install_requires=['bashlex'], description="Build Python wheels on CI with minimal configuration.", long_description='For readme please see http://github.com/joerick/cibuildwheel', author="Joe Rickerby", diff --git a/unit_test/environment_test.py b/unit_test/environment_test.py index 9ac295f6..a1556c87 100644 --- a/unit_test/environment_test.py +++ b/unit_test/environment_test.py @@ -5,8 +5,8 @@ def test_basic_parsing(): environment_recipe = parse_environment('VAR=1 VBR=2') environment_dict = environment_recipe.as_dictionary( - prev_environment={}, - shell=lambda cmd: '') + prev_environment={} + ) environment_cmds = environment_recipe.as_shell_commands() assert environment_dict == {'VAR': '1', 'VBR': '2'} @@ -16,20 +16,31 @@ def test_quotes(): environment_recipe = parse_environment('A=1 VAR="1 NOT_A_VAR=2" VBR=\'vbr\'') environment_dict = environment_recipe.as_dictionary( - prev_environment={}, - shell=lambda cmd: '') + prev_environment={} + ) environment_cmds = environment_recipe.as_shell_commands() assert environment_dict == {'A': '1', 'VAR': '1 NOT_A_VAR=2', 'VBR': 'vbr'} - assert environment_cmds == ['export A=1', 'export VAR="1 NOT_A_VAR=2"', 'export ABR=vbr'] + assert environment_cmds == ['export A=1', 'export VAR="1 NOT_A_VAR=2"', 'export VBR=\'vbr\''] def test_inheritance(): environment_recipe = parse_environment('PATH=$PATH:/usr/local/bin') environment_dict = environment_recipe.as_dictionary( - prev_environment={'PATH': '/usr/bin'}, - shell=lambda cmd: '') + prev_environment={'PATH': '/usr/bin'} + ) environment_cmds = environment_recipe.as_shell_commands() assert environment_dict == {'PATH': '/usr/bin:/usr/local/bin'} assert environment_cmds == ['export PATH=$PATH:/usr/local/bin'] + +def test_empty_var(): + environment_recipe = parse_environment('CFLAGS=') + + environment_dict = environment_recipe.as_dictionary( + prev_environment={'CFLAGS': '-Wall'} + ) + environment_cmds = environment_recipe.as_shell_commands() + + assert environment_dict == {'CFLAGS': ''} + assert environment_cmds == ['export CFLAGS=']