forked from mozilla/mozbase-deprecated
-
Notifications
You must be signed in to change notification settings - Fork 0
/
versionbump.py
executable file
·336 lines (304 loc) · 13 KB
/
versionbump.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
#!/usr/bin/env python
"""bump mozbase versions"""
import optparse
import os
import pkg_resources
import re
import subprocess
import sys
import xmlrpclib
# import setup_development.py from the same directory
import setup_development
here = setup_development.here
REPOSITORY_URL = 'git@github.com:mozilla/mozbase.git'
REPOSITORY_PULL_URL = 'git://github.com/mozilla/mozbase.git'
class CalledProcessError(Exception):
"""error for bad calls"""
def format_version(**dep):
"""formats a dependency version"""
return '%(Name)s %(Type)s %(Version)s' % dep
def call(cmd, **kwargs):
print "Running %s, %s" % (cmd, kwargs)
if dry_run:
return
kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.PIPE)
process = subprocess.Popen(cmd, **kwargs)
stdout, stderr = process.communicate()
if process.returncode:
print 'stdout:'
print stdout
print 'stderr:'
print stderr
raise CalledProcessError("Error running %s: %d" % (cmd,
process.returncode))
def revert(git):
"""revert the repository on error"""
call([git, 'reset', '--hard', 'HEAD'])
def main(args=sys.argv[1:]):
# parse command line options
usage = '%prog [options] packageA=0.1.2 <packageB=1.2> <...>'
parser = optparse.OptionParser(usage=usage, description=__doc__)
parser.add_option('--info', dest='info',
action='store_true', default=False,
help="display package version information and exit")
parser.add_option('--dry-run', dest='dry_run',
action='store_true', default=False,
help="don't make changes, just display what will be run")
parser.add_option('--diff', dest='diff',
help="output the diff to this file ('-' for stdout)")
parser.add_option('-m', '--message', dest='message',
help="commit message")
parser.add_option('--strict', dest='strict',
action='store_true', default=False,
help="bump dependencies specified as '==' but not '>='")
parser.add_option('--git', dest='git_path', default='git',
help='git binary to use')
parser.add_option('--pypi', dest='pypi_versions',
action='store_true', default=False,
help="display in-tree package versions and versions on pypi")
options, args = parser.parse_args()
globals()['dry_run'] = options.dry_run
# get package information
info = {}
dependencies = {}
directories = {}
for package in setup_development.mozbase_packages:
directory = os.path.join(here, package)
info[directory] = setup_development.info(directory)
name, _dependencies = setup_development.get_dependencies(directory)
assert name == info[directory]['Name']
directories[name] = directory
dependencies[name] = _dependencies
if options.info:
# print package version information and exit
for value in info.values():
print '%s %s : %s' % (value['Name'], value['Version'],
', '.join(dependencies[value['Name']]))
parser.exit()
if options.pypi_versions:
# print package version information and version info from pypi and exit
client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
for value in info.values():
versions = client.package_releases(value['Name'])
if versions:
version = max(versions, key=lambda x: pkg_resources.parse_version(x))
else:
version = None
print '%s %s : pypi version %s' % (value['Name'], value['Version'], version)
parser.exit()
# check for pypirc file
if not options.diff: # you don't need this to write the diff
home = os.environ['HOME']
pypirc = os.path.join(home, '.pypirc')
print "Checking for pypirc: %s" % pypirc
if not os.path.exists(pypirc):
parser.error("%s not found." % pypirc)
# ensure git sanity
# - ensure you are on the master branch
cmd = [options.git_path, 'branch']
process = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=here)
stdout, stderr = process.communicate()
if stderr or process.returncode:
print 'stdout:'
print stdout
print 'stderr:'
print stderr
raise CalledProcessError("Error running %s: %d" % (cmd,
process.returncode))
branch = [line for line in stdout.splitlines() if line.startswith('*')][0]
branch = branch.split('*', 1)[-1].strip()
if branch != 'master':
parser.error("versionbump.py must be used on the master branch")
# - ensure there are no changes
cmd = [options.git_path, 'status', '-s']
process = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=here)
stdout, stderr = process.communicate()
if stderr or process.returncode:
raise CalledProcessError("Error running %s: %d" % (cmd,
process.returncode))
if stdout.strip():
parser.error("%s directory unclean when running git status -s" % here)
# find desired versions
if not args:
parser.error("Please supply versions to bump")
versions = {}
msg = "Versions should be of the form 'package=version' (You gave: '%s')"
for arg in args:
if arg.count('=') != 1:
parser.error(msg % arg)
package, version = arg.split('=')
versions[package] = version
unrecognized = [package for package in versions
if package not in dependencies]
if unrecognized:
parser.error("Not a package: %s" % ', '.join(unrecognized))
# record ancillary packages that need bumping
# and ensure that if you're bumping versions, you're
# bumping them for all packages affected
dependent_versions = {}
missing = []
types = ['==']
if not options.strict:
types.append('>=')
for name, deps in dependencies.items():
for dep in deps:
dep_info = setup_development.dependency_info(dep)
if dep_info['Type'] in types and dep_info['Name'] in versions:
if name not in versions:
missing.append(name)
dependent_versions.setdefault(name, []).append(dep_info)
if missing:
missing = dict([('%s %s' % (i, info[directories[i]]['Version']),
[format_version(**k) for k in j])
for i, j in dependent_versions.items()
if i in missing])
parser.error("Bumping %s, but you also need to bump %s" % (versions,
missing))
# ensure you are up to date
print "Pulling from %s master" % REPOSITORY_PULL_URL
call([options.git_path, 'pull', REPOSITORY_PULL_URL, 'master'],
stdout=None, stderr=None, cwd=here)
# bump versions of desired files
for name, newversion in versions.items():
directory = directories[name]
oldversion = info[directory]['Version']
print "Bumping %s == %s => %s" % (name, oldversion, newversion)
setup_py = os.path.join(directory, 'setup.py')
f = file(setup_py)
lines = f.readlines()
f.close()
regex_string = r"""PACKAGE_VERSION *= *['"]%s["'].*""" % re.escape(oldversion)
regex = re.compile(regex_string)
for index, line in enumerate(lines):
if regex.match(line):
break
else:
revert(options.git_path)
parser.error('PACKAGE_VERSION = "%s" not found in %s' % (version,
setup_py))
if not options.dry_run:
lines[index] = "PACKAGE_VERSION = '%s'\n" % newversion
f = file(setup_py, 'w')
for line in lines:
f.write(line)
f.close()
# bump version of dependencies
for package, deps in dependent_versions.items():
print "Bumping dependencies %s of %s" % ([format_version(**dep)
for dep in deps],
package)
regexes = [(dep,
re.compile(r"%s *%s *%s" % (re.escape(dep['Name']),
re.escape(dep['Type']),
re.escape(dep['Version'])),
flags=re.MULTILINE)
)
for dep in deps]
setup_py = os.path.join(directories[package], 'setup.py')
assert os.path.exists(setup_py)
f = file(setup_py)
contents = f.read()
f.close()
matched = set()
for dep, regex in regexes:
newversion = '%s %s %s' % (dep['Name'], dep['Type'],
versions[dep['Name']])
formatted = format_version(**dep)
print "- Bumping dependency %s => %s" % (formatted, newversion)
matches = regex.findall(contents)
if len(matches) != 1:
revert(options.git_path)
if not matches:
msg = "Could not find dependency %s in %s" % (formatted,
setup_py)
else:
msg = "Multiple matches for %s in %s" % (formatted,
setup_py)
parser.error(msg)
if not options.dry_run:
contents = regex.sub(newversion, contents)
if not options.dry_run:
f = file(setup_py, 'w')
f.write(contents)
f.close()
if options.diff and not options.dry_run:
# write the diff
if options.diff == '-':
f = sys.stdout
filename = 'stdout'
else:
f = file(options.diff, 'w')
filename = options.diff
print "Writing diff to %s" % filename
process = subprocess.Popen([options.git_path, 'diff'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=here)
stdout, stderr = process.communicate()
if process.returncode:
print 'stdout:'
print stdout
print 'stderr:'
print stderr
parser.error("Error running `%s diff`" % options.git_path)
f.write(stdout)
if options.diff != '-':
f.close()
revert(options.git_path) # get back to your old state
sys.exit()
# push the changes
if not options.message:
print "No commit --message given; not updating git or pushing to pypi"
sys.exit()
print "Commit changes to %s: %s" % (REPOSITORY_URL, options.message)
call([options.git_path, 'commit', '-a', '-m', options.message], cwd=here)
call([options.git_path, 'push', REPOSITORY_URL, 'master'],
stdout=None, stderr=None, cwd=here)
# git tag the said versions
tags = [('%s-%s' % (package, version))
for package, version in versions.items()]
print "Updating tags for %s: %s" % (REPOSITORY_URL, ', '.join(tags))
call([options.git_path, 'pull', '--tags', REPOSITORY_URL, 'master'],
stdout=None, stderr=None, cwd=here)
for tag in tags:
call([options.git_path, 'tag', tag], cwd=here)
try:
call([options.git_path, 'push', '--tags', REPOSITORY_URL, 'master'],
stdout=None, stderr=None, cwd=here)
except CalledProcessError, e:
print "Failure pushing tags."
raise e
# upload to pypi
formatted_deps = dict([(package, set([dep['Name'] for dep in deps]))
for package, deps in dependent_versions.items()])
for package in versions.keys():
formatted_deps.setdefault(package, set())
unrolled = setup_development.unroll_dependencies(formatted_deps)
print "Uploading to pypi: %s" % ', '.join([('%s-%s' % (package,
versions[package]))
for package in unrolled])
for package in unrolled:
directory = directories[package]
cmd = [sys.executable,
'setup.py',
'egg_info',
'-RDb',
'',
'sdist',
'upload']
try:
call(cmd, cwd=directory)
except CalledProcessError, e:
print """Failure uploading package %s to pypi.
Make sure you have permission to update the package
and that your ~/.pypirc file is correct""" % package
raise e
if __name__ == '__main__':
main()