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