1#!/usr/bin/env python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License.""" 16"""Helper functions and types for command processing for difftool.""" 17 18import os 19import pathlib 20 21 22class CommandInfo: 23 """Contains information about an action commandline.""" 24 25 def __init__(self, tool, args): 26 self.tool = tool 27 self.args = args 28 29 def __str__(self): 30 s = "CommandInfo:\n" 31 s += " Tool:\n" 32 s += " " + self.tool + "\n" 33 s += " Args:\n" 34 for x in self.args: 35 s += " " + x + "\n" 36 return s 37 38 39def parse_flag_groups(args, custom_flag_group=None): 40 """Returns a list of flag groups based on the given args. 41 42 An arg group consists of one-arg flags, two-arg groups, or positional args. 43 44 Positional arguments (for example `a.out`) are returned as strings in the 45 list. 46 One-arg groups consist of a flag with no argument (for example, `--verbose`), 47 and are returned as a tuple of size one in the list. 48 Two-arg groups consist of a flag with a single argument (for example, 49 `--file bar.txt` or `--mode=verbose`), 50 and are returned as a tuple of size two in the list. 51 52 Also accepts an optional function `custom_flag_group` to determine if a 53 single arg comprises a group. (custom_flag_group(x) should return a flag 54 group abiding by the above convention, or None to use non-custom logic. 55 This may be required to accurately parse arg groups. For example, `-a b` may 56 be either a one-arg group `-a` followed by a positonal group `b`, or a two-arg 57 group `-a b`. 58 """ 59 flag_groups = [] 60 61 i = 0 62 while i < len(args): 63 if custom_flag_group: 64 g = custom_flag_group(args[i]) 65 if g is not None: 66 flag_groups += [g] 67 i += 1 68 continue 69 70 g = one_arg_group(args[i]) 71 if g is not None: 72 flag_groups += [g] 73 i += 1 74 continue 75 76 # Look for a two-arg group if there are at least 2 elements left. 77 if i < len(args) - 1: 78 g = two_arg_group(args[i], args[i + 1]) 79 if g is not None: 80 flag_groups += [g] 81 i += 2 82 continue 83 84 # Not a recognized one arg group or two arg group. 85 if args[i].startswith("-"): 86 flag_groups += [(args[i])] 87 else: 88 flag_groups += [args[i]] 89 i += 1 90 91 return flag_groups 92 93 94def remove_hyphens(x): 95 """Returns the given string with leading '--' or '-' removed.""" 96 if x.startswith("--"): 97 return x[2:] 98 elif x.startswith("-"): 99 return x[1:] 100 else: 101 return x 102 103 104def two_arg_group(a, b): 105 """Determines whether two consecutive args belong to a single flag group. 106 107 Two arguments belong to a single flag group if the first arg contains 108 a hyphen and the second does not. For example: `-foo bar` is a flag, 109 but `foo bar` and `--foo --bar` are not. 110 111 Returns: 112 A tuple of the two args without hyphens if they belong to a single 113 flag, or None if they do not. 114 """ 115 if a.startswith("-") and (not b.startswith("-")): 116 return (remove_hyphens(a), b) 117 else: 118 return None 119 120 121def one_arg_group(x): 122 """Determines whether an arg comprises a complete flag group. 123 124 An argument comprises a single flag group if it is of the form of 125 `-key=value` or `--key=value`. 126 127 Returns: 128 A tuple of `(key, value)` of the flag group, if the arg comprises a 129 complete flag group, or None if it does not. 130 """ 131 tokens = x.split("=") 132 if len(tokens) == 2: 133 return (remove_hyphens(tokens[0]), tokens[1]) 134 else: 135 return None 136 137 138def is_flag_starts_with(prefix, x): 139 if isinstance(x, tuple): 140 return x[0].startswith(prefix) 141 else: 142 return x.startswith("--" + prefix) or x.startswith("-" + prefix) 143 144 145def flag_repr(x): 146 if isinstance(x, tuple): 147 return f"-{x[0]} {x[1]}" 148 else: 149 return x 150 151 152def expand_rsp(arglist: list[str]) -> list[str]: 153 expanded_command = [] 154 for arg in arglist: 155 if len(arg) > 4 and arg[-4:] == ".rsp": 156 if arg[0] == "@": 157 arg = arg[1:] 158 with open(arg) as f: 159 expanded_command.extend([f for l in f.readlines() for f in l.split()]) 160 else: 161 expanded_command.append(arg) 162 return expanded_command 163 164 165def should_ignore_path_argument(arg) -> bool: 166 if arg.startswith("bazel-out"): 167 return True 168 if arg.startswith("out/soong/.intermediates"): 169 return True 170 return False 171 172 173def extract_paths_from_action_args( 174 args: list[str], 175) -> (list[pathlib.Path], list[pathlib.Path]): 176 paths = [] 177 other_args = [] 178 for arg in args: 179 p = pathlib.Path(arg) 180 if p.is_file(): 181 paths.append(p) 182 else: 183 other_args.append(arg) 184 return paths, other_args 185 186 187def sanitize_bazel_path(path: str) -> pathlib.Path: 188 if path[:3] == "lib": 189 path = path[3:] 190 path = path.replace("_bp2build_cc_library_static", "") 191 return pathlib.Path(path) 192 193 194def find_matching_path( 195 path: pathlib.Path, other_paths: list[pathlib.Path] 196) -> pathlib.Path: 197 multiple_best_matches = False 198 best = (0, None) 199 for op in other_paths: 200 common = os.path.commonpath([path, op]) 201 similarity = len(common.split(os.sep)) if common else 0 202 if similarity == best[0]: 203 multiple_best_matches = True 204 if similarity > best[0]: 205 multiple_best_matches = False 206 best = (similarity, op) 207 if multiple_best_matches: 208 print( 209 f"WARNING: path `{path}` had multiple best matches in list" 210 f" `{other_paths}`" 211 ) 212 return best[1] 213 214 215def _reverse_path(p: pathlib.Path) -> str: 216 return os.path.join(*reversed(os.path.normpath(p).split(os.sep))) 217 218 219def _reverse_paths(paths: list[pathlib.Path]) -> list[pathlib.Path]: 220 return [_reverse_path(p) for p in paths] 221 222 223def match_paths( 224 bazel_paths: list[str], soong_paths: list[str] 225) -> dict[str, str]: 226 reversed_bazel_paths = _reverse_paths(bazel_paths) 227 reversed_soong_paths = _reverse_paths(soong_paths) 228 closest_path = {p: (0, None) for p in reversed_bazel_paths} 229 for bp in reversed_bazel_paths: 230 bp_soong_name = sanitize_bazel_path(bp) 231 closest_path[bp] = find_matching_path(bp_soong_name, reversed_soong_paths) 232 matched_paths = {} 233 for path, match in closest_path.items(): 234 p1 = _reverse_path(path) 235 p2 = _reverse_path(match) if match is not None else None 236 matched_paths[p1] = p2 237 return matched_paths 238