1#!/usr/bin/env python3 2# Copyright 2019, The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16 17import argparse 18import os 19import shlex 20import subprocess 21import sys 22 23 24PREBUILTS_DIR = os.path.dirname(__file__) 25PROJECTS = {'acloud', 'aidegen', 'atest'} 26ARCHS = {'linux-x86', 'darwin-x86'} 27SMOKE_TEST = 'smoke_tests' 28EXIT_TEST_PASS = 0 29EXIT_TEST_FAIL = 1 30EXIT_INVALID_BINS = 2 31 32 33def _get_prebuilt_bins(): 34 """Get asuite prebuilt binaries. 35 36 Returns: 37 A set of prebuilt binaries. 38 """ 39 bins = {os.path.join(prj, arch, prj) for prj in PROJECTS for arch in ARCHS} 40 bins.add('atest/linux-x86/atest-py3') 41 return bins 42 43 44def _get_prebuilt_dirs(): 45 """Get asuite prebuilt directories. 46 47 Returns: 48 A set of prebuilt paths of binaries. 49 """ 50 return {os.path.dirname(bin) for bin in _get_prebuilt_bins()} 51 52 53def _get_smoke_tests_bins(): 54 """Get asuite smoke test scripts. 55 56 Returns: 57 A dict of project and smoke test script paths. 58 """ 59 return {prj: os.path.join(prj, SMOKE_TEST) for prj in PROJECTS} 60 61 62def _is_executable(bin_path): 63 """Check if the given file is executable. 64 65 Args: 66 bin_path: a string of a file path. 67 68 Returns: 69 True if it is executable, false otherwise. 70 """ 71 return os.access(bin_path, os.X_OK) 72 73 74def check_uploaded_bins(preupload_files): 75 """This method validates the uploaded files. 76 77 If the uploaded file is in prebuilt_bins, ensure: 78 - it is executable. 79 - only one at a time. 80 If the uploaded file is a smoke_test script, ensure: 81 - it is executable. 82 If the uploaded file is placed in prebuilt_dirs, ensure: 83 - it is not executable. 84 (It is to ensure PATH is not contaminated. 85 e.g. atest/linux-x86/atest-dev will override $OUT/bin/atest-dev, or 86 atest/linux-x86/rm does fraud/harmful things.) 87 88 Args: 89 preupload_files: A list of preuploaded files. 90 91 Returns: 92 True is the above criteria are all fulfilled, otherwise None. 93 """ 94 prebuilt_bins = _get_prebuilt_bins() 95 prebuilt_dirs = _get_prebuilt_dirs() 96 smoke_tests_bins = _get_smoke_tests_bins().values() 97 # Store valid executables. 98 target_bins = set() 99 # Unexpected executable files which may cause issues(they are in $PATH). 100 illegal_bins = set() 101 # Store prebuilts or smoke test script that are inexecutable. 102 insufficient_perm_bins = set() 103 for f in preupload_files: 104 # Ensure target_bins are executable. 105 if f in prebuilt_bins: 106 if _is_executable(f): 107 target_bins.add(f) 108 else: 109 insufficient_perm_bins.add(f) 110 # Ensure smoke_tests scripts are executable. 111 elif f in smoke_tests_bins and not _is_executable(f): 112 insufficient_perm_bins.add(f) 113 # Avoid fraud commands in $PATH. e.g. atest/linux-x86/rm. 114 # must not be executable. 115 elif os.path.dirname(f) in prebuilt_dirs and _is_executable(f): 116 illegal_bins.add(f) 117 if len(target_bins) > 1: 118 print('\nYou\'re uploading multiple binaries: %s' 119 % ' '.join(target_bins)) 120 print('\nPlease upload one prebuilt at a time.') 121 return False 122 if insufficient_perm_bins: 123 print('\nInsufficient permission found: %s' 124 % ' '.join(insufficient_perm_bins)) 125 print('\nPlease run:\n\tchmod 0755 %s\nand try again.' 126 % ' '.join(insufficient_perm_bins)) 127 return False 128 if illegal_bins: 129 illegal_dirs = {os.path.dirname(bin) for bin in illegal_bins} 130 print('\nIt is forbidden to upload executable file: %s' 131 % '\n - %s\n' % '\n - '.join(illegal_bins)) 132 print('Because they are in the project paths: %s' 133 % '\n - %s\n' % '\n - '.join(illegal_dirs)) 134 print('Please remove the binaries or make the files non-executable.') 135 return False 136 return True 137 138 139def run_smoke_tests_pass(files_to_check): 140 """Run smoke tests. 141 142 Args: 143 files_to_check: A list of preuploaded files to check. 144 145 Returns: 146 True when test passed or no need to test. 147 False when test failed. 148 """ 149 for target in files_to_check: 150 if target in _get_prebuilt_bins(): 151 project = target.split(os.path.sep)[0] 152 test_file = _get_smoke_tests_bins().get(project) 153 if os.path.exists(test_file): 154 try: 155 print(f'Running test binary {test_file} which may take' 156 ' several minutes...') 157 subprocess.check_output(test_file, encoding='utf-8', 158 stderr=subprocess.STDOUT) 159 except subprocess.CalledProcessError as error: 160 print('Smoke tests failed at:\n\n%s' % error.output) 161 return False 162 except OSError as oserror: 163 print('%s: Missing the header of the script.' % oserror) 164 print('Please define shebang like:\n') 165 print('#!/usr/bin/env bash\nor') 166 print('#!/usr/bin/env python3\n') 167 return False 168 return True 169 170 171if __name__ == '__main__': 172 parser = argparse.ArgumentParser() 173 parser.add_argument('--skip-smoke-test', '-s', action="store_true", 174 help='Disable smoke testing.') 175 parser.add_argument('preupload_files', nargs='*', help='Files to be uploaded.') 176 args = parser.parse_args() 177 files_to_check = args.preupload_files 178 # Pre-process files_to_check(run directly by users.) 179 if not files_to_check: 180 # Only consider added(A), renamed(R) and modified(M) files. 181 cmd = "git status --short | egrep ^[ARM] | awk '{print $NF}'" 182 preupload_files = subprocess.check_output(cmd, shell=True, 183 encoding='utf-8').splitlines() 184 if preupload_files: 185 print('validating: %s' % preupload_files) 186 files_to_check = preupload_files 187 # Validating uploaded files and run smoke test script(run by repohook). 188 if not check_uploaded_bins(files_to_check): 189 sys.exit(EXIT_INVALID_BINS) 190 if not args.skip_smoke_test and not run_smoke_tests_pass(files_to_check): 191 sys.exit(EXIT_TEST_FAIL) 192 sys.exit(EXIT_TEST_PASS) 193