Blame build.py

73d6db7d8f0b
#!/usr/bin/env python3
73d6db7d8f0b
# coding=utf-8
73d6db7d8f0b
#
73d6db7d8f0b
# build.py Create lapwing.org website
1a152cb88c28
# Copyright (C) 2014, 2017 Sam Black <samwwwblack@lapwing.org>
73d6db7d8f0b
# 
73d6db7d8f0b
# This program is free software: you can redistribute it and/or modify
73d6db7d8f0b
# it under the terms of the GNU Affero General Public License as published by
73d6db7d8f0b
# the Free Software Foundation, either version 3 of the License, or
73d6db7d8f0b
# (at your option) any later version.
73d6db7d8f0b
# 
73d6db7d8f0b
# This program is distributed in the hope that it will be useful,
73d6db7d8f0b
# but WITHOUT ANY WARRANTY; without even the implied warranty of
73d6db7d8f0b
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
73d6db7d8f0b
# GNU Affero General Public License for more details.
73d6db7d8f0b
#
73d6db7d8f0b
# You should have received a copy of the GNU Affero General Public License
73d6db7d8f0b
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
89c464505bbe
#
89c464505bbe
import distutils.dir_util
49a817930f15
import logging
73d6db7d8f0b
import os
eaa9d60c5786
import shutil
876ce8d30315
import time
73d6db7d8f0b
73d6db7d8f0b
import click
73d6db7d8f0b
import staticjinja
73d6db7d8f0b
import webassets
73d6db7d8f0b
33c994d6643e
from datetime import datetime
876ce8d30315
from http.server import HTTPServer
876ce8d30315
from http.server import SimpleHTTPRequestHandler
876ce8d30315
from multiprocessing import Lock
876ce8d30315
from multiprocessing import Process
49a817930f15
from webassets.ext.jinja2 import AssetsExtension
49a817930f15
876ce8d30315
logger = logging.getLogger(__name__)
876ce8d30315
logger.setLevel(logging.INFO)
876ce8d30315
logger.addHandler(logging.StreamHandler())
73d6db7d8f0b
89c464505bbe
pwd = os.getcwd()
89c464505bbe
config = {
89c464505bbe
    "site": {
89c464505bbe
        "name": "Lapwing.Org",
89c464505bbe
        "email": "contact@lapwing.org",
89c464505bbe
    },
0bac8e35c64b
    "menu": (
0bac8e35c64b
        ("Blog", "https://samwwwblack.lapwing.org"),
0bac8e35c64b
        ("Code", "https://code.lapwing.org")
0bac8e35c64b
    ),
0bac8e35c64b
    "projects": (
0bac8e35c64b
        ("Vowel", "Virtual Online Workspace for Education and Learning.",
0bac8e35c64b
         "https://code.lapwing.org/vowel/vowel"),
0bac8e35c64b
        ("Sponson",
0bac8e35c64b
         "Sponson is a tool to create and setup systemd-nspawn containers in "
0bac8e35c64b
            "a Docker-like way, without using Docker.",
0bac8e35c64b
         "https://code.lapwing.org/devops/sponson"),
0bac8e35c64b
    ),
89c464505bbe
    "output": os.path.join(pwd, "build", "html"),
89c464505bbe
    "templates": os.path.join(pwd, "templates"),
89c464505bbe
    "assets": os.path.join(pwd, "assets"),
89c464505bbe
    "asset_cache": os.path.join(pwd, "build", "webassets-cache")
89c464505bbe
}
0bac8e35c64b
paths = ["output", "templates", "assets", "asset_cache"]
7691229a4ef9
7691229a4ef9
89c464505bbe
def generate_assets(assets_path, output_path, debug=False):
7691229a4ef9
    """
7691229a4ef9
    Generate a webasset.Environment to pass into the Jinja2 template.
7691229a4ef9
89c464505bbe
    :param assets_path: directory path to asset items.
89c464505bbe
    :type assets_path: str
a1c04e576970
    :param output_path: directory path to output files.
a1c04e576970
    :type output_path: str
a1c04e576970
    :param debug: minimize JS and CSS assets, default no
a1c04e576970
    :type debug: bool
7691229a4ef9
    :return: webassets environment
7691229a4ef9
    :rtype: webassets.Environment
7691229a4ef9
    """
a1c04e576970
    webasset_env = webassets.Environment(output_path, "/")
89c464505bbe
    webasset_env.append_path(assets_path)
49a817930f15
a1c04e576970
    webasset_env.debug = debug
49a817930f15
49a817930f15
    # Javascript
