1#!/usr/bin/python
2#
3# Copyright (C) 2017 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
17"""
18Compares one or more corresponding files from ojluni against one or
19more upstream or from upstreams against each other.
20The repositories (default: ojluni vs. expected current upstream) and
21the diff tool (default: meld) can be specified by command line options.
22
23This tool is for libcore maintenance; if you're not maintaining libcore,
24you won't need it (and might not have access to some of the instructions
25below).
26
27The naming of the repositories (expected, ojluni, 7u40, 8u121-b13,
289b113+, 9+181) is based on the directory name where corresponding
29snapshots are stored when following the instructions at
30http://go/libcore-o-verify
31
32This in turn derives from the instructions at the top of:
33libcore/tools/upstream/src/main/java/libcore/CompareUpstreams.java
34
35Possible uses:
36
37To verify that ArrayList has been updated to the expected upstream
38and that all local patches carry change markers, we compare that
39file from ojluni against the expected upstream (the default):
40  upstream-diff java/util/ArrayList.java
41
42To verify multiple files:
43  upstream-diff java.util.ArrayList java.util.LinkedList
44
45To verify a folder:
46  upstream-diff java/util/concurrent
47
48To verify a package:
49  upstream-diff java.util.concurrent
50
51Use a three-way merge to integrate changes from 9+181 into ArrayList:
52  upstream-diff -r 8u121-b13,ojluni,9+181 java/util/ArrayList.java
53or to investigate which version of upstream introduced a change:
54  upstream-diff -r 7u40,8u60,8u121-b13 java/util/ArrayList.java
55"""
56
57import argparse
58import os
59import os.path
60import re
61import subprocess
62import sys
63
64
65def get_path_type(rel_path):
66    ext = os.path.splitext(rel_path)[-1]
67    # Check the extension and if it is a C/C++ extension then we're dealing
68    # with a native path. Otherwise we would be dealing with a java path, which
69    # can be a filename, folder name, package name, or fully qualified class
70    # name.
71    if re.match('\\.(c|cc|cpp|cxx|h|hpp|hxx|icc)$', ext):
72        return 'native'
73    return 'java'
74
75
76def normalize_java_path(rel_path):
77    if re.match('.+\\.java$', rel_path):
78        # Path ends in '.java' so a filename with its path is expected
79        return rel_path
80
81    if '/' not in rel_path:
82        # Convert package name, or fully qualified class name into path
83        rel_path = rel_path.replace('.', '/')
84
85    if any(c.isupper() for c in rel_path):
86        # If the name includes an uppercase character, we guess that this is a
87        # class rather than a package name, so the extension needs to be appended
88        # to get the full filename
89        # Note: Test packages may have upper case characters in the package name,
90        # so, if trying to diff a test package, the ".java" suffix will be added
91        # unnecessarily, causing a wrong diff input. Therefore, for test packages,
92        # the tool should be used for each file individually
93        rel_path += '.java'
94    elif rel_path[-1] != '/':
95        # No upper case characters, so this must be a folder/package
96        rel_path += '/'
97
98    return rel_path
99
100
101def run_diff(diff, repositories, rel_paths):
102    # Root of checked-out Android sources, set by the "lunch" command.
103    android_build_top = os.environ['ANDROID_BUILD_TOP']
104    # Root of repository snapshots. See go/libcore-o-verify for how you'd want
105    # to set this.
106    ojluni_upstreams = os.environ['OJLUNI_UPSTREAMS']
107    for rel_path in rel_paths:
108        path_type = get_path_type(rel_path)
109        if path_type == 'java':
110            rel_path = normalize_java_path(rel_path)
111        paths = []
112
113        for repository in repositories:
114            if repository == 'ojluni':
115                paths.append('%s/libcore/ojluni/src/main/%s/%s'
116                             % (android_build_top, path_type, rel_path))
117            else:
118                paths.append('%s/%s/%s' % (ojluni_upstreams, repository, rel_path))
119        subprocess.call([diff] + paths)
120
121
122def main():
123    parser = argparse.ArgumentParser(
124        description='Compare files between libcore/ojluni and ${OJLUNI_UPSTREAMS}.',
125        formatter_class=argparse.ArgumentDefaultsHelpFormatter, # include default values in help
126    )
127    upstreams = os.environ['OJLUNI_UPSTREAMS']
128    # natsort.natsorted() would be a nicer sort order, but I'd rather avoid the dependency
129    repositories = ['ojluni'] + sorted(
130        [d for d in os.listdir(upstreams) if os.path.isdir(os.path.join(upstreams, d))]
131    )
132    parser.add_argument('-r', '--repositories', default='ojluni,expected',
133                    help='Comma-separated list of 2-3 repositories, to compare, in order; '
134                          'available repositories: ' + ' '.join(repositories) + '.')
135    parser.add_argument('-d', '--diff', default='meld',
136                        help='Application to use for diffing.')
137    parser.add_argument('rel_path', nargs="+",
138                        help='File to compare: either a relative path below libcore/ojluni/'
139                             'src/main/{java,native}, or a fully qualified class name.')
140    args = parser.parse_args()
141    repositories = args.repositories.split(',')
142    if (len(repositories) < 2):
143        print('Expected >= 2 repositories to compare, got: ' + str(repositories))
144        parser.print_help()
145        sys.exit(1)
146    run_diff(args.diff, repositories, args.rel_path)
147
148
149if __name__ == "__main__":
150    main()
151