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