68eb9a035d5a
    js_lapwing = webassets.Bundle("uikit/uikit.js",
4e250f7fe9b1
                                  "website/js/faconfig.js",  # This must be before fontawesome.js
68eb9a035d5a
                                  "fontawesome/js/fontawesome-all.js",
de0bdaff657d
                                  filters="uglifyjs",
89c464505bbe
                                  output="js/lapwing.%(version)s.js")
89c464505bbe
    webasset_env.register("js_lapwing", js_lapwing)
49a817930f15
49a817930f15
    # SCSS -> CSS
89c464505bbe
    css_lapwing = webassets.Bundle("website/scss/lapwing.scss",
68eb9a035d5a
                                   "fontawesome/css/fa-svg-with-js.css",
ed46fa2dba84
                                   filters="libsass,cssmin",
3e757b04fcde
                                   output="css/lapwing.%(version)s.css")
3e757b04fcde
    webasset_env.register("css_lapwing", css_lapwing)
49a817930f15
49a817930f15
    return webasset_env
73d6db7d8f0b
73d6db7d8f0b
89c464505bbe
def safe_copy(src, dest):
89c464505bbe
    """
89c464505bbe
    Copy files or directories.
89c464505bbe
89c464505bbe
    Source and destination must be absolute paths.
89c464505bbe
89c464505bbe
    :param src: source file or directory
89c464505bbe
    :type src: str
89c464505bbe
    :param dest: destination of the file or directory
89c464505bbe
    :type dest: str
89c464505bbe
    """
89c464505bbe
    src = src.rstrip("/")
89c464505bbe
    if os.path.isdir(dest) and (os.path.isfile(src) or os.path.isdir(src)):
89c464505bbe
        dest = os.path.join(dest, os.path.basename(src))
89c464505bbe
89c464505bbe
    if os.path.isdir(src):
89c464505bbe
        if os.path.isdir(dest):
89c464505bbe
            distutils.dir_util.copy_tree(src, dest, preserve_symlinks=True)
89c464505bbe
        else:
89c464505bbe
            shutil.copytree(src, dest, True)
89c464505bbe
    else:
89c464505bbe
        shutil.copy2(src, dest)
89c464505bbe
89c464505bbe
0bac8e35c64b
def build_website(minimize=False, lock=None):
73d6db7d8f0b
    """
73d6db7d8f0b
    Build website.
73d6db7d8f0b
73d6db7d8f0b
    :param minimize: minimize the CSS, JS or other assets.
73d6db7d8f0b
    :type minimize: bool
876ce8d30315
    :param lock: multiprocessing lock
876ce8d30315
    :type lock: multiprocessing.Lock or None
73d6db7d8f0b
    """
49a817930f15
    # If the paths aren't absolute, bail.
49a817930f15
    for path_name, path in config.items():
0bac8e35c64b
        if path_name not in paths:
89c464505bbe
            continue
49a817930f15
        if not os.path.isabs(path):
49a817930f15
            raise Exception("Path for '{}' is not absolute".format(path_name))
49a817930f15
eaa9d60c5786
    # We have to remove the output dir first
eaa9d60c5786
    if os.path.exists(config["output"]):
49a817930f15
        logger.info("Removing output directory")
0cb248cdcf3d
        if lock:
0cb248cdcf3d
            with lock:
0cb248cdcf3d
                shutil.rmtree(config["output"])
0cb248cdcf3d
        else:
0cb248cdcf3d
            shutil.rmtree(config["output"])
eaa9d60c5786
a1c04e576970
    webasset_debug = not minimize
73d6db7d8f0b
89c464505bbe
    webassets_env = generate_assets(config["assets"], config["output"],
89c464505bbe
                                    webasset_debug)
0cb248cdcf3d
0cb248cdcf3d
    if webasset_debug and os.path.isdir(config["asset_cache"]):
0cb248cdcf3d
        shutil.rmtree(config["asset_cache"])
89c464505bbe
    for d in (config["output"], config["asset_cache"]):
89c464505bbe
        if not os.path.isdir(d):
89c464505bbe
            os.makedirs(d, 0o755, True)
0cb248cdcf3d
1d93a8a89ec3
    webassets_env.cache = config["asset_cache"]
1d93a8a89ec3
89c464505bbe
    # Static files need to be relative paths to the template path
89c464505bbe
    # to be copied into the HTML output
68eb9a035d5a
    staticfiles = ["website/fonts", "website/img",
89c464505bbe
                   "website/favicon.ico", "website/robots.txt"]
89c464505bbe
    for staticpath in staticfiles:
89c464505bbe
        srcpath = os.path.join(pwd, "assets", staticpath)
89c464505bbe
        safe_copy(srcpath, config["output"])
