Skip to content

Commit 971d916

Browse files
committed
Don't use process isolation when bootstrapping
1 parent 1864386 commit 971d916

File tree

1 file changed

+60
-19
lines changed

1 file changed

+60
-19
lines changed

colcon_core/package_identification/python.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright 2019 Rover Robotics
33
# Licensed under the Apache License, Version 2.0
44

5+
import distutils.core
56
import multiprocessing
67
import os
78
from typing import Optional
@@ -36,7 +37,16 @@ def identify(self, desc): # noqa: D102
3637
# after this point, we are convinced this is a Python package,
3738
# so we should fail with an Exception instead of silently
3839

39-
config = get_setup_result(setup_py, env=None)
40+
if os.path.realpath(__file__).startswith(
41+
os.path.realpath(str(desc.path))
42+
):
43+
# Bootstrapping colcon.
44+
# todo: is this really necessary?
45+
get_setup_fn = get_setup_result_in_process
46+
else:
47+
get_setup_fn = get_setup_result
48+
49+
config = get_setup_fn(setup_py, env=None)
4050

4151
name = config['metadata'].name
4252
if not name:
@@ -64,8 +74,8 @@ def identify(self, desc): # noqa: D102
6474
for d in config[option_name] or ()}
6575

6676
def getter(env):
67-
nonlocal setup_py
68-
return get_setup_result(setup_py, env=env)
77+
nonlocal setup_py, get_setup_fn
78+
return get_setup_fn(setup_py, env=env)
6979

7080
desc.metadata['get_python_setup_options'] = getter
7181

@@ -134,7 +144,6 @@ def _get_setup_result_target(setup_py: str, env: dict, conn_send):
134144
:param conn_send: Connection to send the result as either a dict or an
135145
error string
136146
"""
137-
import distutils.core
138147
import traceback
139148
try:
140149
# need to be in setup.py's parent dir to detect any setup.cfg
@@ -146,25 +155,57 @@ def _get_setup_result_target(setup_py: str, env: dict, conn_send):
146155
result = distutils.core.run_setup(
147156
str(setup_py), ('--dry-run',), stop_after='config')
148157

149-
# could just return all attrs in result.__dict__, but we take this
150-
# opportunity to filter a few things that don't need to be there
151-
conn_send.send({
152-
attr: value for attr, value in result.__dict__.items()
153-
if (
154-
# These *seem* useful but always have the value 0.
155-
# Look for their values in the 'metadata' object instead.
156-
attr not in result.display_option_names
157-
# Getter methods
158-
and not callable(value)
159-
# Private properties
160-
and not attr.startswith('_')
161-
# Objects that are generally not picklable
162-
and attr not in ('cmdclass', 'distclass', 'ext_modules')
163-
)})
158+
conn_send.send(_distribution_to_dict(result))
164159
except BaseException:
165160
conn_send.send(traceback.format_exc())
166161

167162

163+
def get_setup_result_in_process(setup_py, *, env: Optional[dict]):
164+
"""
165+
Run setup.py in this process.
166+
167+
Prefer get_setup_result, since it provides process isolation is
168+
threadsafe, and returns predictable errors.
169+
:param setup_py: Path to a setup.py script
170+
:param env: Environment variables to set before running setup.py
171+
:return: Dictionary of data describing the package.
172+
:raise: RuntimeError if the script doesn't appear to be a setup script.
173+
Any exception raised in the setup.py script.
174+
"""
175+
save_env = os.environ.copy()
176+
save_cwd = os.getcwd()
177+
178+
try:
179+
if env is not None:
180+
os.environ.update(env)
181+
os.chdir(str(setup_py.parent))
182+
dist = distutils.core.run_setup(
183+
'setup.py', ('--dry-run',), stop_after='config')
184+
finally:
185+
if env is not None:
186+
os.environ.clear()
187+
os.environ.update(save_env)
188+
os.chdir(save_cwd)
189+
return _distribution_to_dict(dist)
190+
191+
192+
def _distribution_to_dict(distribution_object) -> dict:
193+
"""Turn a distribution into a dict, discarding unpicklable attributes."""
194+
return {
195+
attr: value for attr, value in distribution_object.__dict__.items()
196+
if (
197+
# These *seem* useful but always have the value 0.
198+
# Look for their values in the 'metadata' object instead.
199+
attr not in distribution_object.display_option_names
200+
# Getter methods
201+
and not callable(value)
202+
# Private properties
203+
and not attr.startswith('_')
204+
# Objects that are generally not picklable
205+
and attr not in ('cmdclass', 'distclass', 'ext_modules')
206+
)}
207+
208+
168209
def create_dependency_descriptor(requirement_string):
169210
"""
170211
Create a DependencyDescriptor from a PEP440 compliant string.

0 commit comments

Comments
 (0)