2
0
Fork 0
mirror of https://github.com/bazel-contrib/rules_foreign_cc synced 2024-12-03 02:52:58 +00:00
rules_foreign_cc/tools/build_defs/shell_script_helper.bzl
irengrig 3515b20a24
Fix shell_script_helper.bzl to replace function calls inside function… (#375)
Fix shell_script_helper.bzl to replace function calls inside function texts.

Add test for shell_script_helper.bzl.
In particular, this fixes the case when some of the shell toolchain functions calls other shell toolchain functions (symlink_contents_to_dir and symlink_to_dir).
2020-04-30 11:24:25 +02:00

189 lines
6.7 KiB
Python

""" Contains functions for conversion from intermediate multiplatform notation for defining
the shell script into the actual shell script for the concrete platform.
Notation:
1) export <varname>=<value>
Define the environment variable with the name <varname> and value <value>.
If the <value> contains the toolchain command call (see 3), the call is replaced with needed value.
2) $$<varname>$$
Refer the environment variable with the name <varname>,
i.e. this will become $<varname> on Linux/MacOS, and %<varname>% on Windows.
3) ##<funname>## <arg1> ... <argn>
Find the shell toolchain command Starlark method with the name <funname> for that command
in a toolchain, and call it, passing <arg1> .. <argn>.
(see ./shell_toolchain/commands.bzl, ./shell_toolchain/impl/linux_commands.bzl etc.)
The arguments are space-separated; if the argument is quoted, the spaces inside the quites are
ignored.
! Escaping of the quotes inside the quoted argument is not supported, as it was not needed for now.
(quoted arguments are used for paths and never for any arbitrary string.)
The call of a shell toolchain Starlark method is performed through
//tools/build_defs/shell_toolchain/toolchains:access.bzl; please refer there for the details.
Here what is important is that the Starlark method can also add some text (function definitions)
into a "prelude" part of the shell_context.
The resulting script is constructed from the prelude part with function definitions and
the actual translated script part.
Since function definitions can call other functions, we perform the fictive translation
of the function bodies to populate the "prelude" part of the script.
"""
load("//tools/build_defs/shell_toolchain/toolchains:access.bzl", "call_shell", "create_context")
load("//tools/build_defs/shell_toolchain/toolchains:commands.bzl", "PLATFORM_COMMANDS")
def os_name(ctx):
return call_shell(create_context(ctx), "os_name")
def create_function(ctx, name, text):
return call_shell(create_context(ctx), "define_function", name, text)
def convert_shell_script(ctx, script):
""" Converts shell script from the intermediate notation to actual schell script.
Please see the file header for the notation description.
Arguments:
ctx - rule context
script - the array of script strings, each string can be of multiple lines
Output: the string with the shell script for the current execution platform
"""
return convert_shell_script_by_context(create_context(ctx), script)
def convert_shell_script_by_context(shell_context, script):
# 0. Split in lines merged fragments.
new_script = []
for fragment in script:
new_script += fragment.splitlines()
script = new_script
# 1. Call the functions or replace export statements.
script = [do_function_call(line, shell_context) for line in script]
# 2. Make sure functions calls are replaced.
# (it is known there is no deep recursion, do it only once)
script = [do_function_call(line, shell_context) for line in script]
# 3. Same for function bodies.
#
# Since we have some function bodies containing calls to other functions,
# we need to replace calls to the new functions and add the text
# of those functions to shell_context.prelude several times,
# and 4 times is enough for our toolchain.
# Example of such function: 'symlink_contents_to_dir'.
processed_prelude = {}
for i in range(1, 4):
for key in shell_context.prelude.keys():
text = shell_context.prelude[key]
lines = text.splitlines()
replaced = "\n".join([
do_function_call(line.strip(" "), shell_context)
for line in lines
])
processed_prelude[key] = replaced
for key in processed_prelude.keys():
shell_context.prelude[key] = processed_prelude[key]
script = shell_context.prelude.values() + script
# 4. replace all variable references
script = [replace_var_ref(line, shell_context) for line in script]
result = "\n".join(script)
return result
def replace_var_ref(text, shell_context):
parts = []
current = text
# long enough
for i in range(1, 100):
(before, varname, after) = extract_wrapped(current, "$$")
if not varname:
parts.append(current)
break
parts.append(before)
parts.append(shell_context.shell.use_var(varname))
current = after
return "".join(parts)
def replace_exports(text, shell_context):
text = text.strip(" ")
(varname, separator, value) = text.partition("=")
if not separator:
fail("Wrong export declaration")
(funname, after) = get_function_name(value.strip(" "))
if funname:
value = call_shell(shell_context, funname, *split_arguments(after.strip(" ")))
return call_shell(shell_context, "export_var", varname, value)
def get_function_name(text):
(funname, separator, after) = text.partition(" ")
if funname == "export":
return (funname, after)
(before, funname_extracted, after_extracted) = extract_wrapped(funname, "##", "##")
if funname_extracted and PLATFORM_COMMANDS.get(funname_extracted):
if len(before) > 0 or len(after_extracted) > 0:
fail("Something wrong with the shell command call notation: " + text)
return (funname_extracted, after)
return (None, None)
def extract_wrapped(text, prefix, postfix = None):
postfix = postfix or prefix
(before, separator, after) = text.partition(prefix)
if not separator or not after:
return (text, None, None)
(varname, separator2, after2) = after.partition(postfix)
if not separator2:
fail("Variable or function name is not marked correctly in fragment: {}".format(text))
return (before, varname, after2)
def do_function_call(text, shell_context):
(funname, after) = get_function_name(text.strip(" "))
if not funname:
return text
if funname == "export":
return replace_exports(after, shell_context)
arguments = split_arguments(after.strip(" ")) if after else []
return call_shell(shell_context, funname, *arguments)
def split_arguments(text):
parts = []
current = text.strip(" ")
# long enough
for i in range(1, 100):
if not current:
break
# we are ignoring escaped quotes
(before, separator, after) = current.partition("\"")
if not separator:
parts += current.split(" ")
break
(quoted, separator2, after2) = after.partition("\"")
if not separator2:
fail("Incorrect quoting in fragment: {}".format(current))
before = before.strip(" ")
if before:
parts += before.split(" ")
parts.append(quoted)
current = after2
return parts