From 357dbf3ac4105a69a482532de2f245aeb7e0216b Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sun, 29 Jan 2006 15:12:03 +0000 Subject: [PATCH] Added --archive-url, --archive-file and --archive-extract. This adds an element to an whose ID matches the archive's contents. If there is only a local interface, its ID is changed to the digest of the archive. git-svn-id: file:///home/talex/Backups/sf.net/Subversion/zero-install/trunk/0publish@655 9f8c893c-44ee-0310-b757-c8ca8341c71e --- 0publish | 11 ++++--- archive.py | 62 ++++++++++++++++++++++++++++++++++ unpack.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 archive.py create mode 100644 unpack.py diff --git a/0publish b/0publish index fd82d4e..78a6aba 100755 --- a/0publish +++ b/0publish @@ -9,7 +9,9 @@ import edit, validator version = '0.1' parser = OptionParser(usage="usage: %prog [options] interface") -parser.add_option("-a", "--archive", help="add archive and set ID", action='append', metavar='URL') +parser.add_option("--archive-url", help="add archive at this URL", action='store', metavar='URL') +parser.add_option("--archive-file", help="local copy of archive-url", action='store', metavar='FILE') +parser.add_option("--archive-extract", help="subdirectory of archive to extract", action='store', metavar='DIR') parser.add_option("-k", "--key", help="key to use for signing") parser.add_option("-e", "--edit", help="edit with $EDITOR", action='store_true') parser.add_option("-l", "--local", help="create feed from local interface") @@ -112,10 +114,11 @@ if options.set_id or options.set_version or options.set_released or options.set_ import release data = release.make_release(data, options.set_id, options.set_version, options.set_released, options.set_stability) -if options.archive: +if options.archive_url: import archive - for a in options.archive: - data = archive.add_archive(data, a) + data = archive.add_archive(data, options.archive_url, options.archive_file, options.archive_extract) +elif options.archive_file or options.archive_extract: + raise Exception('Must use --archive-uri option') if options.local: import merge data = merge.merge(data, options.local) diff --git a/archive.py b/archive.py new file mode 100644 index 0000000..609b71e --- /dev/null +++ b/archive.py @@ -0,0 +1,62 @@ +from xml.dom import minidom +from zeroinstall.zerostore import Store, manifest +from zeroinstall.injector import namespaces +import os, time, re, shutil, tempfile, sha +import unpack + +def manifest_for_dir(dir): + digest = sha.new() + for line in manifest.generate_manifest(dir): + digest.update(line + '\n') + return 'sha1=' + digest.hexdigest() + +def add_archive(data, url, local_file, extract): + if local_file is None: + raise Exception('Use --archive-file option to specify a local copy') + + doc = minidom.parseString(data) + + all_impls = doc.documentElement.getElementsByTagNameNS(namespaces.XMLNS_IFACE, 'implementation') + tmpdir = tempfile.mkdtemp('-0publish') + try: + unpack.unpack_archive(url, file(local_file), tmpdir, extract) + if extract: + extracted = os.path.join(tmpdir, extract) + else: + extracted = tmpdir + + archive_id = manifest_for_dir(extracted) + finally: + shutil.rmtree(tmpdir) + + local_ifaces = [] + for impl in all_impls: + this_id = impl.getAttribute('id') + if this_id == archive_id: + break + if this_id.startswith('/') or this_id.startswith('.'): + local_ifaces.append(impl) + else: + if len(local_ifaces) == 0: + raise Exception('Nothing with id "%s", and no local implementations' % archive_id) + if len(local_ifaces) > 1: + raise Exception('Nothing with id "%s", and multiple local implementations!' % archive_id) + impl = local_ifaces[0] + impl.setAttribute('id', archive_id) + + assert impl.getAttribute('id') == archive_id + + nl = doc.createTextNode('\n ') + impl.appendChild(nl) + + archive = doc.createElementNS(namespaces.XMLNS_IFACE, 'archive') + impl.appendChild(archive) + archive.setAttribute('href', url) + archive.setAttribute('size', str(os.stat(local_file).st_size)) + if extract is not None: + archive.setAttribute('extract', extract) + + nl = doc.createTextNode('\n ') + impl.appendChild(nl) + + return doc.toxml() diff --git a/unpack.py b/unpack.py new file mode 100644 index 0000000..80a1118 --- /dev/null +++ b/unpack.py @@ -0,0 +1,110 @@ +# This is a copy of the new, unreleased, version in 0launch. Once the new +# version is out, we can drop this copy. + +import os +import shutil +import traceback +from tempfile import mkdtemp, mkstemp +import sha +import re +from logging import debug, info, warn +from zeroinstall import SafeException + +_recent_gnu_tar = None +def recent_gnu_tar(): + global _recent_gnu_tar + if _recent_gnu_tar is None: + _recent_gnu_tar = False + version = os.popen('tar --version 2>&1').next() + if '(GNU tar)' in version: + try: + version = version.split(')', 1)[1].strip() + assert version + version = map(int, version.split('.')) + _recent_gnu_tar = version > [1, 13, 92] + except: + warn("Failed to extract GNU tar version number") + debug("Recent GNU tar = %s", _recent_gnu_tar) + return _recent_gnu_tar + +def unpack_archive(url, data, destdir, extract = None): + """Unpack stream 'data' into directory 'destdir'. If extract is given, extract just + that sub-directory from the archive. Works out the format from the name.""" + url = url.lower() + if url.endswith('.tar.bz2'): + extract_tar(data, destdir, extract, '--bzip2') + elif url.endswith('.rpm'): + extract_rpm(data, destdir, extract) + elif url.endswith('.tar.gz') or url.endswith('.tgz'): + extract_tar(data, destdir, extract, '-z') + else: + raise SafeException('Unknown extension on "%s"; I only know .tgz, .tar.bz2 and .rpm' % url) + +def extract_rpm(stream, destdir, extract = None): + if extract: + raise SafeException('Sorry, but the "extract" attribute is not yet supported for RPMs') + fd, cpiopath = mkstemp('-rpm-tmp') + try: + child = os.fork() + if child == 0: + try: + try: + os.dup2(stream.fileno(), 0) + os.dup2(fd, 1) + os.execlp('rpm2cpio', 'rpm2cpio', '-') + except: + traceback.print_exc() + finally: + os._exit(1) + id, status = os.waitpid(child, 0) + assert id == child + if status != 0: + raise SafeException("rpm2cpio failed; can't unpack RPM archive; exit code %d" % status) + os.close(fd) + fd = None + args = ['cpio', '-mid', '--quiet'] + _extract(file(cpiopath), destdir, args) + # Set the mtime of every directory under 'tmp' to 0, since cpio doesn't + # preserve directory mtimes. + os.path.walk(destdir, lambda arg, dirname, names: os.utime(dirname, (0, 0)), None) + finally: + if fd is not None: + os.close(fd) + os.unlink(cpiopath) + +def extract_tar(stream, destdir, extract, decompress): + if extract: + # Limit the characters we accept, to avoid sending dodgy + # strings to tar + if not re.match('^[a-zA-Z0-9][- _a-zA-Z0-9.]*$', extract): + raise SafeException('Illegal character in extract attribute') + + if recent_gnu_tar(): + args = ['tar', decompress, '-x', '--no-same-owner', '--no-same-permissions'] + else: + args = ['tar', decompress, '-xf', '-'] + + if extract: + args.append(extract) + + _extract(stream, destdir, args) + +def _extract(stream, destdir, command): + """Run execvp('command') inside destdir in a child process, with a + rewound stream as stdin.""" + child = os.fork() + if child == 0: + try: + try: + os.chdir(destdir) + stream.seek(0) + os.dup2(stream.fileno(), 0) + os.execvp(command[0], command) + except: + traceback.print_exc() + finally: + os._exit(1) + id, status = os.waitpid(child, 0) + assert id == child + if status != 0: + raise SafeException('Failed to extract archive; exit code %d' % status) -- 2.11.4.GIT