7691229a4ef9
7a8918ad6a15
    current_year = datetime.now().year
7a8918ad6a15
    if datetime.now() > datetime(current_year, 12, 25):
7a8918ad6a15
        current_year += 1
7a8918ad6a15
89c464505bbe
    env_globals = {
89c464505bbe
        "site_name": config["site"]["name"],
89c464505bbe
        "site_email": config["site"]["email"],
7a8918ad6a15
        "current_year": current_year,
0bac8e35c64b
        "menu": config["menu"],
0bac8e35c64b
        "projects": config["projects"]
89c464505bbe
    }
33c994d6643e
89c464505bbe
    renderer = staticjinja.make_site(config["templates"], config["output"],
89c464505bbe
                                     extensions=[AssetsExtension])
89c464505bbe
    # We need to add webassets and globals to the Jinja Environment
89c464505bbe
    renderer._env.assets_environment = webassets_env
89c464505bbe
    renderer._env.globals.update(env_globals)
876ce8d30315
0bac8e35c64b
    renderer.render()
876ce8d30315
876ce8d30315
876ce8d30315
def run_server(lock=None):
876ce8d30315
    """
876ce8d30315
    Run a server.
876ce8d30315
876ce8d30315
    :param lock: multiprocess data pipe
876ce8d30315
    :type lock: multiprocess.Lock or None
876ce8d30315
    """
876ce8d30315
    if lock:
876ce8d30315
        logger.info("Awaiting renderer")
876ce8d30315
        while True:
876ce8d30315
            time.sleep(2)
876ce8d30315
            if lock.acquire(False):
876ce8d30315
                lock.release()
876ce8d30315
                break
876ce8d30315
            logger.info("Still waiting for renderer")
876ce8d30315
36dabfdd4632
    os.chdir("build/html")
876ce8d30315
    httpd = HTTPServer(("", 8000), SimpleHTTPRequestHandler)
89c464505bbe
    logger.info("Serving website: http://127.0.0.1:8000")
0bac8e35c64b
    httpd.serve_forever()
876ce8d30315
876ce8d30315
876ce8d30315
@click.group()
876ce8d30315
def cli():
876ce8d30315
    """
876ce8d30315
    Build and deploy static websites using
876ce8d30315
    Jinja2 and SCSS templates.
876ce8d30315
    """
876ce8d30315
    pass
876ce8d30315
876ce8d30315
876ce8d30315
@cli.command()
876ce8d30315
@click.option("--minimize", is_flag=True, default=False,
876ce8d30315
              help="minimize the CSS, JS and other assets.")
876ce8d30315
def build(minimize):
876ce8d30315
    """
876ce8d30315
    Build website.
876ce8d30315
876ce8d30315
    :param minimize: minimize the CSS, JS or other assets.
876ce8d30315
    :type minimize: bool
876ce8d30315
    """
876ce8d30315
    build_website(minimize)
876ce8d30315
876ce8d30315
876ce8d30315
@cli.command()
0cb248cdcf3d
@click.option("--minimize", is_flag=True, default=False,
0cb248cdcf3d
              help="minimize the CSS, JS and other assets.")
0bac8e35c64b
def serve(minimize):
876ce8d30315
    """
876ce8d30315
    Build website and run a "http.server" instance in the output directory.
876ce8d30315
0cb248cdcf3d
    :param minimize: minimize CSS/JS output
0cb248cdcf3d
    :type minimize: bool
876ce8d30315
    """
0bac8e35c64b
    lock = Lock()
876ce8d30315
0bac8e35c64b
    build_proc = Process(target=build_website, args=(minimize, lock))
876ce8d30315
    serve_proc = Process(target=run_server, args=(lock,))
876ce8d30315
    build_proc.start()
876ce8d30315
    serve_proc.start()
7691229a4ef9
73d6db7d8f0b
73d6db7d8f0b
@cli.command()
73d6db7d8f0b
@click.option("--rebuild", is_flag=True, default=False,
73d6db7d8f0b
              help="force (re)building the website before deploying.")
73d6db7d8f0b
def deploy(rebuild):
73d6db7d8f0b
    """
73d6db7d8f0b
    Deploy website to remote host.
73d6db7d8f0b
73d6db7d8f0b
    :param rebuild: force (re)building the website before deploying.
73d6db7d8f0b
    :type rebuild: bool
73d6db7d8f0b
    """
73d6db7d8f0b
    if rebuild:
876ce8d30315
        build_website()
73d6db7d8f0b
73d6db7d8f0b
73d6db7d8f0b
if __name__ == "__main__":
73d6db7d8f0b
    cli()