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