1 # Copyright (C) 2007, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
5 import os
, subprocess
, tarfile
, platform
6 import urlparse
, ftplib
, httplib
7 from xml
.dom
import minidom
9 from zeroinstall
import SafeException
10 from zeroinstall
.injector
import model
, qdom
, namespaces
11 from zeroinstall
.support
import ro_rmtree
, portable_rename
12 from logging
import info
14 release_status_file
= os
.path
.abspath('release-status')
16 def check_call(*args
, **kwargs
):
17 exitstatus
= subprocess
.call(*args
, **kwargs
)
19 if type(args
[0]) in (str, unicode):
22 cmd
= ' '.join(args
[0])
23 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus
, cmd
))
25 def show_and_run(cmd
, args
):
26 print "Executing: %s %s" % (cmd
, ' '.join("[%s]" % x
for x
in args
))
27 check_call(['sh', '-c', cmd
, '-'] + args
)
29 def suggest_release_version(snapshot_version
):
30 """Given a snapshot version, suggest a suitable release version.
31 >>> suggest_release_version('1.0-pre')
33 >>> suggest_release_version('0.9-post')
35 >>> suggest_release_version('3')
36 Traceback (most recent call last):
38 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
40 version
= model
.parse_version(snapshot_version
)
43 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version
)
45 # -post, so increment the number
47 version
[-1] = 0 # Remove the modifier
48 return model
.format_version(version
)
50 def publish(feed_path
, **kwargs
):
51 args
= [os
.environ
['0PUBLISH']]
55 args
+= ['--' + k
.replace('_', '-')]
56 elif value
is not None:
57 if platform
.system() == 'Windows':
58 args
+= ['--' + k
.replace('_', '-') + "='" + value
+ "'"]
60 args
+= ['--' + k
.replace('_', '-'), value
]
61 args
.append(feed_path
)
62 info("Executing %s", args
)
65 def get_singleton_impl(feed
):
66 impls
= feed
.implementations
68 raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (feed
.url
, len(impls
)))
69 return impls
.values()[0]
71 def backup_if_exists(name
):
72 if not os
.path
.exists(name
):
75 if os
.path
.exists(backup
):
76 print "(deleting old backup %s)" % backup
77 if os
.path
.isdir(backup
):
81 portable_rename(name
, backup
)
82 print "(renamed old %s as %s; will delete on next run)" % (name
, backup
)
84 def get_choice(options
):
86 choice
= raw_input('/'.join(options
) + ': ').lower()
87 if not choice
: continue
89 if o
.lower().startswith(choice
):
92 def make_archive_name(feed_name
, version
):
93 return feed_name
.lower().replace(' ', '-') + '-' + version
96 for x
in os
.environ
['PATH'].split(':'):
97 if os
.path
.isfile(os
.path
.join(x
, prog
)):
101 def show_diff(from_dir
, to_dir
):
102 for cmd
in [['meld'], ['xxdiff'], ['diff', '-ur']]:
104 code
= os
.spawnvp(os
.P_WAIT
, cmd
[0], cmd
+ [from_dir
, to_dir
])
106 print "WARNING: command %s failed with exit code %d" % (cmd
, code
)
109 class Status(object):
110 __slots__
= ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version',
111 'head_at_release', 'created_archive', 'src_tests_passed', 'tagged', 'verified_uploads', 'updated_master_feed']
113 for name
in self
.__slots
__:
114 setattr(self
, name
, None)
116 if os
.path
.isfile(release_status_file
):
117 for line
in file(release_status_file
):
118 assert line
.endswith('\n')
120 name
, value
= line
.split('=')
121 setattr(self
, name
, value
)
122 info("Loaded status %s=%s", name
, value
)
125 tmp_name
= release_status_file
+ '.new'
126 tmp
= file(tmp_name
, 'w')
128 lines
= ["%s=%s\n" % (name
, getattr(self
, name
)) for name
in self
.__slots
__ if getattr(self
, name
)]
129 tmp
.write(''.join(lines
))
131 portable_rename(tmp_name
, release_status_file
)
132 info("Wrote status to %s", release_status_file
)
138 if hasattr(address
, 'hostname'):
139 return address
.hostname
141 return address
[1].split(':', 1)[0]
144 if hasattr(address
, 'port'):
147 port
= address
[1].split(':', 1)[1:]
153 def get_http_size(url
, ttl
= 1):
154 assert url
.lower().startswith('http://')
156 address
= urlparse
.urlparse(url
)
157 http
= httplib
.HTTPConnection(host(address
), port(address
) or 80)
159 parts
= url
.split('/', 3)
165 http
.request('HEAD', '/' + path
, headers
= {'Host': host(address
)})
166 response
= http
.getresponse()
168 if response
.status
== 200:
169 return response
.getheader('Content-Length')
170 elif response
.status
in (301, 302):
171 new_url_rel
= response
.getheader('Location') or response
.getheader('URI')
172 new_url
= urlparse
.urljoin(url
, new_url_rel
)
174 raise SafeException("HTTP error: got status code %s" % response
.status
)
179 info("Resource moved! Checking new URL %s" % new_url
)
181 return get_http_size(new_url
, ttl
- 1)
183 raise SafeException('Too many redirections.')
185 def get_ftp_size(url
):
186 address
= urlparse
.urlparse(url
)
187 ftp
= ftplib
.FTP(host(address
))
190 return ftp
.size(url
.split('/', 3)[3])
195 scheme
= urlparse
.urlparse(url
)[0].lower()
196 if scheme
.startswith('http'):
197 return get_http_size(url
)
198 elif scheme
.startswith('ftp'):
199 return get_ftp_size(url
)
201 raise SafeException("Unknown scheme '%s' in '%s'" % (scheme
, url
))
203 def unpack_tarball(archive_file
):
204 tar
= tarfile
.open(archive_file
, 'r:bz2')
205 members
= [m
for m
in tar
.getmembers() if m
.name
!= 'pax_global_header']
206 #tar.extractall('.', members = members) # Python >= 2.5 only
207 for tarinfo
in members
:
208 tarinfo
= copy
.copy(tarinfo
)
211 tar
.extract(tarinfo
, '.')
214 with
open(path
, 'rb') as stream
:
215 return model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
217 def get_archive_basename(impl
):
218 # "2" means "path" (for Python 2.4)
219 return os
.path
.basename(urlparse
.urlparse(impl
.download_sources
[0].url
)[2])
221 def make_readonly_recursive(path
):
222 for root
, dirs
, files
in os
.walk(path
):
223 for d
in dirs
+ files
:
224 full
= os
.path
.join(root
, d
)
225 mode
= os
.stat(full
).st_mode
226 os
.chmod(full
, mode
& 0o555)
228 def get_archive_url(options
, release_version
, archive
):
229 if not options
.archive_dir_public_url
:
230 return archive
# Not needed with 0repo
232 archive_dir_public_url
= options
.archive_dir_public_url
.replace('$RELEASE_VERSION', release_version
)
233 if not archive_dir_public_url
.endswith('/'):
234 archive_dir_public_url
+= '/'
235 return archive_dir_public_url
+ archive
237 def make_archives_relative(feed
):
238 with
open(feed
, 'rb') as stream
:
239 doc
= minidom
.parse(stream
)
240 for elem
in doc
.getElementsByTagNameNS(namespaces
.XMLNS_IFACE
, 'archive') + doc
.getElementsByTagNameNS(namespaces
.XMLNS_IFACE
, 'file'):
241 href
= elem
.getAttribute('href')
242 assert href
, 'Missing href on %r' % elem
244 elem
.setAttribute('href', href
.rsplit('/', 1)[1])
245 with
open(feed
, 'wb') as stream
: