#!/usr/bin/env python import sys, os, re, tempfile, time, stat, posixpath, shutil, subprocess import logging logger = logging.getLogger("wwwsearch.release") info = logger.info REPOSITORY_URL = "http://codespeak.net/svn/wwwsearch" def display_log_messages(): logger = logging.getLogger("wwwsearch.release") sh = logging.StreamHandler() logger.addHandler(sh) logger.setLevel(logging.INFO) class SpawnError(Exception): pass def create_svn_www_wc_instance(build_dir, pretend=False): url = posixpath.join(REPOSITORY_URL, "www") fs_path = os.path.join(build_dir, "www") return SvnWorkingCopy(fs_path, url, "HEAD", pretend=pretend) def create_svn_common_wc_instance(build_dir, project_name, pretend=False): url = posixpath.join(REPOSITORY_URL, project_name, "common") fs_path = os.path.join(build_dir, "common-%s" % project_name) return SvnWorkingCopy(fs_path, url, "HEAD", pretend=pretend) def create_svn_proj_wc_instance(build_dir, project_name, branch="trunk", revision="HEAD", pretend=False): url = posixpath.join(REPOSITORY_URL, project_name, branch) fs_path = os.path.join(build_dir, "%s-%s" % (project_name, branch)) return SvnWorkingCopy(fs_path, url, revision, pretend=pretend) def normalize_path(pathname): if hasattr(os.path, "realpath"): pathname = os.path.realpath(pathname) return os.path.normcase(os.path.abspath(pathname)) def relative_location(basedir, target, posix_result=True): # based on a function by Robin Becker import os.path, posixpath basedir = normalize_path(basedir) target = normalize_path(target) baseparts = basedir.split(os.sep) targetparts = target.split(os.sep) nr_base = len(baseparts) nr_target = len(targetparts) nr_common = min(nr_base, nr_target) ii = 0 while ii < nr_common and baseparts[ii] == targetparts[ii]: ii += 1 relative_parts = (nr_base-ii)*['..'] + targetparts[ii:] if posix_result: return posixpath.join(*relative_parts) else: return os.path.join(*relative_parts) def copy(src, dest, pretend=False): info("copying %r to %r", src, dest) if not pretend: shutil.copy(src, dest) class BuildError(Exception): pass class SDist: def __init__(self, project_name, source_dir, version, pretend=False): self.project_name = project_name self.source_dir = source_dir self.version = version self.pretend = pretend self.test_pythons = [(2,4), (2,3), (2,2)] self._file_builders = [] self._filenames = [] self._wcs = [] self._extra_uploadable_files = [] self._versioned_files = [] self._dated_files = [] def filename(self, filename): return os.path.join(self.source_dir, filename) def add_svn_working_copies(self, wcs): self._wcs += wcs def _add_tuples(self, seq_out, seq_in, dest): if type(seq_in[0]) is type(()): seq = seq_in else: if dest is None: dest = self.source_dir seq = zip(seq_in, [dest]*len(seq_in)) seq_out += seq def add_file_builders(self, file_builders, dest=None): self._add_tuples(self._file_builders, file_builders, dest) def add_files(self, filenames, dest=None): self._add_tuples(self._filenames, filenames, dest) def add_uploadable_files(self, filename_tuples): tups = [(self.filename(fn), dest) for fn, dest in filename_tuples] self._extra_uploadable_files += tups def add_versioned_files(self, filenames): self._versioned_files += [self.filename(fn) for fn in filenames] def add_dated_files(self, filenames): self._dated_files += filenames def tag(self, wc, clean): wc.update(clean) svn_base = posixpath.join(REPOSITORY_URL, self.project_name) branch = relative_location(svn_base, wc.url) assert "tag" not in branch, "no point tagging a tag!" # XXX should really be svn remove-ing setup.cfg! self._with_empty_setup_cfg( wc.fs_path, lambda : maketag( self.version, REPOSITORY_URL, self.project_name, branch, wc.revision, pretend=self.pretend, usecwd=True, ) ) def prepare(self, update, clean): if update: for wc in self._wcs: wc.update(clean) for fn, dest in self._filenames: copy(fn, dest, self.pretend) for fb, dest in self._file_builders: for fn in fb(self.pretend): copy(fn, dest, self.pretend) def _without_setup_cfg(self, working_dir, action): self._hide_setup_cfg(working_dir, action, with_empty_cfg=False) def _with_empty_setup_cfg(self, working_dir, action): self._hide_setup_cfg(working_dir, action, with_empty_cfg=True) def _hide_setup_cfg(self, working_dir, action, with_empty_cfg): orig_dir = os.getcwd() chdir(working_dir, self.pretend) try: rename("setup.cfg", "setup.cfg.hidden", self.pretend) try: if with_empty_cfg: touch("setup.cfg", self.pretend) try: action() finally: if with_empty_cfg: remove("setup.cfg", self.pretend) finally: rename("setup.cfg.hidden", "setup.cfg", self.pretend) finally: chdir(orig_dir, self.pretend) def build_archives(self): self._without_setup_cfg( self.source_dir, lambda : system("%s setup.py sdist --formats=gztar,zip" % sys.executable, self.pretend)) def check_versions(self): bad_files = check_versions(self._versioned_files, self.version) return bad_files def check_dates(self): bad_files = check_date(self._dated_files) return bad_files def test(self): cwd = os.getcwd() chdir(self.source_dir, self.pretend) try: for py_version in self.test_pythons: minor_py_version = py_version[:2] if os.name == "nt": py = "C:\\Python%s\\Python.exe" % "".join(minor_py_version) else: py = "python%d.%d" % ( minor_py_version[0], minor_py_version[1]) system("%s %s" % (py, self.filename("test.py")), self.pretend) finally: chdir(cwd, self.pretend) def build(self, update=True, clean=False, check_versions=True, check_dates=True, ): self.prepare(update, clean) if self.pretend: print "(would check versions here)" else: if check_versions: bad_versions = self.check_versions() else: bad_versions = [] if check_dates: bad_dates = self.check_dates() else: bad_dates = [] if bad_versions: raise BuildError( "version doesn't match in %s" % " ".join(bad_versions)) if bad_dates: raise BuildError( "date doesn't match in %s" % " ".join(bad_dates)) self.build_archives() self.test() def upload_to_sourceforge(self): versions = [ "%s-%s.tar.gz" % (self.project_name, self.version), "%s-%s.zip" % (self.project_name, self.version), ] uploadable_files = [ (os.path.join(self.source_dir, "dist", version), '') for version in versions] for src, dest in uploadable_files+self._extra_uploadable_files: if dest is None: dest = os.path.basename(src) dest = "htdocs/%s/src/%s" % (self.project_name, dest) upload_to_sourceforge(src, dest, self.pretend) def upload_to_sourceforge(filename, dest, pretend=False): fs_path = posixpath.join("/home/groups/w/ww/wwwsearch", dest) cmd = "scp %s jjlee@wwwsearch.sourceforge.net:%s" % (filename, dest) #print cmd system(cmd, pretend) def ensure_exists(dir, pretend=False): import errno try: makedirs(dir, pretend) except OSError, exc: if exc.errno != errno.EEXIST: raise class SvnWorkingCopy: def __init__(self, fs_path, url, revision, pretend=False): self.fs_path = fs_path self.url = url self.revision = revision self.pretend = pretend def _ensure_fsdir_exists(self): ensure_exists(os.path.dirname(self.fs_path), self.pretend) def checkout(self, # avoid multiple "attempting to make directory" messages _fsdir_known_to_exist=False, ): if not _fsdir_known_to_exist: self._ensure_fsdir_exists() #spawn("svn", "co -r%s" % self.revision, self.url, self.fs_path) system("svn co -r%s %s %s" % (self.revision, self.url, self.fs_path), self.pretend) def update(self, clean=False): self._ensure_fsdir_exists() if clean: rmtree(self.fs_path, ignore_errors=True, pretend=self.pretend) if not os.path.isdir(os.path.join(self.fs_path, ".svn")): self.checkout(_fsdir_known_to_exist=not clean) else: #spawn("svn", "up -r%s" % self.revision, self.url, self.fs_path) cmd = "svn up -r%s %s" % (self.revision, self.fs_path) system(cmd, self.pretend) def filename(self, filename): return os.path.join(self.fs_path, filename) class FileBuilder: def __init__(self, build, files): self.build = build self.files = files def build_files(self): return self.build(self.files) def system(cmd, pretend=False, stdout=None): print cmd if not pretend: args = cmd.split() assert ">" not in args, "shell redirect in command: "+cmd try: r = subprocess.call(args, stdout=stdout) except OSError, exc: raise RuntimeError("%s while executing: %s" % (exc, args)) if r != 0: raise RuntimeError("%d exit status from: %s" % (r, args)) def rename(src, dest, pretend=False): print "renaming %s --> %s" % (src, dest) if not pretend: os.rename(src, dest) def remove(filename, pretend=False): print "removing %s" % filename if not pretend: os.remove(filename) def touch(filename, pretend=False): print "touching %s" % filename if not pretend: fd = os.open(filename, os.O_WRONLY|os.O_CREAT, 0666) os.close(fd) os.utime(filename, None) def makedirs(dir_name, pretend=False): print "attempting to make directory %s" % dir_name if not pretend: os.makedirs(dir_name) def chdir(dir_name, pretend=False): print "changing directory to %s" % dir_name if not pretend: os.chdir(dir_name) def spawn(cmd, *args): rc = os.spawnlp(os.P_WAIT, cmd, cmd, args) print "spawn return code", rc if rc != 0: raise SpawnError("%s %s failed: %d" % (cmd, " ".join(args), rc)) def wrap_command(command, directory, out_basename, pretend=False): if not pretend: tempdir = tempfile.mkdtemp() else: tempdir = '/tmp/'+os.path.basename(tempfile.mktemp()) out_fn = os.path.join(tempdir, out_basename) cwd = os.getcwd() chdir(directory, pretend) try: command(out_fn) finally: chdir(cwd, pretend) return out_fn def empy(filename, defines=None, pretend=False): def_text = "" if defines: def_text = " %s " % (" ".join(["-D%s" % define for define in defines])) def cmd(fn): system("em.py %s%s" % (filename, def_text), pretend, stdout=open(fn, "w")) out_fn = wrap_command( cmd, os.path.dirname(filename), os.path.splitext(os.path.basename(filename))[0], pretend=pretend, ) return out_fn def lynx_dump(filename, pretend=False): def cmd(fn): return system("lynx -dump %s" % filename, pretend, stdout=open(fn, "w")) out_fn = wrap_command( cmd, os.path.dirname(filename), os.path.splitext(os.path.basename(filename))[0]+".txt", pretend=pretend, ) return out_fn def getargs(argv): import optparse parser = optparse.OptionParser("""\ usage: %prog [options] ["tag"] ["release"] ["upload"] version [branch [revision]] branch may be e.g.: trunk branch/mybranch tagged # last-tagged release revision may be e.g.: HEAD 12345 """) parser.set_defaults(dirName=os.getcwd()) parser.add_option("-n", "--pretend", action="store_true", dest="pretend", default=False, help="Don't actually do anything " "(just print what would do)") parser.add_option("-c", "--clean", action="store_true", dest="clean", default=False, help="Check out fresh copy, don't update") parser.add_option("--no-update", action="store_false", dest="update", default=True, help="Leave svn working copy unchanged " " (do not update or checkout)") parser.add_option("--no-version-check", action="store_false", dest="check_versions", default=True, help="Don't check the version strings that appear in " "various files for correctness.") parser.add_option("--no-date-check", action="store_false", dest="check_dates", default=True, help="Don't check the date strings that appear in " "various files for correctness.") options, args = parser.parse_args() tag = False rel = False upload = False cmds = "tag", "release", "upload" for cmd in cmds: if cmd in args[0:2]: args.remove(cmd) if cmd == "tag": tag = True elif cmd == "release": rel = True else: upload = True if not tag and not upload: rel = True if len(args) not in [1, 2, 3]: parser.error("wrong number of arguments") version = args[0] branch = "trunk" revision = "HEAD" if len(args) in [2, 3]: branch = args[1] if len(args) == 3: revision = args[2] return tag, rel, upload, version, branch, revision, options def gettags(name): import subprocess release_tags_url = posixpath.join(REPOSITORY_URL, name, "tag/release") pipe = subprocess.Popen(["svn", "ls", release_tags_url], stdout=subprocess.PIPE) output = pipe.communicate()[0] tagnames = [posixpath.join(release_tags_url, tagname.strip("/")) for tagname in output.split("\n") if tagname] tagnames.sort() return tagnames def maketag(version, repository_url, name, branch, revision=None, pretend=False, usecwd=False): tag = "%s-%s" % (version, time.strftime("%Y-%m-%dT%H:%M:%S")) if revision is not None: opts = "-r %s" % revision else: opts = "" dest_tag = "%s/%s/tag/release/%s" % (repository_url, name, tag) source_msg = " ".join([branch, opts]) if usecwd: src_tag = "." opts = "" source_msg += ", from working copy" else: src_tag = "%s/%s/%s" % (repository_url, name, branch) msg = "Tagged %s (%s) release %s" % (name, source_msg, tag) cmd = 'svn cp -m "%s" %s' % ( msg, " ".join([x for x in [opts, src_tag, dest_tag] if x])) #assert pretend system(cmd, pretend) def clean(): system("rm -f *~ *.pyc *.pyo MANIFEST") def grep(text, file): f = open(file, "r") found = 0 for line in f.xreadlines(): if line.find(text) != -1: found = 1 break return found def check_date(files): date_string = time.strftime("%B %Y") bad_files = [] for file in files: if not grep(date_string, file): bad_files.append(file) return bad_files def check_versions(files, version): major, minor, bugfix, state, pre, svn = parse_version(version) short_version = unparse_version((major, minor, bugfix, state, pre, None)) bad_files = [] for file in files: if not grep(short_version, file): bad_files.append(file) return bad_files def svn_id_to_time(text): """Ignores the time, just uses the date. >>> time.ctime(time.mktime(svn_id_to_time( ... "$"+"Id: README.html.in 9418 2005-02-22 22:10:19Z jjlee $"))) 'Tue Feb 22 00:00:00 2005' """ try: yr, mon, day = text.split()[3].split("-") except (ValueError, IndexError): raise ValueError("Invalid or empty SVN time string") tt = (int(yr), int(mon), int(day), 0, 0, 0, 0, 0, -1) return time.localtime(time.mktime(tt)) # normalize class Page: def __init__(self, name, title=None, url=None, child=None): self.name = name if title is None: title = name self.title = title if url is None: url = "../%s/" % name self.url = url self.isChild = False self.child = None if child is not None: child.isChild = True self.child = child self.isCurrent = False def setCurrentPageName(self, name): if name == self.name: self.isCurrent = True if self.child is not None: self.child.setCurrentPageName(name) def html(self): if self.isCurrent: if self.isChild: html = '%s
' % self.title else: html = '%s
' % self.title else: if self.isChild: fmt = '%s
' else: fmt = '%s
' html = fmt % (self.url, self.title) if self.child is not None: html = html+"\n"+self.child.html() return html class Sep: def __init__(self): self.name = object() def setCurrentPageName(self, name): pass def html(self): return '
' def navbar(thisPageName=None): pages = [ Page("Home", url=".."), Sep(), Page("GeneralFAQ", title="General FAQs", url="../bits/GeneralFAQ.html"), Sep(), Page("mechanize", child=Page("ccdocs", title="mechanize docs", url="../mechanize/doc.html")), Page("ClientForm"), Sep(), Page("ClientCookie", child=Page("ccdocs", title="ClientCookie docs", url="../ClientCookie/doc.html")), Page("pullparser"), Page("DOMForm"), Page("python-spidermonkey"), Page("ClientTable"), Page("urllib2", title="1.5.2 urllib2.py", url="../bits/urllib2_152.py"), Page("urllib", title="1.5.2 urllib.py", url="../bits/urllib_152.py"), ## Page(""), ## Page(""), ## Page(""), ] html = [] for page in pages: page.setCurrentPageName(thisPageName) html.append(page.html()) return "\n".join(html) ## VERSION_RE = re.compile(r"(?P\d+)\.(?P\d+)\.(?P\d+)" ## r"(?P[ab])?(?:-pre)?(?P
\d+)?"
##                         r"(?:\.dev-r(?P\d+))?$")
VERSION_RE = re.compile(r"(?P\d+)\.(?P\d+)\.(?P\d+)(?P[ab])?"
                        r"(?:-pre)?(?P
\d+)?"
                        r"(?:\.dev-r(?P\d+))?$")
