blob: 8497b79966cafccff1629babee790f9b42eb756a [file] [log] [blame]
#!/usr/bin/python2.6
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Merge multiple csv files representing Portage package data into
one csv file, in preparation for uploading to a Google Docs spreadsheet.
"""
import optparse
import os
import re
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
import chromite.lib.operation as operation
import chromite.lib.table as table
import chromite.lib.upgrade_table as utable
COL_PACKAGE = utable.UpgradeTable.COL_PACKAGE
COL_SLOT = utable.UpgradeTable.COL_SLOT
COL_TARGET = utable.UpgradeTable.COL_TARGET
COL_OVERLAY = utable.UpgradeTable.COL_OVERLAY
ID_COLS = [COL_PACKAGE, COL_SLOT]
oper = operation.Operation('merge_package_status')
oper.verbose = True # Without verbose Info messages don't show up.
# A bit of hard-coding with knowledge of how cros targets work.
CHROMEOS_TARGET_ORDER = ['chromeos', 'chromeos-dev', 'chromeos-test']
def _GetCrosTargetRank(target):
"""Hard-coded ranking of known/expected chromeos root targets for sorting.
The lower the ranking, the earlier in the target list it falls by
convention. In other words, in the typical target combination
"chromeos chromeos-dev", "chromeos" has a lower ranking than "chromeos-dev".
All valid rankings are greater than zero.
Return valid ranking for target or a false value if target is unrecognized."""
for ix, targ in enumerate(CHROMEOS_TARGET_ORDER):
if target == targ:
return ix + 1 # Avoid a 0 (non-true) result
return None
def ProcessTargets(targets, reverse_cros=False):
"""Process a list of |targets| to smaller, sorted list.
For example:
chromeos chromeos-dev -> chromeos-dev
chromeos chromeos-dev world -> chromeos-dev world
world hard-host-depends -> hard-host-depends world
The one chromeos target always comes back first, with targets
otherwise sorted alphabetically. The chromeos target that is
kept will be the one with the highest 'ranking', as decided
by _GetCrosTargetRank. To reverse the ranking sense, specify
|reverse_cros| as True.
These rules are specific to how we want the information to appear
in the final spreadsheet.
"""
if targets:
# Sort cros targets according to "rank".
cros_targets = [t for t in targets if _GetCrosTargetRank(t)]
cros_targets.sort(key=_GetCrosTargetRank, reverse=reverse_cros)
# Don't condense non-cros targets.
other_targets = [t for t in targets if not _GetCrosTargetRank(t)]
other_targets.sort()
# Assemble final target list, with single cros target first.
final_targets = []
if cros_targets:
final_targets.append(cros_targets[-1])
if other_targets:
final_targets.extend(other_targets)
return final_targets
def LoadTable(filepath):
"""Load the csv file at |filepath| into a table.Table object."""
table_name = os.path.basename(filepath)
if table_name.endswith('.csv'):
table_name = table_name[:-4]
return table.Table.LoadFromCSV(filepath, name=table_name)
def MergeTables(tables):
"""Merge all |tables| into one merged table. Return table."""
def TargetMerger(_col, val, other_val):
"""Function to merge two values in Root Target column from two tables."""
targets = []
if val:
targets.extend(val.split())
if other_val:
targets.extend(other_val.split())
processed_targets = ProcessTargets(targets, reverse_cros=True)
return ' '.join(processed_targets)
def DefaultMerger(col, val, other_val):
"""Merge |val| and |other_val| in column |col| for some row."""
# This function is registered as the default merge function,
# so verify that the column is a supported one.
prfx = utable.UpgradeTable.COL_DEPENDS_ON.replace('ARCH', '')
if col.startswith(prfx):
# Merge dependencies by taking the superset.
return MergeToSuperset(col, val, other_val)
prfx = utable.UpgradeTable.COL_USED_BY.replace('ARCH', '')
if col.startswith(prfx):
# Merge users by taking the superset.
return MergeToSuperset(col, val, other_val)
regexp = utable.UpgradeTable.COL_UPGRADED.replace('ARCH', '\S+')
if re.search(regexp, col):
return MergeWithAND(col, val, other_val)
# For any column, if one value is missing just accept the other value.
# For example, when one table has an entry for 'arm version' but
# the other table does not.
if val == table.Table.EMPTY_CELL and other_val != table.Table.EMPTY_CELL:
return other_val
if other_val == table.Table.EMPTY_CELL and val != table.Table.EMPTY_CELL:
return val
# Raise a generic ValueError, which MergeTable function will clarify.
# The effect should be the same as having no merge_rule for this column.
raise ValueError
def MergeToSuperset(_col, val, other_val):
"""Merge |col| values as superset of tokens in |val| and |other_val|."""
tokens = set(val.split())
other_tokens = set(other_val.split())
all_tokens = tokens.union(other_tokens)
return ' '.join(sorted(tok for tok in all_tokens))
# This is only needed because the automake-wrapper package is coming from
# different overlays for different boards right now!
def MergeWithAND(_col, val, other_val):
"""For merging columns that might have differences but should not!."""
if not val:
return '"" AND ' + other_val
if not other_val + ' AND ""':
return val
return val + " AND " + other_val
# Prepare merge_rules with the defined functions.
merge_rules = {COL_TARGET: TargetMerger,
COL_OVERLAY: MergeWithAND,
'__DEFAULT__': DefaultMerger,
}
# Merge each table one by one.
csv_table = tables[0]
if len(tables) > 1:
print "Merging tables into one."
for tmp_table in tables[1:]:
print("Merging '%s' and '%s'." %
(csv_table.GetName(), tmp_table.GetName()))
csv_table.MergeTable(tmp_table, ID_COLS,
merge_rules=merge_rules, allow_new_columns=True)
# Sort the table by package name, then slot.
def IdSort(row):
return tuple(row[col] for col in ID_COLS)
csv_table.Sort(IdSort)
return csv_table
def LoadAndMergeTables(args):
"""Load all csv files in |args| into one merged table. Return table."""
tables = []
for arg in args:
print "Loading csv table from '%s'." % arg
tables.append(LoadTable(arg))
return MergeTables(tables)
# Used by upload_package_status.
def FinalizeTable(csv_table):
"""Process the table to prepare it for upload to online spreadsheet."""
print "Processing final table to prepare it for upload."
col_ver = utable.UpgradeTable.COL_CURRENT_VER
col_arm_ver = utable.UpgradeTable.GetColumnName(col_ver, 'arm')
col_x86_ver = utable.UpgradeTable.GetColumnName(col_ver, 'x86')
# Insert new columns
col_cros_target = "ChromeOS Root Target"
col_host_target = "Host Root Target"
col_cmp_arch = "Comparing arm vs x86 Versions"
csv_table.AppendColumn(col_cros_target)
csv_table.AppendColumn(col_host_target)
csv_table.AppendColumn(col_cmp_arch)
# Row by row processing
for row in csv_table:
# If the row is not unique when just the package
# name is considered, then add a ':<slot>' suffix to the package name.
id_values = { COL_PACKAGE: row[COL_PACKAGE] }
matching_rows = csv_table.GetRowsByValue(id_values)
if len(matching_rows) > 1:
for mr in matching_rows:
mr[COL_PACKAGE] = mr[COL_PACKAGE] + ":" + mr[COL_SLOT]
# Split target column into cros_target and host_target columns
target_str = row.get(COL_TARGET, None)
if target_str:
targets = target_str.split()
cros_targets = []
host_targets = []
for target in targets:
if _GetCrosTargetRank(target):
cros_targets.append(target)
else:
host_targets.append(target)
row[col_cros_target] = ' '.join(cros_targets)
row[col_host_target] = ' '.join(host_targets)
# Compare x86 vs. arm version, add result to col_cmp_arch.
x86_ver = row.get(col_x86_ver, None)
arm_ver = row.get(col_arm_ver, None)
if x86_ver and arm_ver:
if x86_ver != arm_ver:
row[col_cmp_arch] = "different"
elif x86_ver:
row[col_cmp_arch] = "same"
def WriteTable(csv_table, outpath):
"""Write |csv_table| out to |outpath| as csv."""
try:
fh = open(outpath, 'w')
csv_table.WriteCSV(fh)
print "Wrote merged table to '%s'" % outpath
except IOError as ex:
print "Unable to open %s for write: %s" % (outpath, str(ex))
raise
def main():
"""Main function."""
usage = 'Usage: %prog --out=merged_csv_file input_csv_files...'
parser = optparse.OptionParser(usage=usage)
parser.add_option('--out', dest='outpath', type='string',
action='store', default=None,
help="File to write merged results to")
(options, args) = parser.parse_args()
# Check required options
if not options.outpath:
parser.print_help()
oper.Die("The --out option is required.")
if len(args) < 1:
parser.print_help()
oper.Die("At least one input_csv_file is required.")
csv_table = LoadAndMergeTables(args)
WriteTable(csv_table, options.outpath)
if __name__ == '__main__':
main()