]> Cypherpunks.ru repositories - goredo.git/blob - ood.go
Optimise memory storage of dependency information
[goredo.git] / ood.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
4
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.
8
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.
13
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/>.
16 */
17
18 // Out-of-date determination
19
20 package main
21
22 import (
23         "bytes"
24         "errors"
25         "fmt"
26         "io"
27         "io/fs"
28         "log"
29         "os"
30         "path"
31         "strings"
32
33         "golang.org/x/sys/unix"
34 )
35
36 const (
37         DepTypeIfcreate = "ifcreate"
38         DepTypeIfchange = "ifchange"
39         DepTypeAlways   = "always"
40         DepTypeStamp    = "stamp"
41
42         EnvOODTgtsFd     = "REDO_OOD_TGTS_FD"
43         EnvOODTgtsLockFd = "REDO_OOD_TGTS_LOCK_FD"
44 )
45
46 var (
47         OODTgts       map[string]struct{}
48         FdOODTgts     *os.File
49         FdOODTgtsLock *os.File
50
51         OODCache        = make(map[string]bool)
52         FileExistsCache = make(map[string]bool)
53
54         ErrMissingTarget = errors.New("invalid format of .rec: missing Target")
55 )
56
57 func FileExists(p string) bool {
58         if exists, known := FileExistsCache[p]; known {
59                 return exists
60         }
61         _, err := os.Stat(p)
62         if err == nil {
63                 FileExistsCache[p] = true
64                 return true
65         }
66         if errors.Is(err, fs.ErrNotExist) {
67                 FileExistsCache[p] = false
68         }
69         return false
70 }
71
72 type TgtError struct {
73         Tgt string
74         Err error
75 }
76
77 func (e TgtError) Unwrap() error { return e.Err }
78
79 func (e TgtError) Error() string {
80         return fmt.Sprintf("%s: %s", e.Tgt, e.Err)
81 }
82
83 func isSrc(cwd, tgt string) bool {
84         d, f := path.Split(path.Join(cwd, tgt))
85         if !FileExists(path.Join(d, f)) {
86                 return false
87         }
88         if FileExists(path.Join(d, f+".do")) {
89                 return false
90         }
91         if FileExists(path.Join(d, RedoDir, f+DepSuffix)) {
92                 return false
93         }
94         return true
95 }
96
97 func isOODByBuildUUID(cwd, tgtOrig string) bool {
98         cwd, tgt := cwdAndTgt(path.Join(cwd, tgtOrig))
99         depPath := path.Join(cwd, RedoDir, tgt+DepSuffix)
100         build, err := depReadBuild(depPath)
101         return err != nil || build != BuildUUID
102 }
103
104 func isOOD(cwd, tgtOrig string, level int, seen map[string]struct{}) (bool, error) {
105         indent := strings.Repeat(". ", level)
106         tracef(CDebug, "ood: %s%s checking", indent, tgtOrig)
107         cwd, tgt := cwdAndTgt(path.Join(cwd, tgtOrig))
108         ood, cached := OODCache[path.Join(cwd, tgt)]
109         if cached {
110                 tracef(CDebug, "ood: %s%s -> cached: %v", indent, tgtOrig, ood)
111                 return ood, nil
112         }
113         depPath := path.Join(cwd, RedoDir, tgt+DepSuffix)
114         depInfo, err := depRead(depPath)
115         if err != nil {
116                 if errors.Is(err, fs.ErrNotExist) {
117                         if isSrc(cwd, tgt) {
118                                 ood = false
119                                 tracef(CDebug, "ood: %s%s -> is source", indent, tgtOrig)
120                         } else {
121                                 ood = true
122                                 tracef(CDebug, "ood: %s%s -> no dep: %s", indent, tgtOrig, depPath)
123                         }
124                         OODCache[path.Join(cwd, tgt)] = ood
125                         return ood, nil
126                 }
127                 if err != nil {
128                         return true, TgtError{tgtOrig, ErrLine(err)}
129                 }
130         }
131
132         if depInfo.build == BuildUUID {
133                 tracef(CDebug, "ood: %s%s -> already built", indent, tgtOrig)
134                 OODCache[path.Join(cwd, tgt)] = false
135                 return false, nil
136         }
137         if !FileExists(path.Join(cwd, tgt)) {
138                 tracef(CDebug, "ood: %s%s -> non-existent", indent, tgtOrig)
139                 OODCache[path.Join(cwd, tgt)] = true
140                 return true, nil
141         }
142
143         for _, dep := range depInfo.ifcreates {
144                 if FileExists(path.Join(cwd, dep)) {
145                         tracef(CDebug, "ood: %s%s -> %s created", indent, tgtOrig, dep)
146                         ood = true
147                         goto Done
148                 }
149         }
150
151         for _, dep := range depInfo.ifchanges {
152                 if dep.tgt == "" {
153                         return ood, TgtError{tgtOrig, ErrMissingTarget}
154                 }
155                 tracef(CDebug, "ood: %s%s -> %s: checking", indent, tgtOrig, dep.tgt)
156                 ood, cached = OODCache[path.Join(cwd, dep.tgt)]
157                 if cached {
158                         tracef(CDebug, "ood: %s%s -> %s: cached: %v", indent, tgtOrig, dep.tgt, ood)
159                         if ood {
160                                 goto Done
161                         }
162                         continue
163                 }
164
165                 inode, err := inodeFromFileByPath(path.Join(cwd, dep.tgt))
166                 if err != nil {
167                         if errors.Is(err, fs.ErrNotExist) {
168                                 tracef(CDebug, "ood: %s%s -> %s: not exists", indent, tgtOrig, dep.tgt)
169                                 ood = true
170                                 OODCache[path.Join(cwd, dep.tgt)] = ood
171                                 goto Done
172                         }
173                         return ood, TgtError{tgtOrig, ErrLine(err)}
174                 }
175
176                 if inode.Size != dep.inode.Size {
177                         tracef(CDebug, "ood: %s%s -> %s: size differs", indent, tgtOrig, dep.tgt)
178                         ood = true
179                         OODCache[path.Join(cwd, dep.tgt)] = ood
180                         goto Done
181                 }
182                 if InodeTrust != InodeTrustNone && inode.Equals(dep.inode) {
183                         tracef(CDebug, "ood: %s%s -> %s: same inode", indent, tgtOrig, dep.tgt)
184                 } else {
185                         tracef(CDebug, "ood: %s%s -> %s: inode differs", indent, tgtOrig, dep.tgt)
186                         fd, err := os.Open(path.Join(cwd, dep.tgt))
187                         if err != nil {
188                                 return ood, TgtError{tgtOrig, ErrLine(err)}
189                         }
190                         hsh, err := fileHash(fd)
191                         fd.Close()
192                         if err != nil {
193                                 return ood, TgtError{tgtOrig, ErrLine(err)}
194                         }
195                         if !bytes.Equal(dep.hash, hsh) {
196                                 tracef(CDebug, "ood: %s%s -> %s: hash differs", indent, tgtOrig, dep.tgt)
197                                 ood = true
198                                 OODCache[path.Join(cwd, dep.tgt)] = ood
199                                 goto Done
200                         }
201                         tracef(CDebug, "ood: %s%s -> %s: same hash", indent, tgtOrig, dep.tgt)
202                 }
203
204                 if dep.tgt == tgt {
205                         tracef(CDebug, "ood: %s%s -> %s: same target", indent, tgtOrig, dep.tgt)
206                         continue
207                 }
208                 if isSrc(cwd, dep.tgt) {
209                         tracef(CDebug, "ood: %s%s -> %s: is source", indent, tgtOrig, dep.tgt)
210                         OODCache[path.Join(cwd, dep.tgt)] = false
211                         continue
212                 }
213
214                 if _, ok := seen[cwdMustRel(cwd, dep.tgt)]; ok {
215                         tracef(CDebug, "ood: %s%s -> %s: was always built", indent, tgtOrig, dep.tgt)
216                         OODCache[path.Join(cwd, dep.tgt)] = false
217                         continue
218                 }
219
220                 depOOD, err := isOODWithTrace(cwd, dep.tgt, level+1, seen)
221                 if err != nil {
222                         return ood, TgtError{tgtOrig, err}
223                 }
224                 if depOOD {
225                         tracef(CDebug, "ood: %s%s -> %s: ood", indent, tgtOrig, dep.tgt)
226                         ood = true
227                         goto Done
228                 }
229                 tracef(CDebug, "ood: %s%s -> %s: !ood", indent, tgtOrig, dep.tgt)
230         }
231
232 Done:
233         tracef(CDebug, "ood: %s%s: %v", indent, tgtOrig, ood)
234         OODCache[path.Join(cwd, tgt)] = ood
235         return ood, nil
236 }
237
238 func isOODWithTrace(
239         cwd, tgtOrig string,
240         level int,
241         seen map[string]struct{},
242 ) (bool, error) {
243         p := mustAbs(path.Join(cwd, tgtOrig))
244         _, ood := OODTgts[p]
245         var err error
246         if ood {
247                 if !isOODByBuildUUID(cwd, tgtOrig) {
248                         tracef(
249                                 CDebug,
250                                 "ood: %s%s -> already built",
251                                 strings.Repeat(". ", level), tgtOrig,
252                         )
253                         return false, nil
254                 }
255                 tracef(
256                         CDebug,
257                         "ood: %s%s true, external decision",
258                         strings.Repeat(". ", level), tgtOrig,
259                 )
260                 goto RecordOODTgt
261         }
262         ood, err = isOOD(cwd, tgtOrig, level, seen)
263         if !ood {
264                 return ood, err
265         }
266 RecordOODTgt:
267         flock := unix.Flock_t{
268                 Type:   unix.F_WRLCK,
269                 Whence: io.SeekStart,
270         }
271         if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLKW, &flock); err != nil {
272                 log.Fatal(err)
273         }
274         if _, err = FdOODTgts.Seek(0, io.SeekEnd); err != nil {
275                 log.Fatal(err)
276         }
277         if _, err := FdOODTgts.WriteString(p + "\x00"); err != nil {
278                 log.Fatal(err)
279         }
280         flock.Type = unix.F_UNLCK
281         if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLK, &flock); err != nil {
282                 log.Fatal(err)
283         }
284         return true, nil
285 }
286
287 func oodTgtsClear() {
288         var err error
289         flock := unix.Flock_t{
290                 Type:   unix.F_WRLCK,
291                 Whence: io.SeekStart,
292         }
293         if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLKW, &flock); err != nil {
294                 log.Fatal(err)
295         }
296         if err = FdOODTgts.Truncate(0); err != nil {
297                 log.Fatal(err)
298         }
299         flock.Type = unix.F_UNLCK
300         if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLK, &flock); err != nil {
301                 log.Fatal(err)
302         }
303 }