def parse_version(text):
    """
>>> parse_version("0.0.1")
('0', '0', '1', None, None, None)
>>> parse_version("0.2.3b")
('0', '2', '3', 'b', None, None)
>>> parse_version("1.0.3a")
('1', '0', '3', 'a', None, None)
>>> parse_version("123.012.304a")
('123', '012', '304', 'a', None, None)
>>> parse_version("1.0.3c")
Traceback (most recent call last):
ValueError
>>> parse_version("1.0.3a-pre1")
('1', '0', '3', 'a', '1', None)
>>> parse_version("1.0.3a-pre234")
('1', '0', '3', 'a', '234', None)
>>> parse_version("0.0.11a.dev-r20458")
('0', '0', '11', 'a', None, '20458')
>>> parse_version("1.0.3a-preblah")
Traceback (most recent call last):
ValueError

    """
    m = VERSION_RE.match(text)
    #print 'text', text
    if m is None:
        raise ValueError
    return tuple([m.groupdict()[part] for part in
                  ("major", "minor", "bugfix", "state", "pre", "svn")])

def unparse_version(tup):
    """
>>> unparse_version(('0', '0', '1', None, None, None))
'0.0.1'
>>> unparse_version(('0', '2', '3', 'b', None, None))
'0.2.3b'
>>> unparse_version()
Traceback (most recent call last):
  File "", line 1, in ?
TypeError: unparse_version() takes exactly 1 argument (0 given)
>>> unparse_version(('1', '0', '3', 'a', None, None))
'1.0.3a'
>>> unparse_version(('123', '012', '304', 'a', None, None))
'123.012.304a'
>>> unparse_version(('1', '0', '3', 'a', '1', None))
'1.0.3a-pre1'
>>> unparse_version(('1', '0', '3', 'a', '234', None))
'1.0.3a-pre234'

    """
    major, minor, bugfix, state_char, pre, svn = tup
    fmt = "%s.%s.%s"
    args = [major, minor, bugfix]
    if state_char is not None:
        fmt += "%s"
        args.append(state_char)
    if pre is not None:
        fmt += "-pre%s"
        args.append(pre)
    if svn is not None:
        fmt += ".dev-r%s"
        args.append(svn)
    return fmt % tuple(args)

