]> git.decadent.org.uk Git - dak.git/blob - dak/generate_index_diffs.py
Merge commit 'stew/popen2-must-die' into merge
[dak.git] / dak / generate_index_diffs.py
1 #!/usr/bin/env python
2
3 ###########################################################
4 # generates partial package updates list
5
6 # idea and basic implementation by Anthony, some changes by Andreas
7 # parts are stolen from 'dak generate-releases'
8 #
9 # Copyright (C) 2004, 2005, 2006  Anthony Towns <aj@azure.humbug.org.au>
10 # Copyright (C) 2004, 2005  Andreas Barth <aba@not.so.argh.org>
11
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
26
27 # < elmo> bah, don't bother me with annoying facts
28 # < elmo> I was on a roll
29
30
31 ################################################################################
32
33 import sys, os, tempfile
34 import apt_pkg
35 import bz2, gzip, time
36 from daklib import utils
37
38 ################################################################################
39
40 projectB = None
41 Cnf = None
42 Logger = None
43 Options = None
44
45 ################################################################################
46
47 def usage (exit_code=0):
48     print """Usage: dak generate-index-diffs [OPTIONS] [suites]
49 Write out ed-style diffs to Packages/Source lists
50
51   -h, --help            show this help and exit
52   -c                    give the canonical path of the file
53   -p                    name for the patch (defaults to current time)
54   -n                    take no action
55     """
56     sys.exit(exit_code)
57
58
59 def tryunlink(file):
60     try:
61         os.unlink(file)
62     except OSError:
63         print "warning: removing of %s denied" % (file)
64
65 def smartstat(file):
66     for ext in ["", ".gz", ".bz2"]:
67         if os.path.isfile(file + ext):
68             return (ext, os.stat(file + ext))
69     return (None, None)
70
71 def smartlink(f, t):
72     if os.path.isfile(f):
73         os.link(f,t)
74     elif os.path.isfile("%s.gz" % (f)):
75         os.system("gzip -d < %s.gz > %s" % (f, t))
76     elif os.path.isfile("%s.bz2" % (f)):
77         os.system("bzip2 -d < %s.bz2 > %s" % (f, t))
78     else:
79         print "missing: %s" % (f)
80         raise IOError, f
81
82 def smartread(filename):
83     """
84     If filename exists, slurp the contents into a string.
85     if filename.gz or filename.bz2 exists instead, decompress and slurp
86     It returns a tuple of (filename, filecontents)
87     """
88     actual_filename = None
89     contents = None
90     if os.path.isfile(filename):
91         f = open(filename, "r")
92     elif os.path.isfile("%s.gz" % filename):
93         actual_filename = "%s.gz" % filename
94         f = decompressors['zcat'](actual_filename)
95     elif os.path.isfile("%s.bz2" % filename):
96         actual_filename = "%s.bz2" % filename
97         f = decompressors['bzcat'](actual_filename)
98     else:
99         f = None
100
101     if f:
102         contents = f.read()
103
104     return (actual_filename, contents)
105
106 def pipe_file(f, t):
107     f.seek(0)
108     while 1:
109         l = f.read()
110         if not l: break
111         t.write(l)
112     t.close()
113
114 class Updates:
115     def __init__(self, readpath = None, max = 14):
116         self.can_path = None
117         self.history = {}
118         self.history_order = []
119         self.max = max
120         self.readpath = readpath
121         self.filesizesha1 = None
122
123         if readpath:
124             try:
125                 f = open(readpath + "/Index")
126                 x = f.readline()
127
128                 def read_hashs(ind, f, self, x=x):
129                     while 1:
130                         x = f.readline()
131                         if not x or x[0] != " ": break
132                         l = x.split()
133                         if not self.history.has_key(l[2]):
134                             self.history[l[2]] = [None,None]
135                             self.history_order.append(l[2])
136                         self.history[l[2]][ind] = (l[0], int(l[1]))
137                     return x
138
139                 while x:
140                     l = x.split()
141
142                     if len(l) == 0:
143                         x = f.readline()
144                         continue
145
146                     if l[0] == "SHA1-History:":
147                         x = read_hashs(0,f,self)
148                         continue
149
150                     if l[0] == "SHA1-Patches:":
151                         x = read_hashs(1,f,self)
152                         continue
153
154                     if l[0] == "Canonical-Name:" or l[0]=="Canonical-Path:":
155                         self.can_path = l[1]
156
157                     if l[0] == "SHA1-Current:" and len(l) == 3:
158                         self.filesizesha1 = (l[1], int(l[2]))
159
160                     x = f.readline()
161
162             except IOError:
163                 0
164
165     def dump(self, out=sys.stdout):
166         if self.can_path:
167             out.write("Canonical-Path: %s\n" % (self.can_path))
168
169         if self.filesizesha1:
170             out.write("SHA1-Current: %s %7d\n" % (self.filesizesha1))
171
172         hs = self.history
173         l = self.history_order[:]
174
175         cnt = len(l)
176         if cnt > self.max:
177             for h in l[:cnt-self.max]:
178                 tryunlink("%s/%s.gz" % (self.readpath, h))
179                 del hs[h]
180             l = l[cnt-self.max:]
181             self.history_order = l[:]
182
183         out.write("SHA1-History:\n")
184         for h in l:
185             out.write(" %s %7d %s\n" % (hs[h][0][0], hs[h][0][1], h))
186         out.write("SHA1-Patches:\n")
187         for h in l:
188             out.write(" %s %7d %s\n" % (hs[h][1][0], hs[h][1][1], h))
189
190 decompressors = { 'zcat' : gzip.GzipFile,
191                   'bzip2' : bz2.BZ2File }
192
193 def sizesha1_str(s):
194     """
195     given a string, return a tuple containing its (sha1sum, length)
196     """
197     return (apt_pkg.sha1sum(s), len( s ) )
198
199 def sizesha1(f):
200     size = os.fstat(f.fileno())[6]
201     f.seek(0)
202     sha1sum = apt_pkg.sha1sum(f)
203     return (sha1sum, size)
204
205 def genchanges(Options, outdir, oldfile, origfile, maxdiffs = 14):
206     if Options.has_key("NoAct"):
207         return
208
209     patchname = Options["PatchName"]
210
211     # origfile = /path/to/Packages
212     # oldfile  = ./Packages
213     # newfile  = ./Packages.tmp
214     # difffile = outdir/patchname
215     # index   => outdir/Index
216
217     # (outdir, oldfile, origfile) = argv
218
219     newfile = oldfile + ".new"
220     difffile = "%s/%s" % (outdir, patchname)
221
222     upd = Updates(outdir, int(maxdiffs))
223     (oldext, oldstat) = smartstat(oldfile)
224     (origext, origstat) = smartstat(origfile)
225     if not origstat:
226         print "%s: doesn't exist" % (origfile)
227         return
228     if not oldstat:
229         print "%s: initial run" % (origfile)
230         os.link(origfile + origext, oldfile + origext)
231         return
232
233     if oldstat[1:3] == origstat[1:3]:
234         print "%s: hardlink unbroken, assuming unchanged" % (origfile)
235         return
236
237     (oldf,contents) = smartread(oldfile)
238     oldsizesha1 = sizesha1_str(contents)
239
240     # should probably early exit if either of these checks fail
241     # alternatively (optionally?) could just trim the patch history
242
243     if upd.filesizesha1:
244         if upd.filesizesha1 != oldsizesha1:
245             print "warning: old file seems to have changed! %s %s => %s %s" % (upd.filesizesha1 + oldsizesha1)
246
247     # XXX this should be usable now
248     # stew: whatever this is, it won't be usable now that i removed smartopen
249     #
250     #for d in upd.history.keys():
251     #    df = smartopen("%s/%s" % (outdir,d))
252     #    act_sha1size = sizesha1(df)
253     #    df.close()
254     #    exp_sha1size = upd.history[d][1]
255     #    if act_sha1size != exp_sha1size:
256     #        print "patch file %s seems to have changed! %s %s => %s %s" % \
257     #            (d,) + exp_sha1size + act_sha1size
258
259     if Options.has_key("CanonicalPath"): upd.can_path=Options["CanonicalPath"]
260
261     if os.path.exists(newfile): os.unlink(newfile)
262     smartlink(origfile, newfile)
263     newf = open(newfile, "r")
264     newsizesha1 = sizesha1(newf)
265     newf.close()
266
267     if newsizesha1 == oldsizesha1:
268         os.unlink(newfile)
269 #        oldf.close()
270         print "%s: unchanged" % (origfile)
271     else:
272         if not os.path.isdir(outdir): os.mkdir(outdir)
273         os.popen("diff --ed %s %s | gzip -c -9 > %s.gz" %
274                  (oldf, newfile, difffile))
275 #        pipe_file(oldf, w)
276 #        oldf.close()
277
278         (oldf,contents) = smartread(difffile)
279         difsizesha1 = sizesha1_str(contents)
280
281         upd.history[patchname] = (oldsizesha1, difsizesha1)
282         upd.history_order.append(patchname)
283
284         upd.filesizesha1 = newsizesha1
285
286         os.unlink(oldfile + oldext)
287         os.link(origfile + origext, oldfile + origext)
288         os.unlink(newfile)
289
290         f = open(outdir + "/Index", "w")
291         upd.dump(f)
292         f.close()
293
294
295 def main():
296     global Cnf, Options, Logger
297
298     os.umask(0002)
299
300     Cnf = utils.get_conf()
301     Arguments = [ ('h', "help", "Generate-Index-Diffs::Options::Help"),
302                   ('c', None, "Generate-Index-Diffs::Options::CanonicalPath", "hasArg"),
303                   ('p', "patchname", "Generate-Index-Diffs::Options::PatchName", "hasArg"),
304                   ('r', "rootdir", "Generate-Index-Diffs::Options::RootDir", "hasArg"),
305                   ('d', "tmpdir", "Generate-Index-Diffs::Options::TempDir", "hasArg"),
306                   ('m', "maxdiffs", "Generate-Index-Diffs::Options::MaxDiffs", "hasArg"),
307                   ('n', "n-act", "Generate-Index-Diffs::Options::NoAct"),
308                 ]
309     suites = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
310     Options = Cnf.SubTree("Generate-Index-Diffs::Options")
311     if Options.has_key("Help"): usage()
312
313     maxdiffs = Options.get("MaxDiffs::Default", "14")
314     maxpackages = Options.get("MaxDiffs::Packages", maxdiffs)
315     maxcontents = Options.get("MaxDiffs::Contents", maxdiffs)
316     maxsources = Options.get("MaxDiffs::Sources", maxdiffs)
317
318     if not Options.has_key("PatchName"):
319         format = "%Y-%m-%d-%H%M.%S"
320         Options["PatchName"] = time.strftime( format )
321
322     AptCnf = apt_pkg.newConfiguration()
323     apt_pkg.ReadConfigFileISC(AptCnf,utils.which_apt_conf_file())
324
325     if Options.has_key("RootDir"): Cnf["Dir::Root"] = Options["RootDir"]
326
327     if not suites:
328         suites = Cnf.SubTree("Suite").List()
329
330     for suite in suites:
331         print "Processing: " + suite
332         SuiteBlock = Cnf.SubTree("Suite::" + suite)
333
334         if SuiteBlock.has_key("Untouchable"):
335             print "Skipping: " + suite + " (untouchable)"
336             continue
337
338         suite = suite.lower()
339
340         architectures = SuiteBlock.ValueList("Architectures")
341
342         if SuiteBlock.has_key("Components"):
343             components = SuiteBlock.ValueList("Components")
344         else:
345             components = []
346
347         suite_suffix = Cnf.Find("Dinstall::SuiteSuffix")
348         if components and suite_suffix:
349             longsuite = suite + "/" + suite_suffix
350         else:
351             longsuite = suite
352
353         tree = SuiteBlock.get("Tree", "dists/%s" % (longsuite))
354
355         if AptCnf.has_key("tree::%s" % (tree)):
356             sections = AptCnf["tree::%s::Sections" % (tree)].split()
357         elif AptCnf.has_key("bindirectory::%s" % (tree)):
358             sections = AptCnf["bindirectory::%s::Sections" % (tree)].split()
359         else:
360             aptcnf_filename = os.path.basename(utils.which_apt_conf_file())
361             print "ALERT: suite %s not in %s, nor untouchable!" % (suite, aptcnf_filename)
362             continue
363
364         for architecture in architectures:
365             if architecture == "all":
366                 continue
367
368             if architecture != "source":
369                 # Process Contents
370                 file = "%s/Contents-%s" % (Cnf["Dir::Root"] + tree,
371                         architecture)
372                 storename = "%s/%s_contents_%s" % (Options["TempDir"], suite, architecture)
373                 genchanges(Options, file + ".diff", storename, file, \
374                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::Contents" % (suite), maxcontents))
375
376             # use sections instead of components since dak.conf
377             # treats "foo/bar main" as suite "foo", suitesuffix "bar" and
378             # component "bar/main". suck.
379
380             for component in sections:
381                 if architecture == "source":
382                     longarch = architecture
383                     packages = "Sources"
384                     maxsuite = maxsources
385                 else:
386                     longarch = "binary-%s"% (architecture)
387                     packages = "Packages"
388                     maxsuite = maxpackages
389
390                 file = "%s/%s/%s/%s" % (Cnf["Dir::Root"] + tree,
391                            component, longarch, packages)
392                 storename = "%s/%s_%s_%s" % (Options["TempDir"], suite, component, architecture)
393                 genchanges(Options, file + ".diff", storename, file, \
394                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::%s" % (suite, packages), maxsuite))
395
396 ################################################################################
397
398 if __name__ == '__main__':
399     main()