svgui/pyjs/build.py
author Andrey Skvortsov <andrej.skvortzov@gmail.com>
Tue, 09 Oct 2018 11:56:45 +0300
changeset 2439 f0a040f1de1b
parent 2431 6923074540dd
child 2451 6d1bf321fb89
permissions -rw-r--r--
Fix pep8 warning: W605 invalid escape sequence ?x?
#!/usr/bin/env python


from __future__ import absolute_import
from __future__ import print_function
import sys
import shutil
import re
import os
from os.path import join, basename, abspath, split, isfile, isdir
from hashlib import md5
from optparse import OptionParser
from six.moves import cStringIO

from svgui.pyjs import pyjs


usage = """
  usage: %prog [options] <application module name or path>

This is the command line builder for the pyjamas project, which can
be used to build Ajax applications from Python.
For more information, see the website at http://pyjs.org/
"""

# GWT1.2 Impl  | GWT1.2 Output         | Pyjamas 0.2 Platform | Pyjamas 0.2 Output
# -------------+-----------------------+----------------------+----------------------
# IE6          | ie6                   | IE6                  | ie6
# Opera        | opera                 | Opera                | opera
# Safari       | safari                | Safari               | safari
# --           | gecko1_8              | Mozilla              | mozilla
# --           | gecko                 | OldMoz               | oldmoz
# Standard     | all                   | (default code)       | all
# Mozilla      | gecko1_8, gecko       | --                   | --
# Old          | safari, gecko, opera  | --                   | --

version = "%prog pyjamas version 2006-08-19"

# these names in lowercase need match the strings
# returned by "provider$user.agent" in order to be selected corretly
app_platforms = ['IE6', 'Opera', 'OldMoz', 'Safari', 'Mozilla']

# usually defaults to e.g. /usr/share/pyjamas
_data_dir = os.path.join(pyjs.prefix, "share/pyjamas")


# .cache.html files produces look like this
CACHE_HTML_PAT = re.compile(r'^[a-z]*.[0-9a-f]{32}\.cache\.html$')

# ok these are the three "default" library directories, containing
# the builtins (str, List, Dict, ord, round, len, range etc.)
# the main pyjamas libraries (pyjamas.ui, pyjamas.Window etc.)
# and the contributed addons

for p in ["library/builtins",
          "library",
          "addons"]:
    p = os.path.join(_data_dir, p)
    if os.path.isdir(p):
        pyjs.path.append(p)


def read_boilerplate(data_dir, filename):
    return open(join(data_dir, "builder/boilerplate", filename)).read()


def copy_boilerplate(data_dir, filename, output_dir):
    filename = join(data_dir, "builder/boilerplate", filename)
    shutil.copy(filename, output_dir)


# taken and modified from python2.4
def copytree_exists(src, dst, symlinks=False):
    if not os.path.exists(src):
        return

    names = os.listdir(src)
    try:
        os.mkdir(dst)
    except Exception:
        pass

    errors = []
    for name in names:
        if name.startswith('CVS'):
            continue
        if name.startswith('.git'):
            continue
        if name.startswith('.svn'):
            continue

        srcname = os.path.join(src, name)
        dstname = os.path.join(dst, name)
        try:
            if symlinks and os.path.islink(srcname):
                linkto = os.readlink(srcname)
                os.symlink(linkto, dstname)
            elif isdir(srcname):
                copytree_exists(srcname, dstname, symlinks)
            else:
                shutil.copy2(srcname, dstname)
        except (IOError, os.error) as why:
            errors.append((srcname, dstname, why))
    if errors:
        print(errors)


