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