Package backend :: Module mockremote
[hide private]
[frames] | no frames]

Source Code for Module backend.mockremote

  1  #!/usr/bin/python -tt 
  2  # by skvidal 
  3  # This program is free software; you can redistribute it and/or modify 
  4  # it under the terms of the GNU General Public License as published by 
  5  # the Free Software Foundation; either version 2 of the License, or 
  6  # (at your option) any later version. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU Library General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU General Public License 
 14  # along with this program; if not, write to the Free Software 
 15  # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA. 
 16  # copyright 2012 Red Hat, Inc. 
 17   
 18   
 19  # take list of pkgs 
 20  # take single hostname 
 21  # send 1 pkg at a time to host 
 22  # build in remote w/mockchain 
 23  # rsync results back 
 24  # repeat 
 25  # take args from mockchain (more or less) 
 26   
 27   
 28  import fcntl 
 29  import os 
 30  import sys 
 31  import subprocess 
 32   
 33  import ansible.runner 
 34  import optparse 
 35  from operator import methodcaller 
 36  import pipes 
 37  import time 
 38  import socket 
 39  import traceback 
 40  import urllib 
 41   
 42  # where we should execute mockchain from on the remote 
 43  mockchain = '/usr/bin/mockchain' 
 44  # rsync path 
 45  rsync = '/usr/bin/rsync' 
 46   
 47  DEF_REMOTE_BASEDIR = '/var/tmp' 
 48  DEF_TIMEOUT = 3600 
 49  DEF_REPOS = [] 
 50  DEF_CHROOT = None 
 51  DEF_USER = 'mockbuilder' 
 52  DEF_DESTDIR = os.getcwd() 
 53  DEF_MACROS = {} 
 54  DEF_BUILDROOT_PKGS = '' 
