Only add extra quotes on Windows
[0release.git] / support.py
bloba567fee3ec539802320978b8e390713bc716d5c0
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, 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)
18 if exitstatus != 0:
19 if type(args[0]) in (str, unicode):
20 cmd = args[0]
21 else:
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')
32 '1.0'
33 >>> suggest_release_version('0.9-post')
34 '0.10'
35 >>> suggest_release_version('3')
36 Traceback (most recent call last):
37 ...
38 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
39 """
40 version = model.parse_version(snapshot_version)
41 mod = version[-1]
42 if mod == 0:
43 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version)
44 if mod > 0:
45 # -post, so increment the number
46 version[-2][-1] += 1
47 version[-1] = 0 # Remove the modifier
48 return model.format_version(version)
50 def publish(feed_path, **kwargs):
51 args = [os.environ['0PUBLISH']]
52 for k in kwargs:
53 value = kwargs[k]
54 if value is True:
55 args += ['--' + k.replace('_', '-')]
56 elif value is not None:
57 if platform.system() == 'Windows':
58 args += ['--' + k.replace('_', '-') + "='" + value + "'"]
59 else:
60 args += ['--' + k.replace('_', '-'), value]
61 args.append(feed_path)
62 info("Executing %s", args)
63 check_call(args)
65 def get_singleton_impl(feed):
66 impls = feed.implementations
67 if len(impls) != 1:
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):
73 return
74 backup = name + '~'
75 if os.path.exists(backup):
76 print "(deleting old backup %s)" % backup
77 if os.path.isdir(backup):
78 ro_rmtree(backup)
79 else:
80 os.unlink(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):
85 while True:
86 choice = raw_input('/'.join(options) + ': ').lower()
87 if not choice: continue
88 for o in options:
89 if o.lower().startswith(choice):
90 return o
92 def make_archive_name(feed_name, version):
93 return feed_name.lower().replace(' ', '-') + '-' + version
95 def in_PATH(prog):
96 for x in os.environ['PATH'].split(':'):
97 if os.path.isfile(os.path.join(x, prog)):
98 return True
99 return False
101 def show_diff(from_dir, to_dir):
102 for cmd in [['meld'], ['xxdiff'], ['diff', '-ur']]:
103 if in_PATH(cmd[0]):
104 code = os.spawnvp(os.P_WAIT, cmd[0], cmd + [from_dir, to_dir])
105 if code:
106 print "WARNING: command %s failed with exit code %d" % (cmd, code)
107 return
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']
112 def __init__(self):
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')
119 line = line[:-1]
120 name, value = line.split('=')
121 setattr(self, name, value)
122 info("Loaded status %s=%s", name, value)
124 def save(self):
125 tmp_name = release_status_file + '.new'
126 tmp = file(tmp_name, 'w')
127 try:
128 lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)]
129 tmp.write(''.join(lines))
130 tmp.close()
131 portable_rename(tmp_name, release_status_file)
132 info("Wrote status to %s", release_status_file)
133 except:
134 os.unlink(tmp_name)
135 raise
137 def host(address):
138 if hasattr(address, 'hostname'):
139 return address.hostname
140 else:
141 return address[1].split(':', 1)[0]
143 def port(address):
144 if hasattr(address, 'port'):
145 return address.port
146 else:
147 port = address[1].split(':', 1)[1:]
148 if port:
149 return int(port[0])
150 else:
151 return None
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)
160 if len(parts) == 4:
161 path = parts[3]
162 else:
163 path = ''
165 http.request('HEAD', '/' + path, headers = {'Host': host(address)})
166 response = http.getresponse()
167 try:
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)
173 else:
174 raise SafeException("HTTP error: got status code %s" % response.status)
175 finally:
176 response.close()
178 if ttl:
179 info("Resource moved! Checking new URL %s" % new_url)
180 assert new_url
181 return get_http_size(new_url, ttl - 1)
182 else:
183 raise SafeException('Too many redirections.')
185 def get_ftp_size(url):
186 address = urlparse.urlparse(url)
187 ftp = ftplib.FTP(host(address))
188 try:
189 ftp.login()
190 return ftp.size(url.split('/', 3)[3])
191 finally:
192 ftp.close()
194 def get_size(url):
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)
200 else:
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)
209 tarinfo.mode |= 0600
210 tarinfo.mode &= 0755
211 tar.extract(tarinfo, '.')
213 def load_feed(path):
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
243 if '/' in href:
244 elem.setAttribute('href', href.rsplit('/', 1)[1])
245 with open(feed, 'wb') as stream:
246 doc.writexml(stream)
247 stream.write(b'\n')