Merge lp:~mterry/snapcraft/ros-project into lp:~snappy-dev/snapcraft/core

Proposed by Michael Terry
Status: Rejected
Rejected by: Sergio Schvezov
Proposed branch: lp:~mterry/snapcraft/ros-project
Merge into: lp:~snappy-dev/snapcraft/core
Diff against target: 485 lines (+338/-29)
12 files modified
examples/ros/README (+7/-0)
examples/ros/meta/package.yaml (+3/-0)
examples/ros/meta/readme.md (+1/-0)
examples/ros/snapcraft.yaml (+141/-0)
plugins/ros-project.yaml (+7/-0)
snapcraft/__init__.py (+13/-10)
snapcraft/common.py (+14/-6)
snapcraft/plugin.py (+6/-3)
snapcraft/plugins/autotools_project.py (+4/-4)
snapcraft/plugins/cmake_project.py (+4/-4)
snapcraft/plugins/go14_project.py (+2/-2)
snapcraft/plugins/ros_project.py (+136/-0)
To merge this branch: bzr merge lp:~mterry/snapcraft/ros-project
Reviewer Review Type Date Requested Status
Sergio Schvezov Disapprove
Review via email: mp+266099@code.launchpad.net

Commit message

Add simple ROS project support.

This is very preliminary. You have to manually add all the depends for your leaf ROS project in the parts list (from catkin up).

This isn't very different from other project types (python3-project for example). But it does make it a bit annoying to deal with. So this is a sort of minimally-viable-product branch.

There is an example with an extensive dependency hierarchy. But it is not a real working example! It needs Ubuntu packages included that aren't captured in snapcraft.yaml (because they would conflict with each other). To fix that, we need to allow the ubuntu plugin to install multiple packages in one part or dependency-flattening support.

This plugin would really benefit from dependency-injecting-and-flattening support. That way it could automatically create the very verbose dependency hierarchy from a few top-level parts. And it could inject all the Ubuntu package dependencies that are listed in each component's package.xml. But that's stage 2. This first branch is just basic ROS support.

I do a new thing in this branch, which is to hold data in a plugin-wide cache dir (~/.cache/snapcraft/plugins/ros-project). I use this for the rosdistro information that tells us where each branch for a given project is, rather than download the whole git branch each time. Might be useful idea in other plugins too (like go1.4 for the Go tarball?).

Description of the change

Add simple ROS project support.

This is very preliminary. You have to manually add all the depends for your leaf ROS project in the parts list (from catkin up).

This isn't very different from other project types (python3-project for example). But it does make it a bit annoying to deal with. So this is a sort of minimally-viable-product branch.

There is an example with an extensive dependency hierarchy. But it is not a real working example! It needs Ubuntu packages included that aren't captured in snapcraft.yaml (because they would conflict with each other). To fix that, we need to allow the ubuntu plugin to install multiple packages in one part or dependency-flattening support.

This plugin would really benefit from dependency-injecting-and-flattening support. That way it could automatically create the very verbose dependency hierarchy from a few top-level parts. And it could inject all the Ubuntu package dependencies that are listed in each component's package.xml. But that's stage 2. This first branch is just basic ROS support.

I do a new thing in this branch, which is to hold data in a plugin-wide cache dir (~/.cache/snapcraft/plugins/ros-project). I use this for the rosdistro information that tells us where each branch for a given project is, rather than download the whole git branch each time. Might be useful idea in other plugins too (like go1.4 for the Go tarball?).

To post a comment you must log in.
lp:~mterry/snapcraft/ros-project updated
110. By Michael Terry

Use package name rather than part name in file tree

111. By Michael Terry

Merge from trunk

112. By Michael Terry

Don't use installdir in env() call, rather use given root

Revision history for this message
Sergio Schvezov (sergiusens) wrote :

I think ted is working on something based or not on this, but I want to get rid of the clutter here :-)

Thanks for contribution in any case, it opened up the path for the work ahead.

review: Disapprove

Unmerged revisions

112. By Michael Terry

Don't use installdir in env() call, rather use given root

111. By Michael Terry

Merge from trunk

110. By Michael Terry

