blob: 6fd8b7a6af0353da8c4e15683c38c126a6c0fdac [file] [log] [blame]
Matt Tennantf34162f2011-06-08 17:24:09 -07001#!/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
7one csv file, in preparation for uploading to a Google Docs spreadsheet.
8"""
9
10import optparse
11import os
12import sys
13
14import cros_portage_upgrade
15
16sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
17import chromite.lib.table as table
18import chromite.lib.cros_build_lib as cros_lib
19
20COL_PACKAGE = cros_portage_upgrade.UpgradeTable.COL_PACKAGE
21COL_SLOT = cros_portage_upgrade.UpgradeTable.COL_SLOT
22COL_TARGET = cros_portage_upgrade.UpgradeTable.COL_TARGET
23COL_OVERLAY = cros_portage_upgrade.UpgradeTable.COL_OVERLAY
24ID_COLS = [COL_PACKAGE, COL_SLOT]
25
26# A bit of hard-coding with knowledge of how cros targets work.
27CHROMEOS_TARGET_ORDER = ['chromeos', 'chromeos-dev', 'chromeos-test']
28def _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
43def _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
78def _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
85def 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
94def 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
158def 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
208def 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
218def 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
246if __name__ == '__main__':
247 main()