appy.bin: generate.py: one less arg: outputFolder has been removed (the script now generates the Zope product in <appFolder>/zope); generate.py: new option '-d', for generating a Debian package from the Python (Appy) app.

This commit is contained in:
Gaetan Delannay 2012-01-18 14:27:24 +01:00
parent a89d65afc6
commit 13443ea79e
5 changed files with 171 additions and 121 deletions

View file

@ -1,17 +1,17 @@
'''This script allows to generate a product from a Appy application.''' '''This script allows to generate a Zope product from a Appy application.'''
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import sys, os.path import sys, os.path
from optparse import OptionParser from optparse import OptionParser
from appy.gen.generator import GeneratorError, ZopeGenerator from appy.gen.generator import GeneratorError, ZopeGenerator
from appy.shared.utils import LinesCounter from appy.shared.utils import LinesCounter
from appy.shared.packaging import Debianizer
import appy.version import appy.version
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ERROR_CODE = 1 ERROR_CODE = 1
APP_NOT_FOUND = 'Application not found at %s.' APP_NOT_FOUND = 'Application not found at %s.'
WRONG_NG_OF_ARGS = 'Wrong number of arguments.' WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.'
C_OPTION = 'Removes from i18n files all labels that are not automatically ' \ C_OPTION = 'Removes from i18n files all labels that are not automatically ' \
'generated from your gen-application. It can be useful during ' \ 'generated from your gen-application. It can be useful during ' \
'development, when you do lots of name changes (classes, ' \ 'development, when you do lots of name changes (classes, ' \
@ -32,26 +32,24 @@ S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \
'i18n file. When the development is finished, it may be a good ' \ 'i18n file. When the development is finished, it may be a good ' \
'idea to sort the labels to get a clean and logically ordered ' \ 'idea to sort the labels to get a clean and logically ordered ' \
'set of translation files.' 'set of translation files.'
D_OPTION = 'Generates a Debian package for this app. The Debian package will ' \
'be generated at the same level as the root application folder.'
class GeneratorScript: class GeneratorScript:
'''usage: %prog [options] app outputFolder '''usage: %prog [options] app
"app" is the path to your Appy application, which must be a "app" is the path to your Appy application, which must be a
Python package (= a folder containing a file named Python package (= a folder containing a file named
__init__.py). Your app may reside anywhere, but needs to __init__.py). Your app may reside anywhere, but needs to
be accessible by Zope. Typically, it may be or symlinked be accessible by Zope. Typically, it may be or symlinked
in <yourZopeInstance>/lib/python, but not within the in <yourZopeInstance>/lib/python.
generated product, stored or symlinked in
<yourZopeInstance>/Products.
"outputFolder" is the folder where the Zope product will be generated. This command generates a Zope product in <app>/zope, which must be
For example, if you develop your application in or symlinked in <yourZopeInstance>/Products.
/home/gdy/MyProject/MyProject, you typically specify
"/home/gdy/MyProject/zope" as outputFolder.
''' '''
def manageArgs(self, parser, options, args): def manageArgs(self, parser, options, args):
# Check number of args # Check number of args
if len(args) != 2: if len(args) != 1:
print WRONG_NG_OF_ARGS print WRONG_NG_OF_ARGS
parser.print_help() parser.print_help()
sys.exit(ERROR_CODE) sys.exit(ERROR_CODE)
@ -59,13 +57,8 @@ class GeneratorScript:
if not os.path.exists(args[0]): if not os.path.exists(args[0]):
print APP_NOT_FOUND % args[0] print APP_NOT_FOUND % args[0]
sys.exit(ERROR_CODE) sys.exit(ERROR_CODE)
# Check existence of outputFolder # Convert app path to an absolute path
if not os.path.exists(args[1]): args[0] = os.path.abspath(args[0])
print WRONG_OUTPUT_FOLDER
sys.exit(ERROR_CODE)
# Convert all paths in absolute paths
for i in (0,1):
args[i] = os.path.abspath(args[i])
def run(self): def run(self):
optParser = OptionParser(usage=GeneratorScript.__doc__) optParser = OptionParser(usage=GeneratorScript.__doc__)
@ -73,14 +66,26 @@ class GeneratorScript:
dest='i18nClean', default=False, help=C_OPTION) dest='i18nClean', default=False, help=C_OPTION)
optParser.add_option("-s", "--i18n-sort", action='store_true', optParser.add_option("-s", "--i18n-sort", action='store_true',
dest='i18nSort', default=False, help=S_OPTION) dest='i18nSort', default=False, help=S_OPTION)
optParser.add_option("-d", "--debian", action='store_true',
dest='debian', default=False, help=D_OPTION)
(options, args) = optParser.parse_args() (options, args) = optParser.parse_args()
try: try:
self.manageArgs(optParser, options, args) self.manageArgs(optParser, options, args)
print 'Appy version:', appy.version.verbose print 'Appy version:', appy.version.verbose
print 'Generating Zope product in %s...' % args[1] print 'Generating Zope product in %s/zope...' % args[0]
ZopeGenerator(args[0], args[1], options).run() ZopeGenerator(args[0], options).run()
# Give the user some statistics about its code # Give the user some statistics about its code
LinesCounter(args[0]).run() LinesCounter(args[0], excludes=['%szope' % os.sep]).run()
# Generates a Debian package for this app if required
if options.debian:
app = args[0]
appDir = os.path.dirname(app)
# Get the app version from zope/version.txt
f = file(os.path.join(app, 'zope', 'version.txt'))
version = f.read()
f.close()
version = version[:version.find('build')-1]
Debianizer(app, appDir, appVersion=version).run()
except GeneratorError, ge: except GeneratorError, ge:
sys.stderr.write(str(ge)) sys.stderr.write(str(ge))
sys.stderr.write('\n') sys.stderr.write('\n')

