Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 193 lines (153 sloc) 6.47 KB
#!/usr/bin/python
import argparse
import cPickle
import imp
import os
import shutil
import subprocess
import sys
import time
import traceback
BLOG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
PRODUCTION_CONF = os.path.join(BLOG_ROOT, "pelicanconf.py")
STAGING_CONF = os.path.join(BLOG_ROOT, "pelicanconfstaging.py")
UPDATE_CSS = os.path.join(BLOG_ROOT, "themes", "graphite", "updatecss.py")
# We try to get the pelican binary from a virtualenv, otherwise we fall back
# on a system-wide installation version. Last fallback is to just search PATH.
PELICAN_VIRTUALENVS = (
os.path.abspath(os.path.join(BLOG_ROOT, "..", "pelican-virtualenv")),
os.path.expanduser("~/.virtualenvs/pelican"),
os.path.expanduser("~/virtualenvs/pelican"),
"/usr/local",
"/usr")
for candidate in (os.path.join(path, "bin", "pelican")
for path in PELICAN_VIRTUALENVS):
if os.path.exists(candidate):
PELICAN_BIN = candidate
break
else:
PELICAN_BIN = "pelican"
def get_argument_parser():
"""Construct and return argparse.ArgumentParser instance."""
parser = argparse.ArgumentParser()
parser.add_argument(
"--staging",
action='store_true',
help="build in staging area instead of production")
parser.add_argument(
"--force",
action="store_true",
help="rebuild site even if no files have changed")
parser.add_argument(
"--quiet",
action="store_true",
help="suppress non-fatal error output")
return parser
def save_state(state, state_path):
"""Save the state generated by get_state()."""
with open(state_path, "w") as fd:
cPickle.dump(state, fd)
def load_saved_state(state_path):
"""Load a saved filesystem state from a previous run."""
if os.path.exists(state_path):
with open(state_path, "r") as fd:
return cPickle.load(fd)
return {}
def get_state(root_dir):
"""Obtain the latest Git commit."""
proc = subprocess.Popen(
["git", "-C", root_dir, "log", "-1", "--pretty=oneline"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
sys.stderr.write(
"Output from git process:\n----<stdout>----\n"
+ stdout + "----<stderr>----\n" + stderr + "--------\n")
raise Exception("git exited with status " + str(proc.returncode))
return out.strip()
def get_config_path(conf_file, setting):
"""Obtain the specified setting path as an absolute directory."""
config = imp.load_source("pelicanconf", conf_file)
path = getattr(config, setting)
if os.path.isabs(path):
return path
return os.path.abspath(os.path.normpath(os.path.join(
os.path.dirname(conf_file),
path)))
def get_symlink_target(symlink):
"""Return a symlink target as an absolute path, or None if not there."""
if os.path.exists(symlink):
if not os.path.islink(symlink):
raise Exception("%r is not a symlink" % (symlink,))
target = os.readlink(symlink)
if os.path.isabs(target):
return target
return os.path.normpath(os.path.join(output_parent, old_output_dir))
return None
def run_generation(pelican_conf):
"""Generate the output from the source files."""
# Ensure CSS is up to date with SASS source files in repo.
# (don't bother checking return code, it doesn't set one)
proc = subprocess.Popen([UPDATE_CSS])
proc.wait()
# Obtain intended output directory from the config file.
output_symlink = get_config_path(pelican_conf, "OUTPUT_PATH")
output_parent = os.path.abspath(os.path.join(output_symlink, ".."))
# Read the existing symlink to determine the old output directory.
old_output_dir = get_symlink_target(output_symlink)
# Create a temporary output directory name based on the timestamp. This
# is subject to races if the script is run more than once per second,
# but it's good enough for any reasonable use-case. If the directory
# already exists we should fail somewhat gracefully with an OSError
# from os.mkdir().
new_dir = os.path.join(output_parent, time.strftime("blog-%Y%m%d-%H%M%S"))
os.mkdir(new_dir)
# Create a symlink to the new output directory that we will rename into
# place if the generation succeeds.
tmp_link = new_dir + "-tmp-link"
os.symlink(new_dir, tmp_link)
# Run Pelican to generate the output, and rename into place (if successful)
# or remove the temporary content (if not).
cmdline = [PELICAN_BIN, "--settings", pelican_conf, "-o", new_dir]
try:
subprocess.check_output(cmdline, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError, exc:
sys.stderr.write("Pelican process returned %r, output follows\n" %
(exc.returncode,))
sys.stderr.write("--------\n" + exc.output + "--------\n")
os.remove(tmp_link)
shutil.rmtree(new_dir)
return False
else:
os.rename(tmp_link, output_symlink)
if old_output_dir is not None:
shutil.rmtree(old_output_dir)
return True
def main(argv):
"""Script entry point."""
try:
parser = get_argument_parser()
args = parser.parse_args(args=argv[1:])
pelican_conf = STAGING_CONF if args.staging else PRODUCTION_CONF
env_name = "staging" if args.staging else "production"
# Calculate state even if --force specified so we can save it later.
state_path = os.path.join(BLOG_ROOT, "filestate.%s" % (env_name,))
saved_state = load_saved_state(state_path)
new_saved_state = get_state(BLOG_ROOT)
if args.force or saved_state != new_saved_state:
run_generation(pelican_conf)
# Save state regardless of whether the generation succeeded.
# Reasoning: transient failures are unlikely - most likely a bad
# file was pushed, so another commit will be required to fix it.
# Endless retries will just cause cron spam. In the unlikely event
# of a transient failure, manually re-run with --force.
save_state(new_saved_state, state_path)
return 0
except Exception, exc:
sys.stderr.write("Error scanning articles: %s\n" % (exc,))
if not args.quiet:
traceback.print_exc()
return 1
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))