diff options
author | Trent Mick <trentm@gmail.com> | 2010-09-02 10:08:10 -0700 |
---|---|---|
committer | Trent Mick <trentm@gmail.com> | 2010-09-02 10:08:10 -0700 |
commit | d9b7a0d131a6110173f90739e3cc6025d0282c09 (patch) | |
tree | 6d0c68d227c12e5904b7251008c9c6532c36c52a | |
parent | feee930b2e92c552e9220d05bc09de15273586b6 (diff) | |
download | appdirs-d9b7a0d131a6110173f90739e3cc6025d0282c09.tar.gz |
a start at a 'mk' makefile
-rw-r--r-- | Makefile.py | 300 |
1 files changed, 300 insertions, 0 deletions
diff --git a/Makefile.py b/Makefile.py new file mode 100644 index 0000000..4a43f98 --- /dev/null +++ b/Makefile.py @@ -0,0 +1,300 @@ + +# This is a Makefile for the `mk` tool. (Limited) details for that here: +# <http://svn.openkomodo.com/openkomodo/browse/mk> + +import sys +import os +from os.path import join, dirname, normpath, abspath, exists, basename, expanduser +import re +from glob import glob +import codecs +import webbrowser + +import mklib +assert mklib.__version_info__ >= (0,7,2) # for `mklib.mk` +from mklib.common import MkError +from mklib import Task, mk +from mklib import sh + + +class bugs(Task): + """open bug/issues page""" + def make(self): + webbrowser.open("http://github.com/ActiveState/appdirs/issues") + +class site(Task): + """open project page""" + def make(self): + webbrowser.open("http://github.com/ActiveState/appdirs") + +class pypi(Task): + """open project page""" + def make(self): + webbrowser.open("http://pypi.python.org/pypi/appdirs/") + +class cut_a_release(Task): + """automate the steps for cutting a release + + See <http://github.com/trentm/eol/blob/master/docs/devguide.md> + for details. + """ + proj_name = "appdirs" + version_py_path = "lib/appdirs.py" + version_module = "appdirs" + _changes_parser = re.compile(r'^## %s (?P<ver>[\d\.abc]+)' + r'(?P<nyr>\s+\(not yet released\))?' + r'(?P<body>.*?)(?=^##|\Z)' % proj_name, re.M | re.S) + + def make(self): + DRY_RUN = False + version = self._get_version() + + # Confirm + if not DRY_RUN: + answer = query_yes_no("* * *\n" + "Are you sure you want cut a %s release?\n" + "This will involved commits and a release to pypi." % version, + default="no") + if answer != "yes": + self.log.info("user abort") + return + print "* * *" + self.log.info("cutting a %s release", version) + + # Checks: Ensure there is a section in changes for this version. + changes_path = join(self.dir, "CHANGES.md") + changes_txt = changes_txt_before = codecs.open(changes_path, 'r', 'utf-8').read() + changes_sections = self._changes_parser.findall(changes_txt) + top_ver = changes_sections[0][0] + if top_ver != version: + raise MkError("top section in `CHANGES.md' is for " + "version %r, expected version %r: aborting" + % (top_ver, version)) + top_nyr = changes_sections[0][1] + if not top_nyr: + answer = query_yes_no("\n* * *\n" + "The top section in `CHANGES.md' doesn't have the expected\n" + "'(not yet released)' marker. Has this been released already?", + default="yes") + if answer != "no": + self.log.info("abort") + return + print "* * *" + top_body = changes_sections[0][2] + if top_body.strip() == "(nothing yet)": + raise MkError("top section body is `(nothing yet)': it looks like " + "nothing has been added to this release") + + # Commits to prepare release. + changes_txt = changes_txt.replace(" (not yet released)", "", 1) + if not DRY_RUN and changes_txt != changes_txt_before: + self.log.info("prepare `CHANGES.md' for release") + f = codecs.open(changes_path, 'w', 'utf-8') + f.write(changes_txt) + f.close() + sh.run('git commit %s -m "prepare for %s release"' + % (changes_path, version), self.log.debug) + + # Tag version and push. + curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t) + if not DRY_RUN and version not in curr_tags: + self.log.info("tag the release") + sh.run('git tag -a "%s" -m "version %s"' % (version, version), + self.log.debug) + sh.run('git push --tags', self.log.debug) + + # Release to PyPI. + self.log.info("release to pypi") + if not DRY_RUN: + mk("pypi_upload") + + # Commits to prepare for future dev and push. + next_version = self._get_next_version(version) + self.log.info("prepare for future dev (version %s)", next_version) + marker = "## %s %s\n" % (self.proj_name, version) + if marker not in changes_txt: + raise MkError("couldn't find `%s' marker in `%s' " + "content: can't prep for subsequent dev" % (marker, changes_path)) + changes_txt = changes_txt.replace("## %s %s\n" % (self.proj_name, version), + "## %s %s (not yet released)\n\n(nothing yet)\n\n## %s %s\n" % ( + self.proj_name, next_version, self.proj_name, version)) + if not DRY_RUN: + f = codecs.open(changes_path, 'w', 'utf-8') + f.write(changes_txt) + f.close() + + ver_path = join(self.dir, normpath(self.version_py_path)) + ver_content = codecs.open(ver_path, 'r', 'utf-8').read() + version_tuple = self._tuple_from_version(version) + next_version_tuple = self._tuple_from_version(next_version) + marker = "__version_info__ = %r" % (version_tuple,) + if marker not in ver_content: + raise MkError("couldn't find `%s' version marker in `%s' " + "content: can't prep for subsequent dev" % (marker, ver_path)) + ver_content = ver_content.replace(marker, + "__version_info__ = %r" % (next_version_tuple,)) + if not DRY_RUN: + f = codecs.open(ver_path, 'w', 'utf-8') + f.write(ver_content) + f.close() + + if not DRY_RUN: + sh.run('git commit %s %s -m "prep for future dev"' % ( + changes_path, ver_path)) + sh.run('git push') + + def _tuple_from_version(self, version): + def _intify(s): + try: + return int(s) + except ValueError: + return s + return tuple(_intify(b) for b in version.split('.')) + + def _get_next_version(self, version): + last_bit = version.rsplit('.', 1)[-1] + try: + last_bit = int(last_bit) + except ValueError: # e.g. "1a2" + last_bit = int(re.split('[abc]', last_bit, 1)[-1]) + return version[:-len(str(last_bit))] + str(last_bit + 1) + + def _get_version(self): + lib_dir = join(dirname(abspath(__file__)), "lib") + sys.path.insert(0, lib_dir) + try: + mod = __import__(self.version_module) + return mod.__version__ + finally: + del sys.path[0] + + +class clean(Task): + """Clean generated files and dirs.""" + def make(self): + patterns = [ + "dist", + "build", + "MANIFEST", + "*.pyc", + "lib/*.pyc", + ] + for pattern in patterns: + p = join(self.dir, pattern) + for path in glob(p): + sh.rm(path, log=self.log) + +class sdist(Task): + """python setup.py sdist""" + def make(self): + sh.run_in_dir("%spython setup.py sdist --formats zip" + % _setup_command_prefix(), + self.dir, self.log.debug) + +class pypi_upload(Task): + """Upload release to pypi.""" + def make(self): + sh.run_in_dir("%spython setup.py sdist --formats zip upload" + % _setup_command_prefix(), + self.dir, self.log.debug) + + sys.path.insert(0, join(self.dir, "lib")) + url = "http://pypi.python.org/pypi/appdirs/" + import webbrowser + webbrowser.open_new(url) + +class test(Task): + """Run all tests (except known failures).""" + def make(self): + for ver, python in self._gen_pythons(): + if ver < (2,3): + # Don't support Python < 2.3. + continue + #elif ver >= (3, 0): + # # Don't yet support Python 3. + # continue + ver_str = "%s.%s" % ver + print "-- test with Python %s (%s)" % (ver_str, python) + assert ' ' not in python + sh.run_in_dir("%s test.py -- -knownfailure" % python, + join(self.dir, "test")) + + def _python_ver_from_python(self, python): + assert ' ' not in python + o = os.popen('''%s -c "import sys; print(sys.version)"''' % python) + ver_str = o.read().strip() + ver_bits = re.split("\.|[^\d]", ver_str, 2)[:2] + ver = tuple(map(int, ver_bits)) + return ver + + def _gen_python_names(self): + yield "python" + for ver in [(2,4), (2,5), (2,6), (2,7), (3,0), (3,1)]: + yield "python%d.%d" % ver + if sys.platform == "win32": + yield "python%d%d" % ver + + def _gen_pythons(self): + import which # `pypm|pip install which` + python_from_ver = {} + for name in self._gen_python_names(): + for python in which.whichall(name): + ver = self._python_ver_from_python(python) + if ver not in python_from_ver: + python_from_ver[ver] = python + for ver, python in sorted(python_from_ver.items()): + yield ver, python + + + + +#---- internal support stuff + +## {{{ http://code.activestate.com/recipes/577058/ (r2) +def query_yes_no(question, default="yes"): + """Ask a yes/no question via raw_input() and return their answer. + + "question" is a string that is presented to the user. + "default" is the presumed answer if the user just hits <Enter>. + It must be "yes" (the default), "no" or None (meaning + an answer is required of the user). + + The "answer" return value is one of "yes" or "no". + """ + valid = {"yes":"yes", "y":"yes", "ye":"yes", + "no":"no", "n":"no"} + if default == None: + prompt = " [y/n] " + elif default == "yes": + prompt = " [Y/n] " + elif default == "no": + prompt = " [y/N] " + else: + raise ValueError("invalid default answer: '%s'" % default) + + while 1: + sys.stdout.write(question + prompt) + choice = raw_input().lower() + if default is not None and choice == '': + return default + elif choice in valid.keys(): + return valid[choice] + else: + sys.stdout.write("Please respond with 'yes' or 'no' "\ + "(or 'y' or 'n').\n") +## end of http://code.activestate.com/recipes/577058/ }}} + + +def _setup_command_prefix(): + prefix = "" + if sys.platform == "darwin": + # http://forums.macosxhints.com/archive/index.php/t-43243.html + # This is an Apple customization to `tar` to avoid creating + # '._foo' files for extended-attributes for archived files. + prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 " + return prefix + +def _capture_stdout(argv): + import subprocess + p = subprocess.Popen(argv, stdout=subprocess.PIPE) + return p.communicate()[0] |