blob: f292289533da808c6d6c994fa601d19436c00efe [file] [log] [blame]
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)
}
}
}
}