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
1=== added directory 'examples/ros'
2=== added file 'examples/ros/README'
3--- examples/ros/README 1970-01-01 00:00:00 +0000
4+++ examples/ros/README 2015-07-29 17:26:38 +0000
5@@ -0,0 +1,7 @@
6+This 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.
7+
8+To 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).
9+
10+Also, 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.
11+
12+So this example is incomplete. It is work-in-progress.
13
14=== added directory 'examples/ros/meta'
15=== added file 'examples/ros/meta/package.yaml'
16--- examples/ros/meta/package.yaml 1970-01-01 00:00:00 +0000
17+++ examples/ros/meta/package.yaml 2015-07-29 17:26:38 +0000
18@@ -0,0 +1,3 @@
19+name: rosexample
20+version: 1.0
21+vendor: "Mike Terry <mterry@ubuntu.com>"
22
23=== added file 'examples/ros/meta/readme.md'
24--- examples/ros/meta/readme.md 1970-01-01 00:00:00 +0000
25+++ examples/ros/meta/readme.md 2015-07-29 17:26:38 +0000
26@@ -0,0 +1,1 @@
27+ros example
28
29=== added file 'examples/ros/snapcraft.yaml'
30--- examples/ros/snapcraft.yaml 1970-01-01 00:00:00 +0000
31+++ examples/ros/snapcraft.yaml 2015-07-29 17:26:38 +0000
32@@ -0,0 +1,141 @@
33+parts:
34+ catkin:
35+ plugin: ros-project
36+ release: jade
37+ cpp_common:
38+ plugin: ros-project
39+ release: jade
40+ after: [catkin]
41+ navigation:
42+ plugin: ros-project
43+ release: jade
44+ after: [catkin]
45+ genmsg:
46+ plugin: ros-project
47+ release: jade
48+ after: [catkin]
49+ gencpp:
50+ plugin: ros-project
51+ release: jade
52+ after: [catkin, genmsg]
53+ geneus:
54+ plugin: ros-project
55+ release: jade
56+ after: [catkin, genmsg]
57+ genlisp:
58+ plugin: ros-project
59+ release: jade
60+ after: [catkin, genmsg]
61+ genpy:
62+ plugin: ros-project
63+ release: jade
64+ after: [catkin, genmsg]
65+ rostime:
66+ plugin: ros-project
67+ release: jade
68+ after: [catkin, cpp_common]
69+ rosunit:
70+ plugin: ros-project
71+ release: jade
72+ after: [catkin]
73+ rosconsole:
74+ plugin: ros-project
75+ release: jade
76+ after: [catkin, rostime, rosunit]
77+ message_generation:
78+ plugin: ros-project
79+ release: jade
80+ after: [catkin, gencpp]
81+ roscpp_traits:
82+ plugin: ros-project
83+ release: jade
84+ after: [catkin]
85+ roscpp_serialization:
86+ plugin: ros-project
87+ release: jade
88+ after: [catkin, cpp_common, roscpp_traits, rostime]
89+ std_msgs:
90+ plugin: ros-project
91+ release: jade
92+ after: [catkin, message_generation, geneus, genlisp, genpy]
93+ message_runtime:
94+ plugin: ros-project
95+ release: jade
96+ after: [catkin]
97+ rosgraph_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+ xmlrpcpp:
102+ plugin: ros-project
103+ release: jade
104+ after: [catkin, cpp_common]
105+ roscpp:
106+ plugin: ros-project
107+ release: jade
108+ after: [catkin, cpp_common, message_generation, geneus, genlisp, genpy, rosconsole, roscpp_serialization, rosgraph_msgs, xmlrpcpp]
109+ angles:
110+ plugin: ros-project
111+ release: jade
112+ after: [catkin]
113+ geometry_msgs:
114+ plugin: ros-project
115+ release: jade
116+ after: [catkin, message_generation, geneus, genlisp, genpy, std_msgs, message_runtime, cpp_common, roscpp_serialization]
117+ rostest:
118+ plugin: ros-project
119+ release: jade
120+ after: [catkin]
121+ message_filters:
122+ plugin: ros-project
123+ release: jade
124+ after: [catkin, roscpp, rostest]
125+ sensor_msgs:
126+ plugin: ros-project
127+ release: jade
128+ after: [catkin, geometry_msgs]
129+ actionlib_msgs:
130+ plugin: ros-project
131+ release: jade
132+ after: [catkin, message_generation, geneus, genlisp, genpy, std_msgs, message_runtime, cpp_common, roscpp_serialization]
133+ actionlib:
134+ plugin: ros-project
135+ release: jade
136+ after: [catkin, actionlib_msgs, roscpp, rostest]
137+ rosgraph:
138+ plugin: ros-project
139+ release: jade
140+ after: [catkin]
141+ rospy:
142+ plugin: ros-project
143+ release: jade
144+ after: [catkin]
145+ tf2_msgs:
146+ plugin: ros-project
147+ release: jade
148+ after: [catkin, geometry_msgs, actionlib_msgs]
149+ tf2:
150+ plugin: ros-project
151+ release: jade
152+ after: [catkin, geometry_msgs, tf2_msgs]
153+ tf2_py:
154+ plugin: ros-project
155+ release: jade
156+ after: [catkin, rospy, tf2]
157+ tf2_ros:
158+ plugin: ros-project
159+ release: jade
160+ after: [catkin, actionlib, geometry_msgs, message_filters, rosgraph, rospy, tf2, tf2_py]
161+ tf:
162+ plugin: ros-project
163+ release: jade
164+ after: [catkin, angles, sensor_msgs, tf2_ros]
165+ nav_msgs:
166+ plugin: ros-project
167+ release: jade
168+ after: [catkin, geometry_msgs, actionlib_msgs]
169+ map_server:
170+ plugin: ros-project
171+ release: jade
172+ after: [catkin, roscpp, tf, nav_msgs]
173+snappy-metadata: meta
174
175=== added file 'plugins/ros-project.yaml'
176--- plugins/ros-project.yaml 1970-01-01 00:00:00 +0000
177+++ plugins/ros-project.yaml 2015-07-29 17:26:38 +0000
178@@ -0,0 +1,7 @@
179+module: ros_project
180+build-tools: [cmake]
181+options:
182+ package:
183+ required: false
184+ release:
185+ required: true
186
187=== modified file 'snapcraft/__init__.py'
188--- snapcraft/__init__.py 2015-07-29 04:33:25 +0000
189+++ snapcraft/__init__.py 2015-07-29 17:26:38 +0000
190@@ -65,33 +65,36 @@
191 def isurl(self, url):
192 return urllib.parse.urlparse(url).scheme != ""
193
194- def pull_bzr(self, source, source_tag=None):
195+ def pull_bzr(self, source, source_tag=None, destdir=None):
196+ destdir = destdir or self.sourcedir
197 tag_opts = []
198 if source_tag:
199 tag_opts = ['-r', 'tag:' + source_tag]
200- if os.path.exists(os.path.join(self.sourcedir, ".bzr")):
201- return self.run(['bzr', 'pull'] + tag_opts + [source, '-d', self.sourcedir], cwd=os.getcwd())
202+ if os.path.exists(os.path.join(destdir, ".bzr")):
203+ return self.run(['bzr', 'pull'] + tag_opts + [source, '-d', destdir], cwd=os.getcwd())
204 else:
205- os.rmdir(self.sourcedir)
206- return self.run(['bzr', 'branch'] + tag_opts + [source, self.sourcedir], cwd=os.getcwd())
207-
208- def pull_git(self, source, source_tag=None, source_branch=None):
209+ os.rmdir(destdir)
210+ return self.run(['bzr', 'branch'] + tag_opts + [source, destdir], cwd=os.getcwd())
211+
212+ def pull_git(self, source, source_tag=None, source_branch=None, destdir=None):
213+ destdir = destdir or self.sourcedir
214+
215 if source_tag and source_branch:
216 logger.error("You can't specify both source-tag and source-branch for a git source (part '%s')." % self.name)
217 snapcraft.common.fatal()
218
219- if os.path.exists(os.path.join(self.sourcedir, ".git")):
220+ if os.path.exists(os.path.join(destdir, ".git")):
221 refspec = 'HEAD'
222 if source_branch:
223 refspec = 'refs/heads/' + source_branch
224 elif source_tag:
225 refspec = 'refs/tags/' + source_tag
226- return self.run(['git', '-C', self.sourcedir, 'pull', source, refspec], cwd=os.getcwd())
227+ return self.run(['git', '-C', destdir, 'pull', source, refspec], cwd=os.getcwd())
228 else:
229 branch_opts = []
230 if source_tag or source_branch:
231 branch_opts = ['--branch', source_tag or source_branch]
232- return self.run(['git', 'clone'] + branch_opts + [source, self.sourcedir], cwd=os.getcwd())
233+ return self.run(['git', 'clone'] + branch_opts + [source, destdir], cwd=os.getcwd())
234
235 def pull_tarball(self, source, destdir=None):
236 destdir = destdir or self.sourcedir
237
238=== modified file 'snapcraft/common.py'
239--- snapcraft/common.py 2015-07-23 17:19:25 +0000
240+++ snapcraft/common.py 2015-07-29 17:26:38 +0000
241@@ -29,15 +29,17 @@
242 env = []
243
244
245-def assemble_env():
246- return '\n'.join(['export ' + e for e in env])
247-
248-
249-def run(cmd, **kwargs):
250+def assemble_env(extra_env=[]):
251+ # Use a set to reduce duplicates (which can sometimes cause very long
252+ # lines that exceed linux limits) and order shouldn't matter for these.
253+ return '\n'.join(['export ' + e for e in set(env + extra_env)])
254+
255+
256+def run(cmd, extra_env=[], **kwargs):
257 assert isinstance(cmd, list), "run command must be a list"
258 # FIXME: This is gross to keep writing this, even when env is the same
259 with tempfile.NamedTemporaryFile(mode='w+') as f:
260- f.write(assemble_env())
261+ f.write(assemble_env(extra_env=extra_env))
262 f.write('\n')
263 f.write('exec $*')
264 f.flush()
265@@ -63,3 +65,9 @@
266
267 def get_plugindir():
268 return _plugindir
269+
270+
271+def get_cachedir():
272+ home_cache = os.path.join(os.environ['HOME'], '.cache')
273+ env_cache = os.environ.get('XDG_CACHE_HOME', home_cache)
274+ return os.path.join(env_cache, 'snapcraft')
275
276=== modified file 'snapcraft/plugin.py'
277--- snapcraft/plugin.py 2015-07-24 16:55:44 +0000
278+++ snapcraft/plugin.py 2015-07-29 17:26:38 +0000
279@@ -113,9 +113,12 @@
280
281 for propName in dir(module):
282 prop = getattr(module, propName)
283- if issubclass(prop, snapcraft.BasePlugin):
284- self.code = prop(part_name, options)
285- break
286+ try:
287+ if issubclass(prop, snapcraft.BasePlugin):
288+ self.code = prop(part_name, options)
289+ break
290+ except TypeError:
291+ continue
292
293 def __str__(self):
294 return self.part_names[0]
295
296=== modified file 'snapcraft/plugins/autotools_project.py'
297--- snapcraft/plugins/autotools_project.py 2015-07-22 18:23:16 +0000
298+++ snapcraft/plugins/autotools_project.py 2015-07-29 17:26:38 +0000
299@@ -15,10 +15,10 @@
300 # along with this program. If not, see <http://www.gnu.org/licenses/>.
301
302 import os
303-from snapcraft.plugins.make_project import MakePlugin
304-
305-
306-class AutotoolsPlugin(MakePlugin):
307+from snapcraft.plugins import make_project
308+
309+
310+class AutotoolsPlugin(make_project.MakePlugin):
311 def __init__(self, name, options):
312 super().__init__(name, options)
313 if self.options.configflags is None:
314
315=== modified file 'snapcraft/plugins/cmake_project.py'
316--- snapcraft/plugins/cmake_project.py 2015-07-22 18:23:16 +0000
317+++ snapcraft/plugins/cmake_project.py 2015-07-29 17:26:38 +0000
318@@ -14,10 +14,10 @@
319 # You should have received a copy of the GNU General Public License
320 # along with this program. If not, see <http://www.gnu.org/licenses/>.
321
322-from snapcraft.plugins.make_project import MakePlugin
323-
324-
325-class CMakePlugin(MakePlugin):
326+from snapcraft.plugins import make_project
327+
328+
329+class CMakePlugin(make_project.MakePlugin):
330 def __init__(self, name, options):
331 super().__init__(name, options)
332 if self.options.configflags is None:
333
334=== modified file 'snapcraft/plugins/go14_project.py'
335--- snapcraft/plugins/go14_project.py 2015-07-22 18:23:16 +0000
336+++ snapcraft/plugins/go14_project.py 2015-07-29 17:26:38 +0000
337@@ -37,5 +37,5 @@
338 return self.run(['cp', '-a', os.path.join(self.builddir, 'bin'), self.installdir])
339
340 def run(self, cmd, **kwargs):
341- cmd = ['env', 'GOPATH=' + self.builddir] + cmd
342- return super().run(cmd, **kwargs)
343+ extra_env = ['GOPATH=' + self.builddir]
344+ return super().run(cmd, extra_env=extra_env, **kwargs)
345
346=== added file 'snapcraft/plugins/ros_project.py'
347--- snapcraft/plugins/ros_project.py 1970-01-01 00:00:00 +0000
348+++ snapcraft/plugins/ros_project.py 2015-07-29 17:26:38 +0000
349@@ -0,0 +1,136 @@
350+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
351+#
352+# Copyright (C) 2015 Canonical Ltd
353+#
354+# This program is free software: you can redistribute it and/or modify
355+# it under the terms of the GNU General Public License version 3 as
356+# published by the Free Software Foundation.
357+#
358+# This program is distributed in the hope that it will be useful,
359+# but WITHOUT ANY WARRANTY; without even the implied warranty of
360+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
361+# GNU General Public License for more details.
362+#
363+# You should have received a copy of the GNU General Public License
364+# along with this program. If not, see <http://www.gnu.org/licenses/>.
365+
366+import logging
367+import os
368+import xml.etree.ElementTree as ET # vulnerabilities aren't a concern here
369+
370+import yaml
371+
372+from snapcraft import common
373+from snapcraft.plugins import make_project
374+
375+
376+logger = logging.getLogger(__name__)
377+
378+
379+class RosProjectPlugin(make_project.MakePlugin):
380+
381+ def __init__(self, name, options):
382+ super().__init__(name, options)
383+ if not self.options.package:
384+ # User didn't specify a package, use the part name
385+ if name == 'ros-project':
386+ logger.error('Part {} needs either a package option or a name'.format(name))
387+ common.fatal()
388+ self.options.package = name
389+ self.packageinfo = ET.ElementTree()
390+
391+ def pull(self):
392+ # Get main rosdistro info
393+ url, tag = self.get_package_info()
394+ # per http://www.ros.org/reps/rep-0141.html all release urls are git
395+ if not self.get_source(url, source_tag=tag, source_type='git'):
396+ return False
397+
398+ self.parse_package_xml()
399+ return self.install_build_depends()
400+
401+ def build(self):
402+ # We use a /ros/NAME prefix because the ROS build scripts don't handle
403+ # an empty prefix well (which is our normal cmake prefix). And each
404+ # ROS project installs some common files that would conflict if we
405+ # don't namespace them. So we put them each in their own bucket.
406+ return self.run(['cmake', '.', '-DCMAKE_INSTALL_PREFIX=/ros/{}'.format(self.options.package)]) and \
407+ super().build()
408+
409+ def env(self, root):
410+ env = []
411+
412+ package_root = os.path.join(root, 'ros', self.options.package)
413+
414+ # Our own python modules
415+ pydirs = self.get_python_dirs(os.path.join(package_root, 'lib'))
416+ # And our own build-depends sometimes need to be exported (listed in
417+ # package.xml via <build_export_depend>, but let's just do them all)
418+ pydirs += self.get_python_dirs(os.path.join(package_root, 'usr', 'local', 'lib'))
419+ if pydirs:
420+ env += ['PYTHONPATH={}:$PYTHONPATH'.format(':'.join(pydirs))]
421+
422+ # Tell cmake how to find our macro files
423+ env += ['CMAKE_PREFIX_PATH={}:$CMAKE_PREFIX_PATH'.format(os.path.join(root, 'ros', self.options.package))]
424+
425+ return env
426+
427+ def snap_files(self):
428+ # Skip our cmake files
429+ return (['*'], ['ros/{0}/share/{0}/cmake'.format(self.options.package)])
430+
431+ def parse_package_xml(self):
432+ package_file = os.path.join(self.sourcedir, 'package.xml')
433+ if os.path.exists(package_file):
434+ self.packageinfo.parse(package_file)
435+
436+ def install_build_depends(self):
437+ depends = self.packageinfo.findall('./depend')
438+ build_depends = self.packageinfo.findall('./build_depend')
439+ pyprefix = 'python-'
440+ pydir = os.path.join(self.installdir, 'ros', self.options.package)
441+ for depend in depends + build_depends:
442+ if depend.text.startswith(pyprefix):
443+ self.makedirs(pydir)
444+ pkgname = depend.text[len(pyprefix):].replace('-', '_')
445+ if not self.run(['pip3', 'install', '--system', '--root', pydir, pkgname]):
446+ return False
447+ return True
448+
449+ def get_package_info(self):
450+ rosdistrodir = os.path.join(common.get_cachedir(), 'plugins', 'ros-project', 'rosdistro')
451+ self.makedirs(rosdistrodir)
452+ self.pull_git('git://github.com/ros/rosdistro.git', destdir=rosdistrodir)
453+ with open(os.path.join(rosdistrodir, self.options.release, 'distribution.yaml')) as fp:
454+ return self.find_package_info(yaml.load(fp))
455+
456+ def find_package_info(self, distinfo):
457+ for repo in distinfo['repositories']:
458+ if 'release' not in distinfo['repositories'][repo]:
459+ continue
460+ releaseinfo = distinfo['repositories'][repo]['release']
461+ packages = releaseinfo.get('packages', [repo])
462+ if self.options.package in packages:
463+ tagscheme = releaseinfo['tags']['release']
464+ tagscheme = tagscheme.replace('{package}', self.options.package)
465+ tagscheme = tagscheme.replace('{version}', releaseinfo['version'])
466+ return releaseinfo['url'], tagscheme
467+ logger.error('Could not find release information for ROS package {}'.format(self.options.package))
468+ common.fatal()
469+ return None, None
470+
471+ def get_python_dirs(self, root):
472+ try:
473+ alldirs = os.listdir(root)
474+ except Exception:
475+ return []
476+ return [os.path.join(root, x, 'dist-packages') for x in alldirs if x.startswith('python')]
477+
478+ def run(self, cmd, **kwargs):
479+ extra_env = []
480+ pylibdir = os.path.join(self.installdir, 'ros', self.options.package, 'usr', 'local', 'lib')
481+ pydirs = self.get_python_dirs(pylibdir)
482+ if pydirs:
483+ pypaths = ':'.join(pydirs)
484+ extra_env = ['PYTHONPATH={}:$PYTHONPATH'.format(pypaths)]
485+ return super().run(cmd, extra_env=extra_env, **kwargs)

Subscribers

People subscribed via source and target branches