View file

@ -1,9 +1,10 @@
#!/usr/bin/python #!/usr/bin/python
# Imports ---------------------------------------------------------------------- # Imports ----------------------------------------------------------------------
import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time, subprocess, md5 import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time
import appy import appy
from appy.shared import appyPath from appy.shared import appyPath
from appy.shared.utils import FolderDeleter, LinesCounter from appy.shared.utils import FolderDeleter, LinesCounter
from appy.shared.packaging import Debianizer
from appy.bin.clean import Cleaner from appy.bin.clean import Cleaner
from appy.gen.utils import produceNiceMessage from appy.gen.utils import produceNiceMessage
@ -26,34 +27,6 @@ recursive-include appy/gen *
recursive-include appy/pod * recursive-include appy/pod *
recursive-include appy/shared * recursive-include appy/shared *
''' '''
debianInfo = '''Package: python-appy
Version: %s
Architecture: all
Maintainer: Gaetan Delannay <gaetan.delannay@geezteem.com>
Installed-Size: %d
Depends: python (>= 2.6), python (<< 3.0)
Section: python
Priority: optional
Homepage: http://appyframework.org
Description: Appy builds simple but complex web Python apps.
'''
debianPostInst = '''#!/bin/sh
set -e
if [ -e /usr/bin/python2.6 ]
then
/usr/bin/python2.6 -m compileall -q /usr/lib/python2.6/appy 2> /dev/null
fi
if [ -e /usr/bin/python2.7 ]
then
/usr/bin/python2.7 -m compileall -q /usr/lib/python2.7/appy 2> /dev/null
fi
'''
debianPreRm = '''#!/bin/sh
set -e
find /usr/lib/python2.6/appy -name "*.pyc" -delete
find /usr/lib/python2.7/appy -name "*.pyc" -delete
'''
def askLogin(): def askLogin():
print 'Login: ', print 'Login: ',
login = sys.stdin.readline().strip() login = sys.stdin.readline().strip()
@ -242,9 +215,6 @@ class Text2Html:
class Publisher: class Publisher:
'''Publishes Appy on the web.''' '''Publishes Appy on the web.'''
pageBody = re.compile('<body.*?>(.*)</body>', re.S) pageBody = re.compile('<body.*?>(.*)</body>', re.S)
eggVersion = re.compile('version\s*=\s*".*?"')
pythonTargets = ('2.4', '2.5')
svnServer = 'http://svn.communesplone.org/svn/communesplone/appy'
def __init__(self): def __init__(self):
self.genFolder = '%s/temp' % appyPath self.genFolder = '%s/temp' % appyPath
@ -303,66 +273,9 @@ class Publisher:
def createDebianRelease(self): def createDebianRelease(self):
'''Creates a Debian package for Appy.''' '''Creates a Debian package for Appy.'''
curdir = os.getcwd() j = os.path.join
# Create a temp folder for creating the Debian files hierarchy. Debianizer(j(self.genFolder, 'appy'), j(appyPath, 'versions'),
srcFolder = os.path.join(self.genFolder, 'debian', 'usr', 'lib') appVersion=self.versionShort).run()
os.makedirs(os.path.join(srcFolder, 'python2.6'))
os.makedirs(os.path.join(srcFolder, 'python2.7'))
# Copy Appy sources in it
py26 = os.path.join(srcFolder, 'python2.6', 'appy')
os.rename(os.path.join(self.genFolder, 'appy'), py26)
shutil.copytree(py26, os.path.join(srcFolder, 'python2.7', 'appy'))
# Create data.tar.gz based on it.
debFolder = os.path.join(self.genFolder, 'debian')
os.chdir(debFolder)
os.system('tar czvf data.tar.gz ./usr')
# Get the size of Appy, in Kb.
cmd = subprocess.Popen(['du', '-b', '-s', 'usr'],stdout=subprocess.PIPE)
size = int(int(cmd.stdout.read().split()[0])/1024.0)
# Create control file
f = file('control', 'w')
f.write(debianInfo % (self.versionShort, size))
f.close()
# Create md5sum file
f = file('md5sums', 'w')
for dir, dirnames, filenames in os.walk('usr'):
for name in filenames:
m = md5.new()
pathName = os.path.join(dir, name)
currentFile = file(pathName, 'rb')
while True:
data = currentFile.read(8096)
if not data:
break
m.update(data)
currentFile.close()
# Add the md5 sum to the file
f.write('%s %s\n' % (m.hexdigest(), pathName))
f.close()
# Create postinst and prerm
f = file('postinst', 'w')
f.write(debianPostInst)
f.close()
f = file('prerm', 'w')
f.write(debianPreRm)
f.close()
# Create control.tar.gz
os.system('tar czvf control.tar.gz ./control ./md5sums ./postinst ' \
'./prerm')
# Create debian-binary
f = file('debian-binary', 'w')
f.write('2.0\n')
f.close()
# Create the .deb package
debName = 'python-appy-%s.deb' % self.versionShort
os.system('ar -r %s debian-binary control.tar.gz data.tar.gz' % \
debName)
os.chdir(curdir)
# Move it to folder "versions".
os.rename(os.path.join(debFolder, debName),
os.path.join(appyPath, 'versions', debName))
# Clean temp files
FolderDeleter.delete(debFolder)
def createDistRelease(self): def createDistRelease(self):
'''Create the distutils package.''' '''Create the distutils package.'''

