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 }