from __future__ import with_statement __license__ = 'GPL 3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import shutil, os, glob, re, cStringIO, sys, tempfile, time, textwrap, socket, \ struct, subprocess from datetime import datetime from setuptools.command.build_py import build_py as _build_py, convert_path from distutils.core import Command from subprocess import check_call, call, Popen from distutils.command.build import build as _build raw = open(os.path.join('src', 'calibre', 'constants.py'), 'rb').read() __version__ = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', raw).group(1) __appname__ = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', raw).group(1) PREFIX = "/var/www/calibre.kovidgoyal.net" DOWNLOADS = PREFIX+"/htdocs/downloads" BETAS = DOWNLOADS +'/betas' DOCS = PREFIX+"/htdocs/apidocs" USER_MANUAL = PREFIX+'/htdocs/user_manual' HTML2LRF = "src/calibre/ebooks/lrf/html/demo" TXT2LRF = "src/calibre/ebooks/lrf/txt/demo" MOBILEREAD = 'ftp://dev.mobileread.com/calibre/' def get_ip_address(ifname): import fcntl s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) return socket.inet_ntoa(fcntl.ioctl( s.fileno(), 0x8915, # SIOCGIFADDR struct.pack('256s', ifname[:15]) )[20:24]) try: HOST=get_ip_address('eth0') except: try: HOST=get_ip_address('wlan0') except: HOST='unknown' def newer(targets, sources): ''' Return True is sources is newer that targets or if targets does not exist. ''' for f in targets: if not os.path.exists(f): return True ttimes = map(lambda x: os.stat(x).st_mtime, targets) stimes = map(lambda x: os.stat(x).st_mtime, sources) newest_source, oldest_target = max(stimes), min(ttimes) return newest_source > oldest_target class OptionlessCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) class sdist(OptionlessCommand): description = 'Create a source distribution using bzr' def run(self): name = os.path.join('dist', '%s-%s.tar.gz'%(__appname__, __version__)) check_call(('bzr export '+name).split()) self.distribution.dist_files.append(('sdist', '', name)) print 'Source distribution created in', os.path.abspath(name) class pot(OptionlessCommand): description = '''Create the .pot template for all translatable strings''' PATH = os.path.join('src', __appname__, 'translations') def source_files(self): ans = [] for root, _, files in os.walk(os.path.dirname(self.PATH)): for name in files: if name.endswith('.py'): ans.append(os.path.abspath(os.path.join(root, name))) return ans def run(self): sys.path.insert(0, os.path.abspath(self.PATH)) try: pygettext = __import__('pygettext', fromlist=['main']).main files = self.source_files() buf = cStringIO.StringIO() print 'Creating translations template' tempdir = tempfile.mkdtemp() pygettext(buf, ['-k', '__', '-p', tempdir]+files) src = buf.getvalue() pot = os.path.join(self.PATH, __appname__+'.pot') f = open(pot, 'wb') f.write(src) f.close() print 'Translations template:', os.path.abspath(pot) return pot finally: sys.path.remove(os.path.abspath(self.PATH)) class manual(OptionlessCommand): description='''Build the User Manual ''' def run(self): cwd = os.path.abspath(os.getcwd()) os.chdir(os.path.join('src', 'calibre', 'manual')) try: for d in ('.build', 'cli'): if os.path.exists(d): shutil.rmtree(d) os.makedirs(d) if not os.path.exists('.build'+os.sep+'html'): os.makedirs('.build'+os.sep+'html') check_call(['sphinx-build', '-b', 'custom', '-d', '.build/doctrees', '.', '.build/html']) finally: os.chdir(cwd) @classmethod def clean(cls): path = os.path.join('src', 'calibre', 'manual', '.build') if os.path.exists(path): shutil.rmtree(path) class resources(OptionlessCommand): description='''Compile various resource files used in calibre. ''' RESOURCES = dict( opf_template = 'ebooks/metadata/opf.xml', ncx_template = 'ebooks/metadata/ncx.xml', fb2_xsl = 'ebooks/fb2/fb2.xsl', metadata_sqlite = 'library/metadata_sqlite.sql', jquery = 'gui2/viewer/jquery.js', jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js', html_css = 'ebooks/oeb/html.css', ) DEST = os.path.join('src', __appname__, 'resources.py') def get_qt_translations(self): data = {} translations_found = False for TPATH in ('/usr/share/qt4/translations', '/usr/lib/qt4/translations'): if os.path.exists(TPATH): files = glob.glob(TPATH + '/qt_??.qm') for f in files: key = os.path.basename(f).partition('.')[0] data[key] = f translations_found = True break if not translations_found: print 'WARNING: Could not find Qt transations' return data def get_static_resources(self): sdir = os.path.join('src', 'calibre', 'library', 'static') resources, max = {}, 0 for f in os.listdir(sdir): resources[f] = open(os.path.join(sdir, f), 'rb').read() mtime = os.stat(os.path.join(sdir, f)).st_mtime max = mtime if mtime > max else max return resources, max def get_recipes(self): sdir = os.path.join('src', 'calibre', 'web', 'feeds', 'recipes') resources, max = {}, 0 for f in os.listdir(sdir): if f.endswith('.py') and f != '__init__.py': resources[f.replace('.py', '')] = open(os.path.join(sdir, f), 'rb').read() mtime = os.stat(os.path.join(sdir, f)).st_mtime max = mtime if mtime > max else max return resources, max def run(self): data, dest, RESOURCES = {}, self.DEST, self.RESOURCES for key in RESOURCES: path = RESOURCES[key] if not os.path.isabs(path): RESOURCES[key] = os.path.join('src', __appname__, path) translations = self.get_qt_translations() RESOURCES.update(translations) static, smax = self.get_static_resources() recipes, rmax = self.get_recipes() amax = max(rmax, smax) if newer([dest], RESOURCES.values()) or os.stat(dest).st_mtime < amax: print 'Compiling resources...' with open(dest, 'wb') as f: for key in RESOURCES: data = open(RESOURCES[key], 'rb').read() f.write(key + ' = ' + repr(data)+'\n\n') f.write('server_resources = %s\n\n'%repr(static)) f.write('recipes = %s\n\n'%repr(recipes)) f.write('build_time = "%s"\n\n'%time.strftime('%d %m %Y %H%M%S')) else: print 'Resources are up to date' @classmethod def clean(cls): path = cls.DEST for path in glob.glob(path+'*'): if os.path.exists(path): os.remove(path) class translations(OptionlessCommand): description='''Compile the translations''' PATH = os.path.join('src', __appname__, 'translations') DEST = os.path.join(PATH, 'compiled.py') def run(self): sys.path.insert(0, os.path.abspath(self.PATH)) try: files = glob.glob(os.path.join(self.PATH, '*.po')) if newer([self.DEST], files): msgfmt = __import__('msgfmt', fromlist=['main']).main translations = {} print 'Compiling translations...' for po in files: lang = os.path.basename(po).partition('.')[0] buf = cStringIO.StringIO() print 'Compiling', lang msgfmt(buf, [po]) translations[lang] = buf.getvalue() open(self.DEST, 'wb').write('translations = '+repr(translations)) else: print 'Translations up to date' finally: sys.path.remove(os.path.abspath(self.PATH)) @classmethod def clean(cls): path = cls.DEST if os.path.exists(path): os.remove(path) class gui(OptionlessCommand): description='''Compile all GUI forms and images''' PATH = os.path.join('src', __appname__, 'gui2') IMAGES_DEST = os.path.join(PATH, 'images_rc.py') QRC = os.path.join(PATH, 'images.qrc') @classmethod def find_forms(cls): forms = [] for root, _, files in os.walk(cls.PATH): for name in files: if name.endswith('.ui'): forms.append(os.path.abspath(os.path.join(root, name))) return forms @classmethod def form_to_compiled_form(cls, form): return form.rpartition('.')[0]+'_ui.py' def run(self): self.build_forms() self.build_images() def build_images(self): cwd, images = os.getcwd(), os.path.basename(self.IMAGES_DEST) try: os.chdir(self.PATH) sources, files = [], [] for root, _, files in os.walk('images'): for name in files: sources.append(os.path.join(root, name)) if newer([images], sources): print 'Compiling images...' for s in sources: alias = ' alias="library"' if s.endswith('images'+os.sep+'library.png') else '' files.append('%s'%(alias, s)) manifest = '\n\n%s\n\n'%'\n'.join(files) with open('images.qrc', 'wb') as f: f.write(manifest) try: check_call(['pyrcc4', '-o', images, 'images.qrc']) except: import traceback traceback.print_exc() raise Exception('You do not have pyrcc4 in your PATH. ' 'Install the PyQt4 development tools.') else: print 'Images are up to date' finally: os.chdir(cwd) def build_forms(self): from PyQt4.uic import compileUi forms = self.find_forms() for form in forms: compiled_form = self.form_to_compiled_form(form) if not os.path.exists(compiled_form) or os.stat(form).st_mtime > os.stat(compiled_form).st_mtime: print 'Compiling form', form buf = cStringIO.StringIO() compileUi(form, buf) dat = buf.getvalue() dat = dat.replace('__appname__', __appname__) dat = dat.replace('import images_rc', 'from calibre.gui2 import images_rc') dat = dat.replace('from library import', 'from calibre.gui2.library import') dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import') dat = dat.replace('from convert.xpath_wizard import', 'from calibre.gui2.convert.xpath_wizard import') dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(? %s/latest_version'''\ %(__version__, DOWNLOADS), shell=True) class upload_user_manual(OptionlessCommand): description = 'Build and upload the User Manual' sub_commands = [('manual', None)] def run(self): OptionlessCommand.run(self) check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*', 'divok:%s'%USER_MANUAL]), shell=True) class upload_to_pypi(OptionlessCommand): description = 'Upload eggs and source to PyPI' def run(self): check_call('python setup.py register'.split()) check_call('rm -f dist/*', shell=True) check_call('python setup.py build_ext bdist_egg --exclude-source-files upload'.split()) check_call('python setup.py sdist upload'.split()) class stage3(OptionlessCommand): description = 'Stage 3 of the build process' sub_commands = [ ('upload_installers', None), ('upload_user_manual', None), ('upload_to_pypi', None), ('upload_rss', None), ] @classmethod def misc(cls): check_call('ssh divok rm -f %s/calibre-\*.tar.gz'%DOWNLOADS, shell=True) check_call('scp dist/calibre-*.tar.gz divok:%s/'%DOWNLOADS, shell=True) check_call('gpg --armor --detach-sign dist/calibre-*.tar.gz', shell=True) check_call('scp dist/calibre-*.tar.gz.asc divok:%s/signatures/'%DOWNLOADS, shell=True) check_call('''rm -rf dist/* build/*''', shell=True) check_call('ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/', shell=True) check_call('ssh divok bzr update /usr/local/calibre', shell=True) def run(self): OptionlessCommand.run(self) self.misc() class stage2(OptionlessCommand): description = 'Stage 2 of the build process' sub_commands = [ ('build_linux', None), ('build_windows', None), ('build_osx', None) ] def run(self): check_call('rm -rf dist/*', shell=True) OptionlessCommand.run(self) class stage1(OptionlessCommand): description = 'Stage 1 of the build process' sub_commands = [ ('update', None), ('tag_release', None), ('upload_demo', None), ] class betas(OptionlessCommand): description = 'Build an upload beta builds to the servers' sub_commands = [ ('update', None), ('stage2', None) ] def run(self): OptionlessCommand.run(self) check_call('scp dist/* divok:'+BETAS, shell=True) class upload(OptionlessCommand): description = 'Build and upload calibre to the servers' sub_commands = [ ('pot', None), ('stage1', None), ('stage2', None), ('stage3', None) ] try: class upload_rss(OptionlessCommand): from bzrlib import log as blog class ChangelogFormatter(blog.LogFormatter): supports_tags = True supports_merge_revisions = False def __init__(self, num_of_versions=20): from calibre.utils.rss_gen import RSS2 self.num_of_versions = num_of_versions self.rss = RSS2( title = 'calibre releases', link = 'http://calibre.kovidgoyal.net/wiki/Changelog', description = 'Latest release of calibre', lastBuildDate = datetime.utcnow() ) self.current_entry = None def log_revision(self, r): from calibre.utils.rss_gen import RSSItem, Guid if len(self.rss.items) > self.num_of_versions-1: return msg = r.rev.message match = re.match(r'version\s+(\d+\.\d+.\d+)', msg) if match: if self.current_entry is not None: mkup = '
    %s
' self.current_entry.description = mkup%(''.join( self.current_entry.description)) self.rss.items.append(self.current_entry) timestamp = r.rev.timezone + r.rev.timestamp self.current_entry = RSSItem( title = 'calibre %s released'%match.group(1), link = 'http://calibre.kovidgoyal.net/download', guid = Guid(match.group(), False), pubDate = datetime(*time.gmtime(timestamp)[:6]), description = [] ) elif self.current_entry is not None: if re.search(r'[a-zA-Z]', msg) and len(msg.strip()) > 5: if 'translation' not in msg and not msg.startswith('IGN'): msg = msg.replace('<', '<').replace('>', '>') msg = re.sub('#(\d+)', r'#\1', msg) self.current_entry.description.append( '
  • %s
  • '%msg.strip()) def run(self): from bzrlib import log, branch bzr_path = os.path.expanduser('~/work/calibre') b = branch.Branch.open(bzr_path) lf = upload_rss.ChangelogFormatter() log.show_log(b, lf) lf.rss.write_xml(open('/tmp/releases.xml', 'wb')) subprocess.check_call('scp /tmp/releases.xml divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads'.split()) except ImportError: upload_rss = None