#!/usr/bin/python
# Packman Build Server uploader
# Author: Pascal Bleser <guru@unixtech.be>
#
# Help on command-line options: run the script with --help
#
# Does the following:
# - creates a temporary directory
# - auto-detects the spec file in the current directory (if there is only one),
#   may also be specified as an argument
# - parses the spec file (using rpm-python) to get the list of sources
# - copies the spec file and the sources (Source/Patch) into the temporary directory;
#   if source files cannot be found in the current directory and caching is enabled
#   (see CONFIGURATION), it:
#   * looks it up in a local cache directory
#   * if it's a http:// or ftp:// URL, it downloads it and stores it in the
#     local cache directory (if the URL starts with
#     http://(pr)?downloads.(sourceforge|sf).net/
#     the URL is replaced to a direct download URL on your preferred sourceforge
#     mirror (see CONFIGURATION)
# - uses rsync to copy the local temporary directory to the pmbs server
# - triggers a job on the pmbs server by touching a job file
# - removes the local temporary directory
#
# ARGUMENTS
# - passing one or more SUSE version numbers (e.g. 10.3) means:
#   "only build for those versions",
#   e.g.: pm-upload-job.py 10.2 10.3
#         => means: only build for 10.2 and 10.3
# - passing one or more SUSE version numbers prefixed by ^ means:
#   "build for all except those versions",
#   e.g.: pm-upload-job.py ^10.0 ^10.3
#         => means: build for all except 10.0 and 10.3
# - passing one or more architecture identifiers (e.g. x86_64, i586) means:
#   "only build for those architectures",
#   e.g.: pm-upload-job.py x86_64 i686
#         => means: only build for x86_64 and i686
# - passing one or more architecture identifiers prefixed by ^ means:
#   "build for all except those architectures",
#   e.g.: pm-upload-job.py ^x86_64
#         => means: build for all except x86_64
# - of course, those parameters can be combined,
#   e.g.: pm-upload-job.py 10.2 10.3 ^x86_64
#         => means: build only on 10.2 and 10.3 with all architectures except x86_64
#
# CONFIGURATION
# The following options are mandatory in ~/.pmbsrc:
# ---8<---------------------------------------
# [packager]
# name = John Doe
# email = john.doe@links2linux.de
# ---8<---------------------------------------
#
# Additionally, the following options/sections may be added:
# ---8<---------------------------------------
# [download]
# sourceforge = http://surfnet.dl.sourceforge.net/
# [cache]
# dir = ~/download/projects
# ---8<---------------------------------------
# 
# The "sourceforge" option in the [download] section defines the preferred
# sourceforge.net mirror to use for downloading non-cached sources that have an HTTP URL.
# The script will modify the http://(pr)?downloads.(sourceforge|sf).net/... URL
# accordingly to use the mirror defined in the configuration.
# The default is to use surfnet.dl.sf.net
#
# The "dir" option in the [cache] section enables local download caching.
# After being downloaded from the Internet, sources+patches are stored into that
# directory (with a subdir for the project) and picked up from there on subsequent
# transfers.
# 

import sys
import os
import tempfile
import glob
import re
import shutil
import urllib2
urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()))
from ConfigParser import SafeConfigParser
from optparse import OptionParser
import rpm

VERSION = "1.4.1"

DEFAULT_DISTS = [
        ('openSUSE', '11.1'),
        ('openSUSE', '11.0'),
        ('openSUSE', '10.3'),
        ('openSUSE', '10.2'),
        ]
ALL_DISTS = [
        ] + DEFAULT_DISTS
        
ALL_ARCHS = ('i586', 'i686', 'x86_64') # don't add noarch here
DEFAULT_ARCHS = ['i586', 'x86_64'] # don't add noarch here