def check_html_file(source_file, dest_path):
    """ Checks if a base HTML-file is available in the PyJamas
        output directory.
        If the HTML-file isn't available, it will be created.

        If a CSS-file with the same name is available
        in the output directory, a reference to this CSS-file
        is included.

        If no CSS-file is found, this function will look for a special
        CSS-file in the output directory, with the name
        "pyjamas_default.css", and if found it will be referenced
        in the generated HTML-file.

        [thank you to stef mientki for contributing this function]
    """

    base_html = """\
<html>
    <!-- auto-generated html - you should consider editing and
         adapting this to suit your requirements
     -->
    <head>
      <meta name="pygwt:module" content="%(modulename)s">
      %(css)s
      <title>%(title)s</title>
    </head>
    <body bgcolor="white">
      <script language="javascript" src="pygwt.js"></script>
    </body>
</html>
"""

    filename = os.path.split(source_file)[1]
    mod_name = os.path.splitext(filename)[0]
    file_name = os.path.join(dest_path, mod_name + '.html')

    # if html file in output directory exists, leave it alone.
    if os.path.exists(file_name):
        return 0

    if os.path.exists(os.path.join(dest_path, mod_name + '.css')):
        css = "<link rel='stylesheet' href='" + mod_name + ".css'>"
    elif os.path.exists(os.path.join(dest_path, 'pyjamas_default.css')):
        css = "<link rel='stylesheet' href='pyjamas_default.css'>"

    else:
        css = ''

    title = 'PyJamas Auto-Generated HTML file ' + mod_name

    base_html = base_html % {'modulename': mod_name, 'title': title, 'css': css}

    fh = open(file_name, 'w')
    fh.write(base_html)
    fh.close()

    return 1


def build(app_name, output, js_includes=(), debug=False, dynamic=0,
          data_dir=None, cache_buster=False, optimize=False):

    # make sure the output directory is always created in the current working
    # directory or at the place given if it is an absolute path.
    output = os.path.abspath(output)
    msg = "Building '%(app_name)s' to output directory '%(output)s'" % locals()
    if debug:
        msg += " with debugging statements"
    print(msg)

    # check the output directory
    if os.path.exists(output) and not os.path.isdir(output):
        print("Output destination %s exists and is not a directory" % output, file=sys.stderr)
        return
    if not os.path.isdir(output):
        try:
            print("Creating output directory")
            os.mkdir(output)
        except StandardError as e:
            print("Exception creating output directory %s: %s" % (output, e), file=sys.stderr)

    # public dir
    for p in pyjs.path:
        pub_dir = join(p, 'public')
        if isdir(pub_dir):
            print("Copying: public directory of library %r" % p)
            copytree_exists(pub_dir, output)

    # AppName.html - can be in current or public directory
    html_input_filename = app_name + ".html"
    html_output_filename = join(output, basename(html_input_filename))
    if os.path.isfile(html_input_filename):
        if not os.path.isfile(html_output_filename) or \
               os.path.getmtime(html_input_filename) > \
               os.path.getmtime(html_output_filename):
            try:
                shutil.copy(html_input_filename, html_output_filename)
            except Exception:
                print("Warning: Missing module HTML file %s" % html_input_filename, file=sys.stderr)

            print("Copying: %(html_input_filename)s" % locals())

    if check_html_file(html_input_filename, output):
        print("Warning: Module HTML file %s has been auto-generated" % html_input_filename, file=sys.stderr)

    # pygwt.js

    print("Copying: pygwt.js")

    pygwt_js_template = read_boilerplate(data_dir, "pygwt.js")
    pygwt_js_output = open(join(output, "pygwt.js"), "w")

    print(pygwt_js_template, file=pygwt_js_output)

    pygwt_js_output.close()

    # Images

    print("Copying: Images and History")
    copy_boilerplate(data_dir, "corner_dialog_topleft_black.png", output)
    copy_boilerplate(data_dir, "corner_dialog_topright_black.png", output)
    copy_boilerplate(data_dir, "corner_dialog_bottomright_black.png", output)
    copy_boilerplate(data_dir, "corner_dialog_bottomleft_black.png", output)
    copy_boilerplate(data_dir, "corner_dialog_edge_black.png", output)
    copy_boilerplate(data_dir, "corner_dialog_topleft.png", output)
    copy_boilerplate(data_dir, "corner_dialog_topright.png", output)
    copy_boilerplate(data_dir, "corner_dialog_bottomright.png", output)
    copy_boilerplate(data_dir, "corner_dialog_bottomleft.png", output)
    copy_boilerplate(data_dir, "corner_dialog_edge.png", output)
    copy_boilerplate(data_dir, "tree_closed.gif", output)
    copy_boilerplate(data_dir, "tree_open.gif", output)
    copy_boilerplate(data_dir, "tree_white.gif", output)
    copy_boilerplate(data_dir, "history.html", output)

    # all.cache.html
    app_files = generateAppFiles(data_dir, js_includes, app_name, debug,
                                 output, dynamic, cache_buster, optimize)

    # AppName.nocache.html

    print("Creating: %(app_name)s.nocache.html" % locals())

    home_nocache_html_template = read_boilerplate(data_dir, "home.nocache.html")
    home_nocache_html_output = open(join(output, app_name + ".nocache.html"),
                                    "w")

    # the selector templ is added to the selectScript function
    select_tmpl = """O(["true","%s"],"%s");"""
    script_selectors = cStringIO()

    for platform, file_prefix in app_files:
        print(select_tmpl % (platform, file_prefix), file=script_selectors)

    print(
        home_nocache_html_template % dict(
            app_name=app_name,
            script_selectors=script_selectors.getvalue(),
        ), file=home_nocache_html_output)

    home_nocache_html_output.close()

    print("Done. You can run your app by opening '%(html_output_filename)s' in a browser" % locals())


