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.
7 from optparse
import OptionParser
8 import tempfile
, shutil
, os
9 from xml
.dom
import minidom
12 import xml
.etree
.cElementTree
as ET
# Python 2.5
15 import xml
.etree
.ElementTree
as ET
18 import cElementTree
as ET
# http://effbot.org
20 import elementtree
.ElementTree
as ET
24 from subprocess
import check_call
26 def check_call(*popenargs
, **kwargs
):
27 rc
= subprocess
.call(*popenargs
, **kwargs
)
28 if rc
!= 0: raise OSError, rc
30 from zeroinstall
.injector
import model
, qdom
, distro
32 from support
import read_child
, add_node
, Mappings
34 manifest_algorithm
= 'sha1new'
36 deb_category_to_freedesktop
= {
37 'devel' : 'Development',
39 'graphics' : 'Graphics',
43 rpm_group_to_freedesktop
= {
44 'Development/Libraries' : 'Development',
62 # Parse command-line arguments
64 parser
= OptionParser('usage: %prog [options] http://.../package.deb [target-feed.xml]\n'
65 ' %prog [options] http://.../package.rpm [target-feed.xml]\n'
66 ' %prog [options] package-name [target-feed.xml]\n'
67 'Publish a Debian or RPM package in a Zero Install feed.\n'
68 "target-feed.xml is created if it doesn't already exist.")
69 parser
.add_option("-r", "--repomd-file", help="repository metadata file")
70 parser
.add_option("", "--path", help="location of packages [5/os/i386]")
71 parser
.add_option("-p", "--packages-file", help="Debian package index file")
72 parser
.add_option("-m", "--mirror", help="location of packages [http://ftp.debian.org/debian] or [http://mirror.centos.org/centos]")
73 parser
.add_option("-k", "--key", help="key to use for signing")
74 (options
, args
) = parser
.parse_args()
76 if len(args
) < 1 or len(args
) > 2:
80 # Load dependency mappings
96 def __init__(self
, options
):
97 self
.packages_base_url
= (options
.mirror
or 'http://ftp.debian.org/debian') + '/'
98 self
.packages_file
= options
.packages_file
or 'Packages'
100 def get_repo_metadata(self
, pkg_name
):
101 if not os
.path
.isfile(self
.packages_file
):
102 print >>sys
.stderr
, ("File '%s' not found (use -p to give its location).\n"
103 "Either download one (e.g. ftp://ftp.debian.org/debian/dists/stable/main/binary-amd64/Packages.bz2),\n"
104 "or specify the full URL of the .deb package to use.") % self
.packages_file
106 if self
.packages_file
.endswith('.bz2'):
111 pkg_data
= "\n" + opener(self
.packages_file
).read()
113 i
= pkg_data
.index('\nPackage: %s\n' % pkg_name
)
115 raise Exception("Package '%s' not found in Packages file '%s'." % (pkg_name
, self
.packages_file
))
116 j
= pkg_data
.find('\n\n', i
)
118 pkg_info
= pkg_data
[i
:]
120 pkg_info
= pkg_data
[i
:j
]
123 for line
in pkg_info
.split('\n'):
124 if ':' in line
and not line
.startswith(' '):
125 key
, value
= line
.split(':', 1)
126 if key
== 'Filename':
127 filename
= value
.strip()
128 elif key
in ('SHA1', 'SHA256'):
129 digest
[key
.lower()] = value
.strip()
131 raise Exception('Filename: field not found in package data:\n' + pkg_info
)
132 pkg_url
= self
.packages_base_url
+ filename
134 return pkg_url
, digest
136 def get_package_metadata(self
, pkg_file
):
139 details
= read_child(['dpkg-deb', '--info', pkg_file
])
141 description_and_summary
= details
.split('\n Description: ')[1].split('\n')
142 package
.summary
= description_and_summary
[0]
144 for x
in description_and_summary
[1:]:
153 description
+= x
[1:].replace('. ', '. ') + '\n'
154 package
.description
= description
.strip()
156 for line
in details
.split('\n'):
157 if not line
: continue
158 assert line
.startswith(' ')
161 key
, value
= line
.split(':', 1)
162 value
= value
.strip()
164 package
.category
= deb_category_to_freedesktop
.get(value
)
165 if not package
.category
:
167 print >>sys
.stderr
, "Warning: no mapping for Debian category '%s'" % value
168 elif key
== 'Package':
170 elif key
== 'Version':
171 value
= value
.replace('cvs', '')
172 value
= value
.replace('svn', '')
173 package
.version
= distro
.try_cleanup_distro_version(value
)
174 elif key
== 'Architecture':
176 arch
, value
= value
.split('-', 1)
183 package
.arch
= arch
.capitalize() + '-' + value
184 elif key
== 'Depends':
185 for x
in value
.split(','):
186 req
= mappings
.process(x
)
188 package
.requires
.append(req
)
191 def unpack(self
, pkg_file
, tmp
):
192 return read_child(['dpkg-deb', '-X', pkg_file
, tmp
])
195 def __init__(self
, options
):
196 self
.packages_base_url
= (options
.mirror
or 'http://mirror.centos.org/centos') + '/'
197 self
.packages_base_dir
= (options
.path
or '5/os/i386') + '/'
198 self
.repomd_file
= options
.repomd_file
or 'repodata/repomd.xml'
199 if not os
.path
.isfile(self
.repomd_file
):
200 print >>sys
.stderr
, ("File '%s' not found (use -r to give its location).\n"
201 "Either download one (e.g. http://mirror.centos.org/centos/5/os/i386/repodata/repomd.xml),\n"
202 "or specify the full URL of the .rpm package to use.") % self
.repomd_file
205 def get_repo_metadata(self
, pkg_name
):
207 repomd
= minidom
.parse(self
.repomd_file
)
208 repo_top
= os
.path
.dirname(os
.path
.dirname(self
.repomd_file
))
209 for data
in repomd
.getElementsByTagName("data"):
210 if data
.attributes
["type"].nodeValue
== "primary":
211 for node
in data
.getElementsByTagName("location"):
212 primary_file
= os
.path
.join(repo_top
, node
.attributes
["href"].nodeValue
)
214 primary
= ET
.parse(gzip
.open(primary_file
))
215 NS
= "http://linux.duke.edu/metadata/common"
216 metadata
= primary
.getroot()
218 for package
in metadata
.findall("{%s}package" % NS
):
219 if package
.find("{%s}name" % NS
).text
== pkg_name
:
221 location
= pkg_data
.find("{%s}location" % NS
).get("href")
224 raise Exception("Package '%s' not found in repodata." % pkg_name
)
225 checksum
= pkg_data
.find("{%s}checksum" % NS
)
227 if checksum
.get("type") == "sha":
228 digest
["sha1"] = checksum
.text
229 if checksum
.get("type") == "sha256":
230 digest
["sha256"] = checksum
.text
232 raise Exception('location tag not found in primary metadata:\n' + primary_file
)
233 pkg_url
= self
.packages_base_url
+ self
.packages_base_dir
+ location
235 return pkg_url
, digest
237 def get_package_metadata(self
, pkg_file
):
240 query_format
= '%{SUMMARY}\\a%{DESCRIPTION}\\a%{NAME}\\a%{VERSION}\\a%{OS}\\a%{ARCH}\\a%{URL}\\a%{GROUP}\\a%{LICENSE}\\a%{BUILDTIME}\\a[%{REQUIRES}\\n]'
241 headers
= read_child(['rpm', '--qf', query_format
, '-qp', pkg_file
]).split('\a')
243 package
.summary
= headers
[0].strip()
244 package
.description
= headers
[1].strip()
246 package
.name
= headers
[2]
248 value
= value
.replace('cvs', '')
249 value
= value
.replace('svn', '')
250 value
= distro
.try_cleanup_distro_version(value
)
251 package
.version
= value
253 package
.arch
= value
.capitalize()
257 if value
== 'noarch':
259 package
.arch
+= '-' + value
260 value
= headers
[6].strip()
263 value
= headers
[7].strip()
264 package
.category
= rpm_group_to_freedesktop
.get(value
)
266 print >>sys
.stderr
, "Warning: no mapping for RPM group '%s'" % value
268 value
= headers
[8].strip()
269 package
.license
= value
270 value
= headers
[9].strip()
271 package
.buildtime
= long(value
)
272 value
= headers
[10].strip()
273 for x
in value
.split('\n'):
274 if x
.startswith('rpmlib'):
276 req
= mappings
.process(x
)
278 package
.requires
.append(req
)
281 def unpack(self
, pkg_file
, tmp
):
282 files
= read_child(['rpm', '-qlp', pkg_file
])
283 p1
= subprocess
.Popen(['rpm2cpio', pkg_file
], stdout
= subprocess
.PIPE
)
284 IGNORE
= open('/dev/null', 'w')
285 p2
= subprocess
.Popen(['cpio', '-dim'], stdin
= p1
.stdout
, stderr
= IGNORE
, cwd
= tmp
)
290 if args
[0].endswith('.deb') or options
.packages_file
:
291 repo
= DebRepo(options
)
292 elif args
[0].endswith('.rpm') or options
.repomd_file
:
293 repo
= RPMRepo(options
)
295 print >>sys
.stderr
, "Use --packages-file for Debian, or --repomd-file for RPM"
300 scheme
= args
[0].split(':', 1)[0]
301 if scheme
in ('http', 'https', 'ftp'):
305 pkg_url
, digest
= repo
.get_repo_metadata(args
[0])
307 # Download package, if required
309 pkg_file
= os
.path
.abspath(pkg_url
.rsplit('/', 1)[1])
310 if not os
.path
.exists(pkg_file
):
311 print >>sys
.stderr
, "File '%s' not found, so downloading from %s..." % (pkg_file
, pkg_url
)
312 check_call(['wget', pkg_url
])
314 # Check digest, if known
316 if "sha256" in digest
:
318 m
= hashlib
.new('sha256')
319 expected_digest
= digest
["sha256"]
320 elif "sha1" in digest
:
323 m
= hashlib
.new('sha1')
327 expected_digest
= digest
["sha1"]
332 m
.update(file(pkg_file
).read())
333 actual
= m
.hexdigest()
334 if actual
!= expected_digest
:
335 raise Exception("Incorrect digest on package file! Was " + actual
+ ", but expected " + expected_digest
)
337 print "Package's digest matches value in reposistory metadata (" + actual
+ "). Good."
339 print >>sys
.stderr
, "Note: no SHA-1 or SHA-256 digest known for this package, so not checking..."
341 # Extract meta-data from package
343 pkg_metadata
= repo
.get_package_metadata(pkg_file
)
345 # Unpack package, find binaries and .desktop files, and add to cache
349 tmp
= tempfile
.mkdtemp(prefix
= 'pkg2zero-')
351 files
= repo
.unpack(pkg_file
, tmp
)
355 for f
in files
.split('\n'):
356 if f
.startswith('./'):
358 elif f
.startswith('/'):
360 assert not f
.startswith('/')
361 full
= os
.path
.join(tmp
, f
)
362 if f
.endswith('.desktop'):
363 for line
in file(full
):
364 if line
.startswith('Categories'):
365 for cat
in line
.split('=', 1)[1].split(';'):
367 if cat
in valid_categories
:
370 elif line
.startswith('Icon'):
371 icon
= line
.split('=', 1)[1].strip()
372 elif f
.startswith('bin/') or f
.startswith('usr/bin/') or f
.startswith('usr/games/'):
373 if os
.path
.isfile(full
):
374 possible_mains
.append(f
)
375 elif f
.endswith('.png'):
377 images
[os
.path
.basename(f
)] = full
378 # make sure to also map basename without the extension
379 images
[os
.path
.splitext(os
.path
.basename(f
))[0]] = full
383 print "Using %s for icon" % os
.path
.basename(images
[icon
])
384 icondata
= file(images
[icon
]).read()
386 manifest
= read_child(['0store', 'manifest', tmp
, manifest_algorithm
])
387 digest
= manifest
.rsplit('\n', 2)[1]
388 check_call(['0store', 'add', digest
, tmp
])
393 possible_mains
= sorted(possible_mains
, key
= len)
394 pkg_main
= possible_mains
[0]
395 if len(possible_mains
) > 1:
396 print "Warning: several possible main binaries found:"
397 print "- " + pkg_main
+ " (I chose this one)"
398 for x
in possible_mains
[1:]:
403 # Make sure we haven't added this version already...
406 target_feed_file
= args
[1]
407 target_icon_file
= args
[1].replace('.xml', '.png')
409 target_feed_file
= pkg_metadata
.name
+ '.xml'
410 target_icon_file
= pkg_metadata
.name
+ '.png'
414 if os
.path
.isfile(target_feed_file
):
415 dummy_dist
= distro
.Distribution()
416 dom
= qdom
.parse(file(target_feed_file
))
417 old_target_feed
= model
.ZeroInstallFeed(dom
, local_path
= target_feed_file
, distro
= dummy_dist
)
418 existing_impl
= old_target_feed
.implementations
.get(digest
)
420 print >>sys
.stderr
, ("Feed '%s' already contains an implementation with this digest!\n%s" % (target_feed_file
, existing_impl
))
423 # No target, so need to pick a URI
424 feed_uri
= mappings
.lookup(pkg_metadata
.name
)
426 suggestion
= mappings
.get_suggestion(pkg_metadata
.name
)
427 uri
= raw_input('Enter the URI for this feed [%s]: ' % suggestion
).strip()
430 assert uri
.startswith('http://') or uri
.startswith('https://') or uri
.startswith('ftp://'), uri
432 mappings
.add_mapping(pkg_metadata
.name
, uri
)
434 if icondata
and not os
.path
.isfile(target_icon_file
):
435 file = open(target_icon_file
, 'wb')
439 suggestion
= 'http://0install.net/feed_icons/' + target_icon_file
440 uri
= raw_input('Enter the URI for this icon [%s]: ' % suggestion
).strip()
443 assert uri
.startswith('http://') or uri
.startswith('https://') or uri
.startswith('ftp://'), uri
446 # Create a local feed with just the new version...
448 template
= '''<interface xmlns="http://zero-install.sourceforge.net/2004/injector/interface">
450 doc
= minidom
.parseString(template
)
451 root
= doc
.documentElement
453 add_node(root
, 'name', pkg_metadata
.name
)
454 add_node(root
, 'summary', pkg_metadata
.summary
)
455 add_node(root
, 'description', pkg_metadata
.description
)
456 feed_for
= add_node(root
, 'feed-for', '')
458 feed_for
.setAttribute('interface', feed_uri
)
460 icon
= add_node(root
, 'icon')
461 icon
.setAttribute('href', icon_uri
)
462 icon
.setAttribute('type', 'image/png')
463 if pkg_metadata
.homepage
:
464 add_node(root
, 'homepage', pkg_metadata
.homepage
)
465 if pkg_metadata
.category
:
466 add_node(root
, 'category', pkg_metadata
.category
)
468 package
= add_node(root
, 'package-implementation', '')
469 package
.setAttribute('package', pkg_metadata
.name
)
471 group
= add_node(root
, 'group', '')
472 if pkg_metadata
.arch
:
473 group
.setAttribute('arch', pkg_metadata
.arch
)
475 print >>sys
.stderr
, "No Architecture: field in package"
476 if pkg_metadata
.license
:
477 group
.setAttribute('license', pkg_metadata
.license
)
479 for req
in pkg_metadata
.requires
:
480 req_element
= add_node(group
, 'requires', before
= '\n ', after
= '')
481 req_element
.setAttribute('interface', req
.interface
)
482 binding
= add_node(req_element
, 'environment', before
= '\n ', after
= '\n ')
483 binding
.setAttribute('name', 'LD_LIBRARY_PATH')
484 binding
.setAttribute('insert', 'usr/lib')
487 group
.setAttribute('main', pkg_main
)
488 package
.setAttribute('main', '/' + pkg_main
)
490 impl
= add_node(group
, 'implementation', before
= '\n ', after
= '\n ')
491 impl
.setAttribute('id', digest
)
492 assert pkg_metadata
.version
493 impl
.setAttribute('version', pkg_metadata
.version
)
495 if pkg_metadata
.buildtime
:
496 impl
.setAttribute('released', time
.strftime('%Y-%m-%d', time
.localtime(pkg_metadata
.buildtime
)))
498 impl
.setAttribute('released', time
.strftime('%Y-%m-%d'))
500 archive
= add_node(impl
, 'archive', before
= '\n ', after
= '\n ')
501 archive
.setAttribute('href', pkg_url
)
502 archive
.setAttribute('size', str(os
.path
.getsize(pkg_file
)))
504 # Add our new version to the main feed...
506 output_stream
= tempfile
.NamedTemporaryFile(prefix
= 'pkg2zero-')
508 output_stream
.write("<?xml version='1.0'?>\n")
509 root
.writexml(output_stream
)
510 output_stream
.write('\n')
511 output_stream
.flush()
513 publishing_options
= []
515 # Note: 0publish < 0.16 requires the --xmlsign option too
516 publishing_options
+= ['--xmlsign', '--key', options
.key
]
517 check_call([os
.environ
['PUBLISH_COMMAND']] + publishing_options
+ ['--local', output_stream
.name
, target_feed_file
])
518 print "Added version %s to %s" % (pkg_metadata
.version
, target_feed_file
)
520 output_stream
.close()