macros = {
    'kde4': ['cmake_kde4', 'make_jobs', 'kde_post_install', 'create_exclude_filelist', 'create_subdir_filelist', 'kde4_runtime_requires'],
    'gconf2': ['gconf_schemas_prereq', 'end_gconf_schemas', 'def_gconf_schemas', '_add_gconf_schemas', 'add_gconf_schemas', 'find_gconf_schemas'],
    'jpackage': ['_jvmlibdir', '_jvmdatadir', '_jvmsysconfdir', '_jvmcommonlibdir', '_jvmcommondatadir', '_jvmcommonsysconfdir', '_mavendepmapdir', '%_mavendepmapfragdir', 'add_to_maven_depmap', 'update_maven_depmap', '_iconstheme', '_iconsbasedir', '_icons16dir', '_icons22dir', '_icons48dir', '_icons64dir', '_icons96dir', '_icons192dir', '_iconsscaldir', '_miconsdir', '_iconsdir', '_liconsdir', '_desktopdir', '_menudir'],
    'kernel-source': ['_kernel_module_package(n:v:r:s:f:Xp:)', 'kernel_module_package(n:v:r:s:f:xp:)', 'suse_kernel_module_package(n:v:r:s:f:xp:'],
    'mkinitrd': ['install_mkinitrd'],
    'rubygems': ['gem_install', 'gem_cleanup'],
    'tcl': ['tcl_version', 'tcl_archdir', 'tcl_noarchdir', 'tclscriptdir']
}

touch_server = 'pmbs'
touch_basedir = '/srv/pmbs'
upload_baseurl = '%s:/srv/pmbs/files/' % touch_server
rsync_flags = ['--archive', '--verbose', '--human-readable', '--itemize-changes', '--chmod=ug=rwX,o=rX']
rsync_checksum = True
sourceforge_baseurl = 'http://surfnet.dl.sourceforge.net/'
configfile = os.path.expanduser('~/.pmbsrc')
email_default = False
keep_default = False
download_default = False
baselibs_default = False

BLOCKSIZE = 1024
BLOCKDOTS = 50
BLOCKROW = 10

def bool2value(bool_flag):
    if bool_flag:
        return 1
    return 0

def download_urllib2(url, target):
    # set fileout to None here, will be opened on demand,
    # to avoid creating a 0 length file if the download fails
    fileout = None
    fileout_filename = os.path.join(os.path.dirname(target), '.' + os.path.basename(target) + '.part')
    u = urllib2.urlopen(url)
    try:
        for header in u.info().headers:
            if header.startswith('Content-Length:'):
                content_length = re.sub(r"^Content-Length:\s*", "", header).rstrip("\r\n ")
                print "MIME=%s size=%s bytes" % (u.info().gettype(), content_length)
                break
            pass
        pass
    except AttributeError:
        # ignore, it's just not a HTTP connection
        pass
    blocks = 0
    rows = 0
    totalblocks = 0
    while True:
        bytes = u.read(BLOCKSIZE)
        if fileout == None:
            fileout = open(fileout_filename, "wb")
            pass
        fileout.write(bytes)
        if len(bytes) < BLOCKSIZE or (not u) or u == "":
            break
        totalblocks += 1
        blocks += 1
        if blocks >= BLOCKDOTS:
            sys.stdout.write(".")
            rows += 1
            if rows >= BLOCKROW:
                sys.stdout.write(" %d kb\n" % (totalblocks * (BLOCKSIZE / 1024)))
                rows = 0
                pass
            blocks = 0
            sys.stdout.flush()
            pass
        pass
    fileout.close()
    u.close()
    sys.stdout.write("\n")
    os.renames(fileout_filename, target)
    pass

def validate(spec):
    badre = re.compile(r'^\s*(packager|distribution|vendor)\s*:', re.IGNORECASE)
    nrfbre = re.compile(r'^# norootforbuild\s*$')
    brre = re.compile(r'^BuildRoot\s*:', re.IGNORECASE)
    has_nrfb = False
    has_br = False
    valid = True
    s = open(spec, 'r')
    l = 0
    for line in map(lambda x: x.rstrip(), s.readlines()):
        l += 1
        m = badre.search(line)
        if m:
            print >>sys.stderr, "INVALID spec file %s: line %d: offending tag: %s" % (specfile, l, m.group(1))
            valid = False
            pass
        if nrfbre.search(line):
            has_nrfb = True
            pass
        if brre.search(line):
            has_br = True
            pass
        pass
    s.close()
    if not has_nrfb:
        print >>sys.stderr, "INVALID spec file %s: missing \"# norootforbuild\"" % specfile
        valid = False
        pass
    if not has_br:
        print >>sys.stderr, "INVALID spec file %s: missing \"BuildRoot: %{_tmpdir}/build-%{name}-%{version}\"" % specfile
        valid = False
        pass
    return valid