View file

@ -115,12 +115,12 @@ CODE_HEADER = '''# -*- coding: utf-8 -*-
''' '''
class Generator: class Generator:
'''Abstract base class for building a generator.''' '''Abstract base class for building a generator.'''
def __init__(self, application, outputFolder, options): def __init__(self, application, options):
self.application = application self.application = application
# Determine application name # Determine application name
self.applicationName = os.path.basename(application) self.applicationName = os.path.basename(application)
# Determine output folder (where to store the generated product) # Determine output folder (where to store the generated product)
self.outputFolder = outputFolder self.outputFolder = os.path.join(application, 'zope')
self.options = options self.options = options
# Determine templates folder # Determine templates folder
genFolder = os.path.dirname(__file__) genFolder = os.path.dirname(__file__)
@ -186,8 +186,13 @@ class Generator:
IMPORT_ERROR = 'Warning: error while importing module %s (%s)' IMPORT_ERROR = 'Warning: error while importing module %s (%s)'
SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)' SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)'
noVisit = ('tr', 'zope')
def walkModule(self, moduleName): def walkModule(self, moduleName):
'''Visits a given (sub-*)module into the application.''' '''Visits a given (sub-*)module into the application.'''
# Some sub-modules must not be visited
for nv in self.noVisit:
nvName = '%s.%s' % (self.applicationName, nv)
if moduleName == nvName: return
try: try:
exec 'import %s' % moduleName exec 'import %s' % moduleName
exec 'moduleObj = %s' % moduleName exec 'moduleObj = %s' % moduleName
@ -359,7 +364,7 @@ class ZopeGenerator(Generator):
versionRex = re.compile('(.*?\s+build)\s+(\d+)') versionRex = re.compile('(.*?\s+build)\s+(\d+)')
def initialize(self): def initialize(self):
# Determine version number # Determine version number
self.version = '0.1 build 1' self.version = '0.1.0 build 1'
versionTxt = os.path.join(self.outputFolder, 'version.txt') versionTxt = os.path.join(self.outputFolder, 'version.txt')
if os.path.exists(versionTxt): if os.path.exists(versionTxt):
f = file(versionTxt) f = file(versionTxt)

119
shared/packaging.py Normal file
View file

@ -0,0 +1,119 @@
# ------------------------------------------------------------------------------
import os, os.path, subprocess, md5, shutil
from appy.shared.utils import getOsTempFolder, FolderDeleter
# ------------------------------------------------------------------------------
debianInfo = '''Package: python-appy%s
Version: %s
Architecture: all
Maintainer: Gaetan Delannay <gaetan.delannay@geezteem.com>
Installed-Size: %d
Depends: python (>= %s), python (<= %s)%s
Section: python
Priority: optional
Homepage: http://appyframework.org
Description: Appy builds simple but complex web Python apps.
'''
class Debianizer:
'''This class allows to produce a Debian package from a Python (Appy)
package.'''
def __init__(self, app, out, appVersion='0.1.0',
pythonVersions=('2.6', '2.7')):
# app is the path to the Python package to Debianize.
self.app = app
self.appName = os.path.basename(app)
# out is the folder where the Debian package will be generated.
self.out = out
# What is the version number for this app ?
self.appVersion = appVersion
# On which Python versions will the Debian package depend?
self.pythonVersions = pythonVersions
def run(self):
'''Generates the Debian package.'''
curdir = os.getcwd()
j = os.path.join
tempFolder = getOsTempFolder()
# Create, in the temp folder, the required sub-structure for the Debian
# package.
debFolder = j(tempFolder, 'debian')
if os.path.exists(debFolder):
FolderDeleter.delete(debFolder)
# Copy the Python package into it
srcFolder = j(debFolder, 'usr', 'lib')
for version in self.pythonVersions:
libFolder = j(srcFolder, 'python%s' % version)
os.makedirs(libFolder)
shutil.copytree(self.app, j(libFolder, self.appName))
# Create data.tar.gz based on it.
os.chdir(debFolder)
os.system('tar czvf data.tar.gz ./usr')
# Get the size of the app, in Kb.
cmd = subprocess.Popen(['du', '-b', '-s', 'usr'],stdout=subprocess.PIPE)
size = int(int(cmd.stdout.read().split()[0])/1024.0)
# Create the control file
f = file('control', 'w')
nameSuffix = ''
depends = ''
if self.appName != 'appy':
nameSuffix = '-%s' % self.appName
depends = ', python-appy'
f.write(debianInfo % (nameSuffix, self.appVersion, size,
self.pythonVersions[0], self.pythonVersions[1],
depends))
f.close()
# Create md5sum file
f = file('md5sums', 'w')
for dir, dirnames, filenames in os.walk('usr'):
for name in filenames:
m = md5.new()
pathName = j(dir, name)
currentFile = file(pathName, 'rb')
while True:
data = currentFile.read(8096)
if not data:
break
m.update(data)
currentFile.close()
# Add the md5 sum to the file
f.write('%s %s\n' % (m.hexdigest(), pathName))
f.close()
# Create postinst, a script that will bytecompile Python files after the
# Debian install.
f = file('postinst', 'w')
content = '#!/bin/sh\nset -e\n'
for version in self.pythonVersions:
content += 'if [ -e /usr/bin/python%s ]\nthen\n ' \
'/usr/bin/python%s -m compileall -q ' \
'/usr/lib/python%s/%s 2> /dev/null\nfi\n' % \
(version, version, version, self.appName)
f.write(content)
f.close()
# Create prerm, a script that will remove all pyc files before removing
# the Debian package.
f = file('prerm', 'w')
content = '#!/bin/sh\nset -e\n'
for version in self.pythonVersions:
content += 'find /usr/lib/python%s/%s -name "*.pyc" -delete\n' % \
(version, self.appName)
f.write(content)
f.close()
# Create control.tar.gz
os.system('tar czvf control.tar.gz ./control ./md5sums ./postinst ' \
'./prerm')
# Create debian-binary
f = file('debian-binary', 'w')
f.write('2.0\n')
f.close()
# Create the .deb package
debName = 'python-appy%s-%s.deb' % (nameSuffix, self.appVersion)
os.system('ar -r %s debian-binary control.tar.gz data.tar.gz' % \
debName)
# Move it to self.out
os.rename(j(debFolder, debName), j(self.out, debName))
# Clean temp files
FolderDeleter.delete(debFolder)
os.chdir(curdir)
# ------------------------------------------------------------------------------

View file

@ -364,7 +364,10 @@ class CodeAnalysis:
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class LinesCounter: class LinesCounter:
'''Counts and classifies the lines of code within a folder hierarchy.''' '''Counts and classifies the lines of code within a folder hierarchy.'''
def __init__(self, folderOrModule): defaultExcludes = ('%s.svn' % os.sep, '%s.bzr' % os.sep, '%stmp' % os.sep,
'%stemp' % os.sep)
def __init__(self, folderOrModule, excludes=None):
if isinstance(folderOrModule, basestring): if isinstance(folderOrModule, basestring):
# It is the path of some folder # It is the path of some folder
self.folder = folderOrModule self.folder = folderOrModule
@ -378,12 +381,20 @@ class LinesCounter:
True: CodeAnalysis('ZPT (test)')} True: CodeAnalysis('ZPT (test)')}
# Are we currently analysing real or test code? # Are we currently analysing real or test code?
self.inTest = False self.inTest = False
# Which paths to exclude from the analysis?
self.excludes = list(self.defaultExcludes)
if excludes: self.excludes += excludes
def printReport(self): def printReport(self):
'''Displays on stdout a small analysis report about self.folder.''' '''Displays on stdout a small analysis report about self.folder.'''
for zone in (False, True): self.python[zone].printReport() for zone in (False, True): self.python[zone].printReport()
for zone in (False, True): self.zpt[zone].printReport() for zone in (False, True): self.zpt[zone].printReport()
def isExcluded(self, path):
'''Must p_path be excluded from the analysis?'''
for excl in self.excludes:
if excl in path: return True
def run(self): def run(self):
'''Let's start the analysis of self.folder.''' '''Let's start the analysis of self.folder.'''
# The test markers will allow us to know if we are analysing test code # The test markers will allow us to know if we are analysing test code
@ -394,10 +405,7 @@ class LinesCounter:
testMarker4 = '%stests' % os.sep testMarker4 = '%stests' % os.sep
j = os.path.join j = os.path.join
for root, folders, files in os.walk(self.folder): for root, folders, files in os.walk(self.folder):
rootName = os.path.basename(root) if self.isExcluded(root): continue
if rootName.startswith('.') or \
(rootName in ('tmp', 'temp')):
continue
# Are we in real code or in test code ? # Are we in real code or in test code ?
self.inTest = False self.inTest = False
if root.endswith(testMarker2) or (root.find(testMarker1) != -1) or \ if root.endswith(testMarker2) or (root.find(testMarker1) != -1) or \