def generateAppFiles(data_dir, js_includes, app_name, debug, output, dynamic,
                     cache_buster, optimize):

    all_cache_html_template = read_boilerplate(data_dir, "all.cache.html")
    mod_cache_html_template = read_boilerplate(data_dir, "mod.cache.html")

    # clean out the old ones first
    for name in os.listdir(output):
        if CACHE_HTML_PAT.match(name):
            p = join(output, name)
            print("Deleting existing app file %s" % p)
            os.unlink(p)

    app_files = []
    parser = pyjs.PlatformParser("platform")
    app_headers = ''
    scripts = ['<script type="text/javascript" src="%s"></script>' %
               script for script in js_includes]
    app_body = '\n'.join(scripts)

    mod_code = {}
    mod_libs = {}
    modules = {}
    app_libs = {}
    early_app_libs = {}
    app_code = {}
    overrides = {}
    pover = {}
    app_modnames = {}
    mod_levels = {}

    # First, generate all the code.
    # Second, (dynamic only), post-analyse the places where modules
    # haven't changed
    # Third, write everything out.

    for platform in app_platforms:

        mod_code[platform] = {}
        mod_libs[platform] = {}
        modules[platform] = []
        pover[platform] = {}
        app_libs[platform] = ''
        early_app_libs[platform] = ''
        app_code[platform] = {}
        app_modnames[platform] = {}

        # Application.Platform.cache.html

        parser.setPlatform(platform)
        app_translator = pyjs.AppTranslator(
            parser=parser, dynamic=dynamic, optimize=optimize)
        early_app_libs[platform], appcode = \
            app_translator.translate(None, is_app=False,
                                     debug=debug,
                                     library_modules=['dynamicajax.js',
                                                      '_pyjs.js', 'sys',
                                                      'pyjslib'])
        pover[platform].update(app_translator.overrides.items())
        for mname, name in app_translator.overrides.items():
            pd = overrides.setdefault(mname, {})
            pd[platform] = name

        print(appcode)
        # mod_code[platform][app_name] = appcode

        # platform.Module.cache.js

        modules_done = ['pyjslib', 'sys', '_pyjs.js']
        # modules_to_do = [app_name] + app_translator.library_modules
        modules_to_do = [app_name] + app_translator.library_modules

        dependencies = {}

        deps = map(pyjs.strip_py, modules_to_do)
        for d in deps:
            sublist = add_subdeps(dependencies, d)
            modules_to_do += sublist
        deps = uniquify(deps)
        # dependencies[app_name] = deps

        modules[platform] = modules_done + modules_to_do

        while modules_to_do:

            # print "modules to do", modules_to_do

            mn = modules_to_do.pop()
            mod_name = pyjs.strip_py(mn)

            if mod_name in modules_done:
                continue

            modules_done.append(mod_name)

            mod_cache_name = "%s.%s.cache.js" % (platform.lower(), mod_name)

            parser.setPlatform(platform)
            mod_translator = pyjs.AppTranslator(parser=parser, optimize=optimize)
            mod_libs[platform][mod_name], mod_code[platform][mod_name] = \
                mod_translator.translate(mod_name,
                                         is_app=False,
                                         debug=debug)
            pover[platform].update(mod_translator.overrides.items())
            for mname, name in mod_translator.overrides.items():
                pd = overrides.setdefault(mname, {})
                pd[platform] = name

            mods = mod_translator.library_modules
            modules_to_do += mods
            modules[platform] += mods

            deps = map(pyjs.strip_py, mods)
            sd = subdeps(mod_name)
            if len(sd) > 1:
                deps += sd[:-1]
            while mod_name in deps:
                deps.remove(mod_name)

            # print
            # print
            # print "modname preadd:", mod_name, deps
            # print
            # print
            for d in deps:
                sublist = add_subdeps(dependencies, d)
                modules_to_do += sublist
            modules_to_do += add_subdeps(dependencies, mod_name)
            # print "modname:", mod_name, deps
            deps = uniquify(deps)
            # print "modname:", mod_name, deps
            dependencies[mod_name] = deps

        # work out the dependency ordering of the modules

        mod_levels[platform] = make_deps(None, dependencies, modules_done)

    # now write everything out

    for platform in app_platforms:

        early_app_libs_ = early_app_libs[platform]
        app_libs_ = app_libs[platform]
        app_code_ = app_code[platform]
        # modules_ = filter_mods(app_name, modules[platform])
        mods = flattenlist(mod_levels[platform])
        mods.reverse()
        modules_ = filter_mods(None, mods)

        for mod_name in modules_:

            mod_code_ = mod_code[platform][mod_name]

            mod_name = pyjs.strip_py(mod_name)

            override_name = "%s.%s" % (platform.lower(), mod_name)
            if override_name in pover[platform]:
                mod_cache_name = "%s.cache.js" % (override_name)
            else:
                mod_cache_name = "%s.cache.js" % (mod_name)

            print("Creating: " + mod_cache_name)

            modlevels = make_deps(None, dependencies, dependencies[mod_name])

            modnames = []

            for md in modlevels:
                mnames = map(lambda x: "'%s'" % x, md)
                mnames = "new pyjslib.List([\n\t\t\t%s])" % ',\n\t\t\t'.join(mnames)
                modnames.append(mnames)

            modnames.reverse()
            modnames = "new pyjslib.List([\n\t\t%s\n\t])" % ',\n\t\t'.join(modnames)

            # convert the overrides

            overnames = map(lambda x: "'%s': '%s'" % x, pover[platform].items())
            overnames = "new pyjslib.Dict({\n\t\t%s\n\t})" % ',\n\t\t'.join(overnames)

            if dynamic:
                mod_cache_html_output = open(join(output, mod_cache_name), "w")
            else:
                mod_cache_html_output = cStringIO()

            print(mod_cache_html_template % dict(
                mod_name=mod_name,
                app_name=app_name,
                modnames=modnames,
                overrides=overnames,
                mod_libs=mod_libs[platform][mod_name],
                dynamic=dynamic,
                mod_code=mod_code_,
            ), file=mod_cache_html_output)

            if dynamic:
                mod_cache_html_output.close()
            else:
                mod_cache_html_output.seek(0)
                app_libs_ += mod_cache_html_output.read()

        # write out the dependency ordering of the modules

        app_modnames = []

        for md in mod_levels[platform]:
            mnames = map(lambda x: "'%s'" % x, md)
            mnames = "new pyjslib.List([\n\t\t\t%s])" % ',\n\t\t\t'.join(mnames)
            app_modnames.append(mnames)

        app_modnames.reverse()
        app_modnames = "new pyjslib.List([\n\t\t%s\n\t])" % ',\n\t\t'.join(app_modnames)

        # convert the overrides

        overnames = map(lambda x: "'%s': '%s'" % x, pover[platform].items())
        overnames = "new pyjslib.Dict({\n\t\t%s\n\t})" % ',\n\t\t'.join(overnames)

        # print "platform names", platform, overnames
        # print pover

        # now write app.allcache including dependency-ordered list of
        # library modules

        file_contents = all_cache_html_template % dict(
            app_name=app_name,
            early_app_libs=early_app_libs_,
            app_libs=app_libs_,
            app_code=app_code_,
            app_body=app_body,
            overrides=overnames,
            platform=platform.lower(),
            dynamic=dynamic,
            app_modnames=app_modnames,
            app_headers=app_headers
        )
        if cache_buster:
            digest = md5.new(file_contents).hexdigest()
            file_name = "%s.%s.%s" % (platform.lower(), app_name, digest)
        else:
            file_name = "%s.%s" % (platform.lower(), app_name)
        file_name += ".cache.html"
        out_path = join(output, file_name)
        out_file = open(out_path, 'w')
        out_file.write(file_contents)
        out_file.close()
        app_files.append((platform.lower(), file_name))
        print("Created app file %s:%s: %s" % (
            app_name, platform, out_path))

    return app_files


