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