IGN:Improve UI for build system

This commit is contained in:
Kovid Goyal 2009-02-18 19:07:12 -08:00
parent d8b443fd26
commit b096b292bf
2 changed files with 693 additions and 578 deletions

372
setup.py
View File

@ -47,350 +47,15 @@ main_functions = {
if __name__ == '__main__':
from setuptools import setup, find_packages
from setuptools.command.build_py import build_py as _build_py, convert_path
from distutils.command.build import build as _build
from distutils.core import Command as _Command
from pyqtdistutils import PyQtExtension, build_ext, Extension
import subprocess, glob
from upload2 import sdist, pot, build, build_py, manual, \
resources, clean, gui, translations, update, \
tag_release, upload_demo, build_linux, build_windows, \
build_osx, upload_installers, upload_user_manual, \
upload_to_pypi, stage3, stage2, stage1, upload
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 build_py(_build_py):
def find_data_files(self, package, src_dir):
"""
Return filenames for package's data files in 'src_dir'
Modified to treat data file specs as paths not globs
"""
globs = (self.package_data.get('', [])
+ self.package_data.get(package, []))
files = self.manifest_files.get(package, [])[:]
for pattern in globs:
# Each pattern has to be converted to a platform-specific path
pattern = os.path.join(src_dir, convert_path(pattern))
next = glob.glob(pattern)
files.extend(next if next else [pattern])
return self.exclude_data_files(package, src_dir, files)
class Command(_Command):
user_options = []
def initialize_options(self): pass
def finalize_options(self): pass
class sdist(Command):
description = "create a source distribution using bzr"
def run(self):
name = 'dist/calibre-%s.tar.gz'%VERSION
subprocess.check_call(('bzr export '+name).split())
self.distribution.dist_files.append(('sdist', '', name))
class pot(Command):
description = '''Create the .pot template for all translatable strings'''
PATH = os.path.join('src', APPNAME, 'translations')
def source_files(self):
ans = []
for root, dirs, 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:
from pygettext import main as pygettext
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(tempdir, 'calibre.pot')
f = open(pot, 'wb')
f.write(src)
f.close()
print 'Translations template:', pot
return pot
finally:
sys.path.remove(os.path.abspath(self.PATH))
class manual(Command):
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')
subprocess.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(Command):
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/lrf/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(Command):
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):
from msgfmt import main as msgfmt
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(Command):
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, dirs, 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, dirs, 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('<file%s>%s</file>'%(alias, s))
manifest = '<RCC>\n<qresource prefix="/">\n%s\n</qresource>\n</RCC>'%'\n'.join(files)
with open('images.qrc', 'wb') as f:
f.write(manifest)
subprocess.check_call(['pyrcc4', '-o', images, 'images.qrc'])
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 = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
# Workaround bug in Qt 4.4 on Windows
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
print 'Implementing Workaround for buggy pyuic in form', form
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
if form.endswith('viewer%smain.ui'%os.sep):
print 'Promoting WebView'
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
open(compiled_form, 'wb').write(dat)
@classmethod
def clean(cls):
forms = cls.find_forms()
for form in forms:
c = cls.form_to_compiled_form(form)
if os.path.exists(c):
os.remove(c)
for x in (cls.IMAGES_DEST, cls.QRC):
if os.path.exists(x):
os.remove(x)
class clean(Command):
description='''Delete all computer generated files in the source tree'''
def run(self):
print 'Cleaning...'
manual.clean()
gui.clean()
translations.clean()
resources.clean()
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
os.remove(f)
for root, dirs, files in os.walk('.'):
for name in files:
for t in ('.pyc', '.pyo', '~'):
if name.endswith(t):
os.remove(os.path.join(root, name))
break
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
shutil.rmtree(dir, ignore_errors=True)
class build(_build):
sub_commands = [
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('build_ext', lambda self: True),
('build_py', lambda self: True),
('build_clib', _build.has_c_libraries),
('build_scripts', _build.has_scripts),
]
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
entry_points['console_scripts'].append(
'calibre_postinstall = calibre.linux:post_install')
ext_modules = [
Extension('calibre.plugins.lzx',
sources=['src/calibre/utils/lzx/lzxmodule.c',
@ -430,7 +95,9 @@ if __name__ == '__main__':
plugins = ['plugins/%s.so'%(x.name.rpartition('.')[-1]) for x in ext_modules]
else:
plugins = ['plugins/%s.pyd'%(x.name.rpartition('.')[-1]) for x in ext_modules] + \
['plugins/%s.pyd.manifest'%(x.name.rpartition('.')[-1]) for x in ext_modules if 'pictureflow' not in x.name]
['plugins/%s.pyd.manifest'%(x.name.rpartition('.')[-1]) \
for x in ext_modules if 'pictureflow' not in x.name]
setup(
name = APPNAME,
@ -451,7 +118,11 @@ if __name__ == '__main__':
''',
long_description =
'''
%s is an e-book library manager. It can view, convert and catalog e-books in most of the major e-book formats. It can also talk to a few e-book reader devices. It can go out to the internet and fetch metadata for your books. It can download newspapers and convert them into e-books for convenient reading. It is cross platform, running on Linux, Windows and OS X.
%s is an e-book library manager. It can view, convert and catalog e-books \
in most of the major e-book formats. It can also talk to e-book reader \
devices. It can go out to the internet and fetch metadata for your books. \
It can download newspapers and convert them into e-books for convenient \
reading. It is cross platform, running on Linux, Windows and OS X.
For screenshots: https://%s.kovidgoyal.net/wiki/Screenshots
@ -490,6 +161,19 @@ if __name__ == '__main__':
'gui' : gui,
'clean' : clean,
'sdist' : sdist,
'update' : update,
'tag_release' : tag_release,
'upload_demo' : upload_demo,
'build_linux' : build_linux,
'build_windows' : build_windows,
'build_osx' : build_osx,
'upload_installers': upload_installers,
'upload_user_manual': upload_user_manual,
'upload_to_pypi': upload_to_pypi,
'stage3' : stage3,
'stage2' : stage2,
'stage1' : stage1,
'upload' : upload,
},
)

865
upload.py
View File

@ -1,11 +1,30 @@
#!/usr/bin/python
import sys, os, shutil, time, tempfile, socket, fcntl, struct, cStringIO, pycurl, re
sys.path.append('src')
import subprocess
from subprocess import check_call as _check_call
from functools import partial
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import shutil, os, glob, re, cStringIO, sys, tempfile, time, textwrap, socket, \
struct
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"
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(),
@ -16,252 +35,664 @@ def get_ip_address(ifname):
try:
HOST=get_ip_address('eth0')
except:
HOST=get_ip_address('wlan0')
PROJECT=os.path.basename(os.getcwd())
try:
HOST=get_ip_address('wlan0')
except:
HOST='unknown'
from calibre import __version__, __appname__
PREFIX = "/var/www/calibre.kovidgoyal.net"
DOWNLOADS = PREFIX+"/htdocs/downloads"
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/'
BUILD_SCRIPT ='''\
#!/bin/bash
export CALIBRE_BUILDBOT=1
cd ~/build && \
rsync -avz --exclude src/calibre/plugins --exclude calibre/src/calibre.egg-info --exclude docs --exclude .bzr --exclude .build --exclude build --exclude dist --exclude "*.pyc" --exclude "*.pyo" rsync://%(host)s/work/%(project)s . && \
cd %(project)s && \
%%s && \
rm -rf build/* dist/* && \
%%s %%s
'''%dict(host=HOST, project=PROJECT)
check_call = partial(_check_call, shell=True)
#h = Host(hostType=VIX_SERVICEPROVIDER_VMWARE_WORKSTATION)
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
def tag_release():
print 'Tagging release'
check_call('bzr tag '+__version__)
check_call('bzr commit --unchanged -m "IGN:Tag release"')
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(tempdir, __appname__+'.pot')
f = open(pot, 'wb')
f.write(src)
f.close()
print 'Translations template:', 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/lrf/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('<file%s>%s</file>'%(alias, s))
manifest = '<RCC>\n<qresource prefix="/">\n%s\n</qresource>\n</RCC>'%'\n'.join(files)
with open('images.qrc', 'wb') as f:
f.write(manifest)
check_call(['pyrcc4', '-o', images, 'images.qrc'])
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 = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
# Workaround bug in Qt 4.4 on Windows
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
print 'Implementing Workaround for buggy pyuic in form', form
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
if form.endswith('viewer%smain.ui'%os.sep):
print 'Promoting WebView'
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
open(compiled_form, 'wb').write(dat)
@classmethod
def clean(cls):
forms = cls.find_forms()
for form in forms:
c = cls.form_to_compiled_form(form)
if os.path.exists(c):
os.remove(c)
for x in (cls.IMAGES_DEST, cls.QRC):
if os.path.exists(x):
os.remove(x)
class clean(OptionlessCommand):
description='''Delete all computer generated files in the source tree'''
def run(self):
print 'Cleaning...'
manual.clean()
gui.clean()
translations.clean()
resources.clean()
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
os.remove(f)
for root, _, files in os.walk('.'):
for name in files:
for t in ('.pyc', '.pyo', '~'):
if name.endswith(t):
os.remove(os.path.join(root, name))
break
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
shutil.rmtree(dir, ignore_errors=True)
class build_py(_build_py):
def find_data_files(self, package, src_dir):
"""
Return filenames for package's data files in 'src_dir'
Modified to treat data file specs as paths not globs
"""
globs = (self.package_data.get('', [])
+ self.package_data.get(package, []))
files = self.manifest_files.get(package, [])[:]
for pattern in globs:
# Each pattern has to be converted to a platform-specific path
pattern = os.path.join(src_dir, convert_path(pattern))
next = glob.glob(pattern)
files.extend(next if next else [pattern])
return self.exclude_data_files(package, src_dir, files)
class build(_build):
sub_commands = [
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('build_ext', lambda self: True),
('build_py', lambda self: True),
('build_clib', _build.has_c_libraries),
('build_scripts', _build.has_scripts),
]
class update(OptionlessCommand):
description = 'Rebuild plugins and run develop. Should be called after ' +\
' a version update.'
def run(self):
for x in ['build', 'dist', 'docs'] + \
glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
if os.path.exists(x):
if os.path.isdir(x):
check_call('sudo rm -rf '+x, shell=True)
os.mkdir(x)
else:
os.remove(x)
check_call('python setup.py build_ext build'.split())
check_call('sudo python setup.py develop'.split())
class tag_release(OptionlessCommand):
description = 'Tag a new release in bzr'
def run(self):
print 'Tagging release'
check_call(('bzr tag '+__version__).split())
check_call('bzr commit --unchanged -m'.split() + ['IGN:Tag release'])
class upload_demo(OptionlessCommand):
description = 'Rebuild and upload various demos'
def run(self):
check_call(
'''html2lrf --title='Demonstration of html2lrf' --author='Kovid Goyal' '''
'''--header --output=/tmp/html2lrf.lrf %s/demo.html '''
'''--serif-family "/usr/share/fonts/corefonts, Times New Roman" '''
'''--mono-family "/usr/share/fonts/corefonts, Andale Mono" '''
''''''%(HTML2LRF,), shell=True)
check_call(
'cd src/calibre/ebooks/lrf/html/demo/ && '
'zip -j /tmp/html-demo.zip * /tmp/html2lrf.lrf', shell=True)
check_call('scp /tmp/html-demo.zip divok:%s/'%(DOWNLOADS,), shell=True)
check_call(
'''txt2lrf -t 'Demonstration of txt2lrf' -a 'Kovid Goyal' '''
'''--header -o /tmp/txt2lrf.lrf %s/demo.txt'''%(TXT2LRF,), shell=True)
check_call('cd src/calibre/ebooks/lrf/txt/demo/ && '
'zip -j /tmp/txt-demo.zip * /tmp/txt2lrf.lrf', shell=True)
check_call('''scp /tmp/txt-demo.zip divok:%s/'''%(DOWNLOADS,), shell=True)
def installer_name(ext):
if ext in ('exe', 'dmg'):
return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
return 'dist/%s-%s-i686.%s'%(__appname__, __version__, ext)
def start_vm(vm, ssh_host, build_script, sleep=75):
vmware = ('vmware', '-q', '-x', '-n', vm)
subprocess.Popen(vmware)
t = tempfile.NamedTemporaryFile(suffix='.sh')
t.write(build_script)
t.flush()
print 'Waiting for VM to startup'
while subprocess.call('ping -q -c1 '+ssh_host, shell=True, stdout=open('/dev/null', 'w')) != 0:
time.sleep(5)
time.sleep(20)
print 'Trying to SSH into VM'
subprocess.check_call(('scp', t.name, ssh_host+':build-'+PROJECT))
subprocess.check_call('ssh -t %s bash build-%s'%(ssh_host, PROJECT), shell=True)
def run_windows_install_jammer(installer):
ibp = os.path.abspath('installer/windows')
sys.path.insert(0, ibp)
import build_installer
sys.path.remove(ibp)
build_installer.run_install_jammer(installer_name=os.path.basename(installer))
if not os.path.exists(installer):
raise Exception('Failed to run installjammer')
class build_linux(OptionlessCommand):
def build_windows(shutdown=True):
installer = installer_name('exe')
vm = '/mnt/backup/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
start_vm(vm, 'windows', BUILD_SCRIPT%('python setup.py develop', 'python','installer\\\\windows\\\\freeze.py'))
if os.path.exists('build/py2exe'):
shutil.rmtree('build/py2exe')
subprocess.check_call(('scp', '-rp', 'windows:build/%s/build/py2exe'%PROJECT, 'build'))
if not os.path.exists('build/py2exe'):
raise Exception('Failed to run py2exe')
if shutdown:
subprocess.Popen(('ssh', 'windows', 'shutdown', '-s', '-t', '0'))
run_windows_install_jammer(installer)
return os.path.basename(installer)
def run(self):
installer = installer_name('tar.bz2')
locals = {}
exec open('installer/linux/freeze.py') in locals
locals['freeze']()
if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer)
return os.path.basename(installer)
def build_osx(shutdown=True):
installer = installer_name('dmg')
vm = '/vmware/Mac OSX/Mac OSX.vmx'
python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python'
start_vm(vm, 'osx', (BUILD_SCRIPT%('sudo %s setup.py develop'%python, python, 'installer/osx/freeze.py')).replace('rm ', 'sudo rm '))
subprocess.check_call(('scp', 'osx:build/%s/dist/*.dmg'%PROJECT, 'dist'))
if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer)
if shutdown:
subprocess.Popen(('ssh', 'osx', 'sudo', '/sbin/shutdown', '-h', 'now'))
return os.path.basename(installer)
class VMInstaller(OptionlessCommand):
user_options = [('dont-shutdown', 'd', 'Dont shutdown Vm after build')]
boolean_options = ['dont-shutdown']
def initialize_options(self):
self.dont_shutdown = False
BUILD_SCRIPT = textwrap.dedent('''\
#!/bin/bash
export CALIBRE_BUILDBOT=1
cd ~/build && \
rsync -avz --exclude src/calibre/plugins \
--exclude calibre/src/calibre.egg-info --exclude docs \
--exclude .bzr --exclude .build --exclude build --exclude dist \
--exclude "*.pyc" --exclude "*.pyo" \
rsync://%(host)s/work/%(project)s . && \
cd %(project)s && \
%%s && \
rm -rf build/* dist/* && \
%%s %%s
'''%dict(host=HOST, project=__appname__))
def get_build_script(self, subs):
return self.BUILD_SCRIPT%subs
def start_vm(self, ssh_host, build_script, sleep=75):
build_script = self.get_build_script(build_script)
vmware = ('vmware', '-q', '-x', '-n', self.VM)
Popen(vmware)
t = tempfile.NamedTemporaryFile(suffix='.sh')
t.write(build_script)
t.flush()
print 'Waiting for VM to startup'
while call('ping -q -c1 '+ssh_host, shell=True,
stdout=open('/dev/null', 'w')) != 0:
time.sleep(5)
time.sleep(20)
print 'Trying to SSH into VM'
check_call(('scp', t.name, ssh_host+':build-calibre'))
check_call('ssh -t %s bash build-calibre'%ssh_host, shell=True)
class build_windows(VMInstaller):
VM = '/mnt/backup/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
if not os.path.exists(VM):
VM = '/home/kovid/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
def run(self):
installer = installer_name('exe')
self.start_vm('windows', ('python setup.py develop',
'python',
r'installer\\windows\\freeze.py'))
if os.path.exists('build/py2exe'):
shutil.rmtree('build/py2exe')
check_call(('scp', '-rp', 'windows:build/%s/build/py2exe'%__appname__,
'build'))
if not os.path.exists('build/py2exe'):
raise Exception('Failed to run py2exe')
if not self.dont_shutdown:
Popen(('ssh', 'windows', 'shutdown', '-s', '-t', '0'))
self.run_windows_install_jammer(installer)
return os.path.basename(installer)
def run_windows_install_jammer(self, installer):
ibp = os.path.abspath('installer/windows')
sys.path.insert(0, ibp)
build_installer = __import__('build_installer')
sys.path.remove(ibp)
build_installer.run_install_jammer(
installer_name=os.path.basename(installer))
if not os.path.exists(installer):
raise Exception('Failed to run installjammer')
def build_linux(*args, **kwargs):
installer = installer_name('tar.bz2')
exec open('installer/linux/freeze.py')
freeze()
if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer)
return os.path.basename(installer)
class build_osx(VMInstaller):
def build_installers():
return build_linux(), build_windows(), build_osx()
VM = '/vmware/Mac OSX/Mac OSX.vmx'
if not os.path.exists(VM):
VM = '/home/kovid/calibre_os_x/Mac OSX.vmx'
def upload_demo():
check_call('''html2lrf --title='Demonstration of html2lrf' --author='Kovid Goyal' '''
'''--header --output=/tmp/html2lrf.lrf %s/demo.html '''
'''--serif-family "/usr/share/fonts/corefonts, Times New Roman" '''
'''--mono-family "/usr/share/fonts/corefonts, Andale Mono" '''
''''''%(HTML2LRF,))
check_call('cd src/calibre/ebooks/lrf/html/demo/ && zip -j /tmp/html-demo.zip * /tmp/html2lrf.lrf')
check_call('''scp /tmp/html-demo.zip divok:%s/'''%(DOWNLOADS,))
check_call('''txt2lrf -t 'Demonstration of txt2lrf' -a 'Kovid Goyal' '''
'''--header -o /tmp/txt2lrf.lrf %s/demo.txt'''%(TXT2LRF,) )
check_call('cd src/calibre/ebooks/lrf/txt/demo/ && zip -j /tmp/txt-demo.zip * /tmp/txt2lrf.lrf')
check_call('''scp /tmp/txt-demo.zip divok:%s/'''%(DOWNLOADS,))
def get_build_script(self, subs):
return (self.BUILD_SCRIPT%subs).replace('rm ', 'sudo rm ')
def curl_list_dir(url=MOBILEREAD, listonly=1):
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(c.FTP_USE_EPSV, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
c.setopt(c.FTPLISTONLY, listonly)
c.setopt(c.FTP_CREATE_MISSING_DIRS, 1)
b = cStringIO.StringIO()
c.setopt(c.WRITEFUNCTION, b.write)
c.perform()
c.close()
return b.getvalue().split() if listonly else b.getvalue().splitlines()
def curl_delete_file(path, url=MOBILEREAD):
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(c.FTP_USE_EPSV, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
print 'Deleting file %s on %s'%(path, url)
c.setopt(c.QUOTE, ['dele '+ path])
c.perform()
c.close()
def run(self):
installer = installer_name('dmg')
python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python'
self.start_vm('osx', ('sudo %s setup.py develop'%python, python,
'installer/osx/freeze.py'))
check_call(('scp', 'osx:build/calibre/dist/*.dmg', 'dist'), shell=True)
if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer)
if not self.dont_shutdown:
Popen(('ssh', 'osx', 'sudo', '/sbin/shutdown', '-h', 'now'))
return os.path.basename(installer)
def curl_upload_file(stream, url):
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.UPLOAD, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
c.setopt(pycurl.READFUNCTION, stream.read)
stream.seek(0, 2)
c.setopt(pycurl.INFILESIZE_LARGE, stream.tell())
stream.seek(0)
c.setopt(c.NOPROGRESS, 0)
c.setopt(c.FTP_CREATE_MISSING_DIRS, 1)
print 'Uploading file %s to url %s' % (getattr(stream, 'name', ''), url)
try:
class upload_installers(OptionlessCommand):
def curl_list_dir(self, url=MOBILEREAD, listonly=1):
import pycurl
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(c.FTP_USE_EPSV, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
c.setopt(c.FTPLISTONLY, listonly)
c.setopt(c.FTP_CREATE_MISSING_DIRS, 1)
b = cStringIO.StringIO()
c.setopt(c.WRITEFUNCTION, b.write)
c.perform()
c.close()
return b.getvalue().split() if listonly else b.getvalue().splitlines()
def curl_delete_file(self, path, url=MOBILEREAD):
import pycurl
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(c.FTP_USE_EPSV, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
print 'Deleting file %s on %s'%(path, url)
c.setopt(c.QUOTE, ['dele '+ path])
c.perform()
c.close()
except:
pass
files = curl_list_dir(listonly=0)
for line in files:
line = line.split()
if url.endswith(line[-1]):
size = long(line[4])
stream.seek(0,2)
if size != stream.tell():
raise RuntimeError('curl failed to upload %s correctly'%getattr(stream, 'name', ''))
def curl_upload_file(self, stream, url):
import pycurl
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.UPLOAD, 1)
c.setopt(c.NETRC, c.NETRC_REQUIRED)
c.setopt(pycurl.READFUNCTION, stream.read)
stream.seek(0, 2)
c.setopt(pycurl.INFILESIZE_LARGE, stream.tell())
stream.seek(0)
c.setopt(c.NOPROGRESS, 0)
c.setopt(c.FTP_CREATE_MISSING_DIRS, 1)
print 'Uploading file %s to url %s' % (getattr(stream, 'name', ''), url)
try:
c.perform()
c.close()
except:
pass
files = self.curl_list_dir(listonly=0)
for line in files:
line = line.split()
if url.endswith(line[-1]):
size = long(line[4])
stream.seek(0,2)
if size != stream.tell():
raise RuntimeError('curl failed to upload %s correctly'%getattr(stream, 'name', ''))
def upload_installer(name):
if not os.path.exists(name):
return
bname = os.path.basename(name)
pat = re.compile(bname.replace(__version__, r'\d+\.\d+\.\d+'))
for f in curl_list_dir():
if pat.search(f):
curl_delete_file('/calibre/'+f)
curl_upload_file(open(name, 'rb'), MOBILEREAD+os.path.basename(name))
def upload_installer(self, name):
if not os.path.exists(name):
return
bname = os.path.basename(name)
pat = re.compile(bname.replace(__version__, r'\d+\.\d+\.\d+'))
for f in self.curl_list_dir():
if pat.search(f):
self.curl_delete_file('/calibre/'+f)
self.curl_upload_file(open(name, 'rb'), MOBILEREAD+os.path.basename(name))
def upload_installers():
for i in ('dmg', 'exe', 'tar.bz2'):
upload_installer(installer_name(i))
def run(self):
print 'Uploading installers...'
for i in ('dmg', 'exe', 'tar.bz2'):
self.upload_installer(installer_name(i))
check_call('''ssh divok echo %s \\> %s/latest_version'''%(__version__, DOWNLOADS))
check_call('''ssh divok echo %s \\> %s/latest_version'''\
%(__version__, DOWNLOADS), shell=True)
class upload_user_manual(OptionlessCommand):
def upload_docs():
check_call('''epydoc --config epydoc.conf''')
check_call('''scp -r docs/html divok:%s/'''%(DOCS,))
check_call('''epydoc -v --config epydoc-pdf.conf''')
check_call('''scp docs/pdf/api.pdf divok:%s/'''%(DOCS,))
sub_commands = [('manual', None)]
def upload_user_manual():
check_call('python setup.py manual')
check_call('scp -r src/calibre/manual/.build/html/* divok:%s'%USER_MANUAL)
def run(self):
OptionlessCommand.run(self)
check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*',
'divok:%s'%USER_MANUAL]), shell=True)
def build_src_tarball():
check_call('bzr export dist/calibre-%s.tar.gz'%__version__)
class upload_to_pypi(OptionlessCommand):
def upload_src_tarball():
check_call('ssh divok rm -f %s/calibre-\*.tar.gz'%DOWNLOADS)
check_call('scp dist/calibre-*.tar.gz divok:%s/'%DOWNLOADS)
def run(self):
check_call('python setup.py register')
check_call('rm -f dist/*', shell=True)
check_call('sudo rm -rf build src/calibre/plugins/*', shell=True)
os.mkdir('build')
check_call('python2.5 setup.py build_ext bdist_egg --exclude-source-files upload'.split())
check_call('sudo rm -rf build src/calibre/plugins/*', shell=True)
os.mkdir('build')
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload'.split())
check_call('python setup.py sdist upload'.split())
def stage_one():
check_call('sudo rm -rf build src/calibre/plugins/*', shell=True)
os.mkdir('build')
shutil.rmtree('docs')
os.mkdir('docs')
check_call('python setup.py build_ext build', shell=True)
check_call('sudo python setup.py develop', shell=True)
tag_release()
upload_demo()
class stage3(OptionlessCommand):
def stage_two():
subprocess.check_call('rm -rf dist/*', shell=True)
build_installers()
sub_commands = [
('upload_installers', None),
('upload_user_manual', None),
('upload_to_pypi', None),
]
def stage_three():
print 'Uploading installers...'
upload_installers()
print 'Uploading documentation...'
#upload_docs()
upload_user_manual()
print 'Uploading to PyPI...'
check_call('rm -f dist/*')
check_call('python setup.py register')
check_call('sudo rm -rf build src/calibre/plugins/*')
os.mkdir('build')
check_call('python2.5 setup.py build_ext bdist_egg --exclude-source-files upload')
check_call('sudo rm -rf build src/calibre/plugins/*')
os.mkdir('build')
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload')
check_call('python setup.py sdist upload')
upload_src_tarball()
check_call('''rm -rf dist/* build/*''')
check_call('''ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/''')
def run(self):
OptionlessCommand.run(self)
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('''rm -rf dist/* build/*''', shell=True)
check_call('ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/',
shell=True)
def betas():
subprocess.check_call('rm -f dist/*', shell=True)
build_installers()
check_call('ssh divok rm -f /var/www/calibre.kovidgoyal.net/htdocs/downloads/betas/*')
check_call('scp dist/* divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads/betas/')
class stage2(OptionlessCommand):
def main(args=sys.argv):
print 'Starting stage one...'
stage_one()
print 'Starting stage two...'
stage_two()
print 'Starting stage three...'
stage_three()
print 'Finished'
return 0
sub_commands = [
('build_linux', None),
('build_windows', None),
('build_osx', None),
('upload_installers', None),
]
def run(self):
check_call('rm -rf dist/*', shell=True)
OptionlessCommand.run(self)
if __name__ == '__main__':
sys.exit(main())
class stage1(OptionlessCommand):
sub_commands = [
('update', None),
('tag_release', None),
('upload_demo', None),
]
class upload(OptionlessCommand):
sub_commands = [
('stage1', None),
('stage2', None),
('stage3', None)
]