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