Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python3: Support Python2 for lcm when being used by drake_visualizer #9699

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tools/workspace/default.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ def add_default_repositories(excludes = [], mirrors = DEFAULT_MIRRORS):
pycps_repository(name = "pycps", mirrors = mirrors)
if "python" not in excludes:
python_repository(name = "python")
if "python2" not in excludes:
python_repository(name = "python2", compat_version = "2")
if "python3" not in excludes:
python3_repository(name = "python3")
if "qdldl" not in excludes:
Expand Down
2 changes: 1 addition & 1 deletion tools/workspace/drake_visualizer/repository.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ licenses([
py_library(
name = "drake_visualizer_python_deps",
deps = [
"@lcm//:lcm-python",
"@lcm//:lcm-python2",
"@lcmtypes_bot2_core//:lcmtypes_bot2_core_py",
# TODO(eric.cousineau): Expose VTK Python libraries here for Linux.
"@lcmtypes_robotlocomotion//:lcmtypes_robotlocomotion_py",
Expand Down
9 changes: 9 additions & 0 deletions tools/workspace/lcm/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- python -*-

load("//tools/lint:lint.bzl", "add_lint_tests")
load("@drake//tools/skylark:drake_py.bzl", "drake_py_unittest")

exports_files(
[
Expand All @@ -10,4 +11,12 @@ exports_files(
visibility = ["@lcm//:__pkg__"],
)

drake_py_unittest(
name = "lcm_py2_py3_test",
deps = [
"@lcm//:lcm-python",
"@lcm//:lcm-python2",
],
)

add_lint_tests(python_lint_extra_srcs = ["test/lcm-gen_install_test.py"])
80 changes: 60 additions & 20 deletions tools/workspace/lcm/package.BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ load(
"install_files",
)
load("@drake//tools/lint:python_lint.bzl", "python_lint")
load(
"@drake//tools/workspace/python:py2_py3.bzl",
"py2_py3",
"py_select",
)

licenses([
"notice", # BSD-3-Clause
Expand Down Expand Up @@ -169,8 +174,20 @@ cc_binary(
],
)

cc_binary(
name = "_lcm.so",
py2_py3(
rule = cc_binary,
name = "_lcm.@PYTHON_EXT_SUFFIX@",
deps = py_select(
py2 = [
":lcm",
"@python2//:python",
],
py3 = [
":lcm",
# N.B. This will only run when @python refers to Python3.
"@python//:python",
],
),
srcs = [
"lcm-python/module.c",
"lcm-python/pyeventlog.c",
Expand All @@ -184,10 +201,6 @@ cc_binary(
linkshared = 1,
linkstatic = 1,
visibility = ["//visibility:private"],
deps = [
":lcm",
"@python",
],
)

# Downstream users of lcm-python expect to say "import lcm". However, in the
Expand Down Expand Up @@ -219,10 +232,15 @@ generate_file(
name = "__init__.py",
content = """
import ctypes
import os.path
from os.path import realpath
from sysconfig import get_config_var

import six
ctypes.cdll.LoadLibrary(os.path.realpath(__path__[0] + '/_lcm.so'))

_ext = get_config_var("EXT_SUFFIX") or get_config_var("SO")
ctypes.cdll.LoadLibrary(realpath(__path__[0] + '/_lcm' + _ext))
_filename = __path__[0] + \"/lcm-python/lcm/__init__.py\"

if six.PY2:
execfile(_filename)
else:
Expand All @@ -233,17 +251,26 @@ else:
visibility = ["//visibility:private"],
)

py_library(
name = "lcm-python-upstream",
py2_py3(
rule = py_library,
name = py_select(
py2 = "lcm-python2-upstream",
py3 = "lcm-python3-upstream",
),
data = ["_lcm.@PYTHON_EXT_SUFFIX@"],
srcs = ["lcm-python/lcm/__init__.py"], # Actual code from upstream.
data = [":_lcm.so"],
visibility = ["//visibility:private"],
)

py_library(
name = "lcm-python",
py2_py3(
rule = py_library,
alias = "lcm-python",
name = py_select(py2 = "lcm-python2", py3 = "lcm-python3"),
deps = py_select(
py2 = [":lcm-python2-upstream"],
py3 = [":lcm-python3-upstream"],
),
srcs = ["__init__.py"], # Shim, from the genrule above.
deps = [":lcm-python-upstream"],
)

java_library(
Expand Down Expand Up @@ -298,13 +325,23 @@ install_files(
strip_prefix = ["**/"],
)

install(
name = "install_python",
targets = [
":_lcm.so",
":lcm-python-upstream",
],
py2_py3(
rule = install,
alias = "install_python",
name = py_select(py2 = "install_python2", py3 = "install_python3"),
targets = py_select(
py2 = [
"_lcm.@PYTHON_EXT_SUFFIX@",
"lcm-python2-upstream",
],
py3 = [
"_lcm.@PYTHON_EXT_SUFFIX@",
"lcm-python3-upstream",
],
),
library_dest = "@PYTHON_SITE_PACKAGES@/lcm",
# Re-declare for `py2_py3` to intercept.
py_dest = "@PYTHON_SITE_PACKAGES@",
py_strip_prefix = ["lcm-python"],
)

Expand Down Expand Up @@ -340,7 +377,10 @@ install(
deps = [
":install_cmake_config",
":install_extra_cmake",
# Using an alias enables us to install correctly for both Python2 and
# Python3.
":install_python",
":install_python2",
],
)

Expand Down
11 changes: 11 additions & 0 deletions tools/workspace/lcm/test/lcm_py2_py3_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from subprocess import check_call
import unittest

# This will check the Bazel Python version (2 or 3).
from lcm import LCM


class TestLcmPython2and3(unittest.TestCase):
def test_python2(self):
# Explicitly check Python2.
check_call(["python2", "-c", "from lcm import LCM"])
107 changes: 107 additions & 0 deletions tools/workspace/python/py2_py3.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
load(
"@python//:version.bzl",
"PYTHON_EXT_SUFFIX",
"PYTHON_SITE_PACKAGES_RELPATH",
"PYTHON_VERSION",
)
load(
"@python2//:version.bzl",
PYTHON2_EXT_SUFFIX = "PYTHON_EXT_SUFFIX",
PYTHON2_SITE_PACKAGES_RELPATH = "PYTHON_SITE_PACKAGES_RELPATH",
PYTHON2_VERSION = "PYTHON_VERSION",
)

_BAZEL = struct(
version_field = "py" + PYTHON_VERSION.split(".")[0],
ext_suffix = PYTHON_EXT_SUFFIX,
major = PYTHON_VERSION.split(".")[0],
site_packages = PYTHON_SITE_PACKAGES_RELPATH,
)
_PY2 = struct(
version_field = "py2",
ext_suffix = PYTHON2_EXT_SUFFIX,
major = PYTHON2_VERSION.split(".")[0],
site_packages = PYTHON2_SITE_PACKAGES_RELPATH,
)

# Cannot use `type(x) == str`, etc., but can infer from literals?
_list = type([])
_str = type("")
_struct = type(struct())

def py_select(py2, py3):
return struct(py2 = py2, py3 = py3)

def py2_py3(
rule,
alias = None,
**kwargs):
"""
Provides Bazel Python and Python2 targets, without duplication or conflict.

If Bazel is using Python2, only a Python2 target will be defined.
If Bazel using Python3, both a Python2 and Python3 target will be defined.

@param rule
Starlark rule (e.g. `py_library`, `cc_binary`).
@param alias
If specified, will create an alias to the version of the target for
Bazel.
@param kwargs
Arguments for the rule.

Arguments are resolved in the following fashion:
* If an argument's value comes from `py_select`, it will be replaced
based on the Python version using the `py2` or `py3` values.
* Strings have the following tokens replaced:
@PYTHON_SITE_PACKAGES@
@PYTHON_EXT_SUFFIX@
"""

# Define Python2 target.
if _PY2.major != "2":
fail("@python2 has the wrong major version: {}".format(_PY2))
kwargs_py2 = _format(kwargs, _PY2)
rule(**kwargs_py2)
alias_actual = kwargs_py2["name"]

if _BAZEL.major == "3":
# Define Python3 target.
kwargs_py3 = _format(kwargs, _BAZEL)
alias_actual = kwargs_py3["name"]
rule(**kwargs_py3)

# Define alias.
if alias:
native.alias(
name = alias,
actual = alias_actual,
visibility = kwargs_py2.get("visibility"),
)

def _format(raw, build):
# Format a dict of arguments for `py2_py3`.
out = dict()
for key, value in raw.items():
if type(value) == _struct:
value = getattr(value, build.version_field)
out[key] = _format_value(value, build)
return out

def _format_value(value, build):
# N.B. `str` and `list` do not work in this comparison.
if type(value) == _str:
return _format_str(value, build)
elif type(value) == _list:
return [_format_str(x, build) for x in value]
else:
return value

def _format_str(value, build):
subs = (
("@PYTHON_SITE_PACKAGES@", build.site_packages),
("@PYTHON_EXT_SUFFIX@", build.ext_suffix),
)
for old, new in subs:
value = value.replace(old, new)
return value
Loading