2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
31 "github.com/google/uuid"
32 "lukechampine.com/blake3"
40 DepTypeIfcreate = 0x01
41 DepTypeIfchange = 0x02
44 DepTypeIfchangeNonex = 0x05
51 IfchangeCache = make(map[string][]*Ifchange)
52 DepCache = make(map[string]*Dep)
57 func chunkWrite(in []byte) (out []byte) {
63 binary.BigEndian.PutUint16(out[:2], uint16(l))
68 type Ifchange struct {
70 meta [InodeLen + HashLen]byte
73 func (ifchange *Ifchange) Inode() *Inode {
74 inode := Inode(ifchange.meta[:InodeLen])
78 func (ifchange *Ifchange) Hash() Hash {
79 return Hash(ifchange.meta[InodeLen:])
90 func ifcreate(w io.Writer, fdDepName string, tgt string) (err error) {
91 tracef(CDebug, "ifcreate: %s <- %s", fdDepName, tgt)
92 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
93 []byte{DepTypeIfcreate}, []byte(tgt)...,
98 func always(w io.Writer, fdDepName string) (err error) {
99 tracef(CDebug, "always: %s", fdDepName)
100 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(
101 []byte{DepTypeAlways},
106 func stamp(w io.Writer, fdDepName string, hsh Hash) (err error) {
107 tracef(CDebug, "stamp: %s <- %s", fdDepName, hsh)
108 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
109 []byte{DepTypeStamp}, []byte(hsh)...,
114 func fileHash(fd io.Reader) (Hash, error) {
115 h := blake3.New(HashLen, nil)
116 if _, err := io.Copy(h, bufio.NewReader(fd)); err != nil {
119 return Hash(h.Sum(nil)), nil
122 func depWrite(w io.Writer, fdDepName, cwd string, tgt *Tgt, hsh Hash) (err error) {
123 tracef(CDebug, "ifchange: %s <- %s", fdDepName, tgt)
124 fd, err := os.Open(tgt.a)
129 inode, isDir, err := inodeFromFileByFd(fd)
137 hsh, err = fileHash(fd)
142 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
146 []byte(tgt.RelTo(cwd)),
151 func depWriteNonex(w io.Writer, fdDepName, tgtRel string) (err error) {
152 tracef(CDebug, "ifchange: %s <- %s (non-existing)", fdDepName, tgtRel)
153 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
154 {DepTypeIfchangeNonex},
160 func depsWrite(fdDep *os.File, tgts []*Tgt) error {
162 tracef(CDebug, "no opened fdDep: %s", tgts)
167 fdDepW := bufio.NewWriter(fdDep)
168 for _, tgt := range tgts {
170 if DepCwd != "" && Cwd != DepCwd {
173 tgtDir := path.Join(cwd, DirPrefix)
174 if _, errStat := os.Stat(tgt.a); errStat == nil {
175 err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtDir, tgt, ""))
177 tgtRel := tgt.RelTo(tgtDir)
178 err = ErrLine(depWriteNonex(fdDepW, fdDep.Name(), tgtRel))
184 return fdDepW.Flush()
187 func depHeadParse(data []byte) (build uuid.UUID, tail []byte, err error) {
188 if len(data) < len(BinMagic)+1+UUIDLen {
189 err = errors.New("too short header")
192 if !bytes.Equal(data[:len(BinMagic)], []byte(BinMagic)) {
193 err = errors.New("bad magic")
196 data = data[len(BinMagic):]
200 err = errors.New("unknown version")
203 build = uuid.Must(uuid.FromBytes(data[1 : 1+UUIDLen]))
204 tail = data[1+UUIDLen:]
208 func chunkRead(data []byte) (typ byte, chunk []byte, tail []byte, err error) {
210 err = errors.New("no length")
213 l := binary.BigEndian.Uint16(data[:2])
215 err = errors.New("zero length chunk")
218 if len(data) < int(l) {
219 err = errors.New("not enough data")
222 typ, chunk, tail = data[2], data[3:l], data[l:]
226 func depBinIfchangeParse(tgt *Tgt, chunk []byte) (*Ifchange, string, error) {
227 if len(chunk) < InodeLen+HashLen+1 {
228 return nil, "", errors.New("too short \"ifchange\" format")
231 tgtH, _ := pathSplit(tgt.a)
232 name := string(chunk[InodeLen+HashLen:])
233 ifchange := &Ifchange{
234 tgt: NewTgt(path.Join(tgtH, name)),
235 meta: ([InodeLen + HashLen]byte)(chunk),
237 for _, cached := range IfchangeCache[ifchange.tgt.rel] {
238 if ifchange.meta == cached.meta {
239 return cached, name, nil
242 if IfchangeCache != nil {
243 IfchangeCache[ifchange.tgt.rel] = append(IfchangeCache[ifchange.tgt.rel], ifchange)
245 return ifchange, name, nil
248 func depParse(tgt *Tgt, data []byte) (*Dep, error) {
249 build, data, err := depHeadParse(data)
253 dep := Dep{build: build}
257 typ, chunk, data, err = chunkRead(data)
259 return nil, ErrLine(err)
264 return nil, ErrLine(errors.New("bad \"always\" format"))
268 if len(chunk) != HashLen {
269 return nil, ErrLine(errors.New("bad \"stamp\" format"))
271 dep.stamp = Hash(chunk)
272 case DepTypeIfcreate:
274 return nil, ErrLine(errors.New("too short \"ifcreate\" format"))
276 tgtH, _ := pathSplit(tgt.a)
277 dep.ifcreates = append(dep.ifcreates, NewTgt(path.Join(tgtH, string(chunk))))
278 case DepTypeIfchange:
279 ifchange, _, err := depBinIfchangeParse(tgt, chunk)
281 return nil, ErrLine(err)
283 dep.ifchanges = append(dep.ifchanges, ifchange)
284 case DepTypeIfchangeNonex:
286 return nil, ErrLine(errors.New("too short \"ifchange\" format"))
288 dep.ifchanges = append(dep.ifchanges, &Ifchange{tgt: NewTgt(string(chunk))})
290 return nil, ErrLine(errors.New("unknown type"))
296 func depRead(tgt *Tgt) (*Dep, error) {
297 data, err := os.ReadFile(tgt.dep)
301 return depParse(tgt, data)
304 func depReadOnlyIfchanges(pth string) (ifchanges []string, err error) {
305 data, err := os.ReadFile(pth)
309 _, data, err = depHeadParse(data)
316 tgtDummy := NewTgt("")
318 typ, chunk, data, err = chunkRead(data)
320 return nil, ErrLine(err)
323 case DepTypeIfchange:
324 _, tgt, err = depBinIfchangeParse(tgtDummy, chunk)
326 return nil, ErrLine(err)
328 ifchanges = append(ifchanges, tgt)
329 case DepTypeIfchangeNonex:
330 ifchanges = append(ifchanges, string(chunk))
336 func depBuildRead(pth string) (uuid.UUID, error) {
337 fd, err := os.Open(pth)
341 data := make([]byte, len(BinMagic)+1+UUIDLen)
342 _, err = io.ReadFull(fd, data)
347 build, _, err := depHeadParse(data)
351 func depBuildWrite(w io.Writer, build uuid.UUID) (err error) {
352 _, err = io.Copy(w, bytes.NewBuffer(bytes.Join([][]byte{