nardump.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // nardump is like nix-store --dump, but in Go, writing a NAR
  4. // file (tar-like, but focused on being reproducible) to stdout
  5. // or to a hash with the --sri flag.
  6. //
  7. // It lets us calculate a Nix sha256 without the person running
  8. // git-pull-oss.sh having Nix available.
  9. package main
  10. // For the format, see:
  11. // See https://gist.github.com/jbeda/5c79d2b1434f0018d693
  12. import (
  13. "bufio"
  14. "crypto/sha256"
  15. "encoding/base64"
  16. "encoding/binary"
  17. "flag"
  18. "fmt"
  19. "io"
  20. "io/fs"
  21. "log"
  22. "os"
  23. "path"
  24. "sort"
  25. )
  26. var sri = flag.Bool("sri", false, "print SRI")
  27. func main() {
  28. flag.Parse()
  29. if flag.NArg() != 1 {
  30. log.Fatal("usage: nardump <dir>")
  31. }
  32. arg := flag.Arg(0)
  33. if err := os.Chdir(arg); err != nil {
  34. log.Fatal(err)
  35. }
  36. if *sri {
  37. hash := sha256.New()
  38. if err := writeNAR(hash, os.DirFS(".")); err != nil {
  39. log.Fatal(err)
  40. }
  41. fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
  42. return
  43. }
  44. bw := bufio.NewWriter(os.Stdout)
  45. if err := writeNAR(bw, os.DirFS(".")); err != nil {
  46. log.Fatal(err)
  47. }
  48. bw.Flush()
  49. }
  50. // writeNARError is a sentinel panic type that's recovered by writeNAR
  51. // and converted into the wrapped error.
  52. type writeNARError struct{ err error }
  53. // narWriter writes NAR files.
  54. type narWriter struct {
  55. w io.Writer
  56. fs fs.FS
  57. }
  58. // writeNAR writes a NAR file to w from the root of fs.
  59. func writeNAR(w io.Writer, fs fs.FS) (err error) {
  60. defer func() {
  61. if e := recover(); e != nil {
  62. if we, ok := e.(writeNARError); ok {
  63. err = we.err
  64. return
  65. }
  66. panic(e)
  67. }
  68. }()
  69. nw := &narWriter{w: w, fs: fs}
  70. nw.str("nix-archive-1")
  71. return nw.writeDir(".")
  72. }
  73. func (nw *narWriter) writeDir(dirPath string) error {
  74. ents, err := fs.ReadDir(nw.fs, dirPath)
  75. if err != nil {
  76. return err
  77. }
  78. sort.Slice(ents, func(i, j int) bool {
  79. return ents[i].Name() < ents[j].Name()
  80. })
  81. nw.str("(")
  82. nw.str("type")
  83. nw.str("directory")
  84. for _, ent := range ents {
  85. nw.str("entry")
  86. nw.str("(")
  87. nw.str("name")
  88. nw.str(ent.Name())
  89. nw.str("node")
  90. mode := ent.Type()
  91. sub := path.Join(dirPath, ent.Name())
  92. var err error
  93. switch {
  94. case mode.IsRegular():
  95. err = nw.writeRegular(sub)
  96. case mode.IsDir():
  97. err = nw.writeDir(sub)
  98. default:
  99. // TODO(bradfitz): symlink, but requires fighting io/fs a bit
  100. // to get at Readlink or the osFS via fs. But for now
  101. // we don't need symlinks because they're not in Go's archive.
  102. return fmt.Errorf("unsupported file type %v at %q", sub, mode)
  103. }
  104. if err != nil {
  105. return err
  106. }
  107. nw.str(")")
  108. }
  109. nw.str(")")
  110. return nil
  111. }
  112. func (nw *narWriter) writeRegular(path string) error {
  113. nw.str("(")
  114. nw.str("type")
  115. nw.str("regular")
  116. fi, err := fs.Stat(nw.fs, path)
  117. if err != nil {
  118. return err
  119. }
  120. if fi.Mode()&0111 != 0 {
  121. nw.str("executable")
  122. nw.str("")
  123. }
  124. contents, err := fs.ReadFile(nw.fs, path)
  125. if err != nil {
  126. return err
  127. }
  128. nw.str("contents")
  129. if err := writeBytes(nw.w, contents); err != nil {
  130. return err
  131. }
  132. nw.str(")")
  133. return nil
  134. }
  135. func (nw *narWriter) str(s string) {
  136. if err := writeString(nw.w, s); err != nil {
  137. panic(writeNARError{err})
  138. }
  139. }
  140. func writeString(w io.Writer, s string) error {
  141. var buf [8]byte
  142. binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
  143. if _, err := w.Write(buf[:]); err != nil {
  144. return err
  145. }
  146. if _, err := io.WriteString(w, s); err != nil {
  147. return err
  148. }
  149. return writePad(w, len(s))
  150. }
  151. func writeBytes(w io.Writer, b []byte) error {
  152. var buf [8]byte
  153. binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
  154. if _, err := w.Write(buf[:]); err != nil {
  155. return err
  156. }
  157. if _, err := w.Write(b); err != nil {
  158. return err
  159. }
  160. return writePad(w, len(b))
  161. }
  162. func writePad(w io.Writer, n int) error {
  163. pad := n % 8
  164. if pad == 0 {
  165. return nil
  166. }
  167. var zeroes [8]byte
  168. _, err := w.Write(zeroes[:8-pad])
  169. return err
  170. }