optionParser = OptionParser(usage="%prog [options] [spec file]",
        version="%%prog %s" % VERSION,
        description="Command-line frontend for the Packman Build Server")
optionParser.add_option('', '--high', action='store_const', const='jobs-high', dest='queue', default='jobs',
        help="push job into the high priority queue")
optionParser.add_option('', '--low', action='store_const', const='jobs-low', dest='queue', default='jobs',
        help="push job into the low priority queue")
optionParser.add_option('', '--nightly', action='store_const', const='jobs-nightly', dest='queue', default='jobs',
        help="push job into the nightly queue")
optionParser.add_option('', '--force', action='store_true', dest='force', default=False,
        help="create build.conf even if it exists")
optionParser.add_option('', '--force-download', action='store_true', dest='force_download',
        default=os.path.exists('.pm.forcedownload'),
        help="force downloading remote sources even if available in cache")
optionParser.add_option('', '--email', action='store_true', dest='email', default=None,
        help="send email on job completion (default)")
optionParser.add_option('', '--no-email', action='store_false', dest='email', default=None,
        help="do NOT send email on job completion")
optionParser.add_option('', '--commit', action='store_true', dest='commit', default=not os.path.exists('.pm.no.commit'),
        help="commit to Packman website database on success (default)")
optionParser.add_option('', '--no-commit', action='store_false', dest='commit',
        help="do NOT commit to Packman website database")
optionParser.add_option('', '--upload', action='store_true', dest='upload', default=not os.path.exists('.pm.no.upload'),
        help="upload RPM files to pluto on success (default)")
#optionParser.add_option('', '--upload-match', action='store', dest='uploadmatch', default=None,
#        help="only upload RPM files to pluto on success that match the specified regular expression")
optionParser.add_option('', '--no-upload', action='store_false', dest='upload',
        help="do NOT upload RPM files to pluto on success")
optionParser.add_option('', '--baselibs', action='store_true', dest='baselibs', default=None,
        help="generate baselibs")
optionParser.add_option('', '--no-baselibs', action='store_false', dest='baselibs', default=None,
        help="do NOT generate baselibs")
optionParser.add_option('', '--copyfiles', action='store_true', dest='copyfiles', default=os.path.exists('.pm.copyfiles'),
        help="copy resulting package files into /srv/pmbs/packages/<name>/ on the server")
optionParser.add_option('', '--keep-temp', action='store_true', dest='keep_temp', default=False,
        help="keep the local temporary directory (default: remove)")
optionParser.add_option('', '--no-job', action='store_false', dest='startjob', default=True,
        help="don't start the job on the build server")
optionParser.add_option('', '--only-buildconf', action='store_true', dest='only_build_conf', default=False,
        help="only create build.conf and exit (implies --keep-temp and --force)")
optionParser.add_option('', '--progress', action='store_true', dest='rsyncprogress', default=False,
        help="pass --progress to rsync when uploading (more verbose)")
optionParser.add_option('', '--packager', action='store', type='string', dest='packager', default=None,
        help="bypass packager settings in .pmbsrc for this job", metavar="\"firstname lastname <email>\"")
optionParser.add_option('', '--keep', action='store_true', dest='keep', default=None,
        help="keep old releases in the Packman database")
optionParser.add_option('', '--no-delete', action='store_false', dest='rsync_delete', default=True,
        help="tell rsync NOT to delete files that have been removed locally")
optionParser.add_option('', '--download', action='store_true', dest='download', default=None,
        help="download missing sources and send them to the server")
optionParser.add_option('', '--no-download', action='store_false', dest='download', default=None,
        help="do NOT download missing sources and send them to the server (default)")
optionParser.add_option('', '--test', action='store_true', dest='test_build', default=os.path.exists('.pm.test'),
        help="mark the job as a test build (no sign, no createrepo, no upload, no commit)")
optionParser.add_option('', '--split', action='store_true', dest='split', default=False,
        help="split the job into several jobs (one per distribution)")
optionParser.add_option('', '--no-split', action='store_false', dest='split', default=False,
        help="send a single job for all distributions at once")