Use package name rather than part name in file tree

109. By Michael Terry

Add README detailing some of the limitations of the example

108. By Michael Terry

Expand whole dependency hierarchy for test

107. By Michael Terry

Get python build depends in a generic way

106. By Michael Terry

Merge from trunk

105. By Michael Terry

Another snapshot

104. By Michael Terry

Snapshot of ros work

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'examples/ros'
=== added file 'examples/ros/README'
--- examples/ros/README 1970-01-01 00:00:00 +0000
+++ examples/ros/README 2015-07-29 17:26:38 +0000
@@ -0,0 +1,7 @@
1This example is not a real working example. It's just a technical demo of the dependency hierarchy and proof that we can build a bunch of ROS projects. But it currently relies on packages to be installed on the host in order to build. And those packages should really be included in the snap in order for the code to work once installed.
2
3To fix this, we need to allow multiple Ubuntu packages to not conflict with each other (either multiple packages listed in one part or dependency flattening).
4
5Also, ideally the user wouldn't have to list the whole hierarchy like in this snapcraft.yaml example. Ideally, snapcraft would inject all those dependencies for you and you just list the top-level part.
6
7So this example is incomplete. It is work-in-progress.
08
=== added directory 'examples/ros/meta'
=== added file 'examples/ros/meta/package.yaml'
--- examples/ros/meta/package.yaml 1970-01-01 00:00:00 +0000
+++ examples/ros/meta/package.yaml 2015-07-29 17:26:38 +0000
@@ -0,0 +1,3 @@
1name: rosexample
2version: 1.0
3vendor: "Mike Terry <mterry@ubuntu.com>"
04
=== added file 'examples/ros/meta/readme.md'
--- examples/ros/meta/readme.md 1970-01-01 00:00:00 +0000
+++ examples/ros/meta/readme.md 2015-07-29 17:26:38 +0000
@@ -0,0 +1,1 @@
1ros example
02
=== added file 'examples/ros/snapcraft.yaml'
--- examples/ros/snapcraft.yaml 1970-01-01 00:00:00 +0000
+++ examples/ros/snapcraft.yaml 2015-07-29 17:26:38 +0000
@@ -0,0 +1,141 @@
1parts:
2 catkin:
3 plugin: ros-project
4 release: jade
5 cpp_common:
6 plugin: ros-project
7 release: jade
8 after: [catkin]
9 navigation:
10 plugin: ros-project
11 release: jade
12 after: [catkin]
13 genmsg:
14 plugin: ros-project
15 release: jade
16 after: [catkin]
17 gencpp:
18 plugin: ros-project
19 release: jade
20 after: [catkin, genmsg]
21 geneus:
22 plugin: ros-project
23 release: jade
24 after: [catkin, genmsg]
25 genlisp:
26 plugin: ros-project
27 release: jade
28 after: [catkin, genmsg]
29 genpy:
30 plugin: ros-project
31 release: jade
32 after: [catkin, genmsg]
33 rostime:
34 plugin: ros-project
35 release: jade
36 after: [catkin, cpp_common]
37 rosunit:
38 plugin: ros-project
39 release: jade
40 after: [catkin]
41 rosconsole:
42 plugin: ros-project
43 release: jade
44 after: [catkin, rostime, rosunit]
45 message_generation:
46 plugin: ros-project
47 release: jade
48 after: [catkin, gencpp]
49 roscpp_traits:
50 plugin: ros-project
51 release: jade
52 after: [catkin]
53 roscpp_serialization:
54 plugin: ros-project
55 release: jade
56 after: [catkin, cpp_common, roscpp_traits, rostime]
57 std_msgs:
58 plugin: ros-project
59 release: jade
60 after: [catkin, message_generation, geneus, genlisp, genpy]
61 message_runtime:
62 plugin: ros-project
63 release: jade
64 after: [catkin]
65 rosgraph_msgs:
66 plugin: ros-project
67 release: jade
68 after: [catkin, message_generation, geneus, genlisp, genpy, std_msgs, message_runtime, cpp_common, roscpp_serialization]
69 xmlrpcpp:
70 plugin: ros-project
71 release: jade
72 after: [catkin, cpp_common]
73 roscpp:
74 plugin: ros-project
75 release: jade
76 after: [catkin, cpp_common, message_generation, geneus, genlisp, genpy, rosconsole, roscpp_serialization, rosgraph_msgs, xmlrpcpp]
77 angles:
78 plugin: ros-project
79 release: jade
80 after: [catkin]
81 geometry_msgs:
82 plugin: ros-project
83 release: jade
84 after: [catkin, message_generation, geneus, genlisp, genpy, std_msgs, message_runtime, cpp_common, roscpp_serialization]
85 rostest:
86 plugin: ros-project
87 release: jade
88 after: [catkin]
89 message_filters:
90 plugin: ros-project
91 release: jade
92 after: [catkin, roscpp, rostest]
93 sensor_msgs:
94 plugin: ros-project
95 release: jade
96 after: [catkin, geometry_msgs]
97 actionlib_msgs:
98 plugin: ros-project
99 release: jade
100 after: [catkin, message_generation, geneus, genlisp, genpy, std_msgs, message_runtime, cpp_common, roscpp_serialization]
101 actionlib:
102 plugin: ros-project
103 release: jade
104 after: [catkin, actionlib_msgs, roscpp, rostest]
105 rosgraph:
106 plugin: ros-project
107 release: jade
108 after: [catkin]
109 rospy:
110 plugin: ros-project
111 release: jade
112 after: [catkin]
113 tf2_msgs:
114 plugin: ros-project
115 release: jade
116 after: [catkin, geometry_msgs, actionlib_msgs]
117 tf2:
118 plugin: ros-project
119 release: jade
120 after: [catkin, geometry_msgs, tf2_msgs]
121 tf2_py:
122 plugin: ros-project
123 release: jade
124 after: [catkin, rospy, tf2]
125 tf2_ros:
126 plugin: ros-project
127 release: jade
128 after: [catkin, actionlib, geometry_msgs, message_filters, rosgraph, rospy, tf2, tf2_py]
129 tf:
130 plugin: ros-project
131 release: jade
132 after: [catkin, angles, sensor_msgs, tf2_ros]
133 nav_msgs:
134 plugin: ros-project
135 release: jade
136 after: [catkin, geometry_msgs, actionlib_msgs]
137 map_server:
138 plugin: ros-project
139 release: jade
140 after: [catkin, roscpp, tf, nav_msgs]
141snappy-metadata: meta
0142
=== added file 'plugins/ros-project.yaml'
--- plugins/ros-project.yaml 1970-01-01 00:00:00 +0000
+++ plugins/ros-project.yaml 2015-07-29 17:26:38 +0000
@@ -0,0 +1,7 @@
1module: ros_project
2build-tools: [cmake]
3options:
4 package:
5 required: false
6 release:
7 required: true
08
=== modified file 'snapcraft/__init__.py'
--- snapcraft/__init__.py 2015-07-29 04:33:25 +0000
+++ snapcraft/__init__.py 2015-07-29 17:26:38 +0000
@@ -65,33 +65,36 @@
65 def isurl(self, url):65 def isurl(self, url):
66 return urllib.parse.urlparse(url).scheme != ""66 return urllib.parse.urlparse(url).scheme != ""
6767
68 def pull_bzr(self, source, source_tag=None):68 def pull_bzr(self, source, source_tag=None, destdir=None):
69 destdir = destdir or self.sourcedir
69 tag_opts = []70 tag_opts = []
70 if source_tag:71 if source_tag:
71 tag_opts = ['-r', 'tag:' + source_tag]72 tag_opts = ['-r', 'tag:' + source_tag]
72 if os.path.exists(os.path.join(self.sourcedir, ".bzr")):73 if os.path.exists(os.path.join(destdir, ".bzr")):
73 return self.run(['bzr', 'pull'] + tag_opts + [source, '-d', self.sourcedir], cwd=os.getcwd())74 return self.run(['bzr', 'pull'] + tag_opts + [source, '-d', destdir], cwd=os.getcwd())
74 else:75 else:
75 os.rmdir(self.sourcedir)76 os.rmdir(destdir)
76 return self.run(['bzr', 'branch'] + tag_opts + [source, self.sourcedir], cwd=os.getcwd())77 return self.run(['bzr', 'branch'] + tag_opts + [source, destdir], cwd=os.getcwd())
7778
78 def pull_git(self, source, source_tag=None, source_branch=None):79 def pull_git(self, source, source_tag=None, source_branch=None, destdir=None):
80 destdir = destdir or self.sourcedir
81
79 if source_tag and source_branch:82 if source_tag and source_branch:
80 logger.error("You can't specify both source-tag and source-branch for a git source (part '%s')." % self.name)83 logger.error("You can't specify both source-tag and source-branch for a git source (part '%s')." % self.name)
81 snapcraft.common.fatal()84 snapcraft.common.fatal()
8285
83 if os.path.exists(os.path.join(self.sourcedir, ".git")):86 if os.path.exists(os.path.join(destdir, ".git")):
84 refspec = 'HEAD'87 refspec = 'HEAD'
85 if source_branch:88 if source_branch:
86 refspec = 'refs/heads/' + source_branch89 refspec = 'refs/heads/' + source_branch
87 elif source_tag:90 elif source_tag:
88 refspec = 'refs/tags/' + source_tag91 refspec = 'refs/tags/' + source_tag
89 return self.run(['git', '-C', self.sourcedir, 'pull', source, refspec], cwd=os.getcwd())92 return self.run(['git', '-C', destdir, 'pull', source, refspec], cwd=os.getcwd())
90 else:93 else:
91 branch_opts = []94 branch_opts = []
92 if source_tag or source_branch:95 if source_tag or source_branch:
93 branch_opts = ['--branch', source_tag or source_branch]96 branch_opts = ['--branch', source_tag or source_branch]
94 return self.run(['git', 'clone'] + branch_opts + [source, self.sourcedir], cwd=os.getcwd())97 return self.run(['git', 'clone'] + branch_opts + [source, destdir], cwd=os.getcwd())
9598
96 def pull_tarball(self, source, destdir=None):99 def pull_tarball(self, source, destdir=None):
97 destdir = destdir or self.sourcedir100 destdir = destdir or self.sourcedir
98101
=== modified file 'snapcraft/common.py'
--- snapcraft/common.py 2015-07-23 17:19:25 +0000
+++ snapcraft/common.py 2015-07-29 17:26:38 +0000
@@ -29,15 +29,17 @@
29env = []29env = []
3030
3131
32def assemble_env():32def assemble_env(extra_env=[]):
33 return '\n'.join(['export ' + e for e in env])33 # Use a set to reduce duplicates (which can sometimes cause very long
3434 # lines that exceed linux limits) and order shouldn't matter for these.
3535 return '\n'.join(['export ' + e for e in set(env + extra_env)])
36def run(cmd, **kwargs):36
37
38def run(cmd, extra_env=[], **kwargs):
37 assert isinstance(cmd, list), "run command must be a list"39 assert isinstance(cmd, list), "run command must be a list"
38 # FIXME: This is gross to keep writing this, even when env is the same40 # FIXME: This is gross to keep writing this, even when env is the same
39 with tempfile.NamedTemporaryFile(mode='w+') as f:41 with tempfile.NamedTemporaryFile(mode='w+') as f:
40 f.write(assemble_env())42 f.write(assemble_env(extra_env=extra_env))
41 f.write('\n')43 f.write('\n')
42 f.write('exec $*')44 f.write('exec $*')
43 f.flush()45 f.flush()
@@ -63,3 +65,9 @@
6365
64def get_plugindir():66def get_plugindir():
65 return _plugindir67 return _plugindir
68
69
70def get_cachedir():
71 home_cache = os.path.join(os.environ['HOME'], '.cache')
72 env_cache = os.environ.get('XDG_CACHE_HOME', home_cache)
73 return os.path.join(env_cache, 'snapcraft')
6674
=== modified file 'snapcraft/plugin.py'
--- snapcraft/plugin.py 2015-07-24 16:55:44 +0000
+++ snapcraft/plugin.py 2015-07-29 17:26:38 +0000
@@ -113,9 +113,12 @@
113113
114 for propName in dir(module):114 for propName in dir(module):
115 prop = getattr(module, propName)115 prop = getattr(module, propName)
116 if issubclass(prop, snapcraft.BasePlugin):116 try:
117 self.code = prop(part_name, options)117 if issubclass(prop, snapcraft.BasePlugin):
118 break118 self.code = prop(part_name, options)
119 break
120 except TypeError:
121 continue
119122
120 def __str__(self):123 def __str__(self):
121 return self.part_names[0]124 return self.part_names[0]
122125
=== modified file 'snapcraft/plugins/autotools_project.py'
--- snapcraft/plugins/autotools_project.py 2015-07-22 18:23:16 +0000
+++ snapcraft/plugins/autotools_project.py 2015-07-29 17:26:38 +0000
@@ -15,10 +15,10 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from snapcraft.plugins.make_project import MakePlugin18from snapcraft.plugins import make_project
1919
2020
21class AutotoolsPlugin(MakePlugin):21class AutotoolsPlugin(make_project.MakePlugin):
22 def __init__(self, name, options):22 def __init__(self, name, options):
23 super().__init__(name, options)23 super().__init__(name, options)
24 if self.options.configflags is None:24 if self.options.configflags is None:
2525
=== modified file 'snapcraft/plugins/cmake_project.py'
--- snapcraft/plugins/cmake_project.py 2015-07-22 18:23:16 +0000
+++ snapcraft/plugins/cmake_project.py 2015-07-29 17:26:38 +0000
@@ -14,10 +14,10 @@
14# You should have received a copy of the GNU General Public License14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17from snapcraft.plugins.make_project import MakePlugin17from snapcraft.plugins import make_project
1818
1919
20class CMakePlugin(MakePlugin):20class CMakePlugin(make_project.MakePlugin):
21 def __init__(self, name, options):21 def __init__(self, name, options):
22 super().__init__(name, options)22 super().__init__(name, options)
23 if self.options.configflags is None:23 if self.options.configflags is None:
2424
=== modified file 'snapcraft/plugins/go14_project.py'
--- snapcraft/plugins/go14_project.py 2015-07-22 18:23:16 +0000
+++ snapcraft/plugins/go14_project.py 2015-07-29 17:26:38 +0000
@@ -37,5 +37,5 @@
37 return self.run(['cp', '-a', os.path.join(self.builddir, 'bin'), self.installdir])37 return self.run(['cp', '-a', os.path.join(self.builddir, 'bin'), self.installdir])
3838
39 def run(self, cmd, **kwargs):39 def run(self, cmd, **kwargs):
40 cmd = ['env', 'GOPATH=' + self.builddir] + cmd40 extra_env = ['GOPATH=' + self.builddir]
41 return super().run(cmd, **kwargs)41 return super().run(cmd, extra_env=extra_env, **kwargs)
4242
=== added file 'snapcraft/plugins/ros_project.py'
--- snapcraft/plugins/ros_project.py 1970-01-01 00:00:00 +0000
+++ snapcraft/plugins/ros_project.py 2015-07-29 17:26:38 +0000
@@ -0,0 +1,136 @@
1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2#
3# Copyright (C) 2015 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import logging
18import os
19import xml.etree.ElementTree as ET # vulnerabilities aren't a concern here
20
21import yaml
22
23from snapcraft import common
24from snapcraft.plugins import make_project
25
26
27logger = logging.getLogger(__name__)
28
29
30class RosProjectPlugin(make_project.MakePlugin):
31
32 def __init__(self, name, options):
33 super().__init__(name, options)
34 if not self.options.package:
35 # User didn't specify a package, use the part name
36 if name == 'ros-project':
37 logger.error('Part {} needs either a package option or a name'.format(name))
38 common.fatal()
39 self.options.package = name
40 self.packageinfo = ET.ElementTree()
41
42 def pull(self):
43 # Get main rosdistro info
44 url, tag = self.get_package_info()
45 # per http://www.ros.org/reps/rep-0141.html all release urls are git
46 if not self.get_source(url, source_tag=tag, source_type='git'):
47 return False
48
49 self.parse_package_xml()
50 return self.install_build_depends()
51
52 def build(self):
53 # We use a /ros/NAME prefix because the ROS build scripts don't handle
54 # an empty prefix well (which is our normal cmake prefix). And each
55 # ROS project installs some common files that would conflict if we
56 # don't namespace them. So we put them each in their own bucket.
57 return self.run(['cmake', '.', '-DCMAKE_INSTALL_PREFIX=/ros/{}'.format(self.options.package)]) and \
58 super().build()
59
60 def env(self, root):
61 env = []
62
63 package_root = os.path.join(root, 'ros', self.options.package)
64
65 # Our own python modules
66 pydirs = self.get_python_dirs(os.path.join(package_root, 'lib'))
67 # And our own build-depends sometimes need to be exported (listed in
68 # package.xml via <build_export_depend>, but let's just do them all)
69 pydirs += self.get_python_dirs(os.path.join(package_root, 'usr', 'local', 'lib'))
70 if pydirs:
71 env += ['PYTHONPATH={}:$PYTHONPATH'.format(':'.join(pydirs))]
72
73 # Tell cmake how to find our macro files
74 env += ['CMAKE_PREFIX_PATH={}:$CMAKE_PREFIX_PATH'.format(os.path.join(root, 'ros', self.options.package))]
75
76 return env
77
78 def snap_files(self):
79 # Skip our cmake files
80 return (['*'], ['ros/{0}/share/{0}/cmake'.format(self.options.package)])
81
82 def parse_package_xml(self):
83 package_file = os.path.join(self.sourcedir, 'package.xml')
84 if os.path.exists(package_file):
85 self.packageinfo.parse(package_file)
86
87 def install_build_depends(self):
88 depends = self.packageinfo.findall('./depend')
89 build_depends = self.packageinfo.findall('./build_depend')
90 pyprefix = 'python-'
91 pydir = os.path.join(self.installdir, 'ros', self.options.package)
92 for depend in depends + build_depends:
93 if depend.text.startswith(pyprefix):
94 self.makedirs(pydir)
95 pkgname = depend.text[len(pyprefix):].replace('-', '_')
96 if not self.run(['pip3', 'install', '--system', '--root', pydir, pkgname]):
97 return False
98 return True
99
100 def get_package_info(self):
101 rosdistrodir = os.path.join(common.get_cachedir(), 'plugins', 'ros-project', 'rosdistro')
102 self.makedirs(rosdistrodir)
103 self.pull_git('git://github.com/ros/rosdistro.git', destdir=rosdistrodir)
104 with open(os.path.join(rosdistrodir, self.options.release, 'distribution.yaml')) as fp:
105 return self.find_package_info(yaml.load(fp))
106
107 def find_package_info(self, distinfo):
108 for repo in distinfo['repositories']:
109 if 'release' not in distinfo['repositories'][repo]:
110 continue
111 releaseinfo = distinfo['repositories'][repo]['release']
112 packages = releaseinfo.get('packages', [repo])
113 if self.options.package in packages:
114 tagscheme = releaseinfo['tags']['release']
115 tagscheme = tagscheme.replace('{package}', self.options.package)
116 tagscheme = tagscheme.replace('{version}', releaseinfo['version'])
117 return releaseinfo['url'], tagscheme
118 logger.error('Could not find release information for ROS package {}'.format(self.options.package))
119 common.fatal()
120 return None, None
121
122 def get_python_dirs(self, root):
123 try:
124 alldirs = os.listdir(root)
125 except Exception:
126 return []
127 return [os.path.join(root, x, 'dist-packages') for x in alldirs if x.startswith('python')]
128
129 def run(self, cmd, **kwargs):
130 extra_env = []
131 pylibdir = os.path.join(self.installdir, 'ros', self.options.package, 'usr', 'local', 'lib')
132 pydirs = self.get_python_dirs(pylibdir)
133 if pydirs:
134 pypaths = ':'.join(pydirs)
135 extra_env = ['PYTHONPATH={}:$PYTHONPATH'.format(pypaths)]
136 return super().run(cmd, extra_env=extra_env, **kwargs)

Subscribers

People subscribed via source and target branches