mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
822 lines
31 KiB
Python
822 lines
31 KiB
Python
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, 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 if 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', '-t', 'online',
|
|
'.build/doctrees', '.', '.build/html'])
|
|
check_call(['sphinx-build', '-b', 'epub', '-d',
|
|
'.build/doctrees', '.', '.build/epub'])
|
|
j = os.path.join
|
|
shutil.copyfile(j('.build', 'epub', 'calibre.epub'), j('.build',
|
|
'html', 'calibre.epub'))
|
|
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] = self.get(os.path.join(sdir, f))
|
|
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', '')] = self.get(os.path.join(sdir, f))
|
|
mtime = os.stat(os.path.join(sdir, f)).st_mtime
|
|
max = mtime if mtime > max else max
|
|
return resources, max
|
|
|
|
def get_hyphenate(self):
|
|
sdir = os.path.join('src', 'calibre', 'gui2', 'viewer', 'hyphenate')
|
|
resources, max = {}, 0
|
|
languages = set([])
|
|
for f in glob.glob(os.path.join(sdir, 'patterns', '*.js')) + \
|
|
[os.path.join(sdir, 'Hyphenator.js')]:
|
|
f = os.path.abspath(f)
|
|
b = os.path.basename(f)
|
|
resources[b] = self.get(f)
|
|
if b != 'Hyphenator.js':
|
|
languages.add(b.split('.')[0])
|
|
mtime = os.stat(f).st_mtime
|
|
max = mtime if mtime > max else max
|
|
resources['languages'] = ','.join(languages)
|
|
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()
|
|
hyphenate, hmax = self.get_hyphenate()
|
|
amax = max(rmax, smax, hmax, os.stat(__file__).st_mtime)
|
|
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 = self.get(RESOURCES[key])
|
|
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('hyphenate = %s\n\n'%repr(hyphenate))
|
|
f.write('build_time = "%s"\n\n'%time.strftime('%d %m %Y %H%M%S'))
|
|
else:
|
|
print 'Resources are up to date'
|
|
|
|
def get(self, path):
|
|
data = open(path, 'rb').read()
|
|
if path.endswith('.js') and not path.endswith('date.js'):
|
|
data = self.js_minify(data)
|
|
return data
|
|
|
|
def js_minify(self, data):
|
|
from jsmin import jsmin
|
|
return jsmin(data)
|
|
|
|
@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)
|
|
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+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
|
|
dat = dat.replace('_("MMM yyyy")', '"MMM yyyy"')
|
|
|
|
# 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'] + \
|
|
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(
|
|
'''ebook-convert %s/demo.html /tmp/html2lrf.lrf '''
|
|
'''--title='Demonstration of html2lrf' --authors='Kovid Goyal' '''
|
|
'''--header '''
|
|
'''--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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
class build_linux(OptionlessCommand):
|
|
description = 'Build linux 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)
|
|
|
|
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):
|
|
description = 'Build windows installer'
|
|
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)
|
|
|
|
@classmethod
|
|
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')
|
|
|
|
|
|
class build_osx(VMInstaller):
|
|
description = 'Build OS X app bundle'
|
|
VM = '/mnt/backup/calibre_os_x/Mac OSX.vmx'
|
|
if not os.path.exists(VM):
|
|
VM = '/home/kovid/calibre_os_x/Mac OSX.vmx'
|
|
|
|
def get_build_script(self, subs):
|
|
return (self.BUILD_SCRIPT%subs).replace('rm ', 'sudo rm ')
|
|
|
|
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'))
|
|
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)
|
|
|
|
|
|
|
|
class upload_installers(OptionlessCommand):
|
|
description = 'Upload any installers present in dist/'
|
|
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()
|
|
|
|
|
|
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(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 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), 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
|
|
_show_advice = 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 = '<div><ul>%s</ul></div>'
|
|
self.current_entry.description = mkup%(''.join(
|
|
self.current_entry.description))
|
|
if match.group(1) == '0.5.14':
|
|
self.current_entry.description = \
|
|
'''<div>See <a href="http://calibre.kovidgoyal.net/new_in_6">New in
|
|
6</a></div>'''
|
|
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'<a href="http://calibre.kovidgoyal.net/ticket/\1">#\1</a>',
|
|
msg)
|
|
|
|
self.current_entry.description.append(
|
|
'<li>%s</li>'%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
|
|
|