def win_version(text):
    """
>>> win_version("0.0.1")
'0_0_1'
>>> win_version("11.03.12a")
'11_03_12a'
>>> win_version("11.03.12a-pre23")
'11_03_12a-pre23'
>>> win_version("11.03.12a.dev-r12345")
'11_03_12a.dev-r12345'

    """
    major, minor, bugfix, state_char, pre, svn = parse_version(text)
    if bugfix is None: bugfix = ""
    if state_char is None: state_char = ""
    if pre is None:
        pre = ""
    else:
        pre = "-pre%s" % pre
    if svn is None:
        svn = ""
    else:
        svn = ".dev-r%s" % svn
    return "%s_%s_%s%s%s%s" % (major, minor, bugfix, state_char, pre, svn)

# auto_chmod / rmtree pinched from Phillip Eby's setuptools
def auto_chmod(func, arg, exc):
    if func is os.remove and os.name=='nt':
        os.chmod(arg, stat.S_IWRITE)
        return func(arg)
    exc = sys.exc_info()
    raise exc[0], (exc[1][0], exc[1][1] + (" %s %s" % (func,arg)))

def rmtree(path, ignore_errors=False, onerror=auto_chmod, pretend=False,
           depth=0):
    """Recursively delete a directory tree.

    This code is taken from the Python 2.4 version of 'shutil', because
    the 2.3 version doesn't really work right.
    """
    if not depth:
        print "recursively removing", path
    if pretend:
        return

    if ignore_errors:
        def onerror(*args):
            pass
    elif onerror is None:
        def onerror(*args):
            raise
    names = []
    try:
        names = os.listdir(path)
    except os.error, err:
        onerror(os.listdir, path, sys.exc_info())
    for name in names:
        fullname = os.path.join(path, name)
        try:
            mode = os.lstat(fullname).st_mode
        except os.error:
            mode = 0
        if stat.S_ISDIR(mode):
            rmtree(fullname, ignore_errors, onerror, pretend, depth+1)
        else:
            try:
                os.remove(fullname)
            except os.error, err:
                onerror(os.remove, fullname, sys.exc_info())
    try:
        os.rmdir(path)
    except os.error:
        onerror(os.rmdir, path, sys.exc_info())

def _test():
    import doctest
    return doctest.testmod()

if __name__ == "__main__":
    _test()