def flattenlist(ll):
    res = []
    for l in ll:
        res += l
    return res


def subdeps(m):
    """
    creates sub-dependencies e.g. pyjamas.ui.Widget
    creates pyjamas.ui.Widget, pyjamas.ui and pyjamas.
    """
    d = []
    m = m.split(".")
    for i in range(0, len(m)):
        d.append('.'.join(m[:i+1]))
    return d


def add_subdeps(deps, mod_name):
    sd = subdeps(mod_name)
    if len(sd) == 1:
        return []
    # print "subdeps", mod_name, sd
    # print "deps", deps
    res = []
    for i in range(0, len(sd)-1):
        parent = sd[i]
        child = sd[i+1]
        k = deps.get(child, [])
        k.append(parent)
        deps[child] = k
        if parent not in res:
            res.append(parent)
    # print deps
    return res


def uniquify(md):
    """
    makes unique and preserves list order
    """
    res = []
    for m in md:
        if m not in res:
            res.append(m)
    return res


def filter_mods(app_name, md):
    while 'sys' in md:
        md.remove('sys')
    while 'pyjslib' in md:
        md.remove('pyjslib')
    while app_name in md:
        md.remove(app_name)
    md = filter(lambda x: not x.endswith('.js'), md)
    md = map(pyjs.strip_py, md)

    return uniquify(md)


