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