]> 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 smartopen(file):
83     if os.path.isfile(file):
84         f = open(file, "r")
85     elif os.path.isfile("%s.gz" % file):
86         f = decompressors[ 'zcat' ]( file )
87     elif os.path.isfile("%s.bz2" % file):
88         f = decompressors[ 'bzcat' ]( file )
89     else:
90         f = None
91     return f
92
93 def pipe_file(f, t):
94     f.seek(0)
95     while 1:
96         l = f.read()
97         if not l: break
98         t.write(l)
99     t.close()
100
101 class Updates:
102     def __init__(self, readpath = None, max = 14):
103         self.can_path = None
104         self.history = {}
105         self.history_order = []
106         self.max = max
107         self.readpath = readpath
108         self.filesizesha1 = None
109
110         if readpath:
111             try:
112                 f = open(readpath + "/Index")
113                 x = f.readline()
114
115                 def read_hashs(ind, f, self, x=x):
116                     while 1:
117                         x = f.readline()
118                         if not x or x[0] != " ": break
119                         l = x.split()
120                         if not self.history.has_key(l[2]):
121                             self.history[l[2]] = [None,None]
122                             self.history_order.append(l[2])
123                         self.history[l[2]][ind] = (l[0], int(l[1]))
124                     return x
125
126                 while x:
127                     l = x.split()
128
129                     if len(l) == 0:
130                         x = f.readline()
131                         continue
132
133                     if l[0] == "SHA1-History:":
134                         x = read_hashs(0,f,self)
135                         continue
136
137                     if l[0] == "SHA1-Patches:":
138                         x = read_hashs(1,f,self)
139                         continue
140
141                     if l[0] == "Canonical-Name:" or l[0]=="Canonical-Path:":
142                         self.can_path = l[1]
143
144                     if l[0] == "SHA1-Current:" and len(l) == 3:
145                         self.filesizesha1 = (l[1], int(l[2]))
146
147                     x = f.readline()
148
149             except IOError:
150                 0
151
152     def dump(self, out=sys.stdout):
153         if self.can_path:
154             out.write("Canonical-Path: %s\n" % (self.can_path))
155
156         if self.filesizesha1:
157             out.write("SHA1-Current: %s %7d\n" % (self.filesizesha1))
158
159         hs = self.history
160         l = self.history_order[:]
161
162         cnt = len(l)
163         if cnt > self.max:
164             for h in l[:cnt-self.max]:
165                 tryunlink("%s/%s.gz" % (self.readpath, h))
166                 del hs[h]
167             l = l[cnt-self.max:]
168             self.history_order = l[:]
169
170         out.write("SHA1-History:\n")
171         for h in l:
172             out.write(" %s %7d %s\n" % (hs[h][0][0], hs[h][0][1], h))
173         out.write("SHA1-Patches:\n")
174         for h in l:
175             out.write(" %s %7d %s\n" % (hs[h][1][0], hs[h][1][1], h))
176
177 decompressors = { 'zcat' : gzip.GzipFile,
178                   'bzip2' : bz2.BZ2File }
179
180 def sizesha1(f):
181     size = os.fstat(f.fileno())[6]
182     f.seek(0)
183     sha1sum = apt_pkg.sha1sum(f)
184     return (sha1sum, size)
185
186 def genchanges(Options, outdir, oldfile, origfile, maxdiffs = 14):
187     if Options.has_key("NoAct"):
188         return
189
190     patchname = Options["PatchName"]
191
192     # origfile = /path/to/Packages
193     # oldfile  = ./Packages
194     # newfile  = ./Packages.tmp
195     # difffile = outdir/patchname
196     # index   => outdir/Index
197
198     # (outdir, oldfile, origfile) = argv
199
200     newfile = oldfile + ".new"
201     difffile = "%s/%s" % (outdir, patchname)
202
203     upd = Updates(outdir, int(maxdiffs))
204     (oldext, oldstat) = smartstat(oldfile)
205     (origext, origstat) = smartstat(origfile)
206     if not origstat:
207         print "%s: doesn't exist" % (origfile)
208         return
209     if not oldstat:
210         print "%s: initial run" % (origfile)
211         os.link(origfile + origext, oldfile + origext)
212         return
213
214     if oldstat[1:3] == origstat[1:3]:
215         print "%s: hardlink unbroken, assuming unchanged" % (origfile)
216         return
217
218     oldf = smartopen(oldfile)
219     oldsizesha1 = sizesha1(oldf)
220
221     # should probably early exit if either of these checks fail
222     # alternatively (optionally?) could just trim the patch history
223
224     if upd.filesizesha1:
225         if upd.filesizesha1 != oldsizesha1:
226             print "warning: old file seems to have changed! %s %s => %s %s" % (upd.filesizesha1 + oldsizesha1)
227
228     # XXX this should be usable now
229     #
230     #for d in upd.history.keys():
231     #    df = smartopen("%s/%s" % (outdir,d))
232     #    act_sha1size = sizesha1(df)
233     #    df.close()
234     #    exp_sha1size = upd.history[d][1]
235     #    if act_sha1size != exp_sha1size:
236     #        print "patch file %s seems to have changed! %s %s => %s %s" % \
237     #            (d,) + exp_sha1size + act_sha1size
238
239     if Options.has_key("CanonicalPath"): upd.can_path=Options["CanonicalPath"]
240
241     if os.path.exists(newfile): os.unlink(newfile)
242     smartlink(origfile, newfile)
243     newf = open(newfile, "r")
244     newsizesha1 = sizesha1(newf)
245     newf.close()
246
247     if newsizesha1 == oldsizesha1:
248         os.unlink(newfile)
249         oldf.close()
250         print "%s: unchanged" % (origfile)
251     else:
252         if not os.path.isdir(outdir): os.mkdir(outdir)
253         w = os.popen("diff --ed - %s | gzip -c -9 > %s.gz" %
254                          (newfile, difffile), "w")
255         pipe_file(oldf, w)
256         oldf.close()
257
258         difff = smartopen(difffile)
259         difsizesha1 = sizesha1(difff)
260         difff.close()
261
262         upd.history[patchname] = (oldsizesha1, difsizesha1)
263         upd.history_order.append(patchname)
264
265         upd.filesizesha1 = newsizesha1
266
267         os.unlink(oldfile + oldext)
268         os.link(origfile + origext, oldfile + origext)
269         os.unlink(newfile)
270
271         f = open(outdir + "/Index", "w")
272         upd.dump(f)
273         f.close()
274
275
276 def main():
277     global Cnf, Options, Logger
278
279     os.umask(0002)
280
281     Cnf = utils.get_conf()
282     Arguments = [ ('h', "help", "Generate-Index-Diffs::Options::Help"),
283                   ('c', None, "Generate-Index-Diffs::Options::CanonicalPath", "hasArg"),
284                   ('p', "patchname", "Generate-Index-Diffs::Options::PatchName", "hasArg"),
285                   ('r', "rootdir", "Generate-Index-Diffs::Options::RootDir", "hasArg"),
286                   ('d', "tmpdir", "Generate-Index-Diffs::Options::TempDir", "hasArg"),
287                   ('m', "maxdiffs", "Generate-Index-Diffs::Options::MaxDiffs", "hasArg"),
288                   ('n', "n-act", "Generate-Index-Diffs::Options::NoAct"),
289                 ]
290     suites = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
291     Options = Cnf.SubTree("Generate-Index-Diffs::Options")
292     if Options.has_key("Help"): usage()
293
294     maxdiffs = Options.get("MaxDiffs::Default", "14")
295     maxpackages = Options.get("MaxDiffs::Packages", maxdiffs)
296     maxcontents = Options.get("MaxDiffs::Contents", maxdiffs)
297     maxsources = Options.get("MaxDiffs::Sources", maxdiffs)
298
299     if not Options.has_key("PatchName"):
300         format = "%Y-%m-%d-%H%M.%S"
301         Options["PatchName"] = time.strftime( format )
302
303     AptCnf = apt_pkg.newConfiguration()
304     apt_pkg.ReadConfigFileISC(AptCnf,utils.which_apt_conf_file())
305
306     if Options.has_key("RootDir"): Cnf["Dir::Root"] = Options["RootDir"]
307
308     if not suites:
309         suites = Cnf.SubTree("Suite").List()
310
311     for suite in suites:
312         print "Processing: " + suite
313         SuiteBlock = Cnf.SubTree("Suite::" + suite)
314
315         if SuiteBlock.has_key("Untouchable"):
316             print "Skipping: " + suite + " (untouchable)"
317             continue
318
319         suite = suite.lower()
320
321         architectures = SuiteBlock.ValueList("Architectures")
322
323         if SuiteBlock.has_key("Components"):
324             components = SuiteBlock.ValueList("Components")
325         else:
326             components = []
327
328         suite_suffix = Cnf.Find("Dinstall::SuiteSuffix")
329         if components and suite_suffix:
330             longsuite = suite + "/" + suite_suffix
331         else:
332             longsuite = suite
333
334         tree = SuiteBlock.get("Tree", "dists/%s" % (longsuite))
335
336         if AptCnf.has_key("tree::%s" % (tree)):
337             sections = AptCnf["tree::%s::Sections" % (tree)].split()
338         elif AptCnf.has_key("bindirectory::%s" % (tree)):
339             sections = AptCnf["bindirectory::%s::Sections" % (tree)].split()
340         else:
341             aptcnf_filename = os.path.basename(utils.which_apt_conf_file())
342             print "ALERT: suite %s not in %s, nor untouchable!" % (suite, aptcnf_filename)
343             continue
344
345         for architecture in architectures:
346             if architecture == "all":
347                 continue
348
349             if architecture != "source":
350                 # Process Contents
351                 file = "%s/Contents-%s" % (Cnf["Dir::Root"] + tree,
352                         architecture)
353                 storename = "%s/%s_contents_%s" % (Options["TempDir"], suite, architecture)
354                 genchanges(Options, file + ".diff", storename, file, \
355                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::Contents" % (suite), maxcontents))
356
357             # use sections instead of components since dak.conf
358             # treats "foo/bar main" as suite "foo", suitesuffix "bar" and
359             # component "bar/main". suck.
360
361             for component in sections:
362                 if architecture == "source":
363                     longarch = architecture
364                     packages = "Sources"
365                     maxsuite = maxsources
366                 else:
367                     longarch = "binary-%s"% (architecture)
368                     packages = "Packages"
369                     maxsuite = maxpackages
370
371                 file = "%s/%s/%s/%s" % (Cnf["Dir::Root"] + tree,
372                            component, longarch, packages)
373                 storename = "%s/%s_%s_%s" % (Options["TempDir"], suite, component, architecture)
374                 genchanges(Options, file + ".diff", storename, file, \
375                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::%s" % (suite, packages), maxsuite))
376
377 ################################################################################
378
379 if __name__ == '__main__':
380     main()