File: check_gpl_license.py

package info (click to toggle)
android-platform-development 10.0.0%2Br36-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 135,564 kB
  • sloc: java: 160,253; xml: 127,434; python: 40,579; cpp: 17,579; sh: 2,569; javascript: 1,612; ansic: 879; lisp: 261; ruby: 183; makefile: 172; sql: 140; perl: 88
file content (297 lines) | stat: -rw-r--r-- 11,747 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/env python
#
# Copyright (C) 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import glob
import logging
import os
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as xml_tree

import utils


class GPLChecker(object):
    """Checks that all GPL projects in a VNDK snapshot have released sources.

    Makes sure that the current source tree have the sources for all GPL
    prebuilt libraries in a specified VNDK snapshot version.
    """
    MANIFEST_XML = utils.MANIFEST_FILE_NAME
    MODULE_PATHS_TXT = utils.MODULE_PATHS_FILE_NAME

    def __init__(self, install_dir, android_build_top, temp_artifact_dir,
                 remote_git):
        """GPLChecker constructor.

        Args:
          install_dir: string, absolute path to the prebuilts/vndk/v{version}
            directory where the build files will be generated.
          android_build_top: string, absolute path to ANDROID_BUILD_TOP
          temp_artifact_dir: string, temp directory to hold build artifacts
            fetched from Android Build server.
          remote_git: string, remote name to fetch and check if the revision of
            VNDK snapshot is included in the source if it is not in the current
            git repository.
        """
        self._android_build_top = android_build_top
        self._install_dir = install_dir
        self._remote_git = remote_git
        self._manifest_file = os.path.join(temp_artifact_dir,
                                           self.MANIFEST_XML)
        self._notice_files_dir = os.path.join(install_dir,
                                              utils.NOTICE_FILES_DIR_PATH)

        if not os.path.isfile(self._manifest_file):
            raise RuntimeError(
                '{manifest} not found at {manifest_file}'.format(
                    manifest=self.MANIFEST_XML,
                    manifest_file=self._manifest_file))

    def _parse_module_paths(self):
        """Parses the module_paths.txt files into a dictionary,

        Returns:
          module_paths: dict, e.g. {libfoo.so: some/path/here}
        """
        module_paths = dict()
        for file in utils.find(self._install_dir, [self.MODULE_PATHS_TXT]):
            file_path = os.path.join(self._install_dir, file)
            with open(file_path, 'r') as f:
                for line in f.read().strip().split('\n'):
                    paths = line.split(' ')
                    if len(paths) > 1:
                        if paths[0] not in module_paths:
                            module_paths[paths[0]] = paths[1]
        return module_paths

    def _parse_manifest(self):
        """Parses manifest.xml file and returns list of 'project' tags."""

        root = xml_tree.parse(self._manifest_file).getroot()
        return root.findall('project')

    def _get_revision(self, module_path, manifest_projects):
        """Returns revision value recorded in manifest.xml for given project.

        Args:
          module_path: string, project path relative to ANDROID_BUILD_TOP
          manifest_projects: list of xml_tree.Element, list of 'project' tags
        """
        revision = None
        for project in manifest_projects:
            path = project.get('path')
            if module_path.startswith(path):
                revision = project.get('revision')
                break
        return revision

    def _check_revision_exists(self, revision, git_project_path):
        """Checks whether a revision is found in a git project of current tree.

        Args:
          revision: string, revision value recorded in manifest.xml
          git_project_path: string, path relative to ANDROID_BUILD_TOP
        """
        path = utils.join_realpath(self._android_build_top, git_project_path)

        def _check_rev_list(revision):
            """Checks whether revision is reachable from HEAD of git project."""

            logging.info('Checking if revision {rev} exists in {proj}'.format(
                rev=revision, proj=git_project_path))
            try:
                cmd = [
                    'git', '-C', path, 'rev-list', 'HEAD..{}'.format(revision)
                ]
                output = utils.check_output(cmd).strip()
            except subprocess.CalledProcessError as error:
                logging.error('Error: {}'.format(error))
                return False
            else:
                if output:
                    logging.debug(
                        '{proj} does not have the following revisions: {rev}'.
                        format(proj=git_project_path, rev=output))
                    return False
                else:
                    logging.info(
                        'Found revision {rev} in project {proj}'.format(
                            rev=revision, proj=git_project_path))
            return True

        if not _check_rev_list(revision):
            # VNDK snapshots built from a *-release branch will have merge
            # CLs in the manifest because the *-dev branch is merged to the
            # *-release branch periodically. In order to extract the
            # revision relevant to the source of the git_project_path,
            # we fetch the *-release branch and get the revision of the
            # parent commit with FETCH_HEAD^2.
            logging.info(
                'Checking if the parent of revision {rev} exists in {proj}'.
                format(rev=revision, proj=git_project_path))
            try:
                cmd = ['git', '-C', path, 'fetch', self._remote_git, revision]
                utils.check_call(cmd)
                cmd = ['git', '-C', path, 'rev-parse', 'FETCH_HEAD^2']
                parent_revision = utils.check_output(cmd).strip()
            except subprocess.CalledProcessError as error:
                logging.error(
                    'Failed to get parent of revision {rev} from "{remote}": '
                    '{err}'.format(
                        rev=revision, remote=self._remote_git, err=error))
                logging.error('Try --remote to manually set remote name')
                raise
            else:
                if not _check_rev_list(parent_revision):
                    return False

        return True

    def check_gpl_projects(self):
        """Checks that all GPL projects have released sources.

        Raises:
          ValueError: There are GPL projects with unreleased sources.
        """
        logging.info('Starting license check for GPL projects...')

        notice_files = glob.glob('{}/*'.format(self._notice_files_dir))
        if len(notice_files) == 0:
            raise RuntimeError('No license files found in {}'.format(
                self._notice_files_dir))

        gpl_projects = []
        pattern = 'GENERAL PUBLIC LICENSE'
        for notice_file_path in notice_files:
            with open(notice_file_path, 'r') as notice_file:
                if pattern in notice_file.read():
                    lib_name = os.path.splitext(
                        os.path.basename(notice_file_path))[0]
                    gpl_projects.append(lib_name)

        if not gpl_projects:
            logging.info('No GPL projects found.')
            return

        logging.info('GPL projects found: {}'.format(', '.join(gpl_projects)))

        module_paths = self._parse_module_paths()
        manifest_projects = self._parse_manifest()
        released_projects = []
        unreleased_projects = []

        for lib in gpl_projects:
            if lib in module_paths:
                module_path = module_paths[lib]
                revision = self._get_revision(module_path, manifest_projects)
                if not revision:
                    raise RuntimeError(
                        'No project found for {path} in {manifest}'.format(
                            path=module_path, manifest=self.MANIFEST_XML))
                revision_exists = self._check_revision_exists(
                    revision, module_path)
                if not revision_exists:
                    unreleased_projects.append((lib, module_path))
                else:
                    released_projects.append((lib, module_path))
            else:
                raise RuntimeError(
                    'No module path was found for {lib} in {module_paths}'.
                    format(lib=lib, module_paths=self.MODULE_PATHS_TXT))

        if released_projects:
            logging.info('Released GPL projects: {}'.format(released_projects))

        if unreleased_projects:
            raise ValueError(
                ('FAIL: The following GPL projects have NOT been released in '
                 'current tree: {}'.format(unreleased_projects)))

        logging.info('PASS: All GPL projects have source in current tree.')


