]> 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     if Options.has_key("CanonicalPath"): upd.can_path=Options["CanonicalPath"]
241
242     if os.path.exists(newfile): os.unlink(newfile)
243     smartlink(origfile, newfile)
244     newf = open(newfile, "r")
245     newsizesha1 = sizesha1(newf)
246     newf.close()
247
248     if newsizesha1 == oldsizesha1:
249         os.unlink(newfile)
250         oldf.close()
251         print "%s: unchanged" % (origfile)
252     else:
253         if not os.path.isdir(outdir):
254             os.mkdir(outdir)
255
256         cmd = "diff --ed - %s | gzip -c -9 > %s.gz" % (newfile, difffile)
257         # Do we need shell=True?
258         w = subprocess.Popen(cmd, shell=True, stdin=PIPE).stdin
259
260         # I bet subprocess can do that better than this, but lets do little steps
261         pipe_file(oldf, w)
262         oldf.close()
263
264         difff = smartopen(difffile)
265         difsizesha1 = sizesha1(difff)
266         difff.close()
267
268         upd.history[patchname] = (oldsizesha1, difsizesha1)
269         upd.history_order.append(patchname)
270
271         upd.filesizesha1 = newsizesha1
272
273         os.unlink(oldfile + oldext)
274         os.link(origfile + origext, oldfile + origext)
275         os.unlink(newfile)
276
277         f = open(outdir + "/Index", "w")
278         upd.dump(f)
279         f.close()
280
281
282 def main():
283     global Cnf, Options, Logger
284
285     os.umask(0002)
286
287     Cnf = utils.get_conf()
288     Arguments = [ ('h', "help", "Generate-Index-Diffs::Options::Help"),
289                   ('c', None, "Generate-Index-Diffs::Options::CanonicalPath", "hasArg"),
290                   ('p', "patchname", "Generate-Index-Diffs::Options::PatchName", "hasArg"),
291                   ('r', "rootdir", "Generate-Index-Diffs::Options::RootDir", "hasArg"),
292                   ('d', "tmpdir", "Generate-Index-Diffs::Options::TempDir", "hasArg"),
293                   ('m', "maxdiffs", "Generate-Index-Diffs::Options::MaxDiffs", "hasArg"),
294                   ('n', "n-act", "Generate-Index-Diffs::Options::NoAct"),
295                 ]
296     suites = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
297     Options = Cnf.SubTree("Generate-Index-Diffs::Options")
298     if Options.has_key("Help"): usage()
299
300     maxdiffs = Options.get("MaxDiffs::Default", "14")
301     maxpackages = Options.get("MaxDiffs::Packages", maxdiffs)
302     maxcontents = Options.get("MaxDiffs::Contents", maxdiffs)
303     maxsources = Options.get("MaxDiffs::Sources", maxdiffs)
304
305     if not Options.has_key("PatchName"):
306         format = "%Y-%m-%d-%H%M.%S"
307         Options["PatchName"] = time.strftime( format )
308
309     AptCnf = apt_pkg.newConfiguration()
310     apt_pkg.ReadConfigFileISC(AptCnf,utils.which_apt_conf_file())
311
312     if Options.has_key("RootDir"): Cnf["Dir::Root"] = Options["RootDir"]
313
314     if not suites:
315         suites = Cnf.SubTree("Suite").List()
316
317     for suite in suites:
318         print "Processing: " + suite
319         SuiteBlock = Cnf.SubTree("Suite::" + suite)
320
321         if SuiteBlock.has_key("Untouchable"):
322             print "Skipping: " + suite + " (untouchable)"
323             continue
324
325         suite = suite.lower()
326
327         architectures = SuiteBlock.ValueList("Architectures")
328
329         if SuiteBlock.has_key("Components"):
330             components = SuiteBlock.ValueList("Components")
331         else:
332             components = []
333
334         suite_suffix = Cnf.Find("Dinstall::SuiteSuffix")
335         if components and suite_suffix:
336             longsuite = suite + "/" + suite_suffix
337         else:
338             longsuite = suite
339
340         tree = SuiteBlock.get("Tree", "dists/%s" % (longsuite))
341
342         if AptCnf.has_key("tree::%s" % (tree)):
343             sections = AptCnf["tree::%s::Sections" % (tree)].split()
344         elif AptCnf.has_key("bindirectory::%s" % (tree)):
345             sections = AptCnf["bindirectory::%s::Sections" % (tree)].split()
346         else:
347             aptcnf_filename = os.path.basename(utils.which_apt_conf_file())
348             print "ALERT: suite %s not in %s, nor untouchable!" % (suite, aptcnf_filename)
349             continue
350
351         for architecture in architectures:
352             if architecture == "all":
353                 continue
354
355             if architecture != "source":
356                 # Process Contents
357                 file = "%s/Contents-%s" % (Cnf["Dir::Root"] + tree,
358                         architecture)
359                 storename = "%s/%s_contents_%s" % (Options["TempDir"], suite, architecture)
360                 genchanges(Options, file + ".diff", storename, file, \
361                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::Contents" % (suite), maxcontents))
362
363             # use sections instead of components since dak.conf
364             # treats "foo/bar main" as suite "foo", suitesuffix "bar" and
365             # component "bar/main". suck.
366
367             for component in sections:
368                 if architecture == "source":
369                     longarch = architecture
370                     packages = "Sources"
371                     maxsuite = maxsources
372                 else:
373                     longarch = "binary-%s"% (architecture)
374                     packages = "Packages"
375                     maxsuite = maxpackages
376
377                 file = "%s/%s/%s/%s" % (Cnf["Dir::Root"] + tree,
378                            component, longarch, packages)
379                 storename = "%s/%s_%s_%s" % (Options["TempDir"], suite, component, architecture)
380                 genchanges(Options, file + ".diff", storename, file, \
381                   Cnf.get("Suite::%s::Generate-Index-Diffs::MaxDiffs::%s" % (suite, packages), maxsuite))
382
383 ################################################################################
384
385 if __name__ == '__main__':
386     main()