101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
)
|
|
|
|
// Pack tars and gzips a directory the way the build agent does, byte for byte, and answers the archive and its
|
|
// digest, "sha256:" and the hex of the archive's bytes (issue 1479; novox/hq ADR 0405 §7, ADR 0453 §7). It is a
|
|
// copy of mesh-controller's internal/builder/builder.go pack (origin/main 073ccc29, :1351-1425), which stays
|
|
// where it is until the build agent goes: a build module's digest is compared with the build agent's for one
|
|
// fingerprint, so the bytes must be equal.
|
|
//
|
|
// Deterministically: regular files only (a symlink or a device is refused), sorted relative paths with
|
|
// slashes, each header carrying its name, permission bits and size and nothing else, the gzip default writer.
|
|
func Pack(root string) (archive []byte, digest string, err error) {
|
|
raw, err := pack(root)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
sum := sha256.Sum256(raw)
|
|
return raw, "sha256:" + hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
func pack(root string) ([]byte, error) {
|
|
info, err := os.Stat(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, fmt.Errorf("%s is not a directory", root)
|
|
}
|
|
|
|
var paths []string
|
|
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() || !info.Mode().IsRegular() {
|
|
// Only files. A symlink or a device in an archive is refused by the host that unpacks
|
|
// it, so putting one in would build something that cannot be applied.
|
|
if !info.IsDir() && !info.Mode().IsRegular() {
|
|
return fmt.Errorf("%s is neither a file nor a directory, and an archive carries "+
|
|
"only those", path)
|
|
}
|
|
return nil
|
|
}
|
|
paths = append(paths, path)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// filepath.Walk walks in lexical order; the sort is kept as the build agent keeps it, since an archive
|
|
// whose digest changes because the traversal did is a silent failure.
|
|
sort.Strings(paths)
|
|
|
|
var out bytes.Buffer
|
|
zipped := gzip.NewWriter(&out)
|
|
writer := tar.NewWriter(zipped)
|
|
for _, path := range paths {
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
relative, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mode := int64(info.Mode().Perm())
|
|
if err := writer.WriteHeader(&tar.Header{
|
|
Name: filepath.ToSlash(relative), Mode: mode, Size: int64(len(body)),
|
|
Typeflag: tar.TypeReg,
|
|
// Everything else left at its zero value on purpose: no times, owners or names.
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := writer.Write(body); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := zipped.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Bytes(), nil
|
|
}
|