]> arthur.barton.de Git - bup.git/blob - lib/cmd/midx-cmd.py
Bypass Python 3 glibc argv problems by routing args through env
[bup.git] / lib / cmd / midx-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4 export "BUP_ARGV_0"="$0"
5 arg_i=1
6 for arg in "$@"; do
7     export "BUP_ARGV_${arg_i}"="$arg"
8     shift
9     arg_i=$((arg_i + 1))
10 done
11 # Here to end of preamble replaced during install
12 bup_python="$(dirname "$0")/bup-python" || exit $?
13 exec "$bup_python" "$0"
14 """
15 # end of bup preamble
16
17 from __future__ import absolute_import, print_function
18 from binascii import hexlify
19 import glob, math, os, resource, struct, sys, tempfile
20
21 from bup import compat, options, git, midx, _helpers, xstat
22 from bup.compat import argv_bytes, hexstr, range
23 from bup.helpers import (Sha1, add_error, atomically_replaced_file, debug1, fdatasync,
24                          handle_ctrl_c, log, mmap_readwrite, qprogress,
25                          saved_errors, unlink)
26 from bup.io import byte_stream, path_msg
27
28
29 PAGE_SIZE=4096
30 SHA_PER_PAGE=PAGE_SIZE/20.
31
32 optspec = """
33 bup midx [options...] <idxnames...>
34 --
35 o,output=  output midx filename (default: auto-generated)
36 a,auto     automatically use all existing .midx/.idx files as input
37 f,force    merge produce exactly one .midx containing all objects
38 p,print    print names of generated midx files
39 check      validate contents of the given midx files (with -a, all midx files)
40 max-files= maximum number of idx files to open at once [-1]
41 d,dir=     directory containing idx/midx files
42 """
43
44 merge_into = _helpers.merge_into
45
46
47 def _group(l, count):
48     for i in range(0, len(l), count):
49         yield l[i:i+count]
50
51
52 def max_files():
53     mf = min(resource.getrlimit(resource.RLIMIT_NOFILE))
54     if mf > 32:
55         mf -= 20  # just a safety margin
56     else:
57         mf -= 6   # minimum safety margin
58     return mf
59
60
61 def check_midx(name):
62     nicename = git.repo_rel(name)
63     log('Checking %s.\n' % path_msg(nicename))
64     try:
65         ix = git.open_idx(name)
66     except git.GitError as e:
67         add_error('%s: %s' % (pathmsg(name), e))
68         return
69     for count,subname in enumerate(ix.idxnames):
70         sub = git.open_idx(os.path.join(os.path.dirname(name), subname))
71         for ecount,e in enumerate(sub):
72             if not (ecount % 1234):
73                 qprogress('  %d/%d: %s %d/%d\r' 
74                           % (count, len(ix.idxnames),
75                              git.shorten_hash(subname).decode('ascii'),
76                              ecount, len(sub)))
77             if not sub.exists(e):
78                 add_error("%s: %s: %s missing from idx"
79                           % (path_msg(nicename),
80                              git.shorten_hash(subname).decode('ascii'),
81                              hexstr(e)))
82             if not ix.exists(e):
83                 add_error("%s: %s: %s missing from midx"
84                           % (path_msg(nicename),
85                              git.shorten_hash(subname).decode('ascii'),
86                              hexstr(e)))
87     prev = None
88     for ecount,e in enumerate(ix):
89         if not (ecount % 1234):
90             qprogress('  Ordering: %d/%d\r' % (ecount, len(ix)))
91         if e and prev and not e >= prev:
92             add_error('%s: ordering error: %s < %s'
93                       % (nicename, hexstr(e), hexstr(prev)))
94         prev = e
95
96
97 _first = None
98 def _do_midx(outdir, outfilename, infilenames, prefixstr):
99     global _first
100     if not outfilename:
101         assert(outdir)
102         sum = hexlify(Sha1(b'\0'.join(infilenames)).digest())
103         outfilename = b'%s/midx-%s.midx' % (outdir, sum)
104     
105     inp = []
106     total = 0
107     allfilenames = []
108     midxs = []
109     try:
110         for name in infilenames:
111             ix = git.open_idx(name)
112             midxs.append(ix)
113             inp.append((
114                 ix.map,
115                 len(ix),
116                 ix.sha_ofs,
117                 isinstance(ix, midx.PackMidx) and ix.which_ofs or 0,
118                 len(allfilenames),
119             ))
120             for n in ix.idxnames:
121                 allfilenames.append(os.path.basename(n))
122             total += len(ix)
123         inp.sort(reverse=True, key=lambda x: x[0][x[2] : x[2] + 20])
124
125         if not _first: _first = outdir
126         dirprefix = (_first != outdir) and git.repo_rel(outdir) + b': ' or b''
127         debug1('midx: %s%screating from %d files (%d objects).\n'
128                % (dirprefix, prefixstr, len(infilenames), total))
129         if (opt.auto and (total < 1024 and len(infilenames) < 3)) \
130            or ((opt.auto or opt.force) and len(infilenames) < 2) \
131            or (opt.force and not total):
132             debug1('midx: nothing to do.\n')
133             return
134
135         pages = int(total/SHA_PER_PAGE) or 1
136         bits = int(math.ceil(math.log(pages, 2)))
137         entries = 2**bits
138         debug1('midx: table size: %d (%d bits)\n' % (entries*4, bits))
139
140         unlink(outfilename)
141         with atomically_replaced_file(outfilename, 'wb') as f:
142             f.write(b'MIDX')
143             f.write(struct.pack('!II', midx.MIDX_VERSION, bits))
144             assert(f.tell() == 12)
145
146             f.truncate(12 + 4*entries + 20*total + 4*total)
147             f.flush()
148             fdatasync(f.fileno())
149
150             fmap = mmap_readwrite(f, close=False)
151             count = merge_into(fmap, bits, total, inp)
152             del fmap # Assume this calls msync() now.
153             f.seek(0, os.SEEK_END)
154             f.write(b'\0'.join(allfilenames))
155     finally:
156         for ix in midxs:
157             if isinstance(ix, midx.PackMidx):
158                 ix.close()
159         midxs = None
160         inp = None
161
162
163     # This is just for testing (if you enable this, don't clear inp above)
164     if 0:
165         p = midx.PackMidx(outfilename)
166         assert(len(p.idxnames) == len(infilenames))
167         log(repr(p.idxnames) + '\n')
168         assert(len(p) == total)
169         for pe, e in p, git.idxmerge(inp, final_progress=False):
170             pin = next(pi)
171             assert(i == pin)
172             assert(p.exists(i))
173
174     return total, outfilename
175
176
177 def do_midx(outdir, outfilename, infilenames, prefixstr, prout):
178     rv = _do_midx(outdir, outfilename, infilenames, prefixstr)
179     if rv and opt['print']:
180         prout.write(rv[1] + b'\n')
181
182
183 def do_midx_dir(path, outfilename, prout):
184     already = {}
185     sizes = {}
186     if opt.force and not opt.auto:
187         midxs = []   # don't use existing midx files
188     else:
189         midxs = glob.glob(b'%s/*.midx' % path)
190         contents = {}
191         for mname in midxs:
192             m = git.open_idx(mname)
193             contents[mname] = [(b'%s/%s' % (path,i)) for i in m.idxnames]
194             sizes[mname] = len(m)
195                     
196         # sort the biggest+newest midxes first, so that we can eliminate
197         # smaller (or older) redundant ones that come later in the list
198         midxs.sort(key=lambda ix: (-sizes[ix], -xstat.stat(ix).st_mtime))
199         
200         for mname in midxs:
201             any = 0
202             for iname in contents[mname]:
203                 if not already.get(iname):
204                     already[iname] = 1
205                     any = 1
206             if not any:
207                 debug1('%r is redundant\n' % mname)
208                 unlink(mname)
209                 already[mname] = 1
210
211     midxs = [k for k in midxs if not already.get(k)]
212     idxs = [k for k in glob.glob(b'%s/*.idx' % path) if not already.get(k)]
213
214     for iname in idxs:
215         i = git.open_idx(iname)
216         sizes[iname] = len(i)
217
218     all = [(sizes[n],n) for n in (midxs + idxs)]
219     
220     # FIXME: what are the optimal values?  Does this make sense?
221     DESIRED_HWM = opt.force and 1 or 5
222     DESIRED_LWM = opt.force and 1 or 2
223     existed = dict((name,1) for sz,name in all)
224     debug1('midx: %d indexes; want no more than %d.\n' 
225            % (len(all), DESIRED_HWM))
226     if len(all) <= DESIRED_HWM:
227         debug1('midx: nothing to do.\n')
228     while len(all) > DESIRED_HWM:
229         all.sort()
230         part1 = [name for sz,name in all[:len(all)-DESIRED_LWM+1]]
231         part2 = all[len(all)-DESIRED_LWM+1:]
232         all = list(do_midx_group(path, outfilename, part1)) + part2
233         if len(all) > DESIRED_HWM:
234             debug1('\nStill too many indexes (%d > %d).  Merging again.\n'
235                    % (len(all), DESIRED_HWM))
236
237     if opt['print']:
238         for sz,name in all:
239             if not existed.get(name):
240                 prout.write(name + b'\n')
241
242
243 def do_midx_group(outdir, outfilename, infiles):
244     groups = list(_group(infiles, opt.max_files))
245     gprefix = ''
246     for n,sublist in enumerate(groups):
247         if len(groups) != 1:
248             gprefix = 'Group %d: ' % (n+1)
249         rv = _do_midx(outdir, outfilename, sublist, gprefix)
250         if rv:
251             yield rv
252
253
254 handle_ctrl_c()
255
256 o = options.Options(optspec)
257 opt, flags, extra = o.parse(compat.argv[1:])
258 opt.dir = argv_bytes(opt.dir) if opt.dir else None
259 opt.output = argv_bytes(opt.output) if opt.output else None
260
261 if extra and (opt.auto or opt.force):
262     o.fatal("you can't use -f/-a and also provide filenames")
263 if opt.check and (not extra and not opt.auto):
264     o.fatal("if using --check, you must provide filenames or -a")
265
266 git.check_repo_or_die()
267
268 if opt.max_files < 0:
269     opt.max_files = max_files()
270 assert(opt.max_files >= 5)
271
272 extra = [argv_bytes(x) for x in extra]
273
274 if opt.check:
275     # check existing midx files
276     if extra:
277         midxes = extra
278     else:
279         midxes = []
280         paths = opt.dir and [opt.dir] or git.all_packdirs()
281         for path in paths:
282             debug1('midx: scanning %s\n' % path)
283             midxes += glob.glob(os.path.join(path, b'*.midx'))
284     for name in midxes:
285         check_midx(name)
286     if not saved_errors:
287         log('All tests passed.\n')
288 else:
289     if extra:
290         sys.stdout.flush()
291         do_midx(git.repo(b'objects/pack'), opt.output, extra, b'',
292                 byte_stream(sys.stdout))
293     elif opt.auto or opt.force:
294         sys.stdout.flush()
295         paths = opt.dir and [opt.dir] or git.all_packdirs()
296         for path in paths:
297             debug1('midx: scanning %s\n' % path_msg(path))
298             do_midx_dir(path, opt.output, byte_stream(sys.stdout))
299     else:
300         o.fatal("you must use -f or -a or provide input filenames")
301
302 if saved_errors:
303     log('WARNING: %d errors encountered.\n' % len(saved_errors))
304     sys.exit(1)