55 56 -class SortedOptParser(optparse.OptionParser):
57 '''Optparser which sorts the options by opt before outputting --help'''
58 - def format_help(self, formatter=None):
59 self.option_list.sort(key=methodcaller('get_opt_string')) 60 return optparse.OptionParser.format_help(self, formatter=None)
61
62 63 -def createrepo(path):
64 if os.path.exists(path + '/repodata/repomd.xml'): 65 comm = ['/usr/bin/createrepo_c', '--database', '--ignore-lock', '--update', path] 66 else: 67 comm = ['/usr/bin/createrepo_c', '--database', '--ignore-lock', path] 68 cmd = subprocess.Popen(comm, 69 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 70 out, err = cmd.communicate() 71 return cmd.returncode, out, err
72
73 -def read_list_from_file(fn):
74 lst = [] 75 f = open(fn, 'r') 76 for line in f.readlines(): 77 line = line.replace('\n','') 78 line = line.strip() 79 if line.startswith('#'): 80 continue 81 lst.append(line) 82 83 return lst
84
85 -def log(lf, msg, quiet=None):
86 if lf: 87 now = time.time() 88 try: 89 with open(lf, 'a') as lfh: 90 fcntl.flock(lfh, fcntl.LOCK_EX) 91 lfh.write(str(now) + ':' + msg + '\n') 92 fcntl.flock(lfh, fcntl.LOCK_UN) 93 except (IOError, OSError), e: 94 sys.stderr.write('Could not write to logfile %s - %s\n' % (lf, str(e))) 95 if not quiet: 96 print msg
97
98 -def get_ans_results(results, hostname):
99 if hostname in results['dark']: 100 return results['dark'][hostname] 101 if hostname in results['contacted']: 102 return results['contacted'][hostname] 103 104 return {}
105
106 -def _create_ans_conn(hostname, username, timeout):
107 ans_conn = ansible.runner.Runner(remote_user=username, 108 host_list=hostname + ',', pattern=hostname, forks=1, transport='ssh', 109 timeout=timeout) 110 return ans_conn
111
112 -def check_for_ans_error(results, hostname, err_codes=[], success_codes=[0], 113 return_on_error=['stdout', 'stderr']):
114 """ returns True or False + dict 115 dict includes 'msg' 116 may include 'rc', 'stderr', 'stdout' and any other 117 requested result codes 118 """ 119 err_results = {} 120 121 if 'dark' in results and hostname in results['dark']: 122 err_results['msg'] = "Error: Could not contact/connect to %s." % hostname 123 return (True, err_results) 124 125 error = False 126 127 if err_codes or success_codes: 128 if hostname in results['contacted']: 129 if 'rc' in results['contacted'][hostname]: 130 rc = int(results['contacted'][hostname]['rc']) 131 err_results['rc'] = rc 132 # check for err codes first 133 if rc in err_codes: 134 error = True 135 err_results['msg'] = 'rc %s matched err_codes' % rc 136 elif rc not in success_codes: 137 error = True 138 err_results['msg'] = 'rc %s not in success_codes' % rc 139 elif 'failed' in results['contacted'][hostname] and results['contacted'][hostname]['failed']: 140 error = True 141 err_results['msg'] = 'results included failed as true' 142 143 if error: 144 for item in return_on_error: 145 if item in results['contacted'][hostname]: 146 err_results[item] = results['contacted'][hostname][item] 147 148 return error, err_results
149
150 151 -class MockRemoteError(Exception):
152
153 - def __init__(self, msg):
154 super(MockRemoteError, self).__init__() 155 self.msg = msg
156
157 - def __str__(self):
158 return self.msg
159
160 -class BuilderError(MockRemoteError):
161 pass
162
163 -class DefaultCallBack(object):
164 - def __init__(self, **kwargs):
165 self.quiet = kwargs.get('quiet', False) 166 self.logfn = kwargs.get('logfn', None)
167
168 - def start_build(self, pkg):
169 pass
170
171 - def end_build(self, pkg):
172 pass
173
174 - def start_download(self, pkg):
175 pass
176
177 - def end_download(self, pkg):
178 pass
179
180 - def error(self, msg):
181 self.log("Error: %s" % msg)
182
183 - def log(self, msg):
184 if not self.quiet: 185 print msg
186
187 -class CliLogCallBack(DefaultCallBack):
188 - def __init__(self, **kwargs):
189 DefaultCallBack.__init__(self, **kwargs)
190
191 - def start_build(self, pkg):
192 msg = "Start build: %s" % pkg 193 self.log(msg)
194 195
196 - def end_build(self, pkg):
197 msg = "End Build: %s" % pkg 198 self.log(msg)
199
200 - def start_download(self, pkg):
201 msg = "Start retrieve results for: %s" % pkg 202 self.log(msg)
203
204 - def end_download(self, pkg):
205 msg = "End retrieve results for: %s" % pkg 206 self.log(msg)
207
208 - def error(self, msg):
209 self.log("Error: %s" % msg)
210
211 - def log(self, msg):
212 log(self.logfn, msg, self.quiet)
213
214 -class Builder(object):
215 - def __init__(self, hostname, username, timeout, mockremote, buildroot_pkgs):
216 self.hostname = hostname 217 self.username = username 218 self.timeout = timeout 219 self.chroot = mockremote.chroot 220 self.repos = mockremote.repos 221 self.mockremote = mockremote 222 if buildroot_pkgs is None: 223 self.buildroot_pkgs = '' 224 else: 225 self.buildroot_pkgs = buildroot_pkgs 226 self.checked = False 227 self._tempdir = None 228 # if we're at this point we've connected and done stuff on the host 229 self.conn = _create_ans_conn(self.hostname, self.username, self.timeout) 230 self.root_conn = _create_ans_conn(self.hostname, 'root', self.timeout) 231 # check out the host - make sure it can build/be contacted/etc 232 self.check()
233 234 @property
235 - def remote_build_dir(self):
236 return self.tempdir + '/build/'
237 238 @property
239 - def tempdir(self):
240 if self.mockremote.remote_tempdir: 241 return self.mockremote.remote_tempdir 242 243 if self._tempdir: 244 return self._tempdir 245 246 cmd = '/bin/mktemp -d %s/%s-XXXXX' % (self.mockremote.remote_basedir, 'mockremote') 247 self.conn.module_name = "shell" 248 self.conn.module_args = str(cmd) 249 results = self.conn.run() 250 tempdir = None 251 for hn, resdict in results['contacted'].items(): 252 tempdir = resdict['stdout'] 253 254 # if still nothing then we've broken 255 if not tempdir: 256 raise BuilderError('Could not make tmpdir on %s' % self.hostname) 257 258 cmd = "/bin/chmod 755 %s" % tempdir 259 self.conn.module_args = str(cmd) 260 self.conn.run() 261 self._tempdir = tempdir 262 263 return self._tempdir
264 265 @tempdir.setter
266 - def tempdir(self, value):
267 self._tempdir = value
268
269 - def _get_remote_pkg_dir(self, pkg):
270 # the pkg will build into a dir by mockchain named: 271 # $tempdir/build/results/$chroot/$packagename 272 s_pkg = os.path.basename(pkg) 273 pdn = s_pkg.replace('.src.rpm', '') 274 remote_pkg_dir = os.path.normpath(self.remote_build_dir + '/results/' + self.chroot + '/' + pdn) 275 return remote_pkg_dir
276
277 - def modify_base_buildroot(self):
278 """ modify mock config for current chroot 279 280 packages in buildroot_pkgs are added to minimal buildroot """ 281 if "'%s '" % self.buildroot_pkgs != pipes.quote(str(self.buildroot_pkgs)+' '): 282 # just different test if it contains only alphanumeric characters allowed in packages name 283 raise BuilderError("Do not try this kind of attack on me") 284 self.root_conn.module_name = "lineinfile" 285 if (self.chroot == 'epel-7-x86_64'): 286 self.root_conn.module_args = """dest=/etc/mock/epel-7-x86_64.cfg line="config_opts['chroot_setup_cmd'] = 'install bash bzip2 coreutils cpio diffutils findutils gawk gcc gcc-c++ grep gzip info make patch redhat-release-server redhat-rpm-config rpm-build sed shadow-utils tar unzip util-linux which xz %s'" regexp="^.*chroot_setup_cmd.*$" """ % (self.buildroot_pkgs) 287 else: 288 self.root_conn.module_args = """dest=/etc/mock/%s.cfg line="config_opts['chroot_setup_cmd'] = 'install @buildsys-build %s'" regexp="^.*chroot_setup_cmd.*$" """ % (self.chroot, self.buildroot_pkgs) 289 self.mockremote.callback.log('putting %s into minimal buildroot of %s' % (self.buildroot_pkgs, self.chroot)) 290 results = self.root_conn.run() 291 292 is_err, err_results = check_for_ans_error(results, self.hostname, success_codes=[0], 293 return_on_error=['stdout', 'stderr']) 294 if is_err: 295 self.mockremote.callback.log("Error: %s" % err_results) 296 myresults = get_ans_results(results, self.hostname) 297 self.mockremote.callback.log("%s" % myresults)
298 299
300 - def build(self, pkg):
301 302 # build the pkg passed in 303 # add pkg to various lists 304 # check for success/failure of build 305 # return success/failure,stdout,stderr of build command 306 # returns success_bool, out, err 307 308 success = False 309 self.modify_base_buildroot() 310 311 # check if pkg is local or http 312 dest = None 313 if os.path.exists(pkg): 314 dest = os.path.normpath(self.tempdir + '/' + os.path.basename(pkg)) 315 self.conn.module_name = "copy" 316 margs = 'src=%s dest=%s' % (pkg, dest) 317 self.conn.module_args = str(margs) 318 self.mockremote.callback.log("Sending %s to %s to build" % (os.path.basename(pkg), self.hostname)) 319 320 # FIXME should probably check this but <shrug> 321 self.conn.run() 322 else: 323 dest = pkg 324 325 # construct the mockchain command 326 buildcmd = "%s -r %s -l %s " % (mockchain, pipes.quote(self.chroot), pipes.quote(self.remote_build_dir)) 327 for r in self.repos: 328 if "rawhide" in self.chroot: 329 r = r.replace("$releasever", "rawhide") 330 buildcmd += "-a %s " % pipes.quote(r) 331 332 if self.mockremote.macros: 333 for k, v in self.mockremote.macros.items(): 334 mock_opt = '--define=%s %s' % (k, v) 335 buildcmd += '-m %s ' % pipes.quote(mock_opt) 336 337 buildcmd += dest 338 339 #print ' Running %s on %s' % (buildcmd, hostname) 340 # run the mockchain command async 341 # this runs it sync - FIXME 342 self.mockremote.callback.log('executing: %r' % buildcmd) 343 self.conn.module_name = "shell" 344 self.conn.module_args = str(buildcmd) 345 results = self.conn.run() 346 347 is_err, err_results = check_for_ans_error(results, self.hostname, success_codes=[0], 348 return_on_error=['stdout', 'stderr']) 349 if is_err: 350 return success, err_results.get('stdout', ''), err_results.get('stderr', '') 351 352 # we know the command ended successfully but not if the pkg built successfully 353 myresults = get_ans_results(results, self.hostname) 354 out = myresults.get('stdout', '') 355 err = myresults.get('stderr', '') 356 357 successfile = self._get_remote_pkg_dir(pkg) + '/success' 358 testcmd = '/usr/bin/test -f %s' % successfile 359 self.conn.module_args = str(testcmd) 360 results = self.conn.run() 361 is_err, err_results = check_for_ans_error(results, self.hostname, success_codes=[0]) 362 if not is_err: 363 success = True 364 365 return success, out, err
366
367 - def download(self, pkg, destdir):
368 # download the pkg to destdir using rsync + ssh 369 # return success/failure, stdout, stderr 370 371 success = False 372 rpd = self._get_remote_pkg_dir(pkg) 373 destdir = "'" + destdir.replace("'", "'\\''") + "'" # make spaces work w/our rsync command below :( 374 # build rsync command line from the above 375 remote_src = '%s@%s:%s' % (self.username, self.hostname, rpd) 376 ssh_opts = "'ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no'" 377 command = "%s -avH -e %s %s %s/" % (rsync, ssh_opts, remote_src, destdir) 378 cmd = subprocess.Popen(command, shell=True, 379 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 380 381 # rsync results into opts.destdir 382 out, err = cmd.communicate() 383 if cmd.returncode: 384 success = False 385 else: 386 success = True 387 388 return success, out, err
389
390 - def check(self):
391 # do check of host 392 # set checked if successful 393 # return success/failure, errorlist 394 395 if self.checked: 396 return True, [] 397 398 errors = [] 399 400 try: 401 socket.gethostbyname(self.hostname) 402 except socket.gaierror: 403 raise BuilderError('%s could not be resolved' % self.hostname) 404 405 self.conn.module_name = "shell" 406 self.conn.module_args = str("/bin/rpm -q mock rsync") 407 res = self.conn.run() 408 # check for mock/rsync from results 409 is_err, err_results = check_for_ans_error(res, self.hostname, success_codes=[0]) 410 if is_err: 411 if 'rc' in err_results: 412 errors.append('Warning: %s does not have mock or rsync installed' % self.hostname) 413 else: 414 errors.append(err_results['msg']) 415 416 417 # test for path existence for mockchain and chroot config for this chroot 418 self.conn.module_name = "shell" 419 self.conn.module_args = "/usr/bin/test -f %s && /usr/bin/test -f /etc/mock/%s.cfg" % (mockchain, self.chroot) 420 res = self.conn.run() 421 422 is_err, err_results = check_for_ans_error(res, self.hostname, success_codes=[0]) 423 if is_err: 424 if 'rc' in err_results: 425 errors.append('Warning: %s lacks mockchain or the chroot %s' % (self.hostname, self.chroot)) 426 else: 427 errors.append(err_results['msg']) 428 429 if not errors: 430 self.checked = True 431 else: 432 msg = '\n'.join(errors) 433 raise BuilderError(msg)
434
435 436 -class MockRemote(object):
437 - def __init__(self, builder=None, user=DEF_USER, timeout=DEF_TIMEOUT, 438 destdir=DEF_DESTDIR, chroot=DEF_CHROOT, cont=False, recurse=False, 439 repos=DEF_REPOS, callback=None, 440 remote_basedir=DEF_REMOTE_BASEDIR, remote_tempdir=None, 441 macros=DEF_MACROS, 442 buildroot_pkgs=DEF_BUILDROOT_PKGS):
443 444 self.destdir = destdir 445 self.chroot = chroot 446 self.repos = repos 447 self.cont = cont 448 self.recurse = recurse 449 self.callback = callback 450 self.remote_basedir = remote_basedir 451 self.remote_tempdir = remote_tempdir 452 self.macros = macros 453 454 if not self.callback: 455 self.callback = DefaultCallBack() 456 457 self.callback.log("Setting up builder: %s" % builder) 458 self.builder = Builder(builder, user, timeout, self, buildroot_pkgs) 459 460 if not self.chroot: 461 raise MockRemoteError("No chroot specified!") 462 463 464 self.failed = [] 465 self.finished = [] 466 self.pkg_list = []
467 468
469 - def _get_pkg_destpath(self, pkg):
470 s_pkg = os.path.basename(pkg) 471 pdn = s_pkg.replace('.src.rpm', '') 472 resdir = '%s/%s/%s' % (self.destdir, self.chroot, pdn) 473 resdir = os.path.normpath(resdir) 474 return resdir
475
476 - def build_pkgs(self, pkgs=None):
477 478 if not pkgs: 479 pkgs = self.pkg_list 480 481 built_pkgs = [] 482 downloaded_pkgs = {} 483 484 try_again = True 485 to_be_built = pkgs 486 while try_again: 487 self.failed = [] 488 just_built = [] 489 for pkg in to_be_built: 490 pkg = urllib.unquote("%s" % pkg) 491 if pkg in just_built: 492 self.callback.log("skipping duplicate pkg in this list: %s" % pkg) 493 continue 494 else: 495 just_built.append(pkg) 496 497 p_path = self._get_pkg_destpath(pkg) 498 499 # check the destdir to see if these pkgs need to be built 500 if os.path.exists(p_path): 501 if os.path.exists(p_path + '/success'): 502 self.callback.log("Skipping already built pkg %s" % os.path.basename(pkg)) 503 continue 504 # if we're asking to build it and it is marked as fail - nuke 505 # the failure and try rebuilding it 506 elif os.path.exists(p_path + '/fail'): 507 os.unlink(p_path + '/fail') 508 509 # off to the builder object 510 # building 511 self.callback.start_build(pkg) 512 b_status, b_out, b_err = self.builder.build(pkg) 513 self.callback.end_build(pkg) 514 515 # downloading 516 self.callback.start_download(pkg) 517 # mockchain makes things with the chroot appended - so suck down 518 # that pkg subdir from w/i that location 519 chroot_dir = os.path.normpath(self.destdir + '/' + self.chroot) 520 d_ret, d_out, d_err = self.builder.download(pkg, chroot_dir) 521 if not d_ret: 522 msg = "Failure to download %s: %s" % (pkg, d_out + d_err) 523 if not self.cont: 524 raise MockRemoteError, msg 525 self.callback.error(msg) 526 527 self.callback.end_download(pkg) 528 # write out whatever came from the builder call into the destdir/chroot 529 if not os.path.exists(chroot_dir): 530 os.makedirs(self.destdir + '/' + self.chroot) 531 r_log = open(chroot_dir + '/mockchain.log', 'a') 532 fcntl.flock(r_log, fcntl.LOCK_EX) 533 r_log.write('\n\n%s\n\n' % pkg) 534 r_log.write(b_out) 535 if b_err: 536 r_log.write('\nstderr\n') 537 r_log.write(b_err) 538 fcntl.flock(r_log, fcntl.LOCK_UN) 539 r_log.close() 540 541 542 # checking where to stick stuff 543 if not b_status: 544 if self.recurse: 545 self.failed.append(pkg) 546 self.callback.error("Error building %s, will try again" % os.path.basename(pkg)) 547 else: 548 msg = "Error building %s\nSee logs/resultsin %s" % (os.path.basename(pkg), self.destdir) 549 if not self.cont: 550 raise MockRemoteError, msg 551 self.callback.error(msg) 552 553 else: 554 self.callback.log("Success building %s" % os.path.basename(pkg)) 555 built_pkgs.append(pkg) 556 # createrepo with the new pkgs 557 for d in [self.destdir, chroot_dir]: 558 rc, out, err = createrepo(d) 559 if err.strip(): 560 self.callback.error("Error making local repo: %s" % d) 561 self.callback.error("%s" % err) 562 #FIXME - maybe clean up .repodata and .olddata here? 563 564 if self.failed: 565 if len(self.failed) != len(to_be_built): 566 to_be_built = self.failed 567 try_again = True 568 self.callback.log('Trying to rebuild %s failed pkgs' % len(self.failed)) 569 else: 570 self.callback.log("Tried twice - following pkgs could not be successfully built:") 571 for pkg in self.failed: 572 msg = pkg 573 if pkg in downloaded_pkgs: 574 msg = downloaded_pkgs[pkg] 575 self.callback.log(msg) 576 577 try_again = False 578 else: 579 try_again = False
580
581 582 583 -def parse_args(args):
584 585 parser = SortedOptParser("mockremote -b hostname -u user -r chroot pkg pkg pkg") 586 parser.add_option('-r', '--root', default=DEF_CHROOT, dest='chroot', 587 help="chroot config name/base to use in the mock build") 588 parser.add_option('-c', '--continue', default=False, action='store_true', 589 dest='cont', 590 help="if a pkg fails to build, continue to the next one") 591 parser.add_option('-a', '--addrepo', default=DEF_REPOS, action='append', 592 dest='repos', 593 help="add these repo baseurls to the chroot's yum config") 594 parser.add_option('--recurse', default=False, action='store_true', 595 help="if more than one pkg and it fails to build, try to build the rest and come back to it") 596 parser.add_option('--log', default=None, dest='logfile', 597 help="log to the file named by this option, defaults to not logging") 598 parser.add_option("-b", "--builder", dest='builder', default=None, 599 help="builder to use") 600 parser.add_option("-u", dest="user", default=DEF_USER, 601 help="user to run as/connect as on builder systems") 602 parser.add_option("-t", "--timeout", dest="timeout", type="int", 603 default=DEF_TIMEOUT, help="maximum time in seconds a build can take to run") 604 parser.add_option("--destdir", dest="destdir", default=DEF_DESTDIR, 605 help="place to download all the results/packages") 606 parser.add_option("--packages", dest="packages_file", default=None, 607 help="file to read list of packages from") 608 parser.add_option("-q", "--quiet", dest="quiet", default=False, action="store_true", 609 help="output very little to the terminal") 610 611 opts, args = parser.parse_args(args) 612 613 if not opts.builder: 614 sys.stderr.write("Must specify a system to build on") 615 sys.exit(1) 616 617 if opts.packages_file and os.path.exists(opts.packages_file): 618 args.extend(read_list_from_file(opts.packages_file)) 619 620 #args = list(set(args)) # poor man's 'unique' - this also changes the order 621 # :( 622 623 if not args: 624 sys.stderr.write("Must specify at least one pkg to build") 625 sys.exit(1) 626 627 if not opts.chroot: 628 sys.stderr.write("Must specify a mock chroot") 629 sys.exit(1) 630 631 for url in opts.repos: 632 if not (url.startswith('http://') or url.startswith('https://') or url.startswith('file://')): 633 sys.stderr.write("Only http[s] or file urls allowed for repos") 634 sys.exit(1) 635 636 return opts, args
637
638 639 #FIXME 640 # play with createrepo run at the end of each build 641 # need to output the things that actually worked :) 642 643 644 -def main(args):
645 646 # parse args 647 opts, pkgs = parse_args(args) 648 649 if not os.path.exists(opts.destdir): 650 os.makedirs(opts.destdir) 651 652 try: 653 # setup our callback 654 callback = CliLogCallBack(logfn=opts.logfile, quiet=opts.quiet) 655 # our mockremote instance 656 mr = MockRemote(builder=opts.builder, user=opts.user, 657 timeout=opts.timeout, destdir=opts.destdir, chroot=opts.chroot, 658 cont=opts.cont, recurse=opts.recurse, repos=opts.repos, 659 callback=callback) 660 661 # FIXMES 662 # things to think about doing: 663 # output the remote tempdir when you start up 664 # output the number of pkgs 665 # output where you're writing things to 666 # consider option to sync over destdir to the remote system to use 667 # as a local repo for the build 668 # 669 670 if not opts.quiet: 671 print "Building %s pkgs" % len(pkgs) 672 673 mr.build_pkgs(pkgs) 674 675 if not opts.quiet: 676 print "Output written to: %s" % mr.destdir 677 678 except MockRemoteError, e: 679 print >> sys.stderr, "Error on build:" 680 print >> sys.stderr, str(e) 681 return
682 683 684 if __name__ == '__main__': 685 main(sys.argv[1:]) 686