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
diff --git a/tools/gostatic/tarify/BUILD.bazel b/tools/gostatic/tarify/BUILD.bazel
new file mode 100644
index 0000000..7bc841b
--- /dev/null
+++ b/tools/gostatic/tarify/BUILD.bazel
@@ -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"],
+)
diff --git a/tools/gostatic/tarify/main.go b/tools/gostatic/tarify/main.go
new file mode 100644
index 0000000..f292289
--- /dev/null
+++ b/tools/gostatic/tarify/main.go
@@ -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)
+ }
+
+ }
+ }
+}