David Benjamin | 7a68d6e | 2022-02-24 17:11:33 | [diff] [blame] | 1 | #!/usr/bin/env vpython3 |
Avi Drissman | 73a09d1 | 2022-09-08 20:33:38 | [diff] [blame] | 2 | # Copyright 2016 The Chromium Authors |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """Prints all non-system dependencies for the given module. |
| 7 | |
Samuel Huang | be8cde5 | 2020-03-17 19:24:14 | [diff] [blame] | 8 | The primary use-case for this script is to generate the list of python modules |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 9 | required for .isolate files. |
| 10 | """ |
| 11 | |
| 12 | import argparse |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 13 | import os |
| 14 | import pipes |
| 15 | import sys |
| 16 | |
| 17 | # Don't use any helper modules, or else they will end up in the results. |
| 18 | |
| 19 | |
| 20 | _SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) |
| 21 | |
| 22 | |
Andrew Grieve | 5a01ad3 | 2020-06-25 18:06:00 | [diff] [blame] | 23 | def ComputePythonDependencies(): |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 24 | """Gets the paths of imported non-system python modules. |
| 25 | |
| 26 | A path is assumed to be a "system" import if it is outside of chromium's |
| 27 | src/. The paths will be relative to the current directory. |
| 28 | """ |
| 29 | module_paths = (m.__file__ for m in sys.modules.values() |
Andrew Grieve | f19f31c | 2021-03-12 13:16:02 | [diff] [blame] | 30 | if m and hasattr(m, '__file__') and m.__file__) |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 31 | |
| 32 | src_paths = set() |
| 33 | for path in module_paths: |
| 34 | if path == __file__: |
| 35 | continue |
| 36 | path = os.path.abspath(path) |
| 37 | if not path.startswith(_SRC_ROOT): |
| 38 | continue |
| 39 | |
jbudorick | 276cc56 | 2017-04-29 01:34:58 | [diff] [blame] | 40 | if (path.endswith('.pyc') |
| 41 | or (path.endswith('c') and not os.path.splitext(path)[1])): |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 42 | path = path[:-1] |
| 43 | src_paths.add(path) |
| 44 | |
| 45 | return src_paths |
| 46 | |
| 47 | |
Bruce Dawson | 090de071 | 2022-05-04 20:16:47 | [diff] [blame] | 48 | def quote(string): |
| 49 | if string.count(' ') > 0: |
| 50 | return '"%s"' % string |
| 51 | else: |
| 52 | return string |
| 53 | |
| 54 | |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 55 | def _NormalizeCommandLine(options): |
| 56 | """Returns a string that when run from SRC_ROOT replicates the command.""" |
| 57 | args = ['build/print_python_deps.py'] |
| 58 | root = os.path.relpath(options.root, _SRC_ROOT) |
| 59 | if root != '.': |
| 60 | args.extend(('--root', root)) |
| 61 | if options.output: |
| 62 | args.extend(('--output', os.path.relpath(options.output, _SRC_ROOT))) |
Andrew Luo | b2e4b34 | 2018-09-20 19:32:39 | [diff] [blame] | 63 | if options.gn_paths: |
| 64 | args.extend(('--gn-paths',)) |
Nate Fischer | ba8f415 | 2021-03-10 17:34:40 | [diff] [blame] | 65 | for allowlist in sorted(options.allowlists): |
| 66 | args.extend(('--allowlist', os.path.relpath(allowlist, _SRC_ROOT))) |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 67 | args.append(os.path.relpath(options.module, _SRC_ROOT)) |
Bruce Dawson | 090de071 | 2022-05-04 20:16:47 | [diff] [blame] | 68 | if os.name == 'nt': |
| 69 | return ' '.join(quote(x) for x in args).replace('\\', '/') |
| 70 | else: |
| 71 | return ' '.join(pipes.quote(x) for x in args) |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 72 | |
| 73 | |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 74 | def _FindPythonInDirectory(directory, allow_test): |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 75 | """Returns an iterable of all non-test python files in the given directory.""" |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 76 | for root, _dirnames, filenames in os.walk(directory): |
| 77 | for filename in filenames: |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 78 | if filename.endswith('.py') and (allow_test |
| 79 | or not filename.endswith('_test.py')): |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 80 | yield os.path.join(root, filename) |
| 81 | |
| 82 | |
Samuel Huang | 2c08fcf0 | 2020-04-10 05:28:18 | [diff] [blame] | 83 | def _ImportModuleByPath(module_path): |
| 84 | """Imports a module by its source file.""" |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 85 | # Replace the path entry for print_python_deps.py with the one for the given |
| 86 | # module. |
Samuel Huang | 2c08fcf0 | 2020-04-10 05:28:18 | [diff] [blame] | 87 | sys.path[0] = os.path.dirname(module_path) |
Takuto Ikuta | cd8a2a08 | 2022-06-02 13:34:48 | [diff] [blame] | 88 | |
| 89 | # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly |
| 90 | module_name = os.path.splitext(os.path.basename(module_path))[0] |
| 91 | import importlib.util # Python 3 only, since it's unavailable in Python 2. |
| 92 | spec = importlib.util.spec_from_file_location(module_name, module_path) |
| 93 | module = importlib.util.module_from_spec(spec) |
| 94 | sys.modules[module_name] = module |
| 95 | spec.loader.exec_module(module) |
Samuel Huang | 2c08fcf0 | 2020-04-10 05:28:18 | [diff] [blame] | 96 | |
| 97 | |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 98 | def main(): |
| 99 | parser = argparse.ArgumentParser( |
| 100 | description='Prints all non-system dependencies for the given module.') |
| 101 | parser.add_argument('module', |
| 102 | help='The python module to analyze.') |
| 103 | parser.add_argument('--root', default='.', |
| 104 | help='Directory to make paths relative to.') |
| 105 | parser.add_argument('--output', |
| 106 | help='Write output to a file rather than stdout.') |
David 'Digit' Turner | 0006f473 | 2018-08-07 07:12:36 | [diff] [blame] | 107 | parser.add_argument('--inplace', action='store_true', |
| 108 | help='Write output to a file with the same path as the ' |
| 109 | 'module, but with a .pydeps extension. Also sets the ' |
| 110 | 'root to the module\'s directory.') |
Andrew Grieve | 0bb79bb | 2018-06-27 03:14:09 | [diff] [blame] | 111 | parser.add_argument('--no-header', action='store_true', |
| 112 | help='Do not write the "# Generated by" header.') |
| 113 | parser.add_argument('--gn-paths', action='store_true', |
| 114 | help='Write paths as //foo/bar/baz.py') |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 115 | parser.add_argument('--did-relaunch', action='store_true', |
| 116 | help=argparse.SUPPRESS) |
Nate Fischer | ba8f415 | 2021-03-10 17:34:40 | [diff] [blame] | 117 | parser.add_argument('--allowlist', |
| 118 | default=[], |
| 119 | action='append', |
| 120 | dest='allowlists', |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 121 | help='Recursively include all non-test python files ' |
| 122 | 'within this directory. May be specified multiple times.') |
| 123 | options = parser.parse_args() |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 124 | |
David 'Digit' Turner | 0006f473 | 2018-08-07 07:12:36 | [diff] [blame] | 125 | if options.inplace: |
| 126 | if options.output: |
| 127 | parser.error('Cannot use --inplace and --output at the same time!') |
| 128 | if not options.module.endswith('.py'): |
| 129 | parser.error('Input module path should end with .py suffix!') |
| 130 | options.output = options.module + 'deps' |
| 131 | options.root = os.path.dirname(options.module) |
| 132 | |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 133 | modules = [options.module] |
| 134 | if os.path.isdir(options.module): |
| 135 | modules = list(_FindPythonInDirectory(options.module, allow_test=True)) |
| 136 | if not modules: |
| 137 | parser.error('Input directory does not contain any python files!') |
| 138 | |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 139 | is_vpython = 'vpython' in sys.executable |
Takuto Ikuta | cd8a2a08 | 2022-06-02 13:34:48 | [diff] [blame] | 140 | if not is_vpython: |
Samuel Huang | be8cde5 | 2020-03-17 19:24:14 | [diff] [blame] | 141 | # Prevent infinite relaunch if something goes awry. |
| 142 | assert not options.did_relaunch |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 143 | # Re-launch using vpython will cause us to pick up modules specified in |
| 144 | # //.vpython, but does not cause it to pick up modules defined inline via |
| 145 | # [VPYTHON:BEGIN] ... [VPYTHON:END] comments. |
| 146 | # TODO(agrieve): Add support for this if the need ever arises. |
Takuto Ikuta | cd8a2a08 | 2022-06-02 13:34:48 | [diff] [blame] | 147 | os.execvp('vpython3', ['vpython3'] + sys.argv + ['--did-relaunch']) |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 148 | |
Takuto Ikuta | cd8a2a08 | 2022-06-02 13:34:48 | [diff] [blame] | 149 | # Work-around for protobuf library not being loadable via importlib |
| 150 | # This is needed due to compile_resources.py. |
| 151 | import importlib._bootstrap_external |
| 152 | importlib._bootstrap_external._NamespacePath.sort = lambda self, **_: 0 |
Andrew Grieve | 97a89bb3 | 2021-04-27 18:19:47 | [diff] [blame] | 153 | |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 154 | paths_set = set() |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 155 | try: |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 156 | for module in modules: |
| 157 | _ImportModuleByPath(module) |
| 158 | paths_set.update(ComputePythonDependencies()) |
Andrew Grieve | f230231 | 2019-03-26 15:08:10 | [diff] [blame] | 159 | except Exception: |
| 160 | # Output extra diagnostics when loading the script fails. |
| 161 | sys.stderr.write('Error running print_python_deps.py.\n') |
| 162 | sys.stderr.write('is_vpython={}\n'.format(is_vpython)) |
| 163 | sys.stderr.write('did_relanuch={}\n'.format(options.did_relaunch)) |
| 164 | sys.stderr.write('python={}\n'.format(sys.executable)) |
| 165 | raise |
| 166 | |
Nate Fischer | ba8f415 | 2021-03-10 17:34:40 | [diff] [blame] | 167 | for path in options.allowlists: |
Peter Kotwicz | 64667b0 | 2020-10-18 06:43:32 | [diff] [blame] | 168 | paths_set.update( |
| 169 | os.path.abspath(p) |
| 170 | for p in _FindPythonInDirectory(path, allow_test=False)) |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 171 | |
| 172 | paths = [os.path.relpath(p, options.root) for p in paths_set] |
| 173 | |
| 174 | normalized_cmdline = _NormalizeCommandLine(options) |
Bruce Dawson | 090de071 | 2022-05-04 20:16:47 | [diff] [blame] | 175 | out = open(options.output, 'w', newline='') if options.output else sys.stdout |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 176 | with out: |
Andrew Grieve | 0bb79bb | 2018-06-27 03:14:09 | [diff] [blame] | 177 | if not options.no_header: |
| 178 | out.write('# Generated by running:\n') |
| 179 | out.write('# %s\n' % normalized_cmdline) |
| 180 | prefix = '//' if options.gn_paths else '' |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 181 | for path in sorted(paths): |
Bruce Dawson | 090de071 | 2022-05-04 20:16:47 | [diff] [blame] | 182 | out.write(prefix + path.replace('\\', '/') + '\n') |
agrieve | f32bcc7 | 2016-04-04 14:57:40 | [diff] [blame] | 183 | |
| 184 | |
| 185 | if __name__ == '__main__': |
| 186 | sys.exit(main()) |