1
0
Fork 0

tools/gostatic: init

This adds Bazel/hscloud integration to gostatic, via gostatic_tarball.

A sample is provided in //tools/gostatic/example, it can be built using:

    bazel build //tools/gostatic/example

The resulting tarball can then be extracted and viewed in a web
browser.

Change-Id: Idf8d4a8e0ee3a5ae07f7449a25909478c2d8b105
master
q3k 2020-10-24 20:22:08 +02:00
parent 94a1af8714
commit 491542589b
11 changed files with 411 additions and 0 deletions

8
tools/gostatic/BUILD Normal file
View File

@ -0,0 +1,8 @@
load("//bzl:rules.bzl", "copy_go_binary")
copy_go_binary(
name = "gostatic",
src = "@com_github_piranha_gostatic//:gostatic",
visibility = ["//visibility:public"],
)

43
tools/gostatic/README.md Normal file
View File

@ -0,0 +1,43 @@
gostatic site generator
=======================
This implements support for [gostatic](https://github.com/piranha/gostatic), a static site generator, inside hscloud.
Creating a gostatic site
------------------------
To get started, copy over the skeleton from //tools/gostatic/example into a new directory.
mkdir -p personal/foo
cp -rv tools/gostatic/example personal/foo/mysite
You can also build your own `gostatic_tarball` from scratch if you are familiar enough with gostatic.
You can then then build a tarball of your site by running:
bazel build //personal/foo/mysite
Your site will be built and tarred up into `bazel-bin/personal/foo/mysite/mysite.tar`. You can then use this to populate a container in `docker_rules`.
TODO(q3k): add a target that starts up a simple web server for testing the rendered site.
Configuring a gostatic site
---------------------------
Configuration is done via the `gostatic_tarball` rule. This mostly generates an upstream gostatic [configuration file](https://github.com/piranha/gostatic#configuration) - please refer to that file for more information.
| Field | Description | Example |
|-------|-------------|---------|
| `templates` | List of template sources/targets. This is used to populate the TEMPLATES config option. | `[ ":site.tmpl" ]` |
| `source_dir` | BUILDfile-relative source directory containing site sources. This is used to populate the SOURCE config option. All files given in `srcs` must be contained within this directory. | `"src"` |
| `srcs` | List of template sources/targets. This is what will be available to gostatic during compilation. | `[ "src/blog/first.md" ]` |
| `extra_config` | Rest of the gostatic config, ie. rules. | |
Running gostatic-the-tool
-------------------------
If you want to run plain gostatic for some odd reason, it's available under:
bazel run //tools/gostatic
TODO(q3k): allow running this against a `gostatic_tarball`'s config.

View File

@ -0,0 +1,45 @@
load("//tools/gostatic:rules.bzl", "gostatic_tarball")
gostatic_tarball(
name = "example",
templates = [
"site.tmpl",
],
source_dir = "src",
extra_config = """
TITLE = Example Site
URL = https://example.com
AUTHOR = Your Name
blog/*.md:
config
ext .html
directorify
tags tags/*.tag
markdown
template post
template page
*.tag: blog/*.md
ext .html
directorify
template tag
markdown
template page
blog.atom: blog/*.md
inner-template
index.html: blog/*.md
config
inner-template
template page
""",
srcs = [
"src/blog/first.md",
"src/static/style.css",
"src/blog.atom",
"src/index.html",
],
)

View File

@ -0,0 +1,50 @@
{{ define "header" }}<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="author" content="{{ html .Site.Other.Author }}">
<link rel="alternate" type="application/atom+xml" title="{{ html .Site.Other.Title }} feed" href="{{ .Rel "blog.atom" }}">
<title>{{ .Site.Other.Title }}{{ if .Title }}: {{ .Title }}{{ end }}</title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="{{ .Rel "static/style.css" }}">
</head>
<body>
{{ end }}
{{ define "footer" }}
</body>
</html>
{{ end }}
{{define "date"}}
<time datetime="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
{{ .Format "2006, January 02" }}
</time>
{{end}}
{{ define "page" }}{{ template "header" . }}
{{ .Content }}
{{ template "footer" . }}{{ end }}
{{ define "post" }}
<article>
<header>
<h1>{{ .Title }}</h1>
<div class="info">
{{ template "date" .Date }} &mdash;
{{ range $i, $t := .Tags }}{{if $i}},{{end}}
<a href="/tags/{{ $t }}/">{{ $t }}</a>{{ end }}
</div>
</header>
<section>
{{ .Content }}
</section>
</article>
{{ end }}
{{define "tag"}}
# Pages tagged with {{ .Title }}
{{ range .Site.Pages.WithTag .Title }}
- [{{ .Title }}](../../{{ .Url }})
{{ end }}
{{ end }}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0">
<id>{{ .Site.Other.Url }}</id>
<title>{{ .Site.Other.Title }}</title>
{{ with .Site.Pages.Children "blog/" }}
<updated>{{ .First.Date.Format "2006-01-02T15:04:05Z07:00" }}</updated>
{{ end }}
<author><name>{{ .Site.Other.Author }}</name></author>
<link href="{{ .Site.Other.Url }}" rel="alternate"></link>
<generator uri="https://github.com/piranha/gostatic">gostatic</generator>
{{ with .Site.Pages.Children "blog/" }}
{{ range .Slice 0 5 }}
<entry>
<id>{{ .Url }}</id>
<author><name>{{ or .Other.Author .Site.Other.Author }}</name></author>
<title type="html">{{ html .Title }}</title>
<published>{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}</published>
{{ range .Tags }}
<category term="{{ . }}"></category>
{{ end }}
<link href="{{ .Site.Other.Url }}/{{ .Url }}" rel="alternate"></link>
<content type="html">
{{/* .Process runs here in case only feed changed */}}
{{ with cut "<section>" "</section>" .Process.Content }}
{{ html . }}
{{ end }}
</content>
</entry>
{{ end }}
{{ end }}
</feed>

View File

@ -0,0 +1,5 @@
title: First Post
date: 2012-12-12
tags: blog
----
My first post with [gostatic](https://github.com/piranha/gostatic).

View File

@ -0,0 +1,9 @@
title: Main Page
----
<ul class="post-list">
{{ range .Site.Pages.Children "blog/" }}
<li>
{{ template "date" .Date }} - <a href="{{ $.Rel .Url }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1 @@
/* put your style rules here */

84
tools/gostatic/rules.bzl Normal file
View File

@ -0,0 +1,84 @@
def _gostatic_tarball_impl(ctx):
out = ctx.actions.declare_directory("out")
tarball_name = ctx.attr.name + ".tar"
tarball = ctx.actions.declare_file(tarball_name)
config_name = ctx.attr.name + ".config"
config = ctx.actions.declare_file(config_name)
# Build path to root of sources, based on source_dir
# and location of the instantiating BUILDfile
# (source_dir is defined as relative to the BUILD file).
source_dir = '/'.join(ctx.build_file_path.split('/')[:-1]) + '/' + ctx.attr.source_dir
# Relative path to go up from generated config to build
# root. This is because gostatic is magical and really
# wants the config to be alongside the source.
up = "../" * (config.path.count("/"))
templates = " ".join([up + f.path for f in ctx.files.templates])
config_lines = [
"OUTPUT = {}".format(up + out.path),
"TEMPLATES = {}".format(templates),
"SOURCE = {}".format(up + source_dir),
]
config_content = "\n".join(config_lines)
config_content += ctx.attr.extra_config
ctx.actions.write(config, config_content)
ctx.actions.run(
outputs = [out],
inputs = [config] + ctx.files.templates + ctx.files.srcs,
executable = ctx.file._gostatic,
arguments = [config.path],
)
ctx.actions.run(
outputs = [tarball],
inputs = [out],
executable = ctx.file._tarify,
arguments = [
"-site", out.path,
"-tarball", tarball.path,
],
)
return [DefaultInfo(files=depset([tarball]))]
gostatic_tarball = rule(
implementation = _gostatic_tarball_impl,
attrs = {
"extra_config": attr.string(
mandatory = True,
doc = """
Gostatic configuration (rules, etc). Do not specify OUTPUT, TEMPLATES
or SOURCES - these are automatically generated.
""",
),
"source_dir": attr.string(
mandatory = True,
doc = "Root of site sources. Relative to BUILDfile.",
),
"srcs": attr.label_list(
allow_files = True,
doc = "Site sources, all must be contained within source_dir"
),
"templates": attr.label_list(
allow_files = True,
doc = "Templates to use (passed to TEMPLATES in gostatic config).",
),
"_gostatic": attr.label(
default = Label("//tools/gostatic"),
allow_single_file = True,
executable = True,
cfg = "exec",
doc = "Path to gostatic binary.",
),
"_tarify": attr.label(
default = Label("//tools/gostatic/tarify"),
allow_single_file = True,
executable = True,
cfg = "exec",
doc = "Path to tarify binary.",
),
},
)

View File

@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "code.hackerspace.pl/hscloud/tools/gostatic/tarify",
visibility = ["//visibility:private"],
deps = ["@com_github_golang_glog//:go_default_library"],
)
go_binary(
name = "tarify",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,119 @@
package main
// tarify implements a minimal, self-contained, hermetic tarball builder.
// It is currently used with gostatic to take a non-hermetic directory and
// turn it into a hermetic tarball via a glob.
//
// For more information about tree artifacts and hermeticity, see:
// https://jmmv.dev/2019/12/bazel-dynamic-execution-tree-artifacts.html
import (
"archive/tar"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/golang/glog"
)
var (
flagSite string
flagTarball string
)
func init() {
flag.Set("logtostderr", "true")
}
func main() {
flag.StringVar(&flagSite, "site", "", "Site sources")
flag.StringVar(&flagTarball, "tarball", "", "Output tarball")
flag.Parse()
if flagSite == "" {
glog.Exitf("-site must be set")
}
if flagTarball == "" {
glog.Exitf("-tarball must be set")
}
f, err := os.Create(flagTarball)
if err != nil {
glog.Exitf("Create(%q): %v", flagTarball, err)
}
defer f.Close()
w := tar.NewWriter(f)
defer w.Close()
flagSite = strings.TrimSuffix(flagSite, "/")
// First retrieve all files and sort. This is required for idempotency.
elems := []struct {
path string
info os.FileInfo
}{}
err = filepath.Walk(flagSite, func(inPath string, _ os.FileInfo, err error) error {
// We don't use the given fileinfo, as we want to deref symlinks.
info, err := os.Stat(inPath)
if err != nil {
return fmt.Errorf("Stat: %w", err)
}
elems = append(elems, struct {
path string
info os.FileInfo
}{inPath, info})
return nil
})
if err != nil {
glog.Exitf("Walk(%q, _): %v", flagSite, err)
}
sort.Slice(elems, func(i, j int) bool { return elems[i].path < elems[j].path })
// Now that we have a sorted list, tar 'em up.
for _, elem := range elems {
inPath := elem.path
info := elem.info
outPath := strings.TrimPrefix(strings.TrimPrefix(inPath, flagSite), "/")
if outPath == "" {
continue
}
if info.IsDir() {
glog.Infof("D %s", outPath)
if err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeDir,
Name: outPath,
Mode: 0755,
}); err != nil {
glog.Exitf("Writing directory header for %q failed: %v", inPath, err)
}
} else {
glog.Infof("F %s", outPath)
if err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: outPath,
Mode: 0644,
// TODO(q3k): this can race (TOCTOU Stat/Open, resulting in "archive/tar: write Too long")
// No idea, how to handle this better though without reading the entire file into memory,
// or trying to do filesystem locks? Besides, in practical use with Bazel this will never
// happen.
Size: info.Size(),
}); err != nil {
glog.Exitf("Writing file header for %q failed: %v", inPath, err)
}
r, err := os.Open(inPath)
if err != nil {
glog.Exitf("Open(%q): %v", inPath, err)
}
defer r.Close()
if _, err := io.Copy(w, r); err != nil {
glog.Exitf("Copy(%q): %v", inPath, err)
}
}
}
}