optionParser.add_option('', '--overwrite', action='store_true', dest='overwrite', default=False,
        help="overwrite existing files in the package publication tree")
optionParser.add_option('', '--no-validation', action='store_false', dest='validate', default=True,
        help="don't validate the spec file (are you sure you'd want to use this ?)")

(options, args) = optionParser.parse_args(sys.argv)

try:
    print "Reading configuration file %s" % configfile
    config = SafeConfigParser()
    config.read(configfile)
except Exception, e:
    sys.stderr.write("ERROR: failed to parse config file %s: %s\n" % (configfile, e.message))
    sys.exit(1)
    pass

if options.packager:
    packager = options.packager
    print "warning: forcing one-shot packager \"%s\" for this job" % packager
elif config.has_option('packager', 'name') and config.has_option('packager', 'email'):
    packager = "%s <%s>" % (config.get('packager', 'name'), config.get('packager', 'email'))
else:
    sys.stderr.write("configuration options \"name\" and/or \"email\" missing in the [packager] section of %s\n" % configfile)
    sys.exit(1)
    pass

if config.has_option('cache', 'dir'):
    cache_dir = os.path.expanduser(config.get('cache', 'dir'))
    print "Using cache dir %s" % cache_dir
else:
    cache_dir = None
    print "Cache disabled"
    pass

if config.has_option('upload', 'checksum'):
    rsync_checksum = config.getboolean('upload', 'checksum')
    pass

if options.email == None:
    if config.has_option('notification', 'email'):
        options.email = config.getboolean('notification', 'email')
    else:
        options.email = email_default
        pass
    pass

if options.keep == None:
    if config.has_option('database', 'keep'):
        options.keep = config.getboolean('database', 'keep')
    else:
        options.keep = keep_default
        pass
    pass

if options.download == None:
    if config.has_option('upload', 'download'):
        options.download = config.getboolean('upload', 'download')
    else:
        options.download = download_default
        pass
    pass

#if options.uploadmatch == None:
#    if os.path.exists('.pm.uploadmatch'):
#        pum = file('.pm.uploadmatch', 'r')
#        options.uploadmatch = pum.readline().strip()
#        pum.close()
#        pass
#    pass

# --force-download implies --download:
if options.force_download:
    options.download = True
    pass

specfile = None
dist_params = []
dist_params_skip = []
arch_params = []
arch_params_skip = []
add_default_dists = True
add_default_archs = True
extra_dist_params = []
for arg in args[1:]:
    if arg.endswith('.spec'):
        specfile = arg
    elif re.match(r"^[\^\-](\d+)\.(\d+)$", arg):
        dist_params_skip.append(arg[1:])
    elif re.match(r"^(\d+)\.(\d+)$", arg):
        dist_params.append(arg)
        add_default_dists = False
    elif re.match(r"^\+(\d+)\.(\d+)$", arg):
        extra_dist_params.append(arg[1:])
    elif (arg.startswith('^') or arg.startswith('-')) and (arg[1:] in ALL_ARCHS + ('noarch',)):
        arch_params_skip.append(arg[1:])
    elif arg in ALL_ARCHS + ('noarch',):
        arch_params.append(arg)
        add_default_archs = False
    else:
        sys.stderr.write("ERROR: unsupported argument \"%s\"" % arg)
        sys.exit(1)
        pass
    pass

if not add_default_dists and len(extra_dist_params) > 0:
    dist_params += extra_dist_params
    pass

if not specfile:
    # try to discover the spec file
    spec_candidates = glob.glob('*.spec')
    if len(spec_candidates) > 1:
        sys.stderr.write("ERROR: no spec file specified as argument and found more than one spec file in current directory\n")
        sys.exit(1)
    elif len(spec_candidates) == 0:
        sys.stderr.write("ERROR: no spec file specified as argument and no spec file found in current directory\n")
        sys.exit(1)
    else:
        specfile = spec_candidates[0]
        pass
    pass

# apply configuration items
if config.has_option('download', 'sourceforge'):
    sourceforge_baseurl = config.get('download', 'sourceforge')
    pass
# some black magic here
if not sourceforge_baseurl.endswith('/'):
    sourceforge_baseurl += '/'
    pass