def get_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'vndk_version',
        type=int,
        help='VNDK snapshot version to check, e.g. "27".')
    parser.add_argument('-b', '--branch', help='Branch to pull manifest from.')
    parser.add_argument('--build', help='Build number to pull manifest from.')
    parser.add_argument(
        '--remote',
        default='aosp',
        help=('Remote name to fetch and check if the revision of VNDK snapshot '
              'is included in the source to conform GPL license. default=aosp'))
    parser.add_argument(
        '-v',
        '--verbose',
        action='count',
        default=0,
        help='Increase output verbosity, e.g. "-v", "-vv".')
    return parser.parse_args()


def main():
    """For local testing purposes.

    Note: VNDK snapshot must be already installed under
      prebuilts/vndk/v{version}.
    """
    ANDROID_BUILD_TOP = utils.get_android_build_top()
    PREBUILTS_VNDK_DIR = utils.join_realpath(ANDROID_BUILD_TOP,
                                             'prebuilts/vndk')

    args = get_args()
    vndk_version = args.vndk_version
    install_dir = os.path.join(PREBUILTS_VNDK_DIR, 'v{}'.format(vndk_version))
    remote = args.remote
    if not os.path.isdir(install_dir):
        raise ValueError(
            'Please provide valid VNDK version. {} does not exist.'
            .format(install_dir))
    utils.set_logging_config(args.verbose)

    temp_artifact_dir = tempfile.mkdtemp()
    os.chdir(temp_artifact_dir)
    manifest_pattern = 'manifest_{}.xml'.format(args.build)
    manifest_dest = os.path.join(temp_artifact_dir, utils.MANIFEST_FILE_NAME)
    logging.info('Fetching {file} from {branch} (bid: {build})'.format(
        file=manifest_pattern, branch=args.branch, build=args.build))
    utils.fetch_artifact(args.branch, args.build, manifest_pattern,
                         manifest_dest)

    license_checker = GPLChecker(install_dir, ANDROID_BUILD_TOP,
                                 temp_artifact_dir, remote)
    try:
        license_checker.check_gpl_projects()
    except ValueError as error:
        logging.error('Error: {}'.format(error))
        raise
    finally:
        logging.info(
            'Deleting temp_artifact_dir: {}'.format(temp_artifact_dir))
        shutil.rmtree(temp_artifact_dir)

    logging.info('Done.')


if __name__ == '__main__':
    main()