Fixed bug in error reporting of failing shell commands.
[0release.git] / support.py
bloba8772434d216a425e788355d45f4270d6802f6ab
1 # Copyright (C) 2007, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os, subprocess, shutil
5 import urlparse, ftplib, httplib
6 from zeroinstall import SafeException
7 from zeroinstall.injector import model
8 from logging import info
10 release_status_file = 'release-status'
12 def check_call(*args, **kwargs):
13 exitstatus = subprocess.call(*args, **kwargs)
14 if exitstatus != 0:
15 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus, ' '.join(args[0])))
17 def show_and_run(cmd, args):
18 print "Executing: %s %s" % (cmd, ' '.join("[%s]" % x for x in args))
19 check_call(['sh', '-c', cmd, '-'] + args)
21 def suggest_release_version(snapshot_version):
22 """Given a snapshot version, suggest a suitable release version.
23 >>> suggest_release_version('1.0-pre')
24 '1.0'
25 >>> suggest_release_version('0.9-post')
26 '0.10'
27 >>> suggest_release_version('3')
28 Traceback (most recent call last):
29 ...
30 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
31 """
32 version = model.parse_version(snapshot_version)
33 mod = version[-1]
34 if mod == 0:
35 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version)
36 if mod > 0:
37 # -post, so increment the number
38 version[-2][-1] += 1
39 version[-1] = 0 # Remove the modifier
40 return model.format_version(version)
42 def publish(iface, **kwargs):
43 args = [os.environ['0PUBLISH']]
44 for k in kwargs:
45 value = kwargs[k]
46 if value is True:
47 args += ['--' + k.replace('_', '-')]
48 elif value is not None:
49 args += ['--' + k.replace('_', '-'), value]
50 args.append(iface)
51 info("Executing %s", args)
52 check_call(args)
54 def get_singleton_impl(iface):
55 impls = iface.implementations
56 if len(impls) != 1:
57 raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (iface.uri, len(impls)))
58 return impls.values()[0]
60 def backup_if_exists(name):
61 if not os.path.exists(name):
62 return
63 backup = name + '~'
64 if os.path.exists(backup):
65 print "(deleting old backup %s)" % backup
66 if os.path.isdir(backup):
67 shutil.rmtree(backup)
68 else:
69 os.unlink(backup)
70 os.rename(name, backup)
71 print "(renamed old %s as %s; will delete on next run)" % (name, backup)
73 def get_choice(options):
74 while True:
75 choice = raw_input('/'.join(options) + ': ').lower()
76 if not choice: continue
77 for o in options:
78 if o.lower().startswith(choice):
79 return o
81 def make_archive_name(feed_name, version):
82 return feed_name.lower().replace(' ', '-') + '-' + version
84 def in_PATH(prog):
85 for x in os.environ['PATH'].split(':'):
86 if os.path.isfile(os.path.join(x, prog)):
87 return True
88 return False
90 def show_diff(from_dir, to_dir):
91 for cmd in [['meld'], ['xxdiff'], ['diff', '-ur']]:
92 if in_PATH(cmd[0]):
93 code = os.spawnvp(os.P_WAIT, cmd[0], cmd + [from_dir, to_dir])
94 if code:
95 print "WARNING: command %s failed with exit code %d" % (cmd, code)
96 return
98 class Status(object):
99 __slots__ = ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version',
100 'head_at_release', 'created_archive', 'tagged', 'uploaded_archive', 'updated_master_feed']
101 def __init__(self):
102 for name in self.__slots__:
103 setattr(self, name, None)
105 if os.path.isfile(release_status_file):
106 for line in file(release_status_file):
107 assert line.endswith('\n')
108 line = line[:-1]
109 name, value = line.split('=')
110 setattr(self, name, value)
111 info("Loaded status %s=%s", name, value)
113 def save(self):
114 tmp_name = release_status_file + '.new'
115 tmp = file(tmp_name, 'w')
116 try:
117 lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)]
118 tmp.write(''.join(lines))
119 tmp.close()
120 os.rename(tmp_name, release_status_file)
121 info("Wrote status to %s", release_status_file)
122 except:
123 os.unlink(tmp_name)
124 raise
126 def host(address):
127 if hasattr(address, 'hostname'):
128 return address.hostname
129 else:
130 return address[1].split(':', 1)[0]
132 def port(address):
133 if hasattr(address, 'port'):
134 return address.port
135 else:
136 port = address[1].split(':', 1)[1:]
137 if port:
138 return int(port[0])
139 else:
140 return None
142 def get_http_size(url, ttl = 1):
143 assert url.lower().startswith('http://')
145 address = urlparse.urlparse(url)
146 http = httplib.HTTPConnection(host(address), port(address) or 80)
148 parts = url.split('/', 3)
149 if len(parts) == 4:
150 path = parts[3]
151 else:
152 path = ''
154 http.request('HEAD', '/' + path, headers = {'Host': host(address)})
155 response = http.getresponse()
156 try:
157 if response.status == 200:
158 return response.getheader('Content-Length')
159 elif response.status in (301, 302):
160 new_url_rel = response.getheader('Location') or response.getheader('URI')
161 new_url = urlparse.urljoin(url, new_url_rel)
162 else:
163 raise SafeException("HTTP error: got status code %s" % response.status)
164 finally:
165 response.close()
167 if ttl:
168 info("Resource moved! Checking new URL %s" % new_url)
169 assert new_url
170 return get_http_size(new_url, ttl - 1)
171 else:
172 raise SafeException('Too many redirections.')
174 def get_ftp_size(url):
175 address = urlparse.urlparse(url)
176 ftp = ftplib.FTP(host(address))
177 try:
178 ftp.login()
179 return ftp.size(url.split('/', 3)[3])
180 finally:
181 ftp.close()
183 def get_size(url):
184 scheme = urlparse.urlparse(url)[0].lower()
185 if scheme.startswith('http'):
186 return get_http_size(url)
187 elif scheme.startswith('ftp'):
188 return get_ftp_size(url)
189 else:
190 raise SafeException("Unknown scheme '%s' in '%s'" % (scheme, url))