Skip to content

Commit

Permalink
a start at a 'mk' makefile
Browse files Browse the repository at this point in the history
  • Loading branch information
trentm committed Sep 2, 2010
1 parent feee930 commit d9b7a0d
Showing 1 changed file with 300 additions and 0 deletions.
300 changes: 300 additions & 0 deletions 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]

0 comments on commit d9b7a0d

Please sign in to comment.