blob: f77f9edecbaf717af5115a92a7c10da4d30800e1 [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 Frysinger087fc4c2023-08-22 16:44:11 -040015import importlib.machinery
16import importlib.util
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050017import os
18import sys
19
20
21# Assert some minimum Python versions as we don't test or support any others.
Jordan Abrahams-Whitehead7498dc32023-09-12 21:44:36 +000022# We only support Python 3.6+.
23if sys.version_info < (3, 6):
Alex Klein1699fab2022-09-08 08:46:06 -060024 print(
Jordan Abrahams-Whitehead7498dc32023-09-12 21:44:36 +000025 '%s: chromite: error: Python-3.6+ is required, but "%s" is "%s"'
Alex Klein1699fab2022-09-08 08:46:06 -060026 % (sys.argv[0], sys.executable, sys.version.replace("\n", " ")),
27 file=sys.stderr,
28 )
29 sys.exit(1)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050030
31
Mike Frysingerd008d9c2021-05-13 15:36:53 -040032CHROMITE_PATH = os.path.dirname(os.path.realpath(__file__))
Alex Klein1699fab2022-09-08 08:46:06 -060033while not os.path.exists(os.path.join(CHROMITE_PATH, "PRESUBMIT.cfg")):
34 CHROMITE_PATH = os.path.dirname(CHROMITE_PATH)
35 assert str(CHROMITE_PATH) != "/", "Unable to locate chromite dir"
36CHROMITE_PATH += "/"
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050037
38
Mike Frysingerd008d9c2021-05-13 15:36:53 -040039# module_repr triggers an abstract warning, but it's deprecated in Python 3.+,
40# so we don't want to bother implementing it.
41# pylint: disable=abstract-method
42class ChromiteLoader(importlib.abc.Loader):
Alex Klein1699fab2022-09-08 08:46:06 -060043 """Virtual chromite module
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050044
Alex Klein1699fab2022-09-08 08:46:06 -060045 If the checkout is not named 'chromite', trying to do 'from chromite.xxx'
46 to import modules fails horribly. Instead, manually locate the chromite
47 directory (whatever it is named), load & return it whenever someone tries
48 to import it. This lets us use the stable name 'chromite' regardless of
49 how things are structured on disk.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050050
Alex Klein1699fab2022-09-08 08:46:06 -060051 This also lets us keep the sys.path search clean. Otherwise we'd have to
52 worry about what other dirs chromite were checked out near to as doing an
53 import would also search those for .py modules.
54 """
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050055
Alex Klein1699fab2022-09-08 08:46:06 -060056 def __init__(self):
Alex Klein345222d2023-01-20 17:33:41 -070057 # When trying to load the chromite dir from disk, we'll get called
58 # again, so make sure to disable our logic to avoid an infinite loop.
Alex Klein1699fab2022-09-08 08:46:06 -060059 self.loading = False
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050060
Alex Klein1699fab2022-09-08 08:46:06 -060061 # pylint: disable=unused-argument
62 def create_module(self, spec):
63 """Load the current dir."""
64 if self.loading:
65 return None
66 path, mod = os.path.split(CHROMITE_PATH[:-1])
67 sys.path.insert(0, path)
68 self.loading = True
69 try:
70 return importlib.import_module(mod)
71 finally:
72 # We can't pop by index as the import might have changed sys.path.
73 sys.path.remove(path)
74 self.loading = False
Mike Frysingerd008d9c2021-05-13 15:36:53 -040075
Alex Klein1699fab2022-09-08 08:46:06 -060076 # pylint: disable=unused-argument
77 def exec_module(self, module):
78 """Required stub as a loader."""
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050079
80
Mike Frysingerd008d9c2021-05-13 15:36:53 -040081class ChromiteFinder(importlib.abc.MetaPathFinder):
Alex Klein1699fab2022-09-08 08:46:06 -060082 """Virtual chromite finder.
Mike Frysingerd008d9c2021-05-13 15:36:53 -040083
Alex Klein1699fab2022-09-08 08:46:06 -060084 We'll route any requests for the 'chromite' module.
85 """
Mike Frysingerd008d9c2021-05-13 15:36:53 -040086
Alex Klein1699fab2022-09-08 08:46:06 -060087 def __init__(self, loader):
88 self._loader = loader
Mike Frysingerd008d9c2021-05-13 15:36:53 -040089
Alex Klein1699fab2022-09-08 08:46:06 -060090 # pylint: disable=unused-argument
91 def find_spec(self, fullname, path=None, target=None):
92 if fullname != "chromite" or self._loader.loading:
93 return None
94 return importlib.machinery.ModuleSpec(fullname, self._loader)
Mike Frysingerd008d9c2021-05-13 15:36:53 -040095
96
97sys.meta_path.insert(0, ChromiteFinder(ChromiteLoader()))
98
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050099
100# We have to put these imports after our meta-importer above.
101# pylint: disable=wrong-import-position
102from chromite.lib import commandline
103
104
105def FindTarget(target):
Alex Klein1699fab2022-09-08 08:46:06 -0600106 """Turn the path into something we can import from the chromite tree.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 This supports a variety of ways of running chromite programs:
109 # Loaded via depot_tools in $PATH.
110 $ cros_sdk --help
111 # Loaded via .../chromite/bin in $PATH.
112 $ cros --help
113 # No $PATH needed.
114 $ ./bin/cros --help
115 # Loaded via ~/bin in $PATH to chromite bin/ subdir.
116 $ ln -s $PWD/bin/cros ~/bin; cros --help
117 # No $PATH needed.
118 $ ./cbuildbot/cbuildbot --help
119 # No $PATH needed, but symlink inside of chromite dir.
120 $ ln -s ./cbuildbot/cbuildbot; ./cbuildbot --help
121 # Loaded via ~/bin in $PATH to non-chromite bin/ subdir.
122 $ ln -s $PWD/cbuildbot/cbuildbot ~/bin/; cbuildbot --help
123 # No $PATH needed, but a relative symlink to a symlink to the chromite dir.
124 $ cd ~; ln -s bin/cbuildbot ./; ./cbuildbot --help
125 # External chromite module
126 $ ln -s ../chromite/scripts/wrapper.py foo; ./foo
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500127
Alex Klein1699fab2022-09-08 08:46:06 -0600128 Args:
Alex Klein345222d2023-01-20 17:33:41 -0700129 target: Path to the script we're trying to run.
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500130
Alex Klein1699fab2022-09-08 08:46:06 -0600131 Returns:
Alex Klein345222d2023-01-20 17:33:41 -0700132 The module main functor.
Alex Klein1699fab2022-09-08 08:46:06 -0600133 """
134 # We assume/require the script we're wrapping ends in a .py.
135 full_path = target + ".py"
136 while True:
137 # Walk back one symlink at a time until we get into the chromite dir.
138 parent, base = os.path.split(target)
139 parent = os.path.realpath(parent)
140 if parent.startswith(CHROMITE_PATH):
141 target = base
142 break
143 target = os.path.join(os.path.dirname(target), os.readlink(target))
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500144
Alex Klein1699fab2022-09-08 08:46:06 -0600145 # If we walked all the way back to wrapper.py, it means we're trying to run
146 # an external module. So we have to import it by filepath and not via the
147 # chromite.xxx.yyy namespace.
148 if target != "wrapper3.py":
149 assert parent.startswith(CHROMITE_PATH), (
150 "could not figure out leading path\n"
151 "\tparent: %s\n"
152 "\tCHROMITE_PATH: %s" % (parent, CHROMITE_PATH)
153 )
154 parent = parent[len(CHROMITE_PATH) :].split(os.sep)
155 target = ["chromite"] + parent + [target.replace("-", "_")]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 if target[1] == "bin":
158 # Convert chromite/bin/foo -> chromite/scripts/foo.
159 # Since chromite/bin/ is in $PATH, we want to keep it clean.
160 target[1] = "scripts"
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500161
Alex Klein1699fab2022-09-08 08:46:06 -0600162 try:
163 module = importlib.import_module(".".join(target))
164 except ImportError as e:
165 print(
166 "%s: could not import chromite module: %s: %s"
167 % (sys.argv[0], full_path, e),
168 file=sys.stderr,
169 )
170 raise
171 else:
172 import types
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500173
Alex Klein1699fab2022-09-08 08:46:06 -0600174 try:
175 loader = importlib.machinery.SourceFileLoader("main", full_path)
176 module = types.ModuleType(loader.name)
177 loader.exec_module(module)
178 except IOError as e:
179 print(
180 "%s: could not import external module: %s: %s"
181 % (sys.argv[0], full_path, e),
182 file=sys.stderr,
183 )
184 raise
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500185
Alex Klein1699fab2022-09-08 08:46:06 -0600186 # Run the module's main func if it has one.
187 main = getattr(module, "main", None)
188 if main:
189 return main
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 # 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(
204 code, {**globals(), "__name__": "__main__"}
205 )
Mike Frysinger98484702021-02-12 14:59:28 -0500206
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500207
208def DoMain():
Alex Klein1699fab2022-09-08 08:46:06 -0600209 commandline.ScriptWrapperMain(FindTarget)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500210
211
Alex Klein1699fab2022-09-08 08:46:06 -0600212if __name__ == "__main__":
213 DoMain()