mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2024-10-15 05:17:45 +00:00
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