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
)
15 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus
, ' '.join(args
)))
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')
25 >>> suggest_release_version('0.9-post')
27 >>> suggest_release_version('3')
28 Traceback (most recent call last):
30 SafeException: Version '3' is not a snapshot version (should end in -pre or -post)
32 version
= model
.parse_version(snapshot_version
)
35 raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version
)
37 # -post, so increment the number
39 version
[-1] = 0 # Remove the modifier
40 return model
.format_version(version
)
42 def publish(iface
, **kwargs
):
43 args
= [os
.environ
['0PUBLISH']]
47 args
+= ['--' + k
.replace('_', '-')]
48 elif value
is not None:
49 args
+= ['--' + k
.replace('_', '-'), value
]
51 info("Executing %s", args
)
54 def get_singleton_impl(iface
):
55 impls
= iface
.implementations
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
):
64 if os
.path
.exists(backup
):
65 print "(deleting old backup %s)" % backup
66 if os
.path
.isdir(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
):
75 choice
= raw_input('/'.join(options
) + ': ').lower()
76 if not choice
: continue
78 if o
.lower().startswith(choice
):
81 def make_archive_name(feed_name
, version
):
82 return feed_name
.lower().replace(' ', '-') + '-' + version
85 for x
in os
.environ
['PATH'].split(':'):
86 if os
.path
.isfile(os
.path
.join(x
, prog
)):
90 def show_diff(from_dir
, to_dir
):
91 for cmd
in [['meld'], ['xxdiff'], ['diff', '-ur']]:
93 code
= os
.spawnvp(os
.P_WAIT
, cmd
[0], cmd
+ [from_dir
, to_dir
])
95 print "WARNING: command %s failed with exit code %d" % (cmd
, code
)
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']
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')
109 name
, value
= line
.split('=')
110 setattr(self
, name
, value
)
111 info("Loaded status %s=%s", name
, value
)
114 tmp_name
= release_status_file
+ '.new'
115 tmp
= file(tmp_name
, 'w')
117 lines
= ["%s=%s\n" % (name
, getattr(self
, name
)) for name
in self
.__slots
__ if getattr(self
, name
)]
118 tmp
.write(''.join(lines
))
120 os
.rename(tmp_name
, release_status_file
)
121 info("Wrote status to %s", release_status_file
)
127 if hasattr(address
, 'hostname'):
128 return address
.hostname
130 return address
[1].split(':', 1)[0]
133 if hasattr(address
, 'port'):
136 port
= address
[1].split(':', 1)[1:]
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)
154 http
.request('HEAD', '/' + path
, headers
= {'Host': host(address
)})
155 response
= http
.getresponse()
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
)
163 raise SafeException("HTTP error: got status code %s" % response
.status
)
168 info("Resource moved! Checking new URL %s" % new_url
)
170 return get_http_size(new_url
, ttl
- 1)
172 raise SafeException('Too many redirections.')
174 def get_ftp_size(url
):
175 address
= urlparse
.urlparse(url
)
176 ftp
= ftplib
.FTP(host(address
))
179 return ftp
.size(url
.split('/', 3)[3])
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
)
190 raise SafeException("Unknown scheme '%s' in '%s'" % (scheme
, url
))