if options.only_build_conf:
    options.keep_temp = True
    options.force = True
    pass
# and some validation
if options.force_download and not cache_dir:
    sys.stderr.write("ERROR: specified --force-download but cache directory not configured\n")
    sys.exit(1)
    pass

# name of the project = name of the spec file - ".spec"
project = os.path.splitext(specfile)[0]
print "[%s] Using spec file %s" % (project, specfile)

# add macro placeholders for validation
for macrofile, macrolist in macros.iteritems():
    if not os.path.exists('/etc/rpm/macros.%s' % macrofile):
        for m in macrolist:
            rpm.addMacro(m, '%{nil}')
        pass
    pass

# parse spec file to discover sources
sources = []
ts = rpm.ts()
spec = ts.parseSpec(specfile)
for (source, source_number, source_type) in spec.sources():
    if source_type == 1:
        source_type_name = 'source'
    elif source_type == 2:
        source_type_name = 'patch'
    else:
        source_type_name = '(unknown)'
        pass
    print "\t%s %d: %s" % (source_type_name, source_number, source)
    sources.append(source)
    pass

# validate spec file
if options.validate:
    if not validate(specfile):
        print >>sys.stderr, "VALIDATION FAILED, aborting."
        sys.exit(1)
        pass
    pass

baselibs = options.baselibs
if options.baselibs == None:
    if os.path.exists('.pm.baselibs') or os.path.exists('baselibs.conf'):
        baselibs = True
    else:
        baselibs = baselibs_default
        pass
    pass

