Jim Tang | 7be9624 | 2019-04-12 12:53:41 +0800 | [diff] [blame] | 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 | |
| 17 | import argparse |
| 18 | import os |
| 19 | import shlex |
| 20 | import subprocess |
| 21 | import sys |
| 22 | |
| 23 | |
| 24 | PREBUILTS_DIR = os.path.dirname(__file__) |
| 25 | PROJECTS = {'acloud', 'aidegen', 'atest'} |
| 26 | ARCHS = {'linux-x86', 'darwin-x86'} |
| 27 | SMOKE_TEST = 'smoke_tests' |
| 28 | EXIT_TEST_PASS = 0 |
| 29 | EXIT_TEST_FAIL = 1 |
| 30 | EXIT_INVALID_BINS = 2 |
| 31 | |
| 32 | |
| 33 | def _get_prebuilt_bins(): |
| 34 | """Get asuite prebuilt binaries. |
| 35 | |
| 36 | Returns: |
| 37 | A set of prebuilt binaries. |
| 38 | """ |
Jim Tang | 66124a3 | 2020-04-15 16:31:11 +0800 | [diff] [blame] | 39 | bins = {os.path.join(prj, arch, prj) for prj in PROJECTS for arch in ARCHS} |
| 40 | # atest becomes an entrypoint which invokes atest-py2 and atest-py3 accordingly. |
| 41 | bins.add('atest/linux-x86/atest-py2') |
| 42 | bins.add('atest/linux-x86/atest-py3') |
| 43 | return bins |
Jim Tang | 7be9624 | 2019-04-12 12:53:41 +0800 | [diff] [blame] | 44 | |
| 45 | |
| 46 | def _get_prebuilt_dirs(): |
| 47 | """Get asuite prebuilt directories. |
| 48 | |
| 49 | Returns: |
| 50 | A set of prebuilt paths of binaries. |
| 51 | """ |
| 52 | return {os.path.dirname(bin) for bin in _get_prebuilt_bins()} |
| 53 | |
| 54 | |
| 55 | def _get_smoke_tests_bins(): |
| 56 | """Get asuite smoke test scripts. |
| 57 | |
| 58 | Returns: |
| 59 | A dict of project and smoke test script paths. |
| 60 | """ |
| 61 | return {prj: os.path.join(prj, SMOKE_TEST) for prj in PROJECTS} |
| 62 | |
| 63 | |
| 64 | def _is_executable(bin_path): |
| 65 | """Check if the given file is executable. |
| 66 | |
| 67 | Args: |
| 68 | bin_path: a string of a file path. |
| 69 | |
| 70 | Returns: |
| 71 | True if it is executable, false otherwise. |
| 72 | """ |
| 73 | return os.access(bin_path, os.X_OK) |
| 74 | |
| 75 | |
| 76 | def check_uploaded_bins(preupload_files): |
| 77 | """This method validates the uploaded files. |
| 78 | |
| 79 | If the uploaded file is in prebuilt_bins, ensure: |
| 80 | - it is executable. |
| 81 | - only one at a time. |
| 82 | If the uploaded file is a smoke_test script, ensure: |
| 83 | - it is executable. |
| 84 | If the uploaded file is placed in prebuilt_dirs, ensure: |
| 85 | - it is not executable. |
| 86 | (It is to ensure PATH is not contaminated. |
| 87 | e.g. atest/linux-x86/atest-dev will override $OUT/bin/atest-dev, or |
| 88 | atest/linux-x86/rm does fraud/harmful things.) |
| 89 | |
| 90 | Args: |
| 91 | preupload_files: A list of preuploaded files. |
| 92 | |
| 93 | Returns: |
| 94 | True is the above criteria are all fulfilled, otherwise None. |
| 95 | """ |
| 96 | prebuilt_bins = _get_prebuilt_bins() |
| 97 | prebuilt_dirs = _get_prebuilt_dirs() |
| 98 | smoke_tests_bins = _get_smoke_tests_bins().values() |
| 99 | # Store valid executables. |
| 100 | target_bins = set() |
| 101 | # Unexpected executable files which may cause issues(they are in $PATH). |
| 102 | illegal_bins = set() |
| 103 | # Store prebuilts or smoke test script that are inexecutable. |
| 104 | insufficient_perm_bins = set() |
| 105 | for f in preupload_files: |
| 106 | # Ensure target_bins are executable. |
| 107 | if f in prebuilt_bins: |
| 108 | if _is_executable(f): |
| 109 | target_bins.add(f) |
| 110 | else: |
| 111 | insufficient_perm_bins.add(f) |
| 112 | # Ensure smoke_tests scripts are executable. |
| 113 | elif f in smoke_tests_bins and not _is_executable(f): |
| 114 | insufficient_perm_bins.add(f) |
| 115 | # Avoid fraud commands in $PATH. e.g. atest/linux-x86/rm. |
| 116 | # must not be executable. |
| 117 | elif os.path.dirname(f) in prebuilt_dirs and _is_executable(f): |
| 118 | illegal_bins.add(f) |
| 119 | if len(target_bins) > 1: |
| 120 | print('\nYou\'re uploading multiple binaries: %s' |
| 121 | % ' '.join(target_bins)) |
| 122 | print('\nPlease upload one prebuilt at a time.') |
| 123 | return False |
| 124 | if insufficient_perm_bins: |
| 125 | print('\nInsufficient permission found: %s' |
| 126 | % ' '.join(insufficient_perm_bins)) |
| 127 | print('\nPlease run:\n\tchmod 0755 %s\nand try again.' |
| 128 | % ' '.join(insufficient_perm_bins)) |
| 129 | return False |
| 130 | if illegal_bins: |
| 131 | illegal_dirs = {os.path.dirname(bin) for bin in illegal_bins} |
| 132 | print('\nIt is forbidden to upload executable file: %s' |
| 133 | % '\n - %s\n' % '\n - '.join(illegal_bins)) |
| 134 | print('Because they are in the project paths: %s' |
| 135 | % '\n - %s\n' % '\n - '.join(illegal_dirs)) |
| 136 | print('Please remove the binaries or make the files non-executable.') |
| 137 | return False |
| 138 | return True |
| 139 | |
| 140 | |
| 141 | def run_smoke_tests_pass(files_to_check): |
| 142 | """Run smoke tests. |
| 143 | |
| 144 | Args: |
| 145 | files_to_check: A list of preuploaded files to check. |
| 146 | |
| 147 | Returns: |
| 148 | True when test passed or no need to test. |
| 149 | False when test failed. |
| 150 | """ |
| 151 | for target in files_to_check: |
| 152 | if target in _get_prebuilt_bins(): |
Jim Tang | 66124a3 | 2020-04-15 16:31:11 +0800 | [diff] [blame] | 153 | project = target.split(os.path.sep)[0] |
Jim Tang | 7be9624 | 2019-04-12 12:53:41 +0800 | [diff] [blame] | 154 | test_file = _get_smoke_tests_bins().get(project) |
| 155 | if os.path.exists(test_file): |
| 156 | try: |
| 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 | |
| 171 | if __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) |