forked from hswaw/hscloud
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
This commit is contained in:
parent
94a1af8714
commit
491542589b
11 changed files with 411 additions and 0 deletions
8
tools/gostatic/BUILD
Normal file
8
tools/gostatic/BUILD
Normal 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
43
tools/gostatic/README.md
Normal 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.
|
45
tools/gostatic/example/BUILD
Normal file
45
tools/gostatic/example/BUILD
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
50
tools/gostatic/example/site.tmpl
Normal file
50
tools/gostatic/example/site.tmpl
Normal 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 }} —
|
||||||
|
{{ 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 }}
|
32
tools/gostatic/example/src/blog.atom
Normal file
32
tools/gostatic/example/src/blog.atom
Normal 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>
|
5
tools/gostatic/example/src/blog/first.md
Normal file
5
tools/gostatic/example/src/blog/first.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
title: First Post
|
||||||
|
date: 2012-12-12
|
||||||
|
tags: blog
|
||||||
|
----
|
||||||
|
My first post with [gostatic](https://github.com/piranha/gostatic).
|
9
tools/gostatic/example/src/index.html
Normal file
9
tools/gostatic/example/src/index.html
Normal 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>
|
1
tools/gostatic/example/src/static/style.css
Normal file
1
tools/gostatic/example/src/static/style.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/* put your style rules here */
|
84
tools/gostatic/rules.bzl
Normal file
84
tools/gostatic/rules.bzl
Normal 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.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
15
tools/gostatic/tarify/BUILD.bazel
Normal file
15
tools/gostatic/tarify/BUILD.bazel
Normal 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"],
|
||||||
|
)
|
119
tools/gostatic/tarify/main.go
Normal file
119
tools/gostatic/tarify/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue