blob: 680a95bc59cf76d14021297a128e1c8a62e29a22 [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'
147 elif target[1] == 'bootstrap' and len(target) == 3:
148 # Convert <git_repo>/bootstrap/foo -> <git_repo>/bootstrap/scripts/foo.
149 target.insert(2, 'scripts')
150
151 try:
152 module = importlib.import_module('.'.join(target))
153 except ImportError as e:
154 print(
155 '%s: could not import chromite module: %s: %s' % (sys.argv[0],
156 full_path, e),
157 file=sys.stderr)
158 raise
159 else:
160 import types
161 try:
162 loader = importlib.machinery.SourceFileLoader('main', full_path)
163 module = types.ModuleType(loader.name)
164 loader.exec_module(module)
165 except IOError as e:
166 print(
167 '%s: could not import external module: %s: %s' % (sys.argv[0],
168 full_path, e),
169 file=sys.stderr)
170 raise
171
172 # Run the module's main func if it has one.
173 main = getattr(module, 'main', None)
174 if main:
175 return main
176
177 # Is this a unittest?
178 if target[-1].rsplit('_', 1)[-1] in ('test', 'unittest'):
179 from chromite.lib import cros_test_lib
180 return lambda _argv: cros_test_lib.main(module=module)
181
182
183def DoMain():
184 commandline.ScriptWrapperMain(FindTarget)
185
186
187if __name__ == '__main__':
188 DoMain()