forked from hswaw/hscloud
120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
|
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)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|