$search
00001 #!/usr/bin/env python 00002 # Software License Agreement (BSD License) 00003 # 00004 # Copyright (c) 2010, Willow Garage, Inc. 00005 # All rights reserved. 00006 # 00007 # Redistribution and use in source and binary forms, with or without 00008 # modification, are permitted provided that the following conditions 00009 # are met: 00010 # 00011 # * Redistributions of source code must retain the above copyright 00012 # notice, this list of conditions and the following disclaimer. 00013 # * Redistributions in binary form must reproduce the above 00014 # copyright notice, this list of conditions and the following 00015 # disclaimer in the documentation and/or other materials provided 00016 # with the distribution. 00017 # * Neither the name of Willow Garage, Inc. nor the names of its 00018 # contributors may be used to endorse or promote products derived 00019 # from this software without specific prior written permission. 00020 # 00021 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 00022 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 00023 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 00024 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 00025 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 00026 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 00027 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 00028 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 00029 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 00030 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 00031 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 00032 # POSSIBILITY OF SUCH DAMAGE. 00033 # 00034 # Revision $Id: create.py 5643 2010-11-11 20:09:27Z kwc $ 00035 # $Author: kwc $ 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 #NOTE: ROS 1.2 does not have the cwd arg 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 # Pretty-print a string version of the commands 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 # brittle test to make sure that user got the args correct 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 # create a separate directory for new tarball inside of stack-specific directory 00201 # - rename vars for URL pattern 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 # check to see if tarball already exists. This happens in 00215 # multi-distro releases. It's best to reuse the existing tarball. 00216 tarball_url = url + '/' + tarball_name 00217 if svn_url_exists(tarball_url): 00218 # no longer ask user to reuse, always reuse b/c people answer 00219 # this wrong and it breaks things. the correct way to 00220 # invalidate is to delete the tarball manually with SVN from 00221 # now on. 00222 print "reusing existing tarball of release for this distribution" 00223 return 00224 00225 # checkout tarball tree so we can add new tarball 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 # write control data to file 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 # svn add tarball and control file data 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 # delete old svn tag if it's present 00277 append_rm_if_exists(tag_url, cmds, 'Making room for new release') 00278 # svn cp command to create new tag 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 # First create a release tag in the bzr repository. 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 #directly create and push the tag to the repo 00305 subprocess.check_call(['bzr', 'tag', '-d', config.dev_branch,'--force',config.release_tag], cwd=temp_repo) 00306 00307 # Now create a distro branch. 00308 # In bzr a branch is a much better solution since 00309 # branches can be force-updated by fetch. 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 # First create a release tag in the git repository. 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 # Now create a distro branch. In git tags are not overwritten 00327 # during updates, so a branch is a much better solution since 00328 # branches can be force-updated by fetch. 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 # we enable one more level down for forwarded depends 00378 # (e.g. metastacks), but go no further. 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 # we enable one more level down for forwarded depends 00387 # (e.g. metastacks), but go no further. 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 # it is okay for a stack to overdeclare as it may be doing 00396 # something metapackage-like, but it must have every dependency 00397 # that is calculated. 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 # load the args 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 # ask if stack got tested 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 # make sure distro_file is up-to-date 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 #checkout the stack 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 # create the tarball 00455 tarball, control = make_dist_of_dir(tmp_dir, name, version, distro_stack) 00456 #tarball, control = make_dist(name, version, distro_stack) 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 # create the VCS tags 00471 tag_release(distro_stack, tmp_dir) 00472 00473 # Remove checkout dir 00474 shutil.rmtree(tmp_dir) 00475 00476 # checkin the tarball 00477 copy_to_server(name, version, tarball, control) 00478 00479 # cleanup temporary file 00480 os.remove(tarball) 00481 00482 # update the rosdistro file 00483 update_rosdistro_yaml(name, version, distro_file) 00484 checkin_distro_file(name, version, distro_file) 00485 00486 # trigger source deb system 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()