blob: 39aba5f3be8ff5649aecf3d73b3dc5a46e3461ed [file] [log] [blame]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -05001#!/usr/bin/env python3
Mike Frysinger1c57dfe2020-02-06 00:28:22 -05002# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Wrapper around chromite executable scripts.
7
8This takes care of creating a consistent environment for chromite scripts
9(like setting up import paths) so we don't have to duplicate the logic in
10lots of places.
11"""
12
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050013import importlib
Mike Frysingerd008d9c2021-05-13 15:36:53 -040014import importlib.abc
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050015import os
16import sys
17
18
19# Assert some minimum Python versions as we don't test or support any others.
20# We only support Python 3.6+.
Mike Frysinger54545bc2020-02-16 05:37:56 +000021if sys.version_info < (3, 6):
Mike Frysinger7e865ed2020-07-29 03:30:51 -040022 print('%s: chromite: error: Python-3.6+ is required, but "%s" is "%s"' %
23 (sys.argv[0], sys.executable, sys.version.replace('\n', ' ')),
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050024 file=sys.stderr)
25 sys.exit(1)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050026
27
Mike Frysingerd008d9c2021-05-13 15:36:53 -040028CHROMITE_PATH = os.path.dirname(os.path.realpath(__file__))
29while not os.path.exists(os.path.join(CHROMITE_PATH, 'PRESUBMIT.cfg')):
30 CHROMITE_PATH = os.path.dirname(CHROMITE_PATH)
31 assert str(CHROMITE_PATH) != '/', 'Unable to locate chromite dir'
32CHROMITE_PATH += '/'
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050033
34
Mike Frysingerd008d9c2021-05-13 15:36:53 -040035# module_repr triggers an abstract warning, but it's deprecated in Python 3.+,
36# so we don't want to bother implementing it.
37# pylint: disable=abstract-method
38class ChromiteLoader(importlib.abc.Loader):
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050039 """Virtual chromite module
40
41 If the checkout is not named 'chromite', trying to do 'from chromite.xxx'
42 to import modules fails horribly. Instead, manually locate the chromite
43 directory (whatever it is named), load & return it whenever someone tries
44 to import it. This lets us use the stable name 'chromite' regardless of
45 how things are structured on disk.
46
47 This also lets us keep the sys.path search clean. Otherwise we'd have to
48 worry about what other dirs chromite were checked out near to as doing an
49 import would also search those for .py modules.
50 """
51
Mike Frysingerd008d9c2021-05-13 15:36:53 -040052 def __init__(self):
53 # When trying to load the chromite dir from disk, we'll get called again,
54 # so make sure to disable our logic to avoid an infinite loop.
55 self.loading = False
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050056
Mike Frysingerd008d9c2021-05-13 15:36:53 -040057 # pylint: disable=unused-argument
58 def create_module(self, spec):
59 """Load the current dir."""
60 if self.loading:
61 return None
62 path, mod = os.path.split(CHROMITE_PATH[:-1])
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050063 sys.path.insert(0, path)
Mike Frysingerd008d9c2021-05-13 15:36:53 -040064 self.loading = True
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050065 try:
66 return importlib.import_module(mod)
67 finally:
68 # We can't pop by index as the import might have changed sys.path.
69 sys.path.remove(path)
Mike Frysingerd008d9c2021-05-13 15:36:53 -040070 self.loading = False
71
72 # pylint: disable=unused-argument
73 def exec_module(self, module):
74 """Required stub as a loader."""
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050075
76
Mike Frysingerd008d9c2021-05-13 15:36:53 -040077class ChromiteFinder(importlib.abc.MetaPathFinder):
78 """Virtual chromite finder.
79
80 We'll route any requests for the 'chromite' module.
81 """
82
83 def __init__(self, loader):
84 self._loader = loader
85
86 # pylint: disable=unused-argument
87 def find_spec(self, fullname, path=None, target=None):
88 if fullname != 'chromite' or self._loader.loading:
89 return None
90 return importlib.machinery.ModuleSpec(fullname, self._loader)
91
92
93sys.meta_path.insert(0, ChromiteFinder(ChromiteLoader()))
94
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050095
96# We have to put these imports after our meta-importer above.
97# pylint: disable=wrong-import-position
98from chromite.lib import commandline
99
100
101def FindTarget(target):
102 """Turn the path into something we can import from the chromite tree.
103
104 This supports a variety of ways of running chromite programs:
105 # Loaded via depot_tools in $PATH.
106 $ cros_sdk --help
107 # Loaded via .../chromite/bin in $PATH.
108 $ cros --help
109 # No $PATH needed.
110 $ ./bin/cros --help
111 # Loaded via ~/bin in $PATH to chromite bin/ subdir.
112 $ ln -s $PWD/bin/cros ~/bin; cros --help
113 # No $PATH needed.
114 $ ./cbuildbot/cbuildbot --help
115 # No $PATH needed, but symlink inside of chromite dir.
116 $ ln -s ./cbuildbot/cbuildbot; ./cbuildbot --help
117 # Loaded via ~/bin in $PATH to non-chromite bin/ subdir.
118 $ ln -s $PWD/cbuildbot/cbuildbot ~/bin/; cbuildbot --help
119 # No $PATH needed, but a relative symlink to a symlink to the chromite dir.
120 $ cd ~; ln -s bin/cbuildbot ./; ./cbuildbot --help
121 # External chromite module
122 $ ln -s ../chromite/scripts/wrapper.py foo; ./foo
123
124 Args:
125 target: Path to the script we're trying to run.
126
127 Returns:
128 The module main functor.
129 """
130 # We assume/require the script we're wrapping ends in a .py.
131 full_path = target + '.py'
132 while True:
133 # Walk back one symlink at a time until we get into the chromite dir.
134 parent, base = os.path.split(target)
135 parent = os.path.realpath(parent)
136 if parent.startswith(CHROMITE_PATH):
137 target = base
138 break
139 target = os.path.join(os.path.dirname(target), os.readlink(target))
140
141 # If we walked all the way back to wrapper.py, it means we're trying to run
142 # an external module. So we have to import it by filepath and not via the
143 # chromite.xxx.yyy namespace.
144 if target != 'wrapper3.py':
145 assert parent.startswith(CHROMITE_PATH), (
146 'could not figure out leading path\n'
147 '\tparent: %s\n'
148 '\tCHROMITE_PATH: %s' % (parent, CHROMITE_PATH))
149 parent = parent[len(CHROMITE_PATH):].split(os.sep)
Mike Frysingerd07b8542021-06-21 11:57:20 -0400150 target = ['chromite'] + parent + [target.replace('-', '_')]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500151
152 if target[1] == 'bin':
153 # Convert chromite/bin/foo -> chromite/scripts/foo.
154 # Since chromite/bin/ is in $PATH, we want to keep it clean.
155 target[1] = 'scripts'
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500156
157 try:
158 module = importlib.import_module('.'.join(target))
159 except ImportError as e:
160 print(
161 '%s: could not import chromite module: %s: %s' % (sys.argv[0],
162 full_path, e),
163 file=sys.stderr)
164 raise
165 else:
166 import types
167 try:
168 loader = importlib.machinery.SourceFileLoader('main', full_path)
169 module = types.ModuleType(loader.name)
170 loader.exec_module(module)
171 except IOError as e:
172 print(
173 '%s: could not import external module: %s: %s' % (sys.argv[0],
174 full_path, e),
175 file=sys.stderr)
176 raise
177
178 # Run the module's main func if it has one.
179 main = getattr(module, 'main', None)
180 if main:
181 return main
182
183 # Is this a unittest?
184 if target[-1].rsplit('_', 1)[-1] in ('test', 'unittest'):
185 from chromite.lib import cros_test_lib
186 return lambda _argv: cros_test_lib.main(module=module)
187
Mike Frysinger98484702021-02-12 14:59:28 -0500188 # Is this a package? Import it like `python -m...` does.
189 if target != 'wrapper3.py':
190 mod_name = '.'.join(target + ['__main__'])
191 try:
192 module = importlib.import_module(mod_name)
193 except ImportError:
194 module = None
195 if module:
196 spec = importlib.util.find_spec(mod_name)
197 loader = spec.loader
198 code = loader.get_code(mod_name)
199 # pylint: disable=exec-used
200 return lambda _argv: exec(code, {**globals(), '__name__': '__main__'})
201
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500202
203def DoMain():
204 commandline.ScriptWrapperMain(FindTarget)
205
206
207if __name__ == '__main__':
208 DoMain()