Matt Tennant | f34162f | 2011-06-08 17:24:09 -0700 | [diff] [blame^] | 1 | #!/usr/bin/python2.6 |
| 2 | # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """Merge multiple csv files representing Portage package data into |
| 7 | one csv file, in preparation for uploading to a Google Docs spreadsheet. |
| 8 | """ |
| 9 | |
| 10 | import optparse |
| 11 | import os |
| 12 | import sys |
| 13 | |
| 14 | import cros_portage_upgrade |
| 15 | |
| 16 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) |
| 17 | import chromite.lib.table as table |
| 18 | import chromite.lib.cros_build_lib as cros_lib |
| 19 | |
| 20 | COL_PACKAGE = cros_portage_upgrade.UpgradeTable.COL_PACKAGE |
| 21 | COL_SLOT = cros_portage_upgrade.UpgradeTable.COL_SLOT |
| 22 | COL_TARGET = cros_portage_upgrade.UpgradeTable.COL_TARGET |
| 23 | COL_OVERLAY = cros_portage_upgrade.UpgradeTable.COL_OVERLAY |
| 24 | ID_COLS = [COL_PACKAGE, COL_SLOT] |
| 25 | |
| 26 | # A bit of hard-coding with knowledge of how cros targets work. |
| 27 | CHROMEOS_TARGET_ORDER = ['chromeos', 'chromeos-dev', 'chromeos-test'] |
| 28 | def _GetCrosTargetRank(target): |
| 29 | """Hard-coded ranking of known/expected chromeos root targets for sorting. |
| 30 | |
| 31 | The lower the ranking, the earlier in the target list it falls by |
| 32 | convention. In other words, in the typical target combination |
| 33 | "chromeos chromeos-dev", "chromeos" has a lower ranking than "chromeos-dev". |
| 34 | |
| 35 | All valid rankings are greater than zero. |
| 36 | |
| 37 | Return valid ranking for target or a false value if target is unrecognized.""" |
| 38 | for ix, targ in enumerate(CHROMEOS_TARGET_ORDER): |
| 39 | if target == targ: |
| 40 | return ix + 1 # Avoid a 0 (non-true) result |
| 41 | return None |
| 42 | |
| 43 | def _ProcessTargets(targets, reverse_cros=False): |
| 44 | """Process a list of |targets| to smaller, sorted list. |
| 45 | |
| 46 | For example: |
| 47 | chromeos chromeos-dev -> chromeos-dev |
| 48 | chromeos chromeos-dev world -> chromeos-dev world |
| 49 | world hard-host-depends -> hard-host-depends world |
| 50 | |
| 51 | The one chromeos target always comes back first, with targets |
| 52 | otherwise sorted alphabetically. The chromeos target that is |
| 53 | kept will be the one with the highest 'ranking', as decided |
| 54 | by _GetCrosTargetRank. To reverse the ranking sense, specify |
| 55 | |reverse_cros| as True. |
| 56 | |
| 57 | These rules are specific to how we want the information to appear |
| 58 | in the final spreadsheet. |
| 59 | """ |
| 60 | if targets: |
| 61 | # Sort cros targets according to "rank". |
| 62 | cros_targets = [t for t in targets if _GetCrosTargetRank(t)] |
| 63 | cros_targets.sort(key=_GetCrosTargetRank, reverse=reverse_cros) |
| 64 | |
| 65 | # Don't condense non-cros targets. |
| 66 | other_targets = [t for t in targets if not _GetCrosTargetRank(t)] |
| 67 | other_targets.sort() |
| 68 | |
| 69 | # Assemble final target list, with single cros target first. |
| 70 | final_targets = [] |
| 71 | if cros_targets: |
| 72 | final_targets.append(cros_targets[-1]) |
| 73 | if other_targets: |
| 74 | final_targets.extend(other_targets) |
| 75 | |
| 76 | return final_targets |
| 77 | |
| 78 | def _ProcessRowTargetValue(row): |
| 79 | """Condense targets like 'chromeos chromeos-dev' to just 'chromeos-dev'.""" |
| 80 | targets = row[COL_TARGET].split() |
| 81 | if targets: |
| 82 | processed_targets = _ProcessTargets(targets) |
| 83 | row[COL_TARGET] = ' '.join(processed_targets) |
| 84 | |
| 85 | def LoadTable(filepath): |
| 86 | """Load the csv file at |filepath| into a table.Table object.""" |
| 87 | csv_table = table.Table.LoadFromCSV(filepath) |
| 88 | |
| 89 | # Process the Target column now. |
| 90 | csv_table.ProcessRows(_ProcessRowTargetValue) |
| 91 | |
| 92 | return csv_table |
| 93 | |
| 94 | def LoadTables(args): |
| 95 | """Load all csv files in |args| into one merged table. Return table.""" |
| 96 | def TargetMerger(col, val, other_val): |
| 97 | """Function to merge two values in Root Target column from two tables.""" |
| 98 | targets = [] |
| 99 | if val: |
| 100 | targets.extend(val.split()) |
| 101 | if other_val: |
| 102 | targets.extend(other_val.split()) |
| 103 | |
| 104 | processed_targets = _ProcessTargets(targets, reverse_cros=True) |
| 105 | return ' '.join(processed_targets) |
| 106 | |
| 107 | def DefaultMerger(col, val, other_val): |
| 108 | """Merge |val| and |other_val| in column |col| for some row.""" |
| 109 | # This function is registered as the default merge function, |
| 110 | # so verify that the column is a supported one. |
| 111 | prfx = cros_portage_upgrade.UpgradeTable.COL_DEPENDS_ON.replace('ARCH', '') |
| 112 | if col.startswith(prfx): |
| 113 | # Merge dependencies by taking the superset. |
| 114 | deps = set(val.split()) |
| 115 | other_deps = set(other_val.split()) |
| 116 | all_deps = deps.union(other_deps) |
| 117 | return ' '.join(sorted(dep for dep in all_deps)) |
| 118 | |
| 119 | # Raise a generic ValueError, which MergeTable function will clarify. |
| 120 | # The effect should be the same as having no merge_rule for this column. |
| 121 | raise ValueError |
| 122 | |
| 123 | # This is only needed because the automake-wrapper package is coming from |
| 124 | # different overlays for different boards right now! |
| 125 | def MergeWithAND(col, val, other_val): |
| 126 | """For merging columns that might have differences but should not!.""" |
| 127 | if not val: |
| 128 | return '"" AND ' + other_val |
| 129 | if not other_val + ' AND ""': |
| 130 | return val |
| 131 | return val + " AND " + other_val |
| 132 | |
| 133 | # Prepare merge_rules with the defined functions. |
| 134 | merge_rules = {COL_TARGET: TargetMerger, |
| 135 | COL_OVERLAY: MergeWithAND, |
| 136 | '__DEFAULT__': DefaultMerger, |
| 137 | } |
| 138 | |
| 139 | # Load and merge the files. |
| 140 | print "Loading csv table from '%s'." % (args[0]) |
| 141 | csv_table = LoadTable(args[0]) |
| 142 | if len(args) > 1: |
| 143 | for arg in args[1:]: |
| 144 | print "Loading csv table from '%s'." % (arg) |
| 145 | tmp_table = LoadTable(arg) |
| 146 | |
| 147 | print "Merging tables into one." |
| 148 | csv_table.MergeTable(tmp_table, ID_COLS, |
| 149 | merge_rules=merge_rules, allow_new_columns=True) |
| 150 | |
| 151 | # Sort the table by package name, then slot. |
| 152 | def IdSort(row): |
| 153 | return tuple(row[col] for col in ID_COLS) |
| 154 | csv_table.Sort(IdSort) |
| 155 | |
| 156 | return csv_table |
| 157 | |
| 158 | def FinalizeTable(csv_table): |
| 159 | """Process the table to prepare it for upload to online spreadsheet.""" |
| 160 | print "Processing final table to prepare it for upload." |
| 161 | |
| 162 | col_ver = cros_portage_upgrade.UpgradeTable.COL_CURRENT_VER |
| 163 | col_arm_ver = cros_portage_upgrade.UpgradeTable.GetColumnName(col_ver, 'arm') |
| 164 | col_x86_ver = cros_portage_upgrade.UpgradeTable.GetColumnName(col_ver, 'x86') |
| 165 | |
| 166 | # Insert new columns |
| 167 | col_cros_target = "ChromeOS Root Target" |
| 168 | col_host_target = "Host Root Target" |
| 169 | col_cmp_arch = "Comparing arm vs x86 Versions" |
| 170 | csv_table.AppendColumn(col_cros_target) |
| 171 | csv_table.AppendColumn(col_host_target) |
| 172 | csv_table.AppendColumn(col_cmp_arch) |
| 173 | |
| 174 | # Row by row processing |
| 175 | for row in csv_table: |
| 176 | # If the row is not unique when just the package |
| 177 | # name is considered, then add a ':<slot>' suffix to the package name. |
| 178 | id_values = { COL_PACKAGE: row[COL_PACKAGE] } |
| 179 | matching_rows = csv_table.GetRowsByValue(id_values) |
| 180 | if len(matching_rows) > 1: |
| 181 | for mr in matching_rows: |
| 182 | mr[COL_PACKAGE] = mr[COL_PACKAGE] + ":" + mr[COL_SLOT] |
| 183 | |
| 184 | # Split target column into cros_target and host_target columns |
| 185 | target_str = row.get(COL_TARGET, None) |
| 186 | if target_str: |
| 187 | targets = target_str.split() |
| 188 | cros_targets = [] |
| 189 | host_targets = [] |
| 190 | for target in targets: |
| 191 | if _GetCrosTargetRank(target): |
| 192 | cros_targets.append(target) |
| 193 | else: |
| 194 | host_targets.append(target) |
| 195 | |
| 196 | row[col_cros_target] = ' '.join(cros_targets) |
| 197 | row[col_host_target] = ' '.join(host_targets) |
| 198 | |
| 199 | # Compare x86 vs. arm version, add result to col_cmp_arch. |
| 200 | x86_ver = row.get(col_x86_ver, None) |
| 201 | arm_ver = row.get(col_arm_ver, None) |
| 202 | if x86_ver and arm_ver: |
| 203 | if x86_ver != arm_ver: |
| 204 | row[col_cmp_arch] = "different" |
| 205 | elif x86_ver: |
| 206 | row[col_cmp_arch] = "same" |
| 207 | |
| 208 | def WriteTable(csv_table, outpath): |
| 209 | """Write |csv_table| out to |outpath| as csv.""" |
| 210 | try: |
| 211 | fh = open(outpath, 'w') |
| 212 | csv_table.WriteCSV(fh) |
| 213 | print "Wrote merged table to '%s'" % outpath |
| 214 | except IOError as ex: |
| 215 | print "Unable to open %s for write: %s" % (outpath, str(ex)) |
| 216 | raise |
| 217 | |
| 218 | def main(): |
| 219 | """Main function.""" |
| 220 | usage = 'Usage: %prog --out=merged_csv_file input_csv_files...' |
| 221 | parser = optparse.OptionParser(usage=usage) |
| 222 | parser.add_option('--finalize-for-upload', dest='finalize', |
| 223 | action='store_true', default=False, |
| 224 | help="Some processing of output for upload purposes.") |
| 225 | parser.add_option('--out', dest='outpath', type='string', |
| 226 | action='store', default=None, |
| 227 | help="File to write merged results to") |
| 228 | |
| 229 | (options, args) = parser.parse_args() |
| 230 | |
| 231 | # Check required options |
| 232 | if not options.outpath: |
| 233 | parser.print_help() |
| 234 | cros_lib.Die("The --out option is required.") |
| 235 | if len(args) < 1: |
| 236 | parser.print_help() |
| 237 | cros_lib.Die("At least one input_csv_file is required.") |
| 238 | |
| 239 | csv_table = LoadTables(args) |
| 240 | |
| 241 | if options.finalize: |
| 242 | FinalizeTable(csv_table) |
| 243 | |
| 244 | WriteTable(csv_table, options.outpath) |
| 245 | |
| 246 | if __name__ == '__main__': |
| 247 | main() |