rename checksums-file to -5
[deb2zero/deb2zero-afb.git] / pkg2zero
blob30807adc9ce3fab9ad63eb6c1a8fbafd2c08f8f2
1 #!/usr/bin/env python
2 # Copyright (C) 2009, Thomas Leonard
3 # Copyright (C) 2008, Anders F Bjorklund
4 # See the COPYING file for details, or visit http://0install.net.
6 import sys, time
7 from optparse import OptionParser
8 import tempfile, shutil, os
9 from xml.dom import minidom
10 import gzip
11 import re
12 import tarfile
13 try:
14 import xml.etree.cElementTree as ET # Python 2.5
15 except ImportError:
16 try:
17 import xml.etree.ElementTree as ET
18 except ImportError:
19 try:
20 import cElementTree as ET # http://effbot.org
21 except ImportError:
22 import elementtree.ElementTree as ET
24 import subprocess
25 try:
26 from subprocess import check_call
27 except ImportError:
28 def check_call(*popenargs, **kwargs):
29 rc = subprocess.call(*popenargs, **kwargs)
30 if rc != 0: raise OSError, rc
32 from zeroinstall.injector import model, qdom, distro
33 from zeroinstall.zerostore import unpack
35 from support import read_child, add_node, Mappings
37 manifest_algorithm = 'sha1new'
39 deb_category_to_freedesktop = {
40 'devel' : 'Development',
41 'web' : 'Network',
42 'graphics' : 'Graphics',
43 'games' : 'Game',
46 rpm_group_to_freedesktop = {
47 'Development/Libraries' : 'Development',
50 rpm_license_to_trove = {
51 'BSD' : "OSI Approved :: BSD License (revised)",
52 'MIT' : "OSI Approved :: MIT/X Consortium License",
53 'GPLv2' : "OSI Approved :: GNU General Public License v2",
54 'GPLv2+' : "OSI Approved :: GNU General Public License v2",
55 'GPLv3' : "OSI Approved :: GNU General Public License v3",
56 'GPLv3+' : "OSI Approved :: GNU General Public License v3",
57 'LGPLv2' : "OSI Approved :: GNU Lesser General Public License (LGPL)",
58 'LGPLv2+' : "OSI Approved :: GNU Lesser General Public License (LGPL)",
59 'GFDL' : "OSI Approved :: GNU Free Documentation License (FDL)",
60 'Python' : "OSI Approved :: Python License",
63 slack_series_to_freedesktop = {
64 'd' : 'Development',
67 valid_categories = [
68 'AudioVideo',
69 'Audio',
70 'Video',
71 'Development',
72 'Education',
73 'Game',
74 'Graphics',
75 'Network',
76 'Office',
77 'Settings',
78 'System',
79 'Utility',
82 # Parse command-line arguments
84 parser = OptionParser('usage: %prog [options] http://.../package.deb [target-feed.xml]\n'
85 ' %prog [options] http://.../package.rpm [target-feed.xml]\n'
86 ' %prog [options] http://.../package.txz [target-feed.xml]\n'
87 ' %prog [options] package-name [target-feed.xml]\n'
88 'Publish a Debian, RPM, or Slack package in a Zero Install feed.\n'
89 "target-feed.xml is created if it doesn't already exist.")
90 parser.add_option("-a", "--archive-url", help="archive to use as the package contents")
91 parser.add_option("", "--archive-extract", help="only extract files under this subdirectory")
92 parser.add_option("", "--license", help="value for 'license' attribute")
93 parser.add_option("-r", "--repomd-file", help="repository metadata file")
94 parser.add_option("", "--path", help="location of packages [5/os/i386]")
95 parser.add_option("-p", "--packages-file", help="Debian/Slack package index file")
96 parser.add_option("-5", "--checksums-file", help="Slack package MD5 checksum file")
97 parser.add_option("-m", "--mirror", help="location of packages [http://ftp.debian.org/debian] or [http://mirror.centos.org/centos]")
98 parser.add_option("-k", "--key", help="key to use for signing")
99 (options, args) = parser.parse_args()
101 if len(args) < 1 or len(args) > 2:
102 parser.print_help()
103 sys.exit(1)
105 # Load dependency mappings
106 mappings = Mappings()
108 class Package:
109 name = '(unknown)'
110 version = None
111 arch = None
112 category = None
113 homepage = None
114 buildtime = None
115 license = None
117 def __init__(self):
118 self.requires = []
120 class DebRepo:
121 def __init__(self, options):
122 self.packages_base_url = (options.mirror or 'http://ftp.debian.org/debian') + '/'
123 self.packages_file = options.packages_file or 'Packages'
125 def get_repo_metadata(self, pkg_name):
126 if not os.path.isfile(self.packages_file):
127 print >>sys.stderr, ("File '%s' not found (use -p to give its location).\n"
128 "Either download one (e.g. ftp://ftp.debian.org/debian/dists/stable/main/binary-amd64/Packages.bz2),\n"
129 "or specify the full URL of the .deb package to use.") % self.packages_file
130 sys.exit(1)
131 if self.packages_file.endswith('.bz2'):
132 import bz2
133 opener = bz2.BZ2File
134 else:
135 opener = open
136 pkg_data = "\n" + opener(self.packages_file).read()
137 try:
138 i = pkg_data.index('\nPackage: %s\n' % pkg_name)
139 except ValueError:
140 raise Exception("Package '%s' not found in Packages file '%s'." % (pkg_name, self.packages_file))
141 j = pkg_data.find('\n\n', i)
142 if j == -1:
143 pkg_info = pkg_data[i:]
144 else:
145 pkg_info = pkg_data[i:j]
146 filename = None
147 digest = {}
148 for line in pkg_info.split('\n'):
149 if ':' in line and not line.startswith(' '):
150 key, value = line.split(':', 1)
151 if key == 'Filename':
152 filename = value.strip()
153 elif key in ('SHA1', 'SHA256'):
154 digest[key.lower()] = value.strip()
155 if filename is None:
156 raise Exception('Filename: field not found in package data:\n' + pkg_info)
157 pkg_url = self.packages_base_url + filename
159 return pkg_url, digest
161 def get_package_metadata(self, pkg_file):
162 package = Package()
164 details = read_child(['dpkg-deb', '--info', pkg_file])
166 description_and_summary = details.split('\n Description: ')[1].split('\n')
167 package.summary = description_and_summary[0]
168 description = ''
169 for x in description_and_summary[1:]:
170 if not x: continue
171 assert x[0] == ' '
172 x = x[1:]
173 if x[0] != ' ':
174 break
175 if x == ' .':
176 description += '\n'
177 else:
178 description += x[1:].replace('. ', '. ') + '\n'
179 package.description = description.strip()
181 for line in details.split('\n'):
182 if not line: continue
183 assert line.startswith(' ')
184 line = line[1:]
185 if ':' in line:
186 key, value = line.split(':', 1)
187 value = value.strip()
188 if key == 'Section':
189 package.category = deb_category_to_freedesktop.get(value)
190 if not package.category:
191 if value != 'libs':
192 print >>sys.stderr, "Warning: no mapping for Debian category '%s'" % value
193 elif key == 'Package':
194 package.name = value
195 elif key == 'Version':
196 value = value.replace('cvs', '')
197 value = value.replace('svn', '')
198 if ':' in value: value = value.split(':', 1)[1]
199 package.version = distro.try_cleanup_distro_version(value)
200 elif key == 'Architecture':
201 if '-' in value:
202 arch, value = value.split('-', 1)
203 else:
204 arch = 'linux'
205 if value == 'amd64':
206 value = 'x86_64'
207 elif value == 'all':
208 value = '*'
209 package.arch = arch.capitalize() + '-' + value
210 elif key == 'Depends':
211 for x in value.split(','):
212 req = mappings.process(x)
213 if req:
214 package.requires.append(req)
215 return package
217 class RPMRepo:
218 def __init__(self, options):
219 self.packages_base_url = (options.mirror or 'http://mirror.centos.org/centos') + '/'
220 self.packages_base_dir = (options.path or '5/os/i386') + '/'
221 self.repomd_file = options.repomd_file or 'repodata/repomd.xml'
222 if not os.path.isfile(self.repomd_file):
223 print >>sys.stderr, ("File '%s' not found (use -r to give its location).\n"
224 "Either download one (e.g. http://mirror.centos.org/centos/5/os/i386/repodata/repomd.xml),\n"
225 "or specify the full URL of the .rpm package to use.") % self.repomd_file
226 sys.exit(1)
228 def get_repo_metadata(self, pkg_name):
229 primary_file = None
230 repomd = minidom.parse(self.repomd_file)
231 repo_top = os.path.dirname(os.path.dirname(self.repomd_file))
232 for data in repomd.getElementsByTagName("data"):
233 if data.attributes["type"].nodeValue == "primary":
234 for node in data.getElementsByTagName("location"):
235 primary_file = os.path.join(repo_top, node.attributes["href"].nodeValue)
236 location = None
237 primary = ET.parse(gzip.open(primary_file))
238 NS = "http://linux.duke.edu/metadata/common"
239 metadata = primary.getroot()
240 pkg_data = None
241 for package in metadata.findall("{%s}package" % NS):
242 if package.find("{%s}name" % NS).text == pkg_name:
243 pkg_data = package
244 location = pkg_data.find("{%s}location" % NS).get("href")
245 break
246 if pkg_data is None:
247 raise Exception("Package '%s' not found in repodata." % pkg_name)
248 checksum = pkg_data.find("{%s}checksum" % NS)
249 digest = {}
250 if checksum.get("type") == "sha":
251 digest["sha1"] = checksum.text
252 if checksum.get("type") == "sha256":
253 digest["sha256"] = checksum.text
254 if location is None:
255 raise Exception('location tag not found in primary metadata:\n' + primary_file)
256 pkg_url = self.packages_base_url + self.packages_base_dir + location
258 return pkg_url, digest
260 def get_package_metadata(self, pkg_file):
261 package = Package()
263 query_format = '%{SUMMARY}\\a%{DESCRIPTION}\\a%{NAME}\\a%{VERSION}\\a%{OS}\\a%{ARCH}\\a%{URL}\\a%{GROUP}\\a%{LICENSE}\\a%{BUILDTIME}\\a[%{REQUIRES}\\n]'
264 headers = read_child(['rpm', '--qf', query_format, '-qp', pkg_file]).split('\a')
266 package.summary = headers[0].strip()
267 package.description = headers[1].strip()
269 package.name = headers[2]
270 value = headers[3]
271 value = value.replace('cvs', '')
272 value = value.replace('svn', '')
273 value = distro.try_cleanup_distro_version(value)
274 package.version = value
275 value = headers[4]
276 package.arch = value.capitalize()
277 value = headers[5]
278 if value == 'amd64':
279 value = 'x86_64'
280 if value == 'noarch':
281 value = '*'
282 package.arch += '-' + value
283 value = headers[6].strip()
284 package.page = value
285 category = None
286 value = headers[7].strip()
287 package.category = rpm_group_to_freedesktop.get(value)
288 if not category:
289 print >>sys.stderr, "Warning: no mapping for RPM group '%s'" % value
291 value = headers[8].strip()
292 package.license = rpm_license_to_trove.get(value, value)
293 value = headers[9].strip()
294 package.buildtime = long(value)
295 value = headers[10].strip()
296 for x in value.split('\n'):
297 if x.startswith('rpmlib'):
298 continue
299 req = mappings.process(x)
300 if req:
301 package.requires.append(req)
302 return package
304 class SlackRepo:
305 def __init__(self, options):
306 self.packages_base_url = (options.mirror or 'ftp://ftp.slackware.com/pub/slackware') + '/'
307 self.packages_base_dir = (options.path or 'slackware-current') + '/'
308 self.packages_file = options.packages_file or 'PACKAGES.TXT'
309 self.checksums_file = options.checksums_file or 'CHECKSUMS.md5'
311 NAMERE = re.compile("^(.+?)-([^-]+-[^-]+-[^-]+?)(.t[gblx]z)?$")
313 def get_package_name(self, pkg_file):
314 m = self.NAMERE.match(pkg_file)
315 if m:
316 return m.groups()[0]
317 else:
318 return None
320 def get_repo_metadata(self, pkg_name):
321 if not os.path.isfile(self.packages_file):
322 print >>sys.stderr, ("File '%s' not found (use -p to give its location).\n"
323 "Either download one (e.g. ftp://ftp.slackware.com/pub/slackware/slackware-current/PACKAGES.TXT),\n"
324 "or specify the full URL of the .txz package to use.") % self.packages_file
325 sys.exit(1)
326 if self.packages_file.endswith('.gz'):
327 import gzip
328 opener = gzip.GZipFile
329 else:
330 opener = open
331 pkg_data = "\n" + opener(self.packages_file).read()
332 try:
333 while True:
334 i = pkg_data.index('\nPACKAGE NAME: %s-' % pkg_name)
335 j = pkg_data.find('\n\n', i)
336 if j == -1:
337 pkg_info = pkg_data[i:]
338 else:
339 pkg_info = pkg_data[i:j]
340 name = self.get_package_name(pkg_info[16:].split('\n')[0])
341 if name != pkg_name:
342 pkg_data = pkg_data[j:]
343 continue
344 break
345 except ValueError:
346 raise Exception("Package '%s' not found in Packages file '%s'." % (pkg_name, self.packages_file))
347 name = None
348 location = None
349 digest = {}
350 description = ""
351 for line in pkg_info.split('\n'):
352 if ':' in line:
353 if line.startswith(pkg_name + ': '):
354 description += line.replace(pkg_name + ': ', "")
355 else:
356 key, value = line.split(':', 1)
357 if key == "PACKAGE NAME":
358 name = value.strip()
359 if key == "PACKAGE LOCATION":
360 location = value.strip()
361 if name is None or location is None:
362 raise Exception('Package filename not found in package data:\n' + pkg_info)
363 pkg_file = os.path.join(location, name)
364 if location.startswith('./'):
365 location = location.replace('./', "")
366 pkg_url = self.packages_base_url + self.packages_base_dir + location + '/' + name
367 if os.path.exists(self.checksums_file):
368 checksums = open(self.checksums_file)
369 for line in checksums:
370 if line.startswith("MD5 message digest"):
371 break
372 for line in checksums:
373 digest, filename = line.strip().split(None, 1)
374 if filename == pkg_file:
375 break
376 if digest:
377 digest = {'md5': digest}
379 return pkg_url, digest
381 def get_package_metadata(self, pkg_file):
382 package = Package()
384 if pkg_file.endswith('.txz'):
385 fd, tmp_file = tempfile.mkstemp(suffix='.tar')
386 check_call(['cp', pkg_file, (tmp_file + '.xz')])
387 check_call(['rm', tmp_file])
388 check_call(['unxz', (tmp_file + '.xz')])
389 pkg_info = tarfile.open(tmp_file)
390 else:
391 tmp_file = None
392 pkg_info = tarfile.open(pkg_file)
393 slack_desc = pkg_info.extractfile('install/slack-desc')
394 pkg_name = self.get_package_name(os.path.basename(pkg_file))
395 description = ""
396 for line in slack_desc:
397 if line.startswith(pkg_name + ': '):
398 description += line.replace(pkg_name + ': ', "")
399 package.summary, package.description = description.split('\n', 1)
400 for line in description:
401 if line.startswith("Homepage: "):
402 package.homepage = line[10:].strip()
403 m = self.NAMERE.match(os.path.basename(pkg_file))
404 package.name = m.group(1)
405 version = m.group(2).split('-')
406 package.version = "%s-%s" % (version[0], version[2])
407 package.arch = "%s-%s" % ("Linux", version[1])
408 if tmp_file:
409 os.unlink(tmp_file)
410 return package
412 if args[0].endswith('.txz') or options.checksums_file:
413 repo = SlackRepo(options)
414 dist = 'Slack'
415 elif args[0].endswith('.deb') or options.packages_file:
416 repo = DebRepo(options)
417 dist = 'Debian'
418 elif args[0].endswith('.rpm') or options.repomd_file:
419 repo = RPMRepo(options)
420 dist = 'RPM'
421 else:
422 print >>sys.stderr, "Use --packages-file for Debian, or --repomd-file for RPM"
423 sys.exit(1)
425 pkg_data = None
427 if options.archive_url:
428 pkg_url = None
429 pkg_file = os.path.abspath(args[0])
430 archive_url = options.archive_url
431 archive_file = os.path.abspath(archive_url.rsplit('/', 1)[1])
432 digest = {}
433 assert os.path.exists(pkg_file), ("%s doesn't exist!" % pkg_file)
434 else:
435 scheme = args[0].split(':', 1)[0]
436 if scheme in ('http', 'https', 'ftp'):
437 archive_url = args[0]
438 digest = {}
439 else:
440 archive_url, digest = repo.get_repo_metadata(args[0])
441 archive_file = os.path.abspath(archive_url.rsplit('/', 1)[1])
442 pkg_url = archive_url
443 pkg_file = archive_file
445 # pkg_url, pkg_archive = .deb or .rpm with the metadata
446 # archive_url, archive_file = .dep, .rpm or .tar.bz2 with the contents
448 # Often pkg == archive, but sometimes it's useful to convert packages to tarballs
449 # so people don't need special tools to extract them.
452 # Download package, if required
454 if not os.path.exists(pkg_file):
455 print >>sys.stderr, "File '%s' not found, so downloading from %s..." % (pkg_file, pkg_url)
456 if os.path.exists('/usr/bin/wget'):
457 check_call(['wget', pkg_url])
458 else:
459 check_call(['curl', '-LO', pkg_url])
461 # Check digest, if known
463 if "sha256" in digest:
464 import hashlib
465 m = hashlib.new('sha256')
466 expected_digest = digest["sha256"]
467 elif "sha1" in digest:
468 try:
469 import hashlib
470 m = hashlib.new('sha1')
471 except ImportError:
472 import sha
473 m = sha.new()
474 expected_digest = digest["sha1"]
475 elif "md5" in digest:
476 try:
477 import hashlib
478 m = hashlib.new('md5')
479 except ImportError:
480 import md5
481 m = md5.new()
482 expected_digest = digest["md5"]
483 else:
484 m = None
486 if m:
487 m.update(file(archive_file).read())
488 actual = m.hexdigest()
489 if actual != expected_digest:
490 raise Exception("Incorrect digest on package file! Was " + actual + ", but expected " + expected_digest)
491 else:
492 print "Package's digest matches value in reposistory metadata (" + actual + "). Good."
493 else:
494 print >>sys.stderr, "Note: no MD5, SHA-1 or SHA-256 digest known for this package, so not checking..."
496 # Extract meta-data from package
498 pkg_metadata = repo.get_package_metadata(pkg_file)
500 # Unpack package, find binaries and .desktop files, and add to cache
502 possible_mains = []
503 icondata = None
504 tmp = tempfile.mkdtemp(prefix = 'pkg2zero-')
505 try:
506 unpack_dir = tmp
507 unpack.unpack_archive(archive_file, open(archive_file), destdir = unpack_dir, extract = options.archive_extract)
508 if options.archive_extract:
509 unpack_dir = os.path.join(unpack_dir, options.archive_extract)
511 icon = None
512 images = {}
513 for root, dirs, files in os.walk(unpack_dir):
514 assert root.startswith(unpack_dir)
515 relative_root = root[len(unpack_dir) + 1:]
516 for name in files:
517 full = os.path.join(root, name)
518 f = os.path.join(relative_root, name)
519 print f
520 if f.endswith('.desktop'):
521 for line in file(full):
522 if line.startswith('Categories'):
523 for cat in line.split('=', 1)[1].split(';'):
524 cat = cat.strip()
525 if cat in valid_categories:
526 category = cat
527 break
528 elif line.startswith('Icon'):
529 icon = line.split('=', 1)[1].strip()
530 elif f.startswith('bin/') or f.startswith('usr/bin/') or f.startswith('usr/games/'):
531 if os.path.isfile(full):
532 possible_mains.append(f)
533 elif f.endswith('.png'):
534 images[f] = full
535 images[os.path.basename(f)] = full
536 # make sure to also map basename without the extension
537 images[os.path.splitext(os.path.basename(f))[0]] = full
539 icondata = None
540 if icon in images:
541 print "Using %s for icon" % os.path.basename(images[icon])
542 icondata = file(images[icon]).read()
544 manifest = read_child(['0store', 'manifest', unpack_dir, manifest_algorithm])
545 digest = manifest.rsplit('\n', 2)[1]
546 check_call(['0store', 'add', digest, unpack_dir])
547 finally:
548 shutil.rmtree(tmp)
550 if possible_mains:
551 possible_mains = sorted(possible_mains, key = len)
552 pkg_main = possible_mains[0]
553 if len(possible_mains) > 1:
554 print "Warning: several possible main binaries found:"
555 print "- " + pkg_main + " (I chose this one)"
556 for x in possible_mains[1:]:
557 print "- " + x
558 else:
559 pkg_main = None
561 # Make sure we haven't added this version already...
563 if len(args) > 1:
564 target_feed_file = args[1]
565 target_icon_file = args[1].replace('.xml', '.png')
566 else:
567 target_feed_file = pkg_metadata.name + '.xml'
568 target_icon_file = pkg_metadata.name + '.png'
570 feed_uri = None
571 icon_uri = None
572 if os.path.isfile(target_feed_file):
573 dom = qdom.parse(file(target_feed_file))
574 old_target_feed = model.ZeroInstallFeed(dom, local_path = target_feed_file)
575 existing_impl = old_target_feed.implementations.get(digest)
576 if existing_impl:
577 print >>sys.stderr, ("Feed '%s' already contains an implementation with this digest!\n%s" % (target_feed_file, existing_impl))
578 sys.exit(1)
579 else:
580 # No target, so need to pick a URI
581 feed_uri = mappings.lookup(pkg_metadata.name)
582 if feed_uri is None:
583 suggestion = mappings.get_suggestion(pkg_metadata.name)
584 uri = raw_input('Enter the URI for this feed [%s]: ' % suggestion).strip()
585 if not uri:
586 uri = suggestion
587 assert uri.startswith('http://') or uri.startswith('https://') or uri.startswith('ftp://'), uri
588 feed_uri = uri
589 mappings.add_mapping(pkg_metadata.name, uri)
591 if icondata and not os.path.isfile(target_icon_file):
592 file = open(target_icon_file, 'wb')
593 file.write(icondata)
594 file.close()
595 if icon_uri is None and os.path.exists(target_icon_file):
596 suggestion = 'http://0install.net/feed_icons/' + target_icon_file
597 uri = raw_input('Enter the URI for this icon [%s]: ' % suggestion).strip()
598 if not uri:
599 uri = suggestion
600 assert uri.startswith('http://') or uri.startswith('https://') or uri.startswith('ftp://'), uri
601 icon_uri = uri
603 # Create a local feed with just the new version...
605 template = '''<interface xmlns="http://zero-install.sourceforge.net/2004/injector/interface">
606 </interface>'''
607 doc = minidom.parseString(template)
608 root = doc.documentElement
610 add_node(root, 'name', pkg_metadata.name)
611 add_node(root, 'summary', pkg_metadata.summary)
612 add_node(root, 'description', pkg_metadata.description)
613 feed_for = add_node(root, 'feed-for', '')
614 if feed_uri:
615 feed_for.setAttribute('interface', feed_uri)
616 if icon_uri:
617 icon = add_node(root, 'icon')
618 icon.setAttribute('href', icon_uri)
619 icon.setAttribute('type', 'image/png')
620 if pkg_metadata.homepage:
621 add_node(root, 'homepage', pkg_metadata.homepage)
622 if pkg_metadata.category:
623 add_node(root, 'category', pkg_metadata.category)
625 package = add_node(root, 'package-implementation', '')
626 package.setAttribute('package', pkg_metadata.name)
627 package.setAttribute('distributions', dist)
629 group = add_node(root, 'group', '')
630 if pkg_metadata.arch:
631 group.setAttribute('arch', pkg_metadata.arch)
632 else:
633 print >>sys.stderr, "No Architecture: field in package"
634 if pkg_metadata.license:
635 group.setAttribute('license', pkg_metadata.license)
637 for req in pkg_metadata.requires:
638 req_element = add_node(group, 'requires', before = '\n ', after = '')
639 req_element.setAttribute('interface', req.interface)
640 binding = add_node(req_element, 'environment', before = '\n ', after = '\n ')
641 binding.setAttribute('name', 'LD_LIBRARY_PATH')
642 binding.setAttribute('insert', 'usr/lib')
644 if pkg_main:
645 group.setAttribute('main', pkg_main)
646 package.setAttribute('main', '/' + pkg_main)
648 impl = add_node(group, 'implementation', before = '\n ', after = '\n ')
649 impl.setAttribute('id', digest)
650 assert pkg_metadata.version
651 impl.setAttribute('version', pkg_metadata.version)
653 if options.license:
654 impl.setAttribute('license', options.license)
656 if pkg_metadata.buildtime:
657 impl.setAttribute('released', time.strftime('%Y-%m-%d', time.localtime(pkg_metadata.buildtime)))
658 else:
659 impl.setAttribute('released', time.strftime('%Y-%m-%d'))
661 archive = add_node(impl, 'archive', before = '\n ', after = '\n ')
662 archive.setAttribute('href', archive_url)
663 archive.setAttribute('size', str(os.path.getsize(archive_file)))
664 if options.archive_extract:
665 archive.setAttribute('extract', options.archive_extract)
667 # Add our new version to the main feed...
669 output_stream = tempfile.NamedTemporaryFile(prefix = 'pkg2zero-')
670 try:
671 output_stream.write("<?xml version='1.0'?>\n")
672 root.writexml(output_stream)
673 output_stream.write('\n')
674 output_stream.flush()
676 publishing_options = []
677 if options.key:
678 # Note: 0publish < 0.16 requires the --xmlsign option too
679 publishing_options += ['--xmlsign', '--key', options.key]
680 PUBLISH_COMMAND = os.environ.get('PUBLISH_COMMAND', '0publish')
681 check_call([PUBLISH_COMMAND] + publishing_options + ['--local', output_stream.name, target_feed_file])
682 print "Added version %s to %s" % (pkg_metadata.version, target_feed_file)
683 finally:
684 output_stream.close()