diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst --- a/llvm/docs/CommandGuide/lit.rst +++ b/llvm/docs/CommandGuide/lit.rst @@ -556,6 +556,7 @@ Otherwise, %t but with a single leading ``/`` removed. %:T On Windows, %/T but a ``:`` is removed if its the second character. Otherwise, %T but with a single leading ``/`` removed. + %{for-each-file } Invokes RUN command for each file in ````. Accepts wildcards. ======================= ============== Other substitutions are provided that are variations on this base set and diff --git a/llvm/docs/ReleaseNotes.rst b/llvm/docs/ReleaseNotes.rst --- a/llvm/docs/ReleaseNotes.rst +++ b/llvm/docs/ReleaseNotes.rst @@ -268,6 +268,8 @@ * Made significant changes to JSON output format of `llvm-readobj`/`llvm-readelf` to improve correctness and clarity. +* llvm-lit now accepts %{for-each-file} in RUN commands. + Changes to LLDB --------------------------------- diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py --- a/llvm/utils/lit/lit/TestRunner.py +++ b/llvm/utils/lit/lit/TestRunner.py @@ -1529,9 +1529,15 @@ # We use #_MARKER_# to hide %% while we do the other substitutions. def escapePercents(ln): + if isinstance(ln, list): + return [_caching_re_compile("%%").sub("#_MARKER_#", line) + for line in ln] return _caching_re_compile("%%").sub("#_MARKER_#", ln) def unescapePercents(ln): + if isinstance(ln, list): + return [_caching_re_compile("#_MARKER_#").sub("%", line) + for line in ln] return _caching_re_compile("#_MARKER_#").sub("%", ln) def substituteIfElse(ln): @@ -1623,6 +1629,43 @@ assert len(ln) == 0 return result + def substituteForEachFile(ln): + from glob import iglob + from pathlib import Path + + if "%{for-each-file" not in ln: + return ln + + lines = [] + pattern = re.compile(r'%{for-each-file ([^}]+)}') + matches = list(pattern.finditer(ln)) + if len(matches) == 0: + raise ValueError( + "%{for-each-file} expects a path, but it is not specified." + ) + elif len(matches) > 1: + # FIXME: here we make sure there is only one %{for-each-file} + # to expand, but we can support multiple via cartesian product. + # It doesn't seem immediately useful, so we are keeping this + # function simple. + raise ValueError( + "Multiple %{for-each-file} per directive are not supported" + " yet." + ) + match = matches[0] + path = Path(match[1]).resolve() + if not path.is_absolute(): + raise ValueError( + "%{for-each-file} expects an absulute path," + " but '{}' is not absolute." + .format(str(path)) + ) + if path.is_dir() and '*' not in str(path): + path = path / '*' + for file in iglob(str(path), recursive=True): + lines.append(pattern.sub(file, ln)) + return lines + def processLine(ln): # Apply substitutions ln = substituteIfElse(escapePercents(ln)) @@ -1639,7 +1682,10 @@ ln = _caching_re_compile(a).sub(str(b), escapePercents(ln)) # Strip the trailing newline and any extra whitespace. - return ln.strip() + ln = ln.strip() + + ln = substituteForEachFile(escapePercents(ln)) + return ln def processLineToFixedPoint(ln): assert isinstance(recursion_limit, int) and recursion_limit >= 0 @@ -1671,7 +1717,11 @@ # Can come from preamble_commands. assert isinstance(directive, str) line = directive - output.append(unescapePercents(process(line))) + line = unescapePercents(process(line)) + if isinstance(line, str): + output.append(line) + elif isinstance(line, list): + output.extend(line) return output