Merge lp:~brian-murray/click/chroots into lp:click
- chroots
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 303 |
Proposed branch: | lp:~brian-murray/click/chroots |
Merge into: | lp:click |
Diff against target: |
377 lines (+368/-0) 2 files modified
click/chroot.py (+277/-0) click/commands/chroot.py (+91/-0) |
To merge this branch: | bzr merge lp:~brian-murray/click/chroots |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson | Approve | ||
Review via email: mp+188879@code.launchpad.net |
Commit message
Description of the change
This branch adds chroot (using schroot) management and creation to click.
Colin Watson (cjwatson) wrote : | # |
- 293. By Brian Murray
-
call subprocess.
check_output with universal_ newlines= True
Colin Watson (cjwatson) wrote : | # |
review needs-fixing
This looks very nearly there; thanks a lot! There are just a few
details to clean up.
On Wed, Oct 02, 2013 at 04:13:25PM -0000, Brian Murray wrote:
> + proxy = subprocess.
> + ["apt-config", "shell", "x", "Acquire:
> + ).decode(
$ apt-config shell x Dir
x='/'
That implies quoting may need to be involved here. Using python-apt
would be cleaner, but is an extra dependency. Hmm. How about something
like this instead:
proxy = subprocess.
'unset x; eval "$(apt-config shell x Acquire:
shell=True, universal_
? I've certainly written cleaner code ... but this seems to avoid
potential quoting problems.
> + build_pkgs = [
> + "build-essential", "fakeroot",
> + "apt-utils", "pkg-create-
> + "pkgbinarymangler", "g++-%s" % target_tuple,
> + "pkg-config-%s" % target_tuple,
> + "dpkg-cross", "libc-dev:%s" % self.target_arch
> + ]
Let's drop pkg-create-dbgsym and pkgbinarymangler; we don't need them
for click packages.
> + shutil.
> + shutil.
Nit: I'd use shutil.copy2. Also, /etc/localtime may be a symlink; check
whether shutil.copy2 preserves those, and if not you'll probably need
some special-casing.
> + chroot_config = "/etc/schroot/
> + with open(chroot_config, "w") as target:
> + admin_groups = "sbuild,root"
The sbuild group shouldn't be involved here.
> + print("[%s]" % self.full_name, file=target)
> + print("
> + self.target_arch, file=target)
> + for group in ["groups", "root-groups", "source-
> + "source-
> + print("%s=%s" % (group, admin_groups), file=target)
I realise this is translated from mk-sbuild, but I'd prefer not to add
things described as group names to source-root-users; even if they
happen to coincide now it's a bit of a trap for the future.
For this application, I would suggest that we want the calling user to
have full access, and also the root user for good measure (it could also
> + print("
> + print("
> + print("
> + print("
Let's drop profile=sbuild. We don't need it here, and it has unhelpful
(for this application) differences from the default profile such as not
bind-mounting the user's home directory.
> + def run(self, *args):
> + if not self.exists():
> + raise ClickChrootExce
> + "Chroot %s does not exist" % self.full_name)
> + command = ["schroot", "-c", self.full_name, "--directory=/", "--"]
Why the --directory=/ here? I'm concerned that that will make it
difficult to do simple "run make in ...
- 294. By Brian Murray
-
modifications based on reviewer feedback
- 295. By Brian Murray
-
modify how apt-config is used to check for a proxy
- 296. By Brian Murray
-
fix argparse imports
Brian Murray (brian-murray) wrote : | # |
On Fri, Oct 04, 2013 at 08:45:46AM -0000, Colin Watson wrote:
> Review: Needs Fixing
>
> review needs-fixing
>
> This looks very nearly there; thanks a lot! There are just a few
> details to clean up.
>
> On Wed, Oct 02, 2013 at 04:13:25PM -0000, Brian Murray wrote:
> > + proxy = subprocess.
> > + ["apt-config", "shell", "x", "Acquire:
> > + ).decode(
>
> $ apt-config shell x Dir
> x='/'
>
> That implies quoting may need to be involved here. Using python-apt
> would be cleaner, but is an extra dependency. Hmm. How about something
> like this instead:
>
> proxy = subprocess.
> 'unset x; eval "$(apt-config shell x Acquire:
> shell=True, universal_
>
> ? I've certainly written cleaner code ... but this seems to avoid
> potential quoting problems.
Changed.
>
> > + build_pkgs = [
> > + "build-essential", "fakeroot",
> > + "apt-utils", "pkg-create-
> > + "pkgbinarymangler", "g++-%s" % target_tuple,
> > + "pkg-config-%s" % target_tuple,
> > + "dpkg-cross", "libc-dev:%s" % self.target_arch
> > + ]
>
> Let's drop pkg-create-dbgsym and pkgbinarymangler; we don't need them
> for click packages.
Changed.
> > + shutil.
> > + shutil.
>
> Nit: I'd use shutil.copy2. Also, /etc/localtime may be a symlink; check
> whether shutil.copy2 preserves those, and if not you'll probably need
> some special-casing.
Done.
> > + chroot_config = "/etc/schroot/
> > + with open(chroot_config, "w") as target:
> > + admin_groups = "sbuild,root"
>
> The sbuild group shouldn't be involved here.
Done.
> > + print("[%s]" % self.full_name, file=target)
> > + print("
> > + self.target_arch, file=target)
> > + for group in ["groups", "root-groups", "source-
> > + "source-
> > + print("%s=%s" % (group, admin_groups), file=target)
>
> I realise this is translated from mk-sbuild, but I'd prefer not to add
> things described as group names to source-root-users; even if they
> happen to coincide now it's a bit of a trap for the future.
>
> For this application, I would suggest that we want the calling user to
> have full access, and also the root user for good measure (it could also
This seems to have been cut off.
> > + print("
> > + print("
> > + print("
> > + print("
>
> Let's drop profile=sbuild. We don't need it here, and it has unhelpful
> (for this application) differences from the default profile such as not
> bind-mounting the user's home directory.
The sbuild profile contains a different nssdatabases profil...
Colin Watson (cjwatson) wrote : | # |
On Mon, Oct 07, 2013 at 09:00:45PM -0000, Brian Murray wrote:
> On Fri, Oct 04, 2013 at 08:45:46AM -0000, Colin Watson wrote:
> > I realise this is translated from mk-sbuild, but I'd prefer not to add
> > things described as group names to source-root-users; even if they
> > happen to coincide now it's a bit of a trap for the future.
> >
> > For this application, I would suggest that we want the calling user to
> > have full access, and also the root user for good measure (it could also
>
> This seems to have been cut off.
I'm not sure what I meant to say there. I don't think it was very
important.
> > > + def run(self, *args):
> > > + if not self.exists():
> > > + raise ClickChrootExce
> > > + "Chroot %s does not exist" % self.full_name)
> > > + command = ["schroot", "-c", self.full_name, "--directory=/", "--"]
> >
> > Why the --directory=/ here? I'm concerned that that will make it
> > difficult to do simple "run make in this directory"-type things, which
> > is a primary goal of this system.
> >
> > Perhaps this was a consequence of profile=sbuild, since not having your
> > home directory mounted would be a bit of a problem. If so, try dropping
> > this argument after dropping that profile.
>
> This is still an issue with my setup because of my encrypted home
> directory. Do you have any suggestions on how we may deal with users
> that also have one?
Maybe you need to bind-mount $HOME rather than /home?
Colin Watson (cjwatson) wrote : | # |
I made various further improvements and merged this. Thanks a lot!
Preview Diff
1 | === added file 'click/chroot.py' |
2 | --- click/chroot.py 1970-01-01 00:00:00 +0000 |
3 | +++ click/chroot.py 2013-10-07 20:57:16 +0000 |
4 | @@ -0,0 +1,277 @@ |
5 | +# Copyright (C) 2013 Canonical Ltd. |
6 | +# Authors: Colin Watson <cjwatson@ubuntu.com>, |
7 | +# Brian Murray <brian@ubuntu.com> |
8 | +# |
9 | +# This program is free software: you can redistribute it and/or modify |
10 | +# it under the terms of the GNU General Public License as published by |
11 | +# the Free Software Foundation; version 3 of the License. |
12 | +# |
13 | +# This program is distributed in the hope that it will be useful, |
14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | +# GNU General Public License for more details. |
17 | +# |
18 | +# You should have received a copy of the GNU General Public License |
19 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
20 | + |
21 | +"""Chroot management for building Click packages.""" |
22 | + |
23 | +from __future__ import print_function |
24 | + |
25 | +__metaclass__ = type |
26 | +__all__ = [ |
27 | + "ClickChroot", |
28 | + "ClickChrootException", |
29 | + ] |
30 | + |
31 | + |
32 | +import os |
33 | +import shutil |
34 | +import stat |
35 | +import subprocess |
36 | + |
37 | + |
38 | +extra_packages = { |
39 | + "ubuntu-sdk-13.10": [ |
40 | + "libqt5opengl5-dev:TARGET", |
41 | + "libqt5webkit5-dev:TARGET", |
42 | + "libqt5xmlpatterns5-dev:TARGET", |
43 | + "qt5-default:TARGET", |
44 | + "qt5-qmake:TARGET", |
45 | + "qtbase5-dev:TARGET", |
46 | + "qtdeclarative5-dev:TARGET", |
47 | + "qtquick1-5-dev:TARGET", |
48 | + "qtscript5-dev:TARGET", |
49 | + "qtsensors5-dev:TARGET", |
50 | + ], |
51 | + } |
52 | + |
53 | + |
54 | +class ClickChrootException(Exception): |
55 | + pass |
56 | + |
57 | + |
58 | +class ClickChroot: |
59 | + def __init__(self, series, target_arch, framework, name=None): |
60 | + if name is None: |
61 | + name = "click" |
62 | + self.series = series |
63 | + self.target_arch = target_arch |
64 | + self.framework = framework |
65 | + self.name = name |
66 | + self.native_arch = subprocess.check_output( |
67 | + ["dpkg", "--print-architecture"], |
68 | + universal_newlines=True).strip() |
69 | + self.chroots_dir = "/var/lib/schroot/chroots" |
70 | + # this doesn't work because we are running this under sudo |
71 | + if 'DEBOOTSTRAP_MIRROR' in os.environ: |
72 | + self.archive = os.environ['DEBOOTSTRAP_MIRROR'] |
73 | + else: |
74 | + self.archive = "http://archive.ubuntu.com/ubuntu" |
75 | + |
76 | + def _generate_sources(self, series, native_arch, target_arch, components): |
77 | + ports_mirror = "http://ports.ubuntu.com/ubuntu-ports" |
78 | + pockets = ['%s' % series] |
79 | + for pocket in ['updates', 'security']: |
80 | + pockets.append('%s-%s' % (series, pocket)) |
81 | + sources = [] |
82 | + if target_arch in ['armhf']: |
83 | + for pocket in pockets: |
84 | + sources.append("deb [arch=%s] %s %s %s" % |
85 | + (target_arch, ports_mirror, pocket, components)) |
86 | + sources.append("deb-src %s %s %s" % |
87 | + (ports_mirror, pocket, components)) |
88 | + if native_arch in ['i386', 'amd64']: |
89 | + for pocket in pockets: |
90 | + sources.append("deb [arch=%s] %s %s %s" % |
91 | + (native_arch, self.archive, pocket, components)) |
92 | + sources.append("deb-src %s %s %s" % |
93 | + (self.archive, pocket, components)) |
94 | + return sources |
95 | + |
96 | + @property |
97 | + def full_name(self): |
98 | + return "%s-%s-%s-%s-%s" % ( |
99 | + self.name, self.framework, self.series, self.native_arch, |
100 | + self.target_arch) |
101 | + |
102 | + def exists(self): |
103 | + command = ["schroot", "-c", self.full_name, "-i"] |
104 | + with open("/dev/null", "w") as devnull: |
105 | + return subprocess.call( |
106 | + command, stdout=devnull, stderr=devnull) == 0 |
107 | + |
108 | + def create(self): |
109 | + if self.exists(): |
110 | + raise ClickChrootException( |
111 | + "Chroot %s already exists" % self.full_name) |
112 | + components = ["main", "restricted", "universe", "multiverse"] |
113 | + mount = "%s/%s" % (self.chroots_dir, self.full_name) |
114 | + proxy = None |
115 | + if not proxy and "http_proxy" in os.environ: |
116 | + proxy = os.environ["http_proxy"] |
117 | + if not proxy: |
118 | + proxy = subprocess.check_output( |
119 | + 'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; echo "$x"', |
120 | + shell=True, universal_newlines=True).strip() |
121 | + with open("/dev/null", "w") as devnull: |
122 | + target_tuple = subprocess.check_output( |
123 | + ["dpkg-architecture", "-a%s" % self.target_arch, |
124 | + "-qDEB_HOST_GNU_TYPE"], stderr=devnull, |
125 | + universal_newlines=True).strip() |
126 | + build_pkgs = [ |
127 | + "build-essential", "fakeroot", |
128 | + "apt-utils", "g++-%s" % target_tuple, |
129 | + "pkg-config-%s" % target_tuple, |
130 | + "dpkg-cross", "libc-dev:%s" % self.target_arch |
131 | + ] |
132 | + for package in extra_packages.get(self.framework, []): |
133 | + package = package.replace(":TARGET", ":%s" % self.target_arch) |
134 | + build_pkgs.append(package) |
135 | + os.makedirs(mount) |
136 | + subprocess.check_call([ |
137 | + "debootstrap", |
138 | + "--arch", self.native_arch, |
139 | + "--variant=buildd", |
140 | + "--components=%s" % ','.join(components), |
141 | + self.series, |
142 | + mount, |
143 | + self.archive |
144 | + ]) |
145 | + sources = self._generate_sources(self.series, self.native_arch, |
146 | + self.target_arch, |
147 | + ' '.join(components)) |
148 | + with open("%s/etc/apt/sources.list" % mount, "w") as sources_list: |
149 | + for line in sources: |
150 | + print(line, file=sources_list) |
151 | + shutil.copy2("/etc/localtime", "%s/etc/" % mount) |
152 | + shutil.copy2("/etc/timezone", "%s/etc/" % mount) |
153 | + chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name |
154 | + with open(chroot_config, "w") as target: |
155 | + admin_groups = "root" |
156 | + print("[%s]" % self.full_name, file=target) |
157 | + print("description=Build chroot for click packages on %s" % |
158 | + self.target_arch, file=target) |
159 | + for group in ["groups", "root-groups", "source-root-users", |
160 | + "source-root-groups"]: |
161 | + print("%s=%s" % (group, admin_groups), file=target) |
162 | + print("type=directory", file=target) |
163 | + print("profile=default", file=target) |
164 | + print("# Not protocols or services see ", file=target) |
165 | + print("# debian bug 557730", file=target) |
166 | + print("setup.nssdatabases=sbuild/nssdatabases", |
167 | + file=target) |
168 | + print("union-type=overlayfs", file=target) |
169 | + print("directory=%s" % mount, file=target) |
170 | + daemon_policy = "%s/usr/sbin/policy-rc.d" % mount |
171 | + with open(daemon_policy, "w") as policy: |
172 | + print("#!/bin/sh", file=policy) |
173 | + print("while true; do", file=policy) |
174 | + print(' case "$1" in', file=policy) |
175 | + print(" -*) shift ;;", file=policy) |
176 | + print(" makedev) exit 0;;", file=policy) |
177 | + print(" x11-common) exit 0;;", file=policy) |
178 | + print(" *) exit 101;;", file=policy) |
179 | + print(" esac", file=policy) |
180 | + print("done", file=policy) |
181 | + os.remove("%s/sbin/initctl" % mount) |
182 | + os.symlink("%s/bin/true" % mount, "%s/sbin/initctl" % mount) |
183 | + finish_script = "%s/finish.sh" % mount |
184 | + with open(finish_script, 'w') as finish: |
185 | + print("#!/bin/bash", file=finish) |
186 | + print("set -e", file=finish) |
187 | + if proxy: |
188 | + print("mkdir -p /etc/apt/apt.conf.d", file=finish) |
189 | + print("cat > /etc/apt/apt.conf.d/99-click-chroot-proxy <<EOF", |
190 | + file=finish) |
191 | + print("// proxy settings copied by click-chroot", file=finish) |
192 | + print('Acquire { HTTP { Proxy "%s"; }; };' % proxy, |
193 | + file=finish) |
194 | + print("EOF", file=finish) |
195 | + print("# Configure target arch", file=finish) |
196 | + print("dpkg --add-architecture %s" % self.target_arch, |
197 | + file=finish) |
198 | + print("# Reload package lists", file=finish) |
199 | + print("apt-get update || true", file=finish) |
200 | + print("# Pull down signature requirements", file=finish) |
201 | + print("apt-get -y --force-yes install \ |
202 | +gnupg ubuntu-keyring", file=finish) |
203 | + print("# Reload package lists", file=finish) |
204 | + print("apt-get update || true", file=finish) |
205 | + print("# Disable debconf questions so that automated \ |
206 | +builds won't prompt", file=finish) |
207 | + print("echo set debconf/frontend Noninteractive | \ |
208 | +debconf-communicate", file=finish) |
209 | + print("echo set debconf/priority critical | \ |
210 | +debconf-communicate", file=finish) |
211 | + print("# Install basic build tool set to match buildd", |
212 | + file=finish) |
213 | + print("apt-get -y --force-yes install %s" |
214 | + % ' '.join(build_pkgs), file=finish) |
215 | + print("# Set up expected /dev entries", file=finish) |
216 | + print("if [ ! -r /dev/stdin ]; \ |
217 | +then ln -s /proc/self/fd/0 /dev/stdin; fi", file=finish) |
218 | + print("if [ ! -r /dev/stdout ]; \ |
219 | +then ln -s /proc/self/fd/1 /dev/stdout; fi", file=finish) |
220 | + print("if [ ! -r /dev/stderr ]; \ |
221 | +then ln -s /proc/self/fd/2 /dev/stderr; fi", file=finish) |
222 | + print("# Clean up", file=finish) |
223 | + print("rm /finish.sh", file=finish) |
224 | + print("apt-get clean", file=finish) |
225 | + os.chmod(finish_script, stat.S_IEXEC) |
226 | + command = ["/finish.sh"] |
227 | + self.maint(*command) |
228 | + |
229 | + def run(self, *args): |
230 | + if not self.exists(): |
231 | + raise ClickChrootException( |
232 | + "Chroot %s does not exist" % self.full_name) |
233 | + command = ["schroot", "-c", self.full_name, "--directory=/", "--"] |
234 | + command.extend(args) |
235 | + subprocess.check_call(command) |
236 | + |
237 | + def maint(self, *args): |
238 | + command = [ |
239 | + # directory is a workaround for the click or user's home directory |
240 | + # not existing |
241 | + "schroot", "-c", "source:%s" % self.full_name, |
242 | + "-u", "root", "--directory=/", "--", |
243 | + ] |
244 | + command.extend(args) |
245 | + subprocess.check_call(command) |
246 | + |
247 | + def install(self, *pkgs): |
248 | + if not self.exists(): |
249 | + raise ClickChrootException( |
250 | + "Chroot %s does not exist" % self.full_name) |
251 | + self.update() |
252 | + command = ["apt-get", "install", "--yes"] |
253 | + command.extend(pkgs) |
254 | + self.maint(*command) |
255 | + self.clean() |
256 | + |
257 | + def clean(self): |
258 | + command = ["apt-get", "clean"] |
259 | + self.maint(*command) |
260 | + |
261 | + def update(self): |
262 | + command = ["apt-get", "update", "--yes"] |
263 | + self.maint(*command) |
264 | + |
265 | + def upgrade(self): |
266 | + if not self.exists(): |
267 | + raise ClickChrootException( |
268 | + "Chroot %s does not exist" % self.full_name) |
269 | + self.update() |
270 | + command = ["apt-get", "dist-upgrade", "--yes"] |
271 | + self.maint(*command) |
272 | + self.clean() |
273 | + |
274 | + def destroy(self): |
275 | + if not self.exists(): |
276 | + raise ClickChrootException( |
277 | + "Chroot %s does not exist" % self.full_name) |
278 | + chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name |
279 | + os.remove(chroot_config) |
280 | + mount = "%s/%s" % (self.chroots_dir, self.full_name) |
281 | + shutil.rmtree(mount) |
282 | |
283 | === added file 'click/commands/chroot.py' |
284 | --- click/commands/chroot.py 1970-01-01 00:00:00 +0000 |
285 | +++ click/commands/chroot.py 2013-10-07 20:57:16 +0000 |
286 | @@ -0,0 +1,91 @@ |
287 | +#! /usr/bin/python3 |
288 | + |
289 | +# Copyright (C) 2013 Canonical Ltd. |
290 | +# Author: Brian Murray <brian@ubuntu.com> |
291 | + |
292 | +# This program is free software: you can redistribute it and/or modify |
293 | +# it under the terms of the GNU General Public License as published by |
294 | +# the Free Software Foundation; version 3 of the License. |
295 | +# |
296 | +# This program is distributed in the hope that it will be useful, |
297 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
298 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
299 | +# GNU General Public License for more details. |
300 | +# |
301 | +# You should have received a copy of the GNU General Public License |
302 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
303 | + |
304 | +"""Use and manage a Click chroot.""" |
305 | + |
306 | +from __future__ import print_function |
307 | + |
308 | +from argparse import ArgumentParser, REMAINDER |
309 | + |
310 | +from click.chroot import ClickChroot |
311 | + |
312 | + |
313 | +def create(args): |
314 | + ClickChroot("saucy", args.architecture, |
315 | + "ubuntu-sdk-13.10").create() |
316 | + |
317 | + |
318 | +def install(args): |
319 | + packages = args.packages |
320 | + ClickChroot("saucy", args.architecture, |
321 | + "ubuntu-sdk-13.10").install(*packages) |
322 | + |
323 | + |
324 | +def destroy(args): |
325 | + # ask for confirmation? |
326 | + ClickChroot("saucy", args.architecture, |
327 | + "ubuntu-sdk-13.10").destroy() |
328 | + |
329 | + |
330 | +def execute(args): |
331 | + # not sure what to do wrt setting up the env |
332 | + program = args.program |
333 | + ClickChroot("saucy", args.architecture, |
334 | + "ubuntu-sdk-13.10").run(*program) |
335 | + |
336 | + |
337 | +def upgrade(args): |
338 | + ClickChroot("saucy", args.architecture, |
339 | + "ubuntu-sdk-13.10").upgrade() |
340 | + |
341 | + |
342 | +def run(argv): |
343 | + parser = ArgumentParser("click chroot") |
344 | + subparsers = parser.add_subparsers( |
345 | + description="management subcommands", |
346 | + help="valid commands") |
347 | + parser.add_argument( |
348 | + "-a", "--architecture", required=True, |
349 | + help="architecture for the chroot") |
350 | + create_parser = subparsers.add_parser( |
351 | + "create", |
352 | + help="create a chroot of the provided architecture") |
353 | + create_parser.set_defaults(func=create) |
354 | + destroy_parser = subparsers.add_parser( |
355 | + "destroy", |
356 | + help="destroy the chroot") |
357 | + destroy_parser.set_defaults(func=destroy) |
358 | + upgrade_parser = subparsers.add_parser( |
359 | + "upgrade", |
360 | + help="upgrade the chroot") |
361 | + upgrade_parser.set_defaults(func=upgrade) |
362 | + install_parser = subparsers.add_parser( |
363 | + "install", |
364 | + help="install packages in the chroot") |
365 | + install_parser.add_argument( |
366 | + "packages", nargs="+", |
367 | + help="packages to install") |
368 | + install_parser.set_defaults(func=install) |
369 | + execute_parser = subparsers.add_parser( |
370 | + "run", |
371 | + help="run a program in the chroot") |
372 | + execute_parser.add_argument( |
373 | + "program", nargs=REMAINDER, |
374 | + help="program to run with arguments") |
375 | + execute_parser.set_defaults(func=execute) |
376 | + args = parser.parse_args(argv) |
377 | + args.func(args) |
Thanks! I'm looking over this; I won't finish until tomorrow. In the meantime, do you think that instead of this pattern:
subprocess. check_output( command) .decode( 'utf-8' )
... you could use this pattern:
subprocess. check_output( command, universal_ newlines= True)
I use this elsewhere because it tends to be a better match to other Python 2 code (click still supports Python 2.7).