blob: f61f9a216d3c88cfa091eb740905df695e23f352 [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
17import os
18import sys
19
20
21# Assert some minimum Python versions as we don't test or support any others.
22# We only support Python 3.6+.
Mike Frysinger54545bc2020-02-16 05:37:56 +000023if sys.version_info < (3, 6):
Mike Frysinger7e865ed2020-07-29 03:30:51 -040024 print('%s: chromite: error: Python-3.6+ is required, but "%s" is "%s"' %
25 (sys.argv[0], sys.executable, sys.version.replace('\n', ' ')),
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050026 file=sys.stderr)
27 sys.exit(1)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050028
29
30CHROMITE_PATH = None
31
32
33class ChromiteImporter(object):
34 """Virtual chromite module
35
36 If the checkout is not named 'chromite', trying to do 'from chromite.xxx'
37 to import modules fails horribly. Instead, manually locate the chromite
38 directory (whatever it is named), load & return it whenever someone tries
39 to import it. This lets us use the stable name 'chromite' regardless of
40 how things are structured on disk.
41
42 This also lets us keep the sys.path search clean. Otherwise we'd have to
43 worry about what other dirs chromite were checked out near to as doing an
44 import would also search those for .py modules.
45 """
46
47 # When trying to load the chromite dir from disk, we'll get called again,
48 # so make sure to disable our logic to avoid an infinite loop.
49 _loading = False
50
51 def find_module(self, fullname, _path=None):
52 """Handle the 'chromite' module"""
53 if fullname == 'chromite' and not self._loading:
54 return self
55 return None
56
57 def load_module(self, _fullname):
58 """Return our cache of the 'chromite' module"""
59 # Locate the top of the chromite dir by searching for the PRESUBMIT.cfg
60 # file. This assumes that file isn't found elsewhere in the tree.
61 path = os.path.dirname(os.path.realpath(__file__))
62 while not os.path.exists(os.path.join(path, 'PRESUBMIT.cfg')):
63 path = os.path.dirname(path)
Mike Frysingerd0b43852021-02-12 19:04:57 -050064 assert path != '/', 'Unable to locate chromite dir'
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050065
66 # pylint: disable=global-statement
67 global CHROMITE_PATH
68 CHROMITE_PATH = path + '/'
69
70 # Finally load the chromite dir.
71 path, mod = os.path.split(path)
72 sys.path.insert(0, path)
73 self._loading = True
74 try:
75 return importlib.import_module(mod)
76 finally:
77 # We can't pop by index as the import might have changed sys.path.
78 sys.path.remove(path)
79 self._loading = False
80
81
82sys.meta_path.insert(0, ChromiteImporter())
83
84# We have to put these imports after our meta-importer above.
85# pylint: disable=wrong-import-position
86from chromite.lib import commandline
87
88
89def FindTarget(target):
90 """Turn the path into something we can import from the chromite tree.
91
92 This supports a variety of ways of running chromite programs:
93 # Loaded via depot_tools in $PATH.
94 $ cros_sdk --help
95 # Loaded via .../chromite/bin in $PATH.
96 $ cros --help
97 # No $PATH needed.
98 $ ./bin/cros --help
99 # Loaded via ~/bin in $PATH to chromite bin/ subdir.
100 $ ln -s $PWD/bin/cros ~/bin; cros --help
101 # No $PATH needed.
102 $ ./cbuildbot/cbuildbot --help
103 # No $PATH needed, but symlink inside of chromite dir.
104 $ ln -s ./cbuildbot/cbuildbot; ./cbuildbot --help
105 # Loaded via ~/bin in $PATH to non-chromite bin/ subdir.
106 $ ln -s $PWD/cbuildbot/cbuildbot ~/bin/; cbuildbot --help
107 # No $PATH needed, but a relative symlink to a symlink to the chromite dir.
108 $ cd ~; ln -s bin/cbuildbot ./; ./cbuildbot --help
109 # External chromite module
110 $ ln -s ../chromite/scripts/wrapper.py foo; ./foo
111
112 Args:
113 target: Path to the script we're trying to run.
114
115 Returns:
116 The module main functor.
117 """
118 # We assume/require the script we're wrapping ends in a .py.
119 full_path = target + '.py'
120 while True:
121 # Walk back one symlink at a time until we get into the chromite dir.
122 parent, base = os.path.split(target)
123 parent = os.path.realpath(parent)
124 if parent.startswith(CHROMITE_PATH):
125 target = base
126 break
127 target = os.path.join(os.path.dirname(target), os.readlink(target))
128
129 # If we walked all the way back to wrapper.py, it means we're trying to run
130 # an external module. So we have to import it by filepath and not via the
131 # chromite.xxx.yyy namespace.
132 if target != 'wrapper3.py':
133 assert parent.startswith(CHROMITE_PATH), (
134 'could not figure out leading path\n'
135 '\tparent: %s\n'
136 '\tCHROMITE_PATH: %s' % (parent, CHROMITE_PATH))
137 parent = parent[len(CHROMITE_PATH):].split(os.sep)
138 target = ['chromite'] + parent + [target]
139
140 if target[1] == 'bin':
141 # Convert chromite/bin/foo -> chromite/scripts/foo.
142 # Since chromite/bin/ is in $PATH, we want to keep it clean.
143 target[1] = 'scripts'
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500144
145 try:
146 module = importlib.import_module('.'.join(target))
147 except ImportError as e:
148 print(
149 '%s: could not import chromite module: %s: %s' % (sys.argv[0],
150 full_path, e),
151 file=sys.stderr)
152 raise
153 else:
154 import types
155 try:
156 loader = importlib.machinery.SourceFileLoader('main', full_path)
157 module = types.ModuleType(loader.name)
158 loader.exec_module(module)
159 except IOError as e:
160 print(
161 '%s: could not import external module: %s: %s' % (sys.argv[0],
162 full_path, e),
163 file=sys.stderr)
164 raise
165
166 # Run the module's main func if it has one.
167 main = getattr(module, 'main', None)
168 if main:
169 return main
170
171 # Is this a unittest?
172 if target[-1].rsplit('_', 1)[-1] in ('test', 'unittest'):
173 from chromite.lib import cros_test_lib
174 return lambda _argv: cros_test_lib.main(module=module)
175
176
177def DoMain():
178 commandline.ScriptWrapperMain(FindTarget)
179
180
181if __name__ == '__main__':
182 DoMain()