def filter_deps(app_name, deps):

    res = {}
    for (k, l) in deps.items():
        mods = filter_mods(k, l)
        while k in mods:
            mods.remove(k)
        res[k] = mods
    return res


def has_nodeps(mod, deps):
    if mod not in deps or not deps[mod]:
        return True
    return False


def nodeps_list(mod_list, deps):
    res = []
    for mod in mod_list:
        if has_nodeps(mod, deps):
            res.append(mod)
    return res

# this function takes a dictionary of dependent modules and
# creates a list of lists.  the first list will be modules
# that have no dependencies; the second list will be those
# modules that have the first list as dependencies; the
# third will be those modules that have the first and second...
# etc.


def make_deps(app_name, deps, mod_list):
    print("Calculating Dependencies ...")
    mod_list = filter_mods(app_name, mod_list)
    deps = filter_deps(app_name, deps)

    if not mod_list:
        return []

    # print mod_list
    # print deps

    ordered_deps = []
    last_len = -1
    while deps:
        l_deps = len(deps)
        # print l_deps
        if l_deps == last_len:
            for m, dl in deps.items():
                for d in dl:
                    if m in deps.get(d, []):
                        raise Exception('Circular Imports found: \n%s %s -> %s %s'
                                        % (m, dl, d, deps[d]))
            # raise Exception('Could not calculate dependencies: \n%s' % deps)
            break
        last_len = l_deps
        # print "modlist", mod_list
        nodeps = nodeps_list(mod_list, deps)
        # print "nodeps", nodeps
        mod_list = filter(lambda x: x not in nodeps, mod_list)
        newdeps = {}
        for k in deps.keys():
            depslist = deps[k]
            depslist = filter(lambda x: x not in nodeps, depslist)
            if depslist:
                newdeps[k] = depslist
        # print "newdeps", newdeps
        deps = newdeps
        ordered_deps.append(nodeps)
        # time.sleep(0)

    if mod_list:
        ordered_deps.append(mod_list)  # last dependencies - usually the app(s)

    ordered_deps.reverse()

    return ordered_deps


