Added <needs-terminal> to validator.
[0publish.git] / unpack.py
blob09e9c6b0fd59623677977477b7504c13c10bee6f
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
3 # Copied from the injector, as versions before 0.18 don't have it.
5 import os
6 import shutil
7 import traceback
8 from tempfile import mkdtemp, mkstemp
9 import sha
10 import re
11 from logging import debug, info, warn
12 from zeroinstall import SafeException
14 _recent_gnu_tar = None
15 def recent_gnu_tar():
16 global _recent_gnu_tar
17 if _recent_gnu_tar is None:
18 _recent_gnu_tar = False
19 version = os.popen('tar --version 2>&1').next()
20 if '(GNU tar)' in version:
21 try:
22 version = version.split(')', 1)[1].strip()
23 assert version
24 version = map(int, version.split('.'))
25 _recent_gnu_tar = version > [1, 13, 92]
26 except:
27 warn("Failed to extract GNU tar version number")
28 debug("Recent GNU tar = %s", _recent_gnu_tar)
29 return _recent_gnu_tar
31 def _find_in_path(prog):
32 for d in os.environ['PATH'].split(':'):
33 path = os.path.join(d, prog)
34 if os.path.isfile(path):
35 return path
36 return None
37 _pola_run = _find_in_path('pola-run')
38 if _pola_run:
39 info('Found pola-run: %s', _pola_run)
40 else:
41 info('pola-run not found; archive extraction will not be sandboxed')
43 def _exec_maybe_sandboxed(writable, prog, *args):
44 """execlp prog, with (only) the 'writable' directory writable if sandboxing is available.
45 If no sandbox is available, run without a sandbox."""
46 prog_path = _find_in_path(prog)
47 if _pola_run is None:
48 os.execlp(prog_path, prog_path, *args)
49 # We have pola-shell :-)
50 pola_args = ['--prog', prog_path, '-f', '/']
51 for a in args:
52 pola_args += ['-a', a]
53 if writable:
54 pola_args += ['-fw', writable]
55 os.execl(_pola_run, _pola_run, *pola_args)
57 def unpack_archive(url, data, destdir, extract = None):
58 """Unpack stream 'data' into directory 'destdir'. If extract is given, extract just
59 that sub-directory from the archive. Works out the format from the name."""
60 url = url.lower()
61 if url.endswith('.tar.bz2'):
62 extract_tar(data, destdir, extract, '--bzip2')
63 elif url.endswith('.deb'):
64 extract_deb(data, destdir, extract)
65 elif url.endswith('.rpm'):
66 extract_rpm(data, destdir, extract)
67 elif url.endswith('.tar.gz') or url.endswith('.tgz'):
68 extract_tar(data, destdir, extract, '-z')
69 else:
70 raise SafeException('Unknown extension on "%s"; I only know .tgz, .tar.bz2 and .rpm' % url)
72 def extract_deb(stream, destdir, extract = None):
73 if extract:
74 raise SafeException('Sorry, but the "extract" attribute is not yet supported for Debs')
76 stream.seek(0)
77 # ar can't read from stdin, so make a copy...
78 deb_copy_name = os.path.join(destdir, 'archive.deb')
79 deb_copy = file(deb_copy_name, 'w')
80 shutil.copyfileobj(stream, deb_copy)
81 deb_copy.close()
82 _extract(stream, destdir, ('ar', 'x', 'archive.deb', 'data.tar.gz'))
83 os.unlink(deb_copy_name)
84 data_name = os.path.join(destdir, 'data.tar.gz')
85 data_stream = file(data_name)
86 os.unlink(data_name)
87 _extract(data_stream, destdir, ('tar', 'xzf', '-'))
89 def extract_rpm(stream, destdir, extract = None):
90 if extract:
91 raise SafeException('Sorry, but the "extract" attribute is not yet supported for RPMs')
92 fd, cpiopath = mkstemp('-rpm-tmp')
93 try:
94 child = os.fork()
95 if child == 0:
96 try:
97 try:
98 os.dup2(stream.fileno(), 0)
99 os.dup2(fd, 1)
100 _exec_maybe_sandboxed(None, 'rpm2cpio', '-')
101 except:
102 traceback.print_exc()
103 finally:
104 os._exit(1)
105 id, status = os.waitpid(child, 0)
106 assert id == child
107 if status != 0:
108 raise SafeException("rpm2cpio failed; can't unpack RPM archive; exit code %d" % status)
109 os.close(fd)
110 fd = None
111 args = ['cpio', '-mid', '--quiet']
112 _extract(file(cpiopath), destdir, args)
113 # Set the mtime of every directory under 'tmp' to 0, since cpio doesn't
114 # preserve directory mtimes.
115 os.path.walk(destdir, lambda arg, dirname, names: os.utime(dirname, (0, 0)), None)
116 finally:
117 if fd is not None:
118 os.close(fd)
119 os.unlink(cpiopath)
121 def extract_tar(stream, destdir, extract, decompress):
122 if extract:
123 # Limit the characters we accept, to avoid sending dodgy
124 # strings to tar
125 if not re.match('^[a-zA-Z0-9][- _a-zA-Z0-9.]*$', extract):
126 raise SafeException('Illegal character in extract attribute')
128 if recent_gnu_tar():
129 args = ['tar', decompress, '-x', '--no-same-owner', '--no-same-permissions']
130 else:
131 args = ['tar', decompress, '-xf', '-']
133 if extract:
134 args.append(extract)
136 _extract(stream, destdir, args)
138 def _extract(stream, destdir, command):
139 """Run execvp('command') inside destdir in a child process, with a
140 rewound stream as stdin."""
141 child = os.fork()
142 if child == 0:
143 try:
144 try:
145 os.chdir(destdir)
146 stream.seek(0)
147 os.dup2(stream.fileno(), 0)
148 _exec_maybe_sandboxed(destdir, *command)
149 except:
150 traceback.print_exc()
151 finally:
152 os._exit(1)
153 id, status = os.waitpid(child, 0)
154 assert id == child
155 if status != 0:
156 raise SafeException('Failed to extract archive; exit code %d' % status)