blob: 526e36504974dfd9a66d878c53d6c1b5ebf13fe9 [file] [log] [blame]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -05001#!/usr/bin/env python3
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04002# Copyright 2020 The ChromiumOS Authors
Mike Frysinger1c57dfe2020-02-06 00:28:22 -05003# 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):
Alex Klein1699fab2022-09-08 08:46:06 -060022 print(
23 '%s: chromite: error: Python-3.6+ is required, but "%s" is "%s"'
24 % (sys.argv[0], sys.executable, sys.version.replace("\n", " ")),
25 file=sys.stderr,
26 )
27 sys.exit(1)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050028
29
Mike Frysingerd008d9c2021-05-13 15:36:53 -040030CHROMITE_PATH = os.path.dirname(os.path.realpath(__file__))
Alex Klein1699fab2022-09-08 08:46:06 -060031while not os.path.exists(os.path.join(CHROMITE_PATH, "PRESUBMIT.cfg")):
32 CHROMITE_PATH = os.path.dirname(CHROMITE_PATH)
33 assert str(CHROMITE_PATH) != "/", "Unable to locate chromite dir"
34CHROMITE_PATH += "/"
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050035
36
Mike Frysingerd008d9c2021-05-13 15:36:53 -040037# module_repr triggers an abstract warning, but it's deprecated in Python 3.+,
38# so we don't want to bother implementing it.
39# pylint: disable=abstract-method
40class ChromiteLoader(importlib.abc.Loader):
Alex Klein1699fab2022-09-08 08:46:06 -060041 """Virtual chromite module
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050042
Alex Klein1699fab2022-09-08 08:46:06 -060043 If the checkout is not named 'chromite', trying to do 'from chromite.xxx'
44 to import modules fails horribly. Instead, manually locate the chromite
45 directory (whatever it is named), load & return it whenever someone tries
46 to import it. This lets us use the stable name 'chromite' regardless of
47 how things are structured on disk.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050048
Alex Klein1699fab2022-09-08 08:46:06 -060049 This also lets us keep the sys.path search clean. Otherwise we'd have to
50 worry about what other dirs chromite were checked out near to as doing an
51 import would also search those for .py modules.
52 """
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050053
Alex Klein1699fab2022-09-08 08:46:06 -060054 def __init__(self):
55 # When trying to load the chromite dir from disk, we'll get called again,
56 # so make sure to disable our logic to avoid an infinite loop.
57 self.loading = False
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050058
Alex Klein1699fab2022-09-08 08:46:06 -060059 # pylint: disable=unused-argument
60 def create_module(self, spec):
61 """Load the current dir."""
62 if self.loading:
63 return None
64 path, mod = os.path.split(CHROMITE_PATH[:-1])
65 sys.path.insert(0, path)
66 self.loading = True
67 try:
68 return importlib.import_module(mod)
69 finally:
70 # We can't pop by index as the import might have changed sys.path.
71 sys.path.remove(path)
72 self.loading = False
Mike Frysingerd008d9c2021-05-13 15:36:53 -040073
Alex Klein1699fab2022-09-08 08:46:06 -060074 # pylint: disable=unused-argument
75 def exec_module(self, module):
76 """Required stub as a loader."""
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050077
78
Mike Frysingerd008d9c2021-05-13 15:36:53 -040079class ChromiteFinder(importlib.abc.MetaPathFinder):
Alex Klein1699fab2022-09-08 08:46:06 -060080 """Virtual chromite finder.
Mike Frysingerd008d9c2021-05-13 15:36:53 -040081
Alex Klein1699fab2022-09-08 08:46:06 -060082 We'll route any requests for the 'chromite' module.
83 """
Mike Frysingerd008d9c2021-05-13 15:36:53 -040084
Alex Klein1699fab2022-09-08 08:46:06 -060085 def __init__(self, loader):
86 self._loader = loader
Mike Frysingerd008d9c2021-05-13 15:36:53 -040087
Alex Klein1699fab2022-09-08 08:46:06 -060088 # pylint: disable=unused-argument
89 def find_spec(self, fullname, path=None, target=None):
90 if fullname != "chromite" or self._loader.loading:
91 return None
92 return importlib.machinery.ModuleSpec(fullname, self._loader)
Mike Frysingerd008d9c2021-05-13 15:36:53 -040093
94
95sys.meta_path.insert(0, ChromiteFinder(ChromiteLoader()))
96
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050097
98# We have to put these imports after our meta-importer above.
99# pylint: disable=wrong-import-position
100from chromite.lib import commandline
101
102
103def FindTarget(target):
Alex Klein1699fab2022-09-08 08:46:06 -0600104 """Turn the path into something we can import from the chromite tree.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500105
Alex Klein1699fab2022-09-08 08:46:06 -0600106 This supports a variety of ways of running chromite programs:
107 # Loaded via depot_tools in $PATH.
108 $ cros_sdk --help
109 # Loaded via .../chromite/bin in $PATH.
110 $ cros --help
111 # No $PATH needed.
112 $ ./bin/cros --help
113 # Loaded via ~/bin in $PATH to chromite bin/ subdir.
114 $ ln -s $PWD/bin/cros ~/bin; cros --help
115 # No $PATH needed.
116 $ ./cbuildbot/cbuildbot --help
117 # No $PATH needed, but symlink inside of chromite dir.
118 $ ln -s ./cbuildbot/cbuildbot; ./cbuildbot --help
119 # Loaded via ~/bin in $PATH to non-chromite bin/ subdir.
120 $ ln -s $PWD/cbuildbot/cbuildbot ~/bin/; cbuildbot --help
121 # No $PATH needed, but a relative symlink to a symlink to the chromite dir.
122 $ cd ~; ln -s bin/cbuildbot ./; ./cbuildbot --help
123 # External chromite module
124 $ ln -s ../chromite/scripts/wrapper.py foo; ./foo
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500125
Alex Klein1699fab2022-09-08 08:46:06 -0600126 Args:
127 target: Path to the script we're trying to run.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500128
Alex Klein1699fab2022-09-08 08:46:06 -0600129 Returns:
130 The module main functor.
131 """
132 # We assume/require the script we're wrapping ends in a .py.
133 full_path = target + ".py"
134 while True:
135 # Walk back one symlink at a time until we get into the chromite dir.
136 parent, base = os.path.split(target)
137 parent = os.path.realpath(parent)
138 if parent.startswith(CHROMITE_PATH):
139 target = base
140 break
141 target = os.path.join(os.path.dirname(target), os.readlink(target))
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500142
Alex Klein1699fab2022-09-08 08:46:06 -0600143 # If we walked all the way back to wrapper.py, it means we're trying to run
144 # an external module. So we have to import it by filepath and not via the
145 # chromite.xxx.yyy namespace.
146 if target != "wrapper3.py":
147 assert parent.startswith(CHROMITE_PATH), (
148 "could not figure out leading path\n"
149 "\tparent: %s\n"
150 "\tCHROMITE_PATH: %s" % (parent, CHROMITE_PATH)
151 )
152 parent = parent[len(CHROMITE_PATH) :].split(os.sep)
153 target = ["chromite"] + parent + [target.replace("-", "_")]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500154
Alex Klein1699fab2022-09-08 08:46:06 -0600155 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
Alex Klein1699fab2022-09-08 08:46:06 -0600160 try:
161 module = importlib.import_module(".".join(target))
162 except ImportError as e:
163 print(
164 "%s: could not import chromite module: %s: %s"
165 % (sys.argv[0], full_path, e),
166 file=sys.stderr,
167 )
168 raise
169 else:
170 import types
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 try:
173 loader = importlib.machinery.SourceFileLoader("main", full_path)
174 module = types.ModuleType(loader.name)
175 loader.exec_module(module)
176 except IOError as e:
177 print(
178 "%s: could not import external module: %s: %s"
179 % (sys.argv[0], full_path, e),
180 file=sys.stderr,
181 )
182 raise
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 # Run the module's main func if it has one.
185 main = getattr(module, "main", None)
186 if main:
187 return main
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500188
Alex Klein1699fab2022-09-08 08:46:06 -0600189 # Is this a unittest?
190 if target[-1].rsplit("_", 1)[-1] in ("test", "unittest"):
191 from chromite.lib import cros_test_lib
192
193 return lambda _argv: cros_test_lib.main(module=module)
194
195 # Is this a package? Import it like `python -m...` does.
196 if target != "wrapper3.py":
197 mod_name = ".".join(target + ["__main__"])
198 try:
199 module = importlib.import_module(mod_name)
200 except ImportError:
201 module = None
202 if module:
203 spec = importlib.util.find_spec(mod_name)
204 loader = spec.loader
205 code = loader.get_code(mod_name)
206 # pylint: disable=exec-used
207 return lambda _argv: exec(
208 code, {**globals(), "__name__": "__main__"}
209 )
Mike Frysinger98484702021-02-12 14:59:28 -0500210
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500211
212def DoMain():
Alex Klein1699fab2022-09-08 08:46:06 -0600213 commandline.ScriptWrapperMain(FindTarget)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500214
215
Alex Klein1699fab2022-09-08 08:46:06 -0600216if __name__ == "__main__":
217 DoMain()