tmpdir = tempfile.mkdtemp('', "pmbsjob-%s-" % (project))
try:
    # copy spec file
    print "%s -> %s" % (specfile, tmpdir)
    shutil.copy(specfile, tmpdir)
    # copy sources
    for source in sources:
        source_local = os.path.basename(source)
        succeeded = False
        is_remote = source.startswith('http://') or source.startswith('ftp://')
        if (not (is_remote and options.force_download)) and os.path.exists(source_local):
            print "%s -> %s" % (source_local, tmpdir)
            shutil.copy(source_local, tmpdir)
            succeeded = True
        else:
            if options.download and cache_dir:
                cache_sub = project
                if not options.force_download:
                    # look it up in the cache dir
                    candidates = []
                    candidates.append(os.path.join(cache_dir, cache_sub, source_local))
                    candidates.append(os.path.join(cache_dir, source_local))
                    for candidate in candidates:
                        print "{{ cache lookup: trying %s:" % candidate,
                        if os.path.exists(candidate):
                            print "found"
                            print "(cached) %s -> %s" % (candidate, tmpdir)
                            shutil.copy(candidate, tmpdir)
                            succeeded = True
                            break
                        else:
                            print "not found"
                            pass
                        pass
                    pass
                # try to download it if it's an URL
                if (not succeeded) and is_remote and options.download:
                # special hack for sourceforge
                    sfhit = re.search(r"http://(pr)?downloads\.(sourceforge|sf)\.net/+(?P<path>.+)$", source, re.IGNORECASE)
                    if sfhit:
                        url = sourceforge_baseurl + sfhit.group('path')
                    else:
                        url = source
                    pass

                    target = os.path.join(cache_dir, cache_sub, source_local)
                    print "Downloading %s => %s" % (url, target)
                    if not os.path.exists(os.path.join(cache_dir, cache_sub)):
                        os.makedirs(os.path.join(cache_dir, cache_sub))
                        pass
                    download_urllib2(url, target)
                    print "(downloaded, cached) %s -> %s" % (target, tmpdir)
                    shutil.copy(target, tmpdir)
                    succeeded = True
                    pass
                pass
            else:
                if options.download:
                    sys.stderr.write("ERROR: source file \"%s\" not found in local directory and no cache configured in %s\n" \
                        % (source_local, configfile))
                    pass
                pass
            pass
        if not succeeded:
            if not options.download:
                print "Not uploading source: %s" % source
            else:
                sys.stderr.write("ERROR: failed to copy source \"%s\" (\"%s\")\n" % (source_local, source))
                if not options.download_source:
                    sys.exit(1)
                    pass
                pass
            pass
        pass

    if baselibs:
        if os.path.exists('baselibs.conf'):
            print "%s -> %s" % ('baselibs.conf', tmpdir)
            shutil.copy('baselibs.conf', tmpdir)
        else:
            if os.path.exists('.pm.baselibs'):
                pmbaselibs = open('.pm.baselibs', 'r')
                baselibs_packages = filter(lambda x: len(x) > 0 and not x.startswith('#'), map(lambda x: x.strip(), pmbaselibs.readlines()))
                pmbaselibs.close()
            else:
                baselibs_packages = []
                pass
            if len(baselibs_packages) < 1:
                baselibs_packages = [ project ]
                pass
            print "Creating %s/baselibs.conf: %s" % (tmpdir, " ".join(baselibs_packages))
            baselibsconf = open(os.path.join(tmpdir, 'baselibs.conf'), 'w')
            for p in baselibs_packages:
                print >>baselibsconf, p
                pass
            baselibsconf.close()
            pass
        pass

    if os.path.exists('build.conf') and not options.force:
        print "%s -> %s" % ('build.conf', tmpdir)
        shutil.copy('build.conf', tmpdir)
    else:
        print "Packager: %s" % packager

        # look for .pm.no.* files to generate build.conf
        dists_display = []
        dists = []
        if len(dist_params) > 0:
            for (dist_name, dist_version) in ALL_DISTS:
                if dist_version in dist_params and (dist_name, dist_version) not in dists:
                    dists.append((dist_name, dist_version))
                    dists_display.append('%s %s' % (dist_name, dist_version))
                    pass
                pass
            pass
        else:
            dists_to_skip = dist_params_skip
            for pm_no_file in glob.glob('.pm.no.*'):
                dists_to_skip.append(pm_no_file[len('.pm.no.'):]) # cut off .pm.no. prefix
                pass
            if add_default_dists:
                for (dist_name, dist_version) in DEFAULT_DISTS:
                    if not dist_version in dists_to_skip:
                        dists.append((dist_name, dist_version))
                        dists_display.append('%s %s' % (dist_name, dist_version))
                        pass
                    pass
                for (dist_name, dist_version) in ALL_DISTS:
                    if dist_version in extra_dist_params and (dist_name, dist_version) not in dists:
                        dists.append((dist_name, dist_version))
                        dists_display.append('%s %s' % (dist_name, dist_version))
                        pass
                    pass
                pass
            pass

        print "Distributions to build: %s" % ", ".join(dists_display)

        # look for BuildArch in spec file
        spec = open(specfile, "r")
        arch = None
        for line in spec.readlines():
            hits = re.match(r"^\s*buildarch(itecture)?\s*:\s*(?P<arch>[^\s]+)\s*$", line, re.IGNORECASE)
            if hits:
                buildarch_arch = hits.group('arch')
                if buildarch_arch != 'noarch' and not buildarch_arch in ALL_ARCHS:
                    sys.stderr.write("ERROR: invalid BuildArch found in %s: \"%s\"\n" % (specfile, buildarch_arch))
                    sys.exit(1)
                else:
                    print "BuildArch forced to %s by spec file tag" % buildarch_arch
                    arch = [ buildarch_arch ]
                    pass
                break
            pass
        if not arch:
            if len(arch_params) > 0:
                arch = arch_params
            else:
                arch = []
                arch_to_skip = arch_params_skip
                for pm_no_file in glob.glob('.pm.no.*'):
                    arch_to_skip.append(pm_no_file[7:]) # cut off .pm.no. prefix
                    pass
                for a in DEFAULT_ARCHS:
                    if not a in arch_to_skip:
                        arch.append(a)
                        pass
                    pass
                pass
            pass
        spec.close()
        print "Architectures to build: %s" % ", ".join(arch)

        if os.path.exists('.pm.keep'):
            options.keep = True
            keepfile = open('.pm.keep', 'r')
            keepmasks = map(lambda x: x[:-1], keepfile.readlines())
            keepfile.close()
            # validate:
            for x in keepmasks:
                re.compile(x)
                pass
            pass
        else:
            keepmasks = []
            pass
        buildconf_filename = os.path.join(tmpdir, "build.conf")
        buildconf = open(buildconf_filename, "w")
        buildconf.write("[info]\n")
        buildconf.write("specfile:       %s\n" % specfile)
        buildconf.write("packager:       %s\n" % packager)
        buildconf.write("upload:         %d\n" % bool2value(options.upload))
        #if options.uploadmatch != None:
        #    buildconf.write("uploadmatch:    %s\n" % options.uploadmatch)
        buildconf.write("keep:           %d\n" % bool2value(options.keep))
        if len(keepmasks) > 0:
            buildconf.write("keepmasks:      %s\n" % ",".join(keepmasks))
            pass
        buildconf.write("overwrite:      %d\n" % bool2value(options.overwrite))
        buildconf.write("commit:         %d\n" % bool2value(options.commit))
        buildconf.write("email:          %d\n" % bool2value(options.email))
        buildconf.write("baselibs:       %d\n" % bool2value(baselibs))
        buildconf.write("testbuild:      %d\n" % bool2value(options.test_build))
        buildconf.write("copyfiles:      %d\n" % bool2value(options.copyfiles))
        buildconf.write("autobuilddeps:  0\n")
        i = 0
        for (dist_name, dist_version) in dists:
            i += 1
            buildconf.write("[%d]\n" % i)
            buildconf.write("dist:           %s\n" % dist_name)
            buildconf.write("release:        %s\n" % dist_version)
            buildconf.write("build:          1\n")
            for a in ALL_ARCHS + ("noarch",):
                buildconf.write("%s:" % a)
                for spacer in xrange(1, 16 - len(a)):
                    buildconf.write(' ')
                    pass
                if a in arch:
                    buildconf.write("1")
                else:
                    buildconf.write("0")
                    pass
                buildconf.write("\n")
                pass
            pass
        buildconf.close()
        print "Wrote %s" % buildconf_filename
        pass

    changes_candidates = glob.glob('*.changes')
    if len(changes_candidates) == 1:
        print "changes file: %s -> %s" % (changes_candidates[0], tmpdir)
        shutil.copy(changes_candidates[0], tmpdir)
    elif len(changes_candidates) > 1:
        print >>sys.stderr, "ERROR: aborting: found more than one .changes file in current directory: " % repr(changes_candidates)
        sys.exit(1)
        pass

    if options.only_build_conf:
        sys.exit(0)
        pass

    upload_url = upload_baseurl + project
    if options.rsyncprogress:
        rsync_flags.append('--progress')
        pass
    if rsync_checksum:
        rsync_flags.append('--checksum')
        pass
    if options.rsync_delete:
        rsync_flags.append('--delete')
        pass
    rsync_cmd = "rsync %s %s/ %s" % (" ".join(rsync_flags), tmpdir, upload_url)
    print "Uploading files to %s: %s" % (upload_url, rsync_cmd)
    rsync_rc = os.system(rsync_cmd)
    if rsync_rc != 0 and rsync_rc != 5888:
        sys.stderr.write("ERROR: rsync upload failed (%d), command was: %s" % (rsync_rc, rsync_cmd))
        sys.exit(2)
        pass

    if options.split:
        touch_paths = map(lambda distname, distversion:
                os.path.join(touch_basedir, options.queue, "%s@%s-%s" % (project, distname, distversion)),
                dists)
    else:
        touch_paths = [ os.path.join(touch_basedir, options.queue, project) ]
        pass

    touch_cmd = "ssh %s touch %s" % (touch_server, " ".join(touch_paths))
    if options.startjob:
        if len(touch_paths) > 1:
            print "Touching to start jobs:"
            for touch_path in touch_paths:
                print "- %s" % touch_path
                pass
            pass
        else:
            print "Touching to start job: %s" % touch_paths[0]
            pass
        if os.system(touch_cmd) != 0:
            sys.stderr.write("ERROR: failed to touch job file, command was: %s" % touch_cmd)
            sys.exit(3)
            pass
        pass
    else:
        print "Not starting job on server because of command-line option"
        print "To start the job, perform the following command:"
        print touch_cmd
        pass

    print "Done."

finally:
    if tmpdir:
        if options.keep_temp:
            print
            print "Not deleting local directory because of parameter --keep-temp"
            print "Temporary directory is %s" % tmpdir
        else:
            shutil.rmtree(tmpdir)
            pass
        pass
    pass

# vim: set sw=4 ts=4 et:
