1 // goredo -- djb's redo implementation on pure Go
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
29 "github.com/google/uuid"
30 "lukechampine.com/blake3"
38 DepTypeIfcreate = 0x01
39 DepTypeIfchange = 0x02
42 DepTypeIfchangeNonex = 0x05
49 IfchangeCache = make(map[string][]*Ifchange)
50 DepCache = make(map[string]*Dep)
55 func chunkWrite(in []byte) (out []byte) {
61 binary.BigEndian.PutUint16(out[:2], uint16(l))
66 type Ifchange struct {
68 meta [InodeLen + HashLen]byte
71 func (ifchange *Ifchange) Inode() *Inode {
72 inode := Inode(ifchange.meta[:InodeLen])
76 func (ifchange *Ifchange) Hash() Hash {
77 return Hash(ifchange.meta[InodeLen:])
88 func ifcreate(w io.Writer, fdDepName string, tgt string) (err error) {
89 tracef(CDebug, "ifcreate: %s <- %s", fdDepName, tgt)
90 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
91 []byte{DepTypeIfcreate}, []byte(tgt)...,
96 func always(w io.Writer, fdDepName string) (err error) {
97 tracef(CDebug, "always: %s", fdDepName)
98 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(
99 []byte{DepTypeAlways},
104 func stamp(w io.Writer, fdDepName string, hsh Hash) (err error) {
105 tracef(CDebug, "stamp: %s <- %s", fdDepName, hsh)
106 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
107 []byte{DepTypeStamp}, []byte(hsh)...,
112 func fileHash(fd io.Reader) (Hash, error) {
113 h := blake3.New(HashLen, nil)
114 if _, err := io.Copy(h, bufio.NewReader(fd)); err != nil {
117 return Hash(h.Sum(nil)), nil
120 func depWrite(w io.Writer, fdDepName, cwd string, tgt *Tgt, hsh Hash) (err error) {
121 tracef(CDebug, "ifchange: %s <- %s", fdDepName, tgt)
122 fd, err := os.Open(tgt.a)
127 inode, isDir, err := inodeFromFileByFd(fd)
135 hsh, err = fileHash(fd)
140 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
144 []byte(tgt.RelTo(cwd)),
149 func depWriteNonex(w io.Writer, fdDepName, tgtRel string) (err error) {
150 tracef(CDebug, "ifchange: %s <- %s (non-existing)", fdDepName, tgtRel)
151 _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
152 {DepTypeIfchangeNonex},
158 func depsWrite(fdDep *os.File, tgts []*Tgt) error {
160 tracef(CDebug, "no opened fdDep: %s", tgts)
165 fdDepW := bufio.NewWriter(fdDep)
166 for _, tgt := range tgts {
168 if DepCwd != "" && Cwd != DepCwd {
171 tgtDir := path.Join(cwd, DirPrefix)
172 if _, errStat := os.Stat(tgt.a); errStat == nil {
173 err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtDir, tgt, ""))
175 tgtRel := tgt.RelTo(tgtDir)
176 err = ErrLine(depWriteNonex(fdDepW, fdDep.Name(), tgtRel))
182 return fdDepW.Flush()
185 func depHeadParse(data []byte) (build uuid.UUID, tail []byte, err error) {
186 if len(data) < len(BinMagic)+1+UUIDLen {
187 err = errors.New("too short header")
190 if !bytes.Equal(data[:len(BinMagic)], []byte(BinMagic)) {
191 err = errors.New("bad magic")
194 data = data[len(BinMagic):]
198 err = errors.New("unknown version")
201 build = uuid.Must(uuid.FromBytes(data[1 : 1+UUIDLen]))
202 tail = data[1+UUIDLen:]
206 func chunkRead(data []byte) (typ byte, chunk []byte, tail []byte, err error) {
208 err = errors.New("no length")
211 l := binary.BigEndian.Uint16(data[:2])
213 err = errors.New("zero length chunk")
216 if len(data) < int(l) {
217 err = errors.New("not enough data")
220 typ, chunk, tail = data[2], data[3:l], data[l:]
224 func depBinIfchangeParse(tgt *Tgt, chunk []byte) (*Ifchange, string, error) {
225 if len(chunk) < InodeLen+HashLen+1 {
226 return nil, "", errors.New("too short \"ifchange\" format")
229 tgtH, _ := pathSplit(tgt.a)
230 name := string(chunk[InodeLen+HashLen:])
231 ifchange := &Ifchange{
232 tgt: NewTgt(path.Join(tgtH, name)),
233 meta: ([InodeLen + HashLen]byte)(chunk),
235 for _, cached := range IfchangeCache[ifchange.tgt.rel] {
236 if ifchange.meta == cached.meta {
237 return cached, name, nil
240 if IfchangeCache != nil {
241 IfchangeCache[ifchange.tgt.rel] = append(IfchangeCache[ifchange.tgt.rel], ifchange)
243 return ifchange, name, nil
246 func depParse(tgt *Tgt, data []byte) (*Dep, error) {
247 build, data, err := depHeadParse(data)
251 dep := Dep{build: build}
255 typ, chunk, data, err = chunkRead(data)
257 return nil, ErrLine(err)
262 return nil, ErrLine(errors.New("bad \"always\" format"))
266 if len(chunk) != HashLen {
267 return nil, ErrLine(errors.New("bad \"stamp\" format"))
269 dep.stamp = Hash(chunk)
270 case DepTypeIfcreate:
272 return nil, ErrLine(errors.New("too short \"ifcreate\" format"))
274 tgtH, _ := pathSplit(tgt.a)
275 dep.ifcreates = append(dep.ifcreates, NewTgt(path.Join(tgtH, string(chunk))))
276 case DepTypeIfchange:
277 ifchange, _, err := depBinIfchangeParse(tgt, chunk)
279 return nil, ErrLine(err)
281 dep.ifchanges = append(dep.ifchanges, ifchange)
282 case DepTypeIfchangeNonex:
284 return nil, ErrLine(errors.New("too short \"ifchange\" format"))
286 dep.ifchanges = append(dep.ifchanges, &Ifchange{tgt: NewTgt(string(chunk))})
288 return nil, ErrLine(errors.New("unknown type"))
294 func depRead(tgt *Tgt) (*Dep, error) {
295 data, err := os.ReadFile(tgt.dep)
299 return depParse(tgt, data)
302 func depReadOnlyIfchanges(pth string) (ifchanges []string, err error) {
303 data, err := os.ReadFile(pth)
307 _, data, err = depHeadParse(data)
314 tgtDummy := NewTgt("")
316 typ, chunk, data, err = chunkRead(data)
318 return nil, ErrLine(err)
321 case DepTypeIfchange:
322 _, tgt, err = depBinIfchangeParse(tgtDummy, chunk)
324 return nil, ErrLine(err)
326 ifchanges = append(ifchanges, tgt)
327 case DepTypeIfchangeNonex:
328 ifchanges = append(ifchanges, string(chunk))
334 func depBuildRead(pth string) (uuid.UUID, error) {
335 fd, err := os.Open(pth)
339 data := make([]byte, len(BinMagic)+1+UUIDLen)
340 _, err = io.ReadFull(fd, data)
345 build, _, err := depHeadParse(data)
349 func depBuildWrite(w io.Writer, build uuid.UUID) (err error) {
350 _, err = io.Copy(w, bytes.NewBuffer(bytes.Join([][]byte{