00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037 PKG = 'release'
00038 NAME="create.py"
00039
00040 VERSION=7
00041
00042 import roslib; roslib.load_manifest(PKG)
00043
00044 import sys
00045 import os
00046 from subprocess import Popen, PIPE, call, check_call
00047 import shutil
00048 import yaml
00049 import urllib2
00050 import tempfile
00051 import subprocess
00052
00053 import hudson
00054 import roslib.packages
00055 import roslib.stacks
00056
00057 from vcstools import VcsClient
00058
00059 from release import ReleaseException, update_rosdistro_yaml, make_dist, \
00060 compute_stack_depends, get_stack_version, \
00061 checkout_svn_to_tmp, get_source_version, checkout_stack, make_dist_of_dir
00062
00063 from rosdistro import Distro
00064
00065 pkg_dir = roslib.packages.get_pkg_dir('release_resources')
00066 distros_dir = os.path.join(pkg_dir, '..', 'distros')
00067
00068 TARBALL_DIR_URL = 'https://code.ros.org/svn/release/download/stacks/%(stack_name)s/%(stack_name)s-%(stack_version)s'
00069 ROSORG_URL = 'http://ros.org/download/stacks/%(stack_name)s/%(stack_name)s-%(stack_version)s.tar.bz2'
00070 SERVER = 'http://build.willowgarage.com/'
00071
00072 VALID_ROSDISTROS = ['cturtle', 'diamondback', 'electric']
00073
00074 def svn_url_exists(url):
00075 """
00076 @return: True if SVN url points to an existing resource
00077 """
00078 import subprocess
00079 try:
00080 p = subprocess.Popen(['svn', 'info', url], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
00081 p.wait()
00082 return p.returncode == 0
00083 except:
00084 return False
00085
00086 def yes_or_no():
00087 print ("(y/n)")
00088 while 1:
00089 input = sys.stdin.readline().strip()
00090 if input in ['y', 'n']:
00091 break
00092 return input == 'y'
00093
00094 def prompt(msg):
00095 while True:
00096 prompt = raw_input(msg)
00097 if prompt == 'y':
00098 return True
00099 elif prompt == 'n':
00100 return False
00101
00102
00103 def ask_and_call(cmds, cwd=None):
00104 """
00105 Pretty print cmds, ask if they should be run, and if so, runs
00106 them using subprocess.check_call.
00107
00108 @return: True if cmds were run.
00109 """
00110
00111 def quote(s):
00112 return '"%s"'%s if ' ' in s else s
00113 print "Okay to execute:\n"
00114 print_bold('\n'.join([' '.join([quote(s) for s in c]) for c in cmds]))
00115 accepted = yes_or_no()
00116 import subprocess
00117 if accepted:
00118 for c in cmds:
00119 if cwd:
00120 subprocess.check_call(c, cwd=cwd)
00121 else:
00122 subprocess.check_call(c)
00123 return accepted
00124
00125 def get_email():
00126 if 'ROS_EMAIL' in os.environ:
00127 email = os.environ['ROS_EMAIL']
00128 else:
00129 print_bold("Please enter e-mail address (set ROS_EMAIL to get rid of this prompt):")
00130 while 1:
00131 email = sys.stdin.readline().strip()
00132 if email:
00133 break
00134 if '@' in email:
00135 return email
00136 else:
00137 return None
00138
00139 def load_sys_args():
00140 """
00141 @return: name, version, distro_file, distro_name
00142 @rtype: (str, str, str, str)
00143 """
00144 from optparse import OptionParser
00145 parser = OptionParser(usage="usage: %prog <stack> <version> <release-name>", prog=NAME)
00146 options, args = parser.parse_args()
00147 if len(args) != 3:
00148 parser.error("""You must specify:
00149 * stack name (e.g. common_msgs)
00150 * version (e.g. 1.0.1)
00151 * distro release name (e.g. cturtle)""")
00152 name, version, release_name = args
00153 if release_name not in VALID_ROSDISTROS:
00154 parser.error("release_name [%s] is not in the valid list [%s], If you are releasing into fuerte please use the new tools http://www.ros.org/wiki/release/Releasing/fuerte"%(release_name, VALID_ROSDISTROS))
00155 distro_file = os.path.join(distros_dir, '%s.rosdistro'%(release_name))
00156 distro_file = os.path.abspath(distro_file)
00157 if not os.path.isfile(distro_file):
00158 parser.error("Could not find rosdistro file for [%s].\nExpected it in %s"%(release_name, distro_file))
00159
00160 if not '.' in version:
00161 parser.error("[%s] doesn't appear to be a version number"%version)
00162 return name, version, distro_file
00163
00164 def load_and_validate_properties(name, distro, distro_file):
00165 """
00166 @return: name, version, distro_file, distro
00167 @rtype: (str, str, str, release.Distro)
00168 """
00169 try:
00170 props = distro.stacks[name]
00171 except KeyError:
00172 raise ReleaseException("%s is not listed in distro file %s"%(name, distro_file))
00173
00174 print_bold("Release Properties")
00175 for p in ['name', 'dev_svn', 'release_svn']:
00176 print " * %s: %s"%(p, getattr(props, p))
00177 print "Release target is [%s]"%(distro.release_name)
00178
00179
00180
00181 def confirm_stack_version(local_path, checkout_path, stack_name, version):
00182 vcs_version = get_stack_version(checkout_path, stack_name)
00183 local_version = get_stack_version(local_path, stack_name)
00184 if vcs_version != version:
00185 raise ReleaseException("The version number of stack %s stored in version control does not match specified release version:\n\n%s"%(stack_name, vcs_version))
00186 if local_version != version:
00187 raise ReleaseException("The version number of stack %s on your ROS_PACKAGE_PATH does not match specified release version:\n\n%s"%(stack_name, local_version))
00188
00189
00190 def copy_to_server(name, version, tarball, control, control_only=False):
00191 """
00192 @param name: stack name
00193 @type name: str
00194 @param version: stack version
00195 @type version: str
00196 @param tarball: path to tarball file to upload
00197 @param control: debian control file data
00198 @type control: dict
00199 """
00200
00201
00202 stack_name = name
00203 stack_version = version
00204 url = TARBALL_DIR_URL%locals()
00205
00206 if not svn_url_exists(url):
00207 cmd = ['svn', 'mkdir', '--parents', "-m", "creating new tarball directory", url]
00208 print "creating new tarball directory"
00209 print ' '.join(cmd)
00210 check_call(cmd)
00211
00212 tarball_name = os.path.basename(tarball)
00213
00214
00215
00216 tarball_url = url + '/' + tarball_name
00217 if svn_url_exists(tarball_url):
00218
00219
00220
00221
00222 print "reusing existing tarball of release for this distribution"
00223 return
00224
00225
00226 dir_name = "%s-%s"%(name, version)
00227 tmp_dir = checkout_svn_to_tmp(dir_name, url)
00228 subdir = os.path.join(tmp_dir, dir_name)
00229 if not control_only:
00230 to_path = os.path.join(subdir, tarball_name)
00231 print "copying %s to %s"%(tarball, to_path)
00232 assert os.path.exists(tarball)
00233 shutil.copyfile(tarball, to_path)
00234
00235
00236 control_f = '%s-%s.yaml'%(name, version)
00237 with open(os.path.join(subdir, control_f), 'w') as f:
00238 f.write(yaml.safe_dump(control))
00239
00240
00241 if not control_only:
00242 check_call(['svn', 'add', tarball_name], cwd=subdir)
00243 check_call(['svn', 'add', control_f], cwd=subdir)
00244 if control_only:
00245 check_call(['svn', 'ci', '-m', "new release %s-%s"%(name, version), control_f], cwd=subdir)
00246 else:
00247 check_call(['svn', 'ci', '-m', "new release %s-%s"%(name, version), tarball_name, control_f], cwd=subdir)
00248
00249 def checkout_distro_stack(distro_stack, from_url, spec):
00250 vcs_type = distro_stack.vcs_config.type
00251 tempdir = tempfile.mkdtemp()
00252 temp_repo = os.path.join(tempdir, distro_stack.name)
00253 client = VcsClient(vcs_type, temp_repo)
00254 client.checkout(from_url, spec)
00255 return temp_repo
00256
00257 def tag_release(distro_stack, checkout_dir):
00258 if 'svn' in distro_stack._rules:
00259 tag_subversion(distro_stack)
00260 elif 'git' in distro_stack._rules:
00261 tag_git(distro_stack, checkout_dir)
00262 elif 'hg' in distro_stack._rules:
00263 tag_mercurial(distro_stack, checkout_dir)
00264 elif 'bzr' in distro_stack._rules:
00265 tag_bzr(distro_stack)
00266 else:
00267 raise Exception("unsupported VCS")
00268
00269 def tag_subversion(distro_stack):
00270 cmds = []
00271 config = distro_stack.vcs_config
00272 for tag_url in [config.release_tag, config.distro_tag]:
00273 from_url = config.dev
00274 release_name = "%s-%s"%(distro_stack.name, distro_stack.version)
00275
00276
00277 append_rm_if_exists(tag_url, cmds, 'Making room for new release')
00278
00279 cmds.append(['svn', 'cp', '--parents', '-m', 'Tagging %s new release'%(release_name), from_url, tag_url])
00280 if not ask_and_call(cmds):
00281 print "create_release will not create this tag in subversion"
00282 return []
00283 else:
00284 return [tag_url]
00285
00286 def tag_mercurial(distro_stack, checkout_dir):
00287 config = distro_stack.vcs_config
00288 from_url = config.repo_uri
00289 temp_repo = os.path.join(checkout_dir, distro_stack.name)
00290
00291 for tag_name in [config.release_tag, config.distro_tag]:
00292 if prompt("Would you like to tag %s as %s in %s, [y/n]"%(config.dev_branch, tag_name, from_url)):
00293 subprocess.check_call(['hg', 'tag', '-f', tag_name], cwd=temp_repo)
00294 subprocess.check_call(['hg', 'push'], cwd=temp_repo)
00295 return [tag_name]
00296
00297 def tag_bzr(distro_stack):
00298 config = distro_stack.vcs_config
00299 from_url = config.repo_uri
00300
00301
00302 if prompt("Would you like to tag %s as %s in %s, [y/n]"%(config.dev_branch, config.release_tag, from_url)):
00303 temp_repo = checkout_distro_stack(distro_stack, from_url, config.dev_branch)
00304
00305 subprocess.check_call(['bzr', 'tag', '-d', config.dev_branch,'--force',config.release_tag], cwd=temp_repo)
00306
00307
00308
00309
00310 branch_name = config.release_tag
00311 if prompt("Would you like to create the branch %s as %s in %s, [y/n]"%(config.dev_branch, branch_name, from_url)):
00312 temp_repo = checkout_distro_stack(distro_stack, from_url, config.dev_branch)
00313 subprocess.check_call(['bzr', 'push', '--create-prefix', from_url+"/"+branch_name], cwd=temp_repo)
00314 return [config.distro_tag]
00315
00316 def tag_git(distro_stack, checkout_dir):
00317 config = distro_stack.vcs_config
00318 from_url = config.repo_uri
00319 temp_repo = os.path.join(checkout_dir, distro_stack.name)
00320
00321
00322 if prompt("Would you like to tag %s as %s in %s, [y/n]"%(config.dev_branch, config.release_tag, from_url)):
00323 subprocess.check_call(['git', 'tag', '-f', config.release_tag], cwd=temp_repo)
00324 subprocess.check_call(['git', 'push', '--tags'], cwd=temp_repo)
00325
00326
00327
00328
00329 branch_name = config.distro_tag
00330 if prompt("Would you like to create the branch %s as %s in %s, [y/n]"%(config.dev_branch, branch_name, from_url)):
00331 subprocess.check_call(['git', 'branch', '-f', branch_name, config.dev_branch], cwd=temp_repo)
00332 subprocess.check_call(['git', 'push', from_url, branch_name], cwd=temp_repo)
00333 return [config.distro_tag]
00334
00335 def print_bold(m):
00336 print '\033[1m%s\033[0m'%m
00337
00338 def append_rm_if_exists(url, cmds, msg):
00339 if svn_url_exists(url):
00340 cmds.append(['svn', 'rm', '-m', msg, url])
00341
00342 def checkin_distro_file(name, version, distro_file):
00343 cwd = os.path.dirname(distro_file)
00344 check_call(['svn', 'diff', distro_file], cwd=cwd)
00345 cmd = ['svn', 'ci', '-m', "%s %s"%(name, version), distro_file]
00346 ask_and_call([cmd], cwd=cwd)
00347
00348 def trigger_hudson_source_deb(name, version, distro):
00349 h = hudson.Hudson(SERVER)
00350 parameters = {
00351 'DISTRO_NAME': distro.release_name,
00352 'STACK_NAME': name,
00353 'STACK_VERSION': version,
00354 }
00355 h.build_job('debbuild-sourcedeb', parameters=parameters, token='RELEASE_SOURCE_DEB')
00356
00357 def trigger_debs(distro, os_platform, arch):
00358 h = hudson.Hudson(SERVER)
00359 parameters = {
00360 'DISTRO_NAME': distro,
00361 'STACK_NAME': 'ALL',
00362 'OS_PLATFORM': os_platform,
00363 'ARCH': arch,
00364 }
00365 h.build_job('debbuild-build-debs-%s-%s-%s'%(distro, os_platform, arch), parameters=parameters, token='RELEASE_BUILD_DEBS')
00366
00367 def check_stack_depends(local_path, stack_name):
00368 """
00369 @param local_path: stack directory
00370 @param stack_name: stack name
00371 @raise ReleaseException: if declared dependencies for stack do not match actual depends
00372 """
00373 depends = compute_stack_depends(local_path)
00374 m = roslib.stack_manifest.parse_file(os.path.join(local_path, roslib.stack_manifest.STACK_FILE))
00375 declared = [d.stack for d in m.depends]
00376
00377
00378
00379 for d in m.depends:
00380 try:
00381 m_depend = roslib.stack_manifest.parse_file(roslib.stack_manifest.stack_file(d.stack))
00382 declared.extend([d.stack for d in m_depend.depends])
00383 except:
00384 pass
00385
00386
00387
00388 for d in m.depends:
00389 try:
00390 m_depend = roslib.stack_manifest.parse_file(roslib.stack_manifest.stack_file(d.stack))
00391 declared.extend([d.stack for d in m_depend.depends])
00392 except:
00393 pass
00394
00395
00396
00397
00398 missing = set(depends) - set(declared)
00399 if missing:
00400 raise ReleaseException("Stack's declared dependencies are missing calculated dependencies:\n"+'\n'.join(list(missing)))
00401
00402
00403 def check_version():
00404 url = 'https://code.ros.org/svn/release/trunk/VERSION'
00405 f = urllib2.urlopen(url)
00406 req_version = int(f.read())
00407 f.close()
00408 if VERSION < req_version:
00409 print "This release script is out-of-date.\nPlease upgrade your release and ros_release scripts"
00410 sys.exit(1)
00411
00412 def main():
00413 check_version()
00414
00415 try:
00416
00417 name, version, distro_file = load_sys_args()
00418 try:
00419 local_path = roslib.stacks.get_stack_dir(name)
00420 except:
00421 sys.stderr.write("ERROR: Cannot find local checkout of stack [%s].\nThis script requires a local version of the stack that you wish to release.\n"%(name))
00422 sys.exit(1)
00423
00424
00425
00426 print 'Did you run prerelease tests on your stack?'
00427 if not yes_or_no():
00428 print """Before releasing a stack, you should make sure your stack works well,
00429 and that the new release does not break any already released stacks
00430 that depend on your stack.
00431 Willow Garage offers a pre-release test set that tests your stack and all
00432 released stacks that depend on your stack, on all distributions and architectures
00433 supported by Willow Garage.
00434 You can trigger pre-release builds for your stack on <http://code.ros.org/prerelease/>"""
00435 return
00436
00437
00438 print "Retrieving up-to-date %s"%(distro_file)
00439 subprocess.check_call(['svn', 'up', distro_file])
00440
00441 distro = Distro(distro_file)
00442 load_and_validate_properties(name, distro, distro_file)
00443
00444 distro_stack = distro.stacks[name]
00445
00446
00447 tmp_dir = checkout_stack(name, distro_stack)
00448 confirm_stack_version(local_path, os.path.join(tmp_dir, name), name, version)
00449 check_stack_depends(local_path, name)
00450
00451 distro_stack.update_version(version)
00452 email = get_email()
00453
00454
00455 tarball, control = make_dist_of_dir(tmp_dir, name, version, distro_stack)
00456
00457 if not control['rosdeps']:
00458 sys.stderr.write("""Misconfiguration: control rosdeps are empty.\n
00459 In order to run create.py, the stack you are releasing must be on your current
00460 ROS_PACKAGE_PATH. This is so create.py can access the stack's rosdeps.\n""")
00461 sys.exit(1)
00462
00463 print_bold("Release should be in %s"%(tarball))
00464 if email:
00465 print "including contact e-mail"
00466 control['contact'] = email
00467 else:
00468 print "no valid contact e-mail, will not send build failure messages"
00469
00470
00471 tag_release(distro_stack, tmp_dir)
00472
00473
00474 shutil.rmtree(tmp_dir)
00475
00476
00477 copy_to_server(name, version, tarball, control)
00478
00479
00480 os.remove(tarball)
00481
00482
00483 update_rosdistro_yaml(name, version, distro_file)
00484 checkin_distro_file(name, version, distro_file)
00485
00486
00487 trigger_hudson_source_deb(name, version, distro)
00488
00489 print """
00490
00491 Now:
00492 * update the changelist at http://www.ros.org/wiki/%s/ChangeList
00493 """%name
00494
00495 except ReleaseException as e:
00496 print >> sys.stderr, "ERROR: %s"%str(e)
00497 sys.exit(1)
00498
00499
00500 if __name__ == '__main__':
00501 main()