def main():
    global app_platforms

    parser = OptionParser(usage=usage, version=version)
    parser.add_option(
        "-o",
        "--output",
        dest="output",
        help="directory to which the webapp should be written"
    )
    parser.add_option(
        "-j",
        "--include-js",
        dest="js_includes",
        action="append",
        help="javascripts to load into the same frame as the rest of the script"
    )
    parser.add_option(
        "-I",
        "--library_dir",
        dest="library_dirs",
        action="append",
        help="additional paths appended to PYJSPATH"
    )
    parser.add_option(
        "-D",
        "--data_dir",
        dest="data_dir",
        help="path for data directory"
    )
    parser.add_option(
        "-m",
        "--dynamic-modules",
        action="store_true",
        dest="dynamic",
        default=False,
        help="Split output into separate dynamically-loaded modules (experimental)"
    )
    parser.add_option(
        "-P",
        "--platforms",
        dest="platforms",
        help="platforms to build for, comma-separated"
    )
    parser.add_option(
        "-d",
        "--debug",
        action="store_true",
        dest="debug"
    )
    parser.add_option(
        "-O",
        "--optimize",
        action="store_true",
        dest="optimize",
        default=False,
        help="Optimize generated code (removes all print statements)",
    )
    parser.add_option(
        "-c",
        "--cache_buster",
        action="store_true",
        dest="cache_buster",
        help="Enable browser cache-busting (MD5 hash added to output filenames)"
    )

    parser.set_defaults(output="output", js_includes=[], library_dirs=[],
                        platforms=(','.join(app_platforms)),
                        data_dir=os.path.join(sys.prefix, "share/pyjamas"),
                        dynamic=False,
                        cache_buster=False,
                        debug=False)
    (options, args) = parser.parse_args()
    if len(args) != 1:
        parser.error("incorrect number of arguments")

    data_dir = abspath(options.data_dir)

    app_path = args[0]
    if app_path.endswith('.py'):
        app_path = abspath(app_path)
        if not isfile(app_path):
            parser.error("Application file not found %r" % app_path)
        app_path, app_name = split(app_path)
        app_name = app_name[:-3]
        pyjs.path.append(app_path)
    elif os.path.sep in app_path:
        parser.error("Not a valid module declaration %r" % app_path)
    else:
        app_name = app_path

    for d in options.library_dirs:
        pyjs.path.append(abspath(d))

    if options.platforms:
        app_platforms = options.platforms.split(',')

    # this is mostly for getting boilerplate stuff
    data_dir = os.path.abspath(options.data_dir)

    build(app_name, options.output, options.js_includes,
          options.debug, options.dynamic and 1 or 0, data_dir,
          options.cache_buster, options.optimize)


if __name__ == "__main__":
    main()