Start development series 0.13-post
[0release.git] / support.py
blob65371885fd2f8e46eade8f5e1819d4b2e321d26d
1 # Copyright (C) 2007, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import copy
5 import os, subprocess, shutil, tarfile
6 import urlparse, ftplib, httplib
7 from zeroinstall import SafeException
8 from zeroinstall.injector import model, qdom
9 from logging import info
11 release_status_file = os.path.abspath('release-status')
13 def check_call(*args, **kwargs):
14 exitstatus = subprocess.call(*args, **kwargs)
15 if exitstatus != 0:
16 if type(args[0]) in (str, unicode):
17 cmd = args[0]
18 else:
19 cmd = ' '.join(args[0])
20 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus, cmd))
22 def show_and_run(cmd, args):
23 print "Executing: %s %s" % (cmd, ' '.join("[%s]" % x for x in args))
24 check_call(['sh', '-c', cmd, '-'] + args)
26 def suggest_release_version(snapshot_version):
27 """Given a snapshot version, suggest a suitable release version.
28 >>> suggest_release_version('1.0-pre')
29 '1.0'
30 >>> suggest_release_version('0.9-post')
31 '0.10'
32 >>> suggest_release_version('3')
33 Traceback (most recent call last):
34 ...
35 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
36 """
37 version = model.parse_version(snapshot_version)
38 mod = version[-1]
39 if mod == 0:
40 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version)
41 if mod > 0:
42 # -post, so increment the number
43 version[-2][-1] += 1
44 version[-1] = 0 # Remove the modifier
45 return model.format_version(version)
47 def publish(iface, **kwargs):
48 args = [os.environ['0PUBLISH']]
49 for k in kwargs:
50 value = kwargs[k]
51 if value is True:
52 args += ['--' + k.replace('_', '-')]
53 elif value is not None:
54 args += ['--' + k.replace('_', '-'), value]
55 args.append(iface)
56 info("Executing %s", args)
57 check_call(args)
59 def get_singleton_impl(iface):
60 impls = iface.implementations
61 if len(impls) != 1:
62 raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (iface.uri, len(impls)))
63 return impls.values()[0]
65 def backup_if_exists(name):
66 if not os.path.exists(name):
67 return
68 backup = name + '~'
69 if os.path.exists(backup):
70 print "(deleting old backup %s)" % backup
71 if os.path.isdir(backup):
72 shutil.rmtree(backup)
73 else:
74 os.unlink(backup)
75 os.rename(name, backup)
76 print "(renamed old %s as %s; will delete on next run)" % (name, backup)
78 def get_choice(options):
79 while True:
80 choice = raw_input('/'.join(options) + ': ').lower()
81 if not choice: continue
82 for o in options:
83 if o.lower().startswith(choice):
84 return o
86 def make_archive_name(feed_name, version):
87 return feed_name.lower().replace(' ', '-') + '-' + version
89 def in_PATH(prog):
90 for x in os.environ['PATH'].split(':'):
91 if os.path.isfile(os.path.join(x, prog)):
92 return True
93 return False
95 def show_diff(from_dir, to_dir):
96 for cmd in [['meld'], ['xxdiff'], ['diff', '-ur']]:
97 if in_PATH(cmd[0]):
98 code = os.spawnvp(os.P_WAIT, cmd[0], cmd + [from_dir, to_dir])
99 if code:
100 print "WARNING: command %s failed with exit code %d" % (cmd, code)
101 return
103 class Status(object):
104 __slots__ = ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version',
105 'head_at_release', 'created_archive', 'src_tests_passed', 'tagged', 'verified_uploads', 'updated_master_feed']
106 def __init__(self):
107 for name in self.__slots__:
108 setattr(self, name, None)
110 if os.path.isfile(release_status_file):
111 for line in file(release_status_file):
112 assert line.endswith('\n')
113 line = line[:-1]
114 name, value = line.split('=')
115 setattr(self, name, value)
116 info("Loaded status %s=%s", name, value)
118 def save(self):
119 tmp_name = release_status_file + '.new'
120 tmp = file(tmp_name, 'w')
121 try:
122 lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)]
123 tmp.write(''.join(lines))
124 tmp.close()
125 os.rename(tmp_name, release_status_file)
126 info("Wrote status to %s", release_status_file)
127 except:
128 os.unlink(tmp_name)
129 raise
131 def host(address):
132 if hasattr(address, 'hostname'):
133 return address.hostname
134 else:
135 return address[1].split(':', 1)[0]
137 def port(address):
138 if hasattr(address, 'port'):
139 return address.port
140 else:
141 port = address[1].split(':', 1)[1:]
142 if port:
143 return int(port[0])
144 else:
145 return None
147 def get_http_size(url, ttl = 1):
148 assert url.lower().startswith('http://')
150 address = urlparse.urlparse(url)
151 http = httplib.HTTPConnection(host(address), port(address) or 80)
153 parts = url.split('/', 3)
154 if len(parts) == 4:
155 path = parts[3]
156 else:
157 path = ''
159 http.request('HEAD', '/' + path, headers = {'Host': host(address)})
160 response = http.getresponse()
161 try:
162 if response.status == 200:
163 return response.getheader('Content-Length')
164 elif response.status in (301, 302):
165 new_url_rel = response.getheader('Location') or response.getheader('URI')
166 new_url = urlparse.urljoin(url, new_url_rel)
167 else:
168 raise SafeException("HTTP error: got status code %s" % response.status)
169 finally:
170 response.close()
172 if ttl:
173 info("Resource moved! Checking new URL %s" % new_url)
174 assert new_url
175 return get_http_size(new_url, ttl - 1)
176 else:
177 raise SafeException('Too many redirections.')
179 def get_ftp_size(url):
180 address = urlparse.urlparse(url)
181 ftp = ftplib.FTP(host(address))
182 try:
183 ftp.login()
184 return ftp.size(url.split('/', 3)[3])
185 finally:
186 ftp.close()
188 def get_size(url):
189 scheme = urlparse.urlparse(url)[0].lower()
190 if scheme.startswith('http'):
191 return get_http_size(url)
192 elif scheme.startswith('ftp'):
193 return get_ftp_size(url)
194 else:
195 raise SafeException("Unknown scheme '%s' in '%s'" % (scheme, url))
197 def unpack_tarball(archive_file):
198 tar = tarfile.open(archive_file, 'r:bz2')
199 members = [m for m in tar.getmembers() if m.name != 'pax_global_header']
200 #tar.extractall('.', members = members) # Python >= 2.5 only
201 for tarinfo in members:
202 tarinfo = copy.copy(tarinfo)
203 tarinfo.mode |= 0600
204 tarinfo.mode &= 0755
205 tar.extract(tarinfo, '.')
207 def load_feed(path):
208 stream = open(path)
209 try:
210 return model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
211 finally:
212 stream.close()
214 def get_archive_basename(impl):
215 # "2" means "path" (for Python 2.4)
216 return os.path.basename(urlparse.urlparse(impl.download_sources[0].url)[2])
218 def relative_path(ancestor, dst):
219 stem = os.path.abspath(os.path.dirname(ancestor))
220 dst = os.path.abspath(dst)
221 if stem != '/':
222 stem += '/'
223 assert dst.startswith(stem)
224 return dst[len(stem):]
226 assert relative_path('/foo', '/foo') == 'foo'
227 assert relative_path('/foo', '/foo/bar') == 'foo/bar'