-rwxr-xr-x | scripts/build | 5 | ||||
-rw-r--r-- | scripts/builder/backendBuilder.py | 89 | ||||
-rw-r--r-- | scripts/builder/cssmin.py | 223 | ||||
-rw-r--r-- | scripts/builder/frontendBuilder.py | 398 | ||||
-rw-r--r-- | scripts/builder/jsmin.py | 246 | ||||
-rwxr-xr-x | scripts/builder/main.py | 166 | ||||
-rw-r--r-- | scripts/builder/phpBuilder.py | 14 | ||||
-rw-r--r-- | scripts/builder/pythonBuilder.py | 14 |
8 files changed, 1155 insertions, 0 deletions
diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..b17a18f --- a/dev/null +++ b/scripts/build @@ -0,0 +1,5 @@ +#!/bin/bash + +readonly CURR_DIR=$(cd "$(dirname "$0")"; pwd -P) + +${CURR_DIR}/builder/main.py $@
\ No newline at end of file diff --git a/scripts/builder/backendBuilder.py b/scripts/builder/backendBuilder.py new file mode 100644 index 0000000..f5dc7b2 --- a/dev/null +++ b/scripts/builder/backendBuilder.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import sys, os, json +import shutil +import main +import hashlib + +class BackendBuilder: + + def __init__ (self, projectTargetDir, frontends, versions, settings): + self.projectTargetDir = projectTargetDir + self.frontends = frontends + self.versions = versions + self.settings = settings + + def name (self): + raise NotImplementedError() + + def relativePath (self): + raise NotImplementedError() + + def compileCode (self): + pass + + def copyCompiledCodeToTargetDir (self): + src = self.sourceFolder() + dst = self.targetFolder() + main.createFolder(os.path.dirname(dst)) + shutil.copytree(src, dst) + + def sourceFolder (self): + return main.projectBaseDir() + '/backend/' + self.relativePath() + '/src' + + + def targetFolder (self): + return self.projectTargetDir + self.relativePath() + + def createTargetFolder (self): + main.createFolder(self.targetFolder()) + + +# def copyFrontendResources (self, frontend): +# print "copying resources for frontend: " + frontend +# print "SETTINGS: " + str(self.settings) + + + def writeToTargetFolder (self, filename, content): + file = open(self.targetFolder() + '/' + filename, 'w') + file.write(content.encode('utf-8')) + file.close() + + + def configureIndexContent (self, indexContent): + result = indexContent + result = result.replace( '@request.path@', self.settings['request.path'] ) + result = result.replace( '@should.pay.toll@', self.settings['should.pay.toll'] ) + + return result + + + def logChecksums (self, content, message): + md5Digest = hashlib.md5(content.encode('utf-8')).hexdigest() + shaDigest = hashlib.sha1(content.encode('utf-8')).hexdigest() + sha256Digest = hashlib.sha256(content.encode('utf-8')).hexdigest() + print message + ": " + md5Digest + " (md5)" + print message + ": " + shaDigest + " (sha1)" + print message + ": " + sha256Digest + " (sha256)" + + + + def run (self): + print self.name() + " - RUN" + + self.compileCode() + self.copyCompiledCodeToTargetDir() + + for frontend in self.frontends: + frontendPath = frontend.module + '/' + if 'debug' in self.versions: + frontend.copyResourcesToTargetFolder(self.targetFolder()) + #self.writeToTargetFolder(frontendPath + 'index_debug.html', self.configureIndexContent(frontend.assembleDebugVersion())) + self.writeToTargetFolder(frontendPath + 'index_debug.html', self.configureIndexContent(frontend.assemble(assemblyMode='DEBUG', versionType='DEBUG'))) + + if 'install' in self.versions: + index = self.configureIndexContent(frontend.assemble()) + self.writeToTargetFolder(frontendPath + 'index.html', index) + self.logChecksums(index, "[" + self.name() + " - " + frontend.module + "] index.html checksum") + diff --git a/scripts/builder/cssmin.py b/scripts/builder/cssmin.py new file mode 100644 index 0000000..32ddf77 --- a/dev/null +++ b/scripts/builder/cssmin.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""`cssmin` - A Python port of the YUI CSS compressor.""" + + +from StringIO import StringIO # The pure-Python StringIO supports unicode. +import re + + +__version__ = '0.1.4' + + +def remove_comments(css): + """Remove all CSS comment blocks.""" + + iemac = False + preserve = False + comment_start = css.find("/*") + while comment_start >= 0: + # Preserve comments that look like `/*!...*/`. + # Slicing is used to make sure we don"t get an IndexError. + preserve = css[comment_start + 2:comment_start + 3] == "!" + + comment_end = css.find("*/", comment_start + 2) + if comment_end < 0: + if not preserve: + css = css[:comment_start] + break + elif comment_end >= (comment_start + 2): + if css[comment_end - 1] == "\\": + # This is an IE Mac-specific comment; leave this one and the + # following one alone. + comment_start = comment_end + 2 + iemac = True + elif iemac: + comment_start = comment_end + 2 + iemac = False + elif not preserve: + css = css[:comment_start] + css[comment_end + 2:] + else: + comment_start = comment_end + 2 + comment_start = css.find("/*", comment_start) + + return css + + +def remove_unnecessary_whitespace(css): + """Remove unnecessary whitespace characters.""" + + def pseudoclasscolon(css): + + """ + Prevents 'p :link' from becoming 'p:link'. + + Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is + translated back again later. + """ + + regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)") + match = regex.search(css) + while match: + css = ''.join([ + css[:match.start()], + match.group().replace(":", "___PSEUDOCLASSCOLON___"), + css[match.end():]]) + match = regex.search(css) + return css + + css = pseudoclasscolon(css) + # Remove spaces from before things. + css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css) + + # If there is a `@charset`, then only allow one, and move to the beginning. + css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css) + css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css) + + # Put the space back in for a few cases, such as `@media screen` and + # `(-webkit-min-device-pixel-ratio:0)`. + css = re.sub(r"\band\(", "and (", css) + + # Put the colons back. + css = css.replace('___PSEUDOCLASSCOLON___', ':') + + # Remove spaces from after things. + css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css) + + return css + + +def remove_unnecessary_semicolons(css): + """Remove unnecessary semicolons.""" + + return re.sub(r";+\}", "}", css) + + +def remove_empty_rules(css): + """Remove empty rules.""" + + return re.sub(r"[^\}\{]+\{\}", "", css) + + +def normalize_rgb_colors_to_hex(css): + """Convert `rgb(51,102,153)` to `#336699`.""" + + regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)") + match = regex.search(css) + while match: + colors = map(lambda s: s.strip(), match.group(1).split(",")) + hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors)) + css = css.replace(match.group(), hexcolor) + match = regex.search(css) + return css + + +def condense_zero_units(css): + """Replace `0(px, em, %, etc)` with `0`.""" + + return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css) + + +def condense_multidimensional_zeros(css): + """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`.""" + + css = css.replace(":0 0 0 0;", ":0;") + css = css.replace(":0 0 0;", ":0;") + css = css.replace(":0 0;", ":0;") + + # Revert `background-position:0;` to the valid `background-position:0 0;`. + css = css.replace("background-position:0;", "background-position:0 0;") + + return css + + +def condense_floating_points(css): + """Replace `0.6` with `.6` where possible.""" + + return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css) + + +def condense_hex_colors(css): + """Shorten colors from #AABBCC to #ABC where possible.""" + + regex = re.compile(r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])") + match = regex.search(css) + while match: + first = match.group(3) + match.group(5) + match.group(7) + second = match.group(4) + match.group(6) + match.group(8) + if first.lower() == second.lower(): + css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first) + match = regex.search(css, match.end() - 3) + else: + match = regex.search(css, match.end()) + return css + + +def condense_whitespace(css): + """Condense multiple adjacent whitespace characters into one.""" + + return re.sub(r"\s+", " ", css) + + +def condense_semicolons(css): + """Condense multiple adjacent semicolon characters into one.""" + + return re.sub(r";;+", ";", css) + + +def wrap_css_lines(css, line_length): + """Wrap the lines of the given CSS to an approximate length.""" + + lines = [] + line_start = 0 + for i, char in enumerate(css): + # It's safe to break after `}` characters. + if char == '}' and (i - line_start >= line_length): + lines.append(css[line_start:i + 1]) + line_start = i + 1 + + if line_start < len(css): + lines.append(css[line_start:]) + return '\n'.join(lines) + + +def cssmin(css, wrap=None): + css = remove_comments(css) + css = condense_whitespace(css) + # A pseudo class for the Box Model Hack + # (see http://tantek.com/CSS/Examples/boxmodelhack.html) + css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___") + css = remove_unnecessary_whitespace(css) + css = remove_unnecessary_semicolons(css) + css = condense_zero_units(css) + css = condense_multidimensional_zeros(css) + css = condense_floating_points(css) + css = normalize_rgb_colors_to_hex(css) + css = condense_hex_colors(css) + if wrap is not None: + css = wrap_css_lines(css, wrap) + css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""') + css = condense_semicolons(css) + return css.strip() + + +def main(): + import optparse + import sys + + p = optparse.OptionParser( + prog="cssmin", version=__version__, + usage="%prog [--wrap N]", + description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""") + + p.add_option( + '-w', '--wrap', type='int', default=None, metavar='N', + help="Wrap output to approximately N chars per line.") + + options, args = p.parse_args() + sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap)) + + +if __name__ == '__main__': + main() diff --git a/scripts/builder/frontendBuilder.py b/scripts/builder/frontendBuilder.py new file mode 100644 index 0000000..b796438 --- a/dev/null +++ b/scripts/builder/frontendBuilder.py @@ -0,0 +1,398 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import sys, os, re +import cssmin +import jsmin +import codecs +import shutil +import StringIO +import urllib + +#from mercurial import ui, hg +#from mercurial.node import hex +from dulwich.repo import Repo + +import main + + + +class FrontendBuilder: + + def __init__ (self, frontend, settings): + if '.' in frontend: + moduleComponents = frontend.split('.') + self.module = moduleComponents[0] + self.submodule = moduleComponents[1] + else: + self.module = frontend + self.submodule = frontend + + self.settings = settings + self.projectDir = main.projectBaseDir() + self.processedFiles = {} + + + def mercurialRepositoryVersion (self): + repo = hg.repository(ui.ui(), self.projectDir) + context = repo['tip'] + result = str(context) + + return result + + + def gitRepositoryVersion (self): + repo = Repo(self.projectDir) + #if repo.is_dirty(): + # print "WARNING: build run with dirty repository" + result = repo.refs['HEAD'] + + return result + + + + def repositoryVersion (self): + cacheKey = 'repositoryVersion' + if not self.processedFiles.has_key(cacheKey): + #result = self.mercurialRepositoryVersion() + result = self.gitRepositoryVersion() + self.processedFiles[cacheKey] = result + else: + result = self.processedFiles[cacheKey] + + return result + + + #def relativePath (self): + # return self.module + # + + def log (self, message): + print "frontend [" + self.module + "]: " + message + + + def absolutePathForSourceFile (self, folder, basePath, file): + return folder + '/frontend/' + self.module + '/' + basePath + '/' + file + + + def absolutePathForTargetFile (self, folder, basePath, file): + return folder + '/' + self.module + '/' + basePath + '/' + file + + def filterFiles (self, files): + result = [] + + for file in files: + if file.startswith('--'): + pass + else: + result.append(file) + + return result + + + def copyResources (self, sourceFolder, destinationFolder, fileType): + for file in self.filterFiles(self.settings[fileType]): + src = self.absolutePathForSourceFile(sourceFolder, fileType, file) + dst = self.absolutePathForTargetFile(destinationFolder, fileType, file) + main.createFolder(os.path.dirname(dst)) + shutil.copy2(src, dst) + + + def copyResourcesToTargetFolder (self, targetFolder): + self.copyResources(self.projectDir, targetFolder, 'css') + self.copyResources(self.projectDir, targetFolder, 'js') + + + def loadFilesContent (self, basePath, files): + result = "" + + for file in self.filterFiles(files): + try: + fileHandler = codecs.open(self.absolutePathForSourceFile(self.projectDir, basePath, file), 'r', 'utf-8') + except: + print "FILE: " + file + + result += fileHandler.read() + '\n' + fileHandler.close() + + return result + + + def template (self): + processedFile = 'html_template' + if not self.processedFiles.has_key(processedFile): + self.processedFiles[processedFile] = self.loadFilesContent('html', ['index_template.html']) + + return self.processedFiles[processedFile] + + + def cssminCompressor (self, css): + # package found here: + # - http://stackoverflow.com/questions/222581/python-script-for-minifying-css/2396777#2396777 + # actual downloaded version: http://pypi.python.org/pypi/cssmin/0.1.4 + return cssmin.cssmin(css) + + + def regexCssCompressor (self, css): + # http://stackoverflow.com/questions/222581/python-script-for-minifying-css/223689#223689 + + # remove comments - this will break a lot of hacks :-P + css = re.sub( r'\s*/\*\s*\*/', "$$HACK1$$", css ) # preserve IE<6 comment hack + css = re.sub( r'/\*[\s\S]*?\*/', "", css ) + css = css.replace( "$$HACK1$$", '/**/' ) # preserve IE<6 comment hack + + # url() doesn't need quotes + css = re.sub( r'url\((["\'])([^)]*)\1\)', r'url(\2)', css ) + + # spaces may be safely collapsed as generated content will collapse them anyway + css = re.sub( r'\s+', ' ', css ) + + # shorten collapsable colors: #aabbcc to #abc + css = re.sub( r'#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3(\s|;)', r'#\1\2\3\4', css ) + + # fragment values can loose zeros + css = re.sub( r':\s*0(\.\d+([cm]m|e[mx]|in|p[ctx]))\s*;', r':\1;', css ) + + for rule in re.findall( r'([^{]+){([^}]*)}', css ): + + # we don't need spaces around operators + selectors = [re.sub( r'(?<=[\[\(>+=])\s+|\s+(?=[=~^$*|>+\]\)])', r'', selector.strip() ) for selector in rule[0].split( ',' )] + + # order is important, but we still want to discard repetitions + properties = {} + porder = [] + for prop in re.findall( '(.*?):(.*?)(;|$)', rule[1] ): + key = prop[0].strip().lower() + if key not in porder: porder.append( key ) + properties[ key ] = prop[1].strip() + + # output rule if it contains any declarations + if properties: + print "%s{%s}" % ( ','.join( selectors ), ''.join(['%s:%s;' % (key, properties[key]) for key in porder])[:-1] ) + + return css + + + def compressCSS (self, css): + self.log("compressing CSS") + #return self.regexCssCompressor(css) + return self.cssminCompressor(css) + + + #========================================================================== + + def compressJS_jsmin (self, js): + self.log("compressing JS code") + original = StringIO.StringIO(js) + output = StringIO.StringIO() + + jsMinifier = jsmin.JavascriptMinify() + jsMinifier.minify(original, output) + + result = output.getvalue() + + original.close() + output.close() + + return result + + def compressJS_closureCompiler (self, js): + # Googles Closure compiler + # java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js + + result = js + + return result + + + def compressJS (self, js): + return self.compressJS_jsmin(js) + #return self.compressJS_closureCompiler(js) + + + #========================================================================== + + def packBookmarklet (self, bookmakeletCode): + replacers = [ + ('isLoginForm', 'ilf'), + ('findLoginForm', 'flf'), + ('findLoginForm', 'flf'), + ('formParameters', 'fp' ), + ('pageParameters', 'pp' ), + ('serializeJSON', 'sj' ), + ('reprString', 'rs' ), + ('logFormParameters', 'lfp'), + ('loadClipperzBookmarklet', 'lcb'), + ('loginForm', 'lf' ), + ('parameters', 'p' ), + ('inputElementValues', 'iev'), + ] + result = self.compressJS(bookmakeletCode) + + result = re.sub('\n', ' ', result) # Fit all in a single line + # result = re.sub('\s+', ' ', result) # Collapse "redundant" spaces. WARNING: this could have some evil side effects on constant strings used inside to code!! + # result = re.sub('\s?([,\+=\(\)\{\};])\s?', '\\1', result) + + for replacer in replacers: + result = re.sub(replacer[0], replacer[1], result) + +# <!-- escaping required to handle the bookmarklet code within the javascript code --> + result = re.sub('\://', '%3a%2f%2f', result) + result = re.sub('/', '%2f', result) +# result = re.sub('"', '%22', result) + result = re.sub('"', '\\"', result) + result = re.sub('\"', '%22', result) + result = re.sub('\'', '%22', result) + result = re.sub('\\\\', '%5c', result) + result = result.strip() + result = 'javascript:' + result + +# replacers = [ +# ('aForm', '_1' ), +# ('inputFields', '_2' ), +# ('passwordFieldsFound', '_3' ), +# ('aDocument', '_6' ), +# ('aLevel', '_7' ), +# # ('result', '_8' ), +# ('documentForms', '_9' ), +# ('iFrames', '_c' ), +# ('anInputElement', '_d' ), +# ('options', '_f' ), +# ('option', '_12'), +# ('aLoginForm', '_13'), +# # ('action', '_17'), +# ('radioValues', '_18'), +# ('radioValueName', '_19'), +# ('inputElement', '_1a'), +# ('elementValues', '_1b'), +# ('radioValue', '_1c'), +# ('values', '_1d'), +# ('objtype', '_21'), +# ('useKey', '_27'), +# ('bookmarkletDiv', '_28'), +# ('someParameters', '_29'), +# ('anException', '_2a'), +# ('newDiv', '_2b'), +# ('base_url', '_2c'), +# ('help_url', '_2d'), +# ('logo_image_url', '_2e'), +# ('background_image_url','_2f'), +# ('close_image_url', '_30'), +# # ('bookmarklet_textarea','_31'), +# ('innerHTML', '_32'), +# ] +# for replacer in replacers: +# result = re.sub('([^\.])' + replacer[0], '\\1' + replacer[1], result) + +# replacers = [ +# ('headNode', '_1' ), +# ('clipperzScriptNode', '_2' ), +# ] +# for replacer in replacers: +# result = re.sub('([^\.])' + replacer[0], '\\1' + replacer[1], result) + +# result = re.sub(';', ';\n', result) + + return result + + + + def bookmarklet (self): + cacheKey = 'bookmarklet' + if not self.processedFiles.has_key(cacheKey): + result = 'bookmarklet="' + self.packBookmarklet(self.loadFilesContent('js', ['Bookmarklet.js'])) + '";bookmarklet_ie="' + self.packBookmarklet(self.loadFilesContent('js', ['Bookmarklet_IE.js'])) + '";' + self.processedFiles[cacheKey] = result + else: + result = self.processedFiles[cacheKey] + + return result + + + def replaceTemplatePlaceholders (self, assemblyMode, pageTitle, copyright, css, code, version, versionType): + result = self.template() + + result = result.replace('@page.title@', pageTitle, 1) + result = result.replace('@copyright@', copyright, 1) + result = result.replace('@css@', css, 1) + #result = result.replace('@bookmarklet@', bookmarklet, 1) + result = result.replace('@application.version@', version, 1) + result = result.replace('@application.version.type@', versionType, 1) + result = result.replace('@js_' + assemblyMode + '@', code, 1) + + result = re.sub('@js_[^@]+@', '', result) + + return result + + + def assembleCopyrightHeader (self): + processedFile = 'copyright' + if not self.processedFiles.has_key(processedFile): + #self.log("assembling copyright header") + copyrightValues = self.settings['copyright.values'] + license = self.loadFilesContent('../../properties', ['license.txt']) + result = self.loadFilesContent('properties', ['creditsAndCopyrights.txt']) + + result = re.sub('@clipperz.license@', license, result) + for key in copyrightValues: + result = re.sub('@'+key+'@', copyrightValues[key], result) + + self.processedFiles[processedFile] = result + + return self.processedFiles[processedFile] + + + def cssTagsForFiles (self, basePath, files): + #<link rel="stylesheet" type="text/css" href="./css/reset-min.css" /> + return '\n'.join(map(lambda file: '<link rel="stylesheet" type="text/css" href="./' + basePath + '/' + file + '" />', files)) + + + def cssTagForContent (self, content): + return '<style type="text/css">' + content + '</style>' + + + def scriptTagsForFiles (self, basePath, files): + #<script type='text/javascript' src='./js/src/bookmarklet.js'></script> + return '\n'.join(map(lambda file: '<script type="text/javascript" src="./' + basePath + '/' + file + '"></script>', files)) + + + def scriptTagForContent (self, content): + return '<script>' + content + '</script>' + + + def assembleVersion (self, assemblyMode, pageTitle, copyright, css, js, version, versionType): + cacheKey = version + "-" + versionType + if not self.processedFiles.has_key(cacheKey): + result = self.replaceTemplatePlaceholders(assemblyMode, pageTitle, copyright, css, js, version, versionType) + self.processedFiles[cacheKey] = result + else: + result = self.processedFiles[cacheKey] + + #self.log("# cacheKey:\n" + result) + return result + + + def assemble (self, assemblyMode='INSTALL', versionType='LIVE'): + pageTitle = "Clipperz - " + self.module + if versionType != 'LIVE': + pageTitle += " [" + versionType + " - " + assemblyMode +"]" + + if assemblyMode == 'INSTALL': + css = self.cssTagForContent(self.compressCSS(self.loadFilesContent('css', self.settings['css']))) + js = self.scriptTagForContent(self.bookmarklet() + '\n' + self.compressJS(self.loadFilesContent('js', self.settings['js']))) + else: + css = self.cssTagsForFiles('css', self.filterFiles(self.settings['css'])) + js = self.scriptTagForContent(self.bookmarklet()) + '\n' + self.scriptTagsForFiles('js', self.filterFiles(self.settings['js'])) + + return self.assembleVersion( + assemblyMode = assemblyMode, + pageTitle = pageTitle, + copyright = self.assembleCopyrightHeader(), + css = css, + js = js, + version = self.repositoryVersion(), + versionType = versionType + ) + + + + diff --git a/scripts/builder/jsmin.py b/scripts/builder/jsmin.py new file mode 100644 index 0000000..91d6307 --- a/dev/null +++ b/scripts/builder/jsmin.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os, os.path, shutil + +# This code is original from jsmin by Douglas Crockford, it was translated to +# Python by Baruch Even. The original code had the following copyright and +# license. +# +# /* jsmin.c +# 2007-05-22 +# +# Copyright (c) 2002 Douglas Crockford (www.crockford.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# The Software shall be used for Good, not Evil. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# */ + +from StringIO import StringIO + +def jsmin(js): + ins = StringIO(js) + outs = StringIO() + JavascriptMinify().minify(ins, outs) + str = outs.getvalue() + if len(str) > 0 and str[0] == '\n': + str = str[1:] + return str + +def isAlphanum(c): + """return true if the character is a letter, digit, underscore, + dollar sign, or non-ASCII character. + """ + return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or + (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126)); + +class UnterminatedComment(Exception): + pass + +class UnterminatedStringLiteral(Exception): + pass + +class UnterminatedRegularExpression(Exception): + pass + +class JavascriptMinify(object): + + def _outA(self): + self.outstream.write(self.theA) + def _outB(self): + self.outstream.write(self.theB) + + def _get(self): + """return the next character from stdin. Watch out for lookahead. If + the character is a control character, translate it to a space or + linefeed. + """ + c = self.theLookahead + self.theLookahead = None + if c == None: + c = self.instream.read(1) + if c >= ' ' or c == '\n': + return c + if c == '': # EOF + return '\000' + if c == '\r': + return '\n' + return ' ' + + def _peek(self): + self.theLookahead = self._get() + return self.theLookahead + + def _next(self): + """get the next character, excluding comments. peek() is used to see + if an unescaped '/' is followed by a '/' or '*'. + """ + c = self._get() + if c == '/' and self.theA != '\\': + p = self._peek() + if p == '/': + c = self._get() + while c > '\n': + c = self._get() + return c + if p == '*': + c = self._get() + while 1: + c = self._get() + if c == '*': + if self._peek() == '/': + self._get() + return ' ' + if c == '\000': + raise UnterminatedComment() + + return c + + def _action(self, action): + """do something! What you do is determined by the argument: + 1 Output A. Copy B to A. Get the next B. + 2 Copy B to A. Get the next B. (Delete A). + 3 Get the next B. (Delete B). + action treats a string as a single character. Wow! + action recognizes a regular expression if it is preceded by ( or , or =. + """ + if action <= 1: + self._outA() + + if action <= 2: + self.theA = self.theB + if self.theA == "'" or self.theA == '"': + while 1: + self._outA() + self.theA = self._get() + if self.theA == self.theB: + break + if self.theA <= '\n': + raise UnterminatedStringLiteral() + if self.theA == '\\': + self._outA() + self.theA = self._get() + + + if action <= 3: + self.theB = self._next() + if self.theB == '/' and (self.theA == '(' or self.theA == ',' or + self.theA == '=' or self.theA == ':' or + self.theA == '[' or self.theA == '?' or + self.theA == '!' or self.theA == '&' or + self.theA == '|' or self.theA == ';' or + self.theA == '{' or self.theA == '}' or + self.theA == '\n'): + self._outA() + self._outB() + while 1: + self.theA = self._get() + if self.theA == '/': + break + elif self.theA == '\\': + self._outA() + self.theA = self._get() + elif self.theA <= '\n': + raise UnterminatedRegularExpression() + self._outA() + self.theB = self._next() + + + def _jsmin(self): + """Copy the input to the output, deleting the characters which are + insignificant to JavaScript. Comments will be removed. Tabs will be + replaced with spaces. Carriage returns will be replaced with linefeeds. + Most spaces and linefeeds will be removed. + """ + self.theA = '\n' + self._action(3) + + while self.theA != '\000': + if self.theA == ' ': + if isAlphanum(self.theB): + self._action(1) + else: + self._action(2) + elif self.theA == '\n': + if self.theB in ['{', '[', '(', '+', '-']: + self._action(1) + elif self.theB == ' ': + self._action(3) + else: + if isAlphanum(self.theB): + self._action(1) + else: + self._action(2) + else: + if self.theB == ' ': + if isAlphanum(self.theA): + self._action(1) + else: + self._action(3) + elif self.theB == '\n': + if self.theA in ['}', ']', ')', '+', '-', '"', '\'']: + self._action(1) + else: + if isAlphanum(self.theA): + self._action(1) + else: + self._action(3) + else: + self._action(1) + + def minify(self, instream, outstream): + self.instream = instream + self.outstream = outstream + self.theA = '\n' + self.theB = None + self.theLookahead = None + + self._jsmin() + self.instream.close() + +def compress(in_files, out_file, in_type='js', verbose=False, temp_file='.temp'): + temp = open(temp_file, 'w') + for f in in_files: + fh = open(f) + data = fh.read() + '\n' + fh.close() + + temp.write(data) + + print ' + %s' % f + temp.close() + + out = open(out_file, 'w') + + jsm = JavascriptMinify() + jsm.minify(open(temp_file,'r'), out) + + out.close() + + org_size = os.path.getsize(temp_file) + new_size = os.path.getsize(out_file) + + print '=> %s' % out_file + print 'Original: %.2f kB' % (org_size / 1024.0) + print 'Compressed: %.2f kB' % (new_size / 1024.0) + print 'Reduction: %.1f%%' % (float(org_size - new_size) / org_size * 100) + print '' + + os.remove(temp_file)
\ No newline at end of file diff --git a/scripts/builder/main.py b/scripts/builder/main.py new file mode 100755 index 0000000..ba0c72a --- a/dev/null +++ b/scripts/builder/main.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import sys, os, json +import shutil +import pprint +import frontendBuilder +import codecs +import itertools + +from collections import deque +from phpBuilder import PhpBuilder +from pythonBuilder import PythonBuilder + +pp = pprint.PrettyPrinter(indent=4, depth=4) + +#-------------------------------------------------------------------- + +def scriptDir (): + return os.path.dirname(sys.argv[0]) + +def projectBaseDir (): + return os.path.abspath(scriptDir() + '/../..') + +def projectTargetDir(): + return projectBaseDir() + '/target/' + +#-------------------------------------------------------------------- + +def createFolder (path): + if not os.path.exists(path): + os.makedirs(path) + +#-------------------------------------------------------------------- + +def loadSettings (component, module): + print "MODULE: " + module + + if '.' in module: + moduleComponents = module.split('.') + module = moduleComponents[0] + submodule = moduleComponents[1] + else: + submodule = module + + settings = codecs.open(projectBaseDir() + '/' + component + '/' + module + '/properties/' + submodule + '.properties.json', 'r', 'utf-8') + result = json.load(settings) + settings.close + + return result + +#==================================================================== +# +# def assembleFrontend (frontend, versions): +# result = {} +# settings = loadSettings('frontend', frontend) +# builder = frontendBuilder.FrontendBuilder(frontend, settings, projectBaseDir()) +# +# for version in versions: +# if version == 'install': +# result[version] = builder.assembleInstallVersion() +# elif version == 'debug': +# result[version] = builder.assembleDebugVersion() +# else: +# raise Exception('unrecognized version: ' + version) +# +# return result +# +#==================================================================== + +def assembleBackend (backend, frontends, versions): + settings = loadSettings('backend', backend) + + if backend == 'php': + backendBuilder = PhpBuilder(projectTargetDir(), frontends, versions, settings) + elif backend == 'python': + backendBuilder = PythonBuilder(projectTargetDir(), frontends, versions, settings) + #elif backend == 'java': + # buildJavaBackend (frontends, versions, settings) + else: + raise Exception('unrecognized backend: ' + backend) + + backendBuilder.run() + +#==================================================================== + +def build (settings): + frontends = [] + + for frontend in settings['frontends']: + frontends.append(frontendBuilder.FrontendBuilder(frontend, loadSettings('frontend', frontend))) + + for backend in settings['backends']: + assembleBackend(backend, frontends, settings['versions']) + +#-------------------------------------------------------------------- + +def clean (): + print "cleaning up …" + if os.path.exists(projectTargetDir()): + shutil.rmtree(projectTargetDir()) + +#-------------------------------------------------------------------- + +def usage (message): + if message != None: + print "ERROR: " + message + + print + print "build.py clean" + print "build.py clean install" + print "build.py install --ALL" + print "build.py install debug --ALL" + print "build.py clean install debug --ALL" + print "build.ph install, debug --backends php java --frontends beta gamma" + print "build.ph install, debug --backends php java --frontends beta gamma gamma.mobile" + exit(1) + +#-------------------------------------------------------------------- + +def main (): + settings = {} + parameters = list(itertools.islice(sys.argv, 1, None)) + + shouldClean = len(filter(lambda x: x == 'clean', parameters)) > 0 + if (shouldClean): + clean () + + parameters = filter(lambda x: x != 'clean', parameters) + versions = list(itertools.takewhile(lambda x: not x.startswith('--'), parameters)) + settings['versions'] = versions; #['debug', 'install'] + parameters = deque(itertools.dropwhile(lambda x: not x.startswith('--'), parameters)) + + if len(parameters) > 0: + parameter = parameters.popleft() + if parameter == "--ALL": + settings['frontends'] = ['beta', 'gamma', 'mobile'] + settings['backends'] = ['php', 'python', 'java'] + else: + while parameter != None: + values = list(itertools.takewhile(lambda x: not x.startswith('--'), parameters)) + + if parameter == "--backends": + settings['backends'] = values + elif parameter == "--frontends": + settings['frontends'] = values + + parameters = deque(itertools.dropwhile(lambda x: not x.startswith('--'), parameters)) + if parameters: + parameter = parameters.popleft() + else: + parameter = None + + if (not settings.has_key('versions')): + usage("missing 'versions'") + if (not settings.has_key('frontends')): + usage("missing 'frontends'") + if (not settings.has_key('backends')): + usage("missing 'backends'") + + build (settings) + + + +if __name__ == "__main__": + main()
\ No newline at end of file diff --git a/scripts/builder/phpBuilder.py b/scripts/builder/phpBuilder.py new file mode 100644 index 0000000..9512192 --- a/dev/null +++ b/scripts/builder/phpBuilder.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +from backendBuilder import BackendBuilder + +class PhpBuilder(BackendBuilder): + + def name(self): + return "PHP builder" + + def relativePath(self): + return 'php' + + diff --git a/scripts/builder/pythonBuilder.py b/scripts/builder/pythonBuilder.py new file mode 100644 index 0000000..44c62a8 --- a/dev/null +++ b/scripts/builder/pythonBuilder.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +from backendBuilder import BackendBuilder + +class PythonBuilder(BackendBuilder): + + def name(self): + return "Python builder" + + def relativePath(self): + return 'python' + + |