1 #!/usr/bin/env python 2 3 import requests 4 import re 5 from in_file import InFile 6 7 BRANCH_FORMAT = "https://src.chromium.org/blink/branches/chromium/%s/%s" 8 TRUNK_PATH = "Source/platform/RuntimeEnabledFeatures.in" 9 TRUNK_URL = "https://src.chromium.org/blink/trunk/%s" % TRUNK_PATH 10 11 12 def features_path(branch): 13 # RuntimeEnabledFeatures has only existed since April 2013: 14 if branch <= 1453: 15 return None 16 # Source/core/page/RuntimeEnabledFeatures.in existed by 1547 17 # but was in an old format without status= arguments. 18 if branch <= 1547: 19 return None 20 if branch <= 1650: 21 return "Source/core/page/RuntimeEnabledFeatures.in" 22 # Modern location: 23 return TRUNK_PATH 24 25 26 def parse_features_file(features_text): 27 valid_values = { 28 'status': ['stable', 'experimental', 'deprecated', 'test'], 29 } 30 defaults = { 31 'condition': None, 32 'depends_on': [], 33 'custom': False, 34 'status': None, 35 } 36 37 # FIXME: in_file.py manually calls str.strip so conver to str here. 38 features_lines = str(features_text).split("\n") 39 return InFile(features_lines, defaults, valid_values) 40 41 42 def stable_features(in_file): 43 return [feature['name'] for feature in in_file.name_dictionaries if feature['status'] == 'stable'] 44 45 46 def branch_from_version(version_string): 47 # Format: 31.0.1650.63, the second digit was only ever used for M4 48 # no clue what it's actually intended for. 49 version_regexp = r"(?P<major>\d+)\.\d+\.(?P<branch>\d+)\.(?P<minor>\d+)" 50 match = re.match(version_regexp, version_string) 51 # if match == None, we'll blow up, so at least provide some debugging information: 52 if not match: 53 print version_string 54 return int(match.group('branch')) 55 56 57 def print_feature_diff(added_features, removed_features): 58 for feature in added_features: 59 print "+ %s" % feature 60 for feature in removed_features: 61 print "- %s" % feature 62 63 64 def historical_versions(os_string, channel): 65 url_pattern = "http://omahaproxy.appspot.com/history?os=%s&channel=%s" 66 url = url_pattern % (os_string, channel) 67 releases_csv = requests.get(url).text.strip("\n") 68 # Format: os,channel,version_string,date_string 69 lines = releases_csv.split('\n') 70 # As of June 2014, omahaproxy is now including headers: 71 assert(lines[0] == 'os,channel,version,timestamp') 72 # FIXME: We could replace this with more generic CSV parsing now that we have headers. 73 return [line.split(',')[2] for line in lines[1:]] 74 75 76 def feature_file_url_for_branch(branch): 77 path = features_path(branch) 78 if not path: 79 return None 80 return BRANCH_FORMAT % (branch, path) 81 82 83 def feature_file_for_branch(branch): 84 url = feature_file_url_for_branch(branch) 85 if not url: 86 return None 87 return parse_features_file(requests.get(url).text) 88 89 90 def historical_feature_tuples(os_string, channel): 91 feature_tuples = [] 92 version_strings = reversed(historical_versions(os_string, channel)) 93 seen_branches = set() 94 95 for version in version_strings: 96 branch = branch_from_version(version) 97 if branch in seen_branches: 98 continue 99 seen_branches.add(branch) 100 101 feature_file = feature_file_for_branch(branch) 102 if not feature_file: 103 continue 104 feature_tuple = (version, feature_file) 105 feature_tuples.append(feature_tuple) 106 return feature_tuples 107 108 109 class FeatureAuditor(object): 110 def __init__(self): 111 self.last_features = [] 112 113 def add_version(self, version_name, feature_file): 114 features = stable_features(feature_file) 115 if self.last_features: 116 added_features = list(set(features) - set(self.last_features)) 117 removed_features = list(set(self.last_features) - set(features)) 118 119 print "\n%s:" % version_name 120 print_feature_diff(added_features, removed_features) 121 122 self.last_features = features 123 124 125 def active_feature_tuples(os_string): 126 feature_tuples = [] 127 current_releases_url = "http://omahaproxy.appspot.com/all.json" 128 trains = requests.get(current_releases_url).json() 129 train = next(train for train in trains if train['os'] == os_string) 130 # FIXME: This is depending on the ordering of the json, we could 131 # use use sorted() with true_branch, but that would put None first. 132 for version in reversed(train['versions']): 133 # FIXME: This is lame to exclude stable, the caller should 134 # ignore it if it doesn't want it. 135 if version['channel'] == 'stable': 136 continue # handled by historical_feature_tuples 137 branch = version['true_branch'] 138 if branch: 139 feature_file = feature_file_for_branch(branch) 140 else: 141 feature_file = parse_features_file(requests.get(TRUNK_URL).text) 142 143 name = "%(version)s %(channel)s" % version 144 feature_tuples.append((name, feature_file)) 145 return feature_tuples 146 147 148 # FIXME: This only really needs feature_files. 149 def stale_features(tuples): 150 last_features = None 151 can_be_removed = set() 152 for _, feature_file in tuples: 153 features = stable_features(feature_file) 154 if last_features: 155 can_be_removed.update(set(features)) 156 removed_features = list(set(last_features) - set(features)) 157 can_be_removed.difference_update(set(removed_features)) 158 last_features = features 159 return sorted(can_be_removed) 160 161 162 def main(): 163 historical_tuples = historical_feature_tuples("win", "stable") 164 active_tuples = active_feature_tuples("win") 165 166 auditor = FeatureAuditor() 167 for version, feature_file in historical_tuples + active_tuples: 168 auditor.add_version(version, feature_file) 169 170 print "\nConsider for removal (have been stable for at least one release):" 171 for feature in stale_features(historical_tuples): 172 print feature 173 174 175 if __name__ == "__main__": 176 main() 177