2 # Copyright (C) 2000 James Troup <james@nocrew.org>
3 # $Id: utils.py,v 1.23 2001-05-24 18:56:23 troup Exp $
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 import commands, os, pwd, re, socket, shutil, stat, string, sys, tempfile
22 re_comments = re.compile(r"\#.*")
23 re_no_epoch = re.compile(r"^\d*\:")
24 re_no_revision = re.compile(r"\-[^-]*$")
25 re_arch_from_filename = re.compile(r"/binary-[^/]+/")
26 re_extract_src_version = re.compile (r"(\S+)\s*\((.*)\)")
27 re_isadeb = re.compile (r".*\.u?deb$");
28 re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)");
30 re_begin_pgp_signature = re.compile("^-----BEGIN PGP SIGNATURE");
31 re_begin_pgp_signed_msg = re.compile("^-----BEGIN PGP SIGNED MESSAGE");
32 re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)");
33 re_multi_line_description = re.compile(r"^ \.$");
34 re_multi_line_field = re.compile(r"^\s(.*)");
36 re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\> \t]+)\>");
38 changes_parse_error_exc = "Can't parse line in .changes file";
39 invalid_dsc_format_exc = "Invalid .dsc file";
40 nk_format_exc = "Unknown Format: in .changes file";
41 no_files_exc = "No Files: field in .dsc file.";
42 cant_open_exc = "Can't read file.";
43 unknown_hostname_exc = "Unknown hostname";
44 cant_overwrite_exc = "Permission denied; can't overwrite existent file."
46 ######################################################################################
48 def open_file(filename, mode):
50 f = open(filename, mode);
52 raise cant_open_exc, filename
55 ######################################################################################
64 sys.stderr.write('\nUser interrupt (^D).\n')
67 ######################################################################################
71 if c not in string.digits:
75 ######################################################################################
78 def extract_component_from_section(section):
81 if string.find(section, '/') != -1:
82 component = string.split(section, '/')[0];
83 if string.lower(component) == "non-us" and string.count(section, '/') > 0:
84 s = string.split(section, '/')[1];
85 if s == "main" or s == "non-free" or s == "contrib": # Avoid e.g. non-US/libs
86 component = string.split(section, '/')[0]+ '/' + string.split(section, '/')[1];
88 if string.lower(section) == "non-us":
89 component = "non-US/main";
93 elif string.lower(component) == "non-us":
94 component = "non-US/main";
96 return (section, component);
98 ######################################################################################
100 # dsc_whitespace_rules turns on strict format checking to avoid
101 # allowing in source packages which are unextracable by the
102 # inappropriately fragile dpkg-source.
107 # o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
108 # followed by any PGP header data and must end with a blank line.
110 # o The data section must end with a blank line and must be followed by
111 # "-----BEGIN PGP SIGNATURE-----".
113 def parse_changes(filename, dsc_whitespace_rules):
114 changes_in = open_file(filename,'r');
117 lines = changes_in.readlines();
120 raise changes_parse_error_exc, "[Empty changes file]";
122 # Reindex by line number so we can easily verify the format of
128 indexed_lines[index] = line[:-1];
130 inside_signature = 0;
132 indices = indexed_lines.keys()
134 while index < max(indices):
136 line = indexed_lines[index];
138 if dsc_whitespace_rules:
140 if index > max(indices):
141 raise invalid_dsc_format_exc, index;
142 line = indexed_lines[index];
143 if not re_begin_pgp_signature.match(line):
144 raise invalid_dsc_format_exc, index;
145 inside_signature = 0;
147 if re_begin_pgp_signature.match(line):
149 if re_begin_pgp_signed_msg.match(line):
150 if dsc_whitespace_rules:
151 inside_signature = 1;
152 while index < max(indices) and line != "":
154 line = indexed_lines[index];
156 slf = re_single_line_field.match(line);
158 field = string.lower(slf.groups()[0]);
159 changes[field] = slf.groups()[1];
162 mld = re_multi_line_description.match(line);
164 changes[field] = changes[field] + '\n';
166 mlf = re_multi_line_field.match(line);
168 if first == 1 and changes[field] != "":
169 changes[field] = changes[field] + '\n';
171 changes[field] = changes[field] + mlf.groups()[0] + '\n';
173 error = error + line;
175 if dsc_whitespace_rules and inside_signature:
176 raise invalid_dsc_format_exc, index;
179 changes["filecontents"] = string.join (lines, "");
182 raise changes_parse_error_exc, error;
186 ######################################################################################
188 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
190 def build_file_list(changes, dsc):
192 format = changes.get("format", "")
194 format = float(format)
195 if dsc == "" and (format < 1.5 or format > 2.0):
196 raise nk_format_exc, changes["format"];
198 # No really, this has happened. Think 0 length .dsc file.
199 if not changes.has_key("files"):
202 for i in string.split(changes["files"], "\n"):
206 section = priority = "";
209 (md5, size, name) = s
211 (md5, size, section, priority, name) = s
213 raise changes_parse_error_exc, i
215 if section == "": section = "-"
216 if priority == "": priority = "-"
218 (section, component) = extract_component_from_section(section);
220 files[name] = { "md5sum" : md5,
223 "priority": priority,
224 "component": component }
228 ######################################################################################
230 # Fix the `Maintainer:' field to be an RFC822 compatible address.
231 # cf. Packaging Manual (4.2.4)
233 # 06:28|<Culus> 'The standard sucks, but my tool is supposed to
234 # interoperate with it. I know - I'll fix the suckage
235 # and make things incompatible!'
237 def fix_maintainer (maintainer):
238 m = re_parse_maintainer.match(maintainer);
242 if m != None and len(m.groups()) == 2:
245 if string.find(name, ',') != -1 or string.find(name, '.') != -1:
246 rfc822 = re_parse_maintainer.sub(r"\2 (\1)", maintainer)
247 return (rfc822, name, email)
249 ######################################################################################
251 # sendmail wrapper, takes _either_ a message string or a file as arguments
252 def send_mail (message, filename):
253 #### FIXME, how do I get this out of Cnf in katie?
254 sendmail_command = "/usr/sbin/sendmail -odq -oi -t";
256 # Sanity check arguments
257 if message != "" and filename != "":
258 sys.stderr.write ("send_mail() can't be called with both arguments as non-null! (`%s' and `%s')\n%s" % (message, filename))
260 # If we've been passed a string dump it into a temporary file
262 filename = tempfile.mktemp()
263 fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
264 os.write (fd, message)
267 (result, output) = commands.getstatusoutput("%s < %s" % (sendmail_command, filename))
269 sys.stderr.write ("Sendmail invocation (`%s') failed for `%s'!\n%s" % (sendmail_command, filename, output))
271 # Clean up any temporary files
275 ######################################################################################
277 def poolify (source, component):
279 component = component + '/';
280 # FIXME: this is nasty
281 component = string.lower(component);
282 component = string.replace(component, 'non-us/', 'non-US/');
283 if source[:3] == "lib":
284 return component + source[:4] + '/' + source + '/'
286 return component + source[:1] + '/' + source + '/'
288 ######################################################################################
290 def move (src, dest):
291 if os.path.exists(dest) and os.path.isdir(dest):
294 dest_dir = os.path.dirname(dest);
295 if not os.path.exists(dest_dir):
296 umask = os.umask(00000);
297 os.makedirs(dest_dir, 02775);
299 #print "Moving %s to %s..." % (src, dest);
300 if os.path.exists(dest) and os.path.isdir(dest):
301 dest = dest + '/' + os.path.basename(src);
302 # Check for overwrite permission on existent files
303 if os.path.exists(dest) and not os.access(dest, os.W_OK):
304 raise cant_overwrite_exc
305 shutil.copy2(src, dest);
306 os.chmod(dest, 0664);
309 def copy (src, dest):
310 if os.path.exists(dest) and os.path.isdir(dest):
313 dest_dir = os.path.dirname(dest);
314 if not os.path.exists(dest_dir):
315 umask = os.umask(00000);
316 os.makedirs(dest_dir, 02775);
318 #print "Copying %s to %s..." % (src, dest);
319 if os.path.exists(dest) and os.path.isdir(dest):
320 dest = dest + '/' + os.path.basename(src);
321 if os.path.exists(dest) and not os.access(dest, os.W_OK):
322 raise cant_overwrite_exc
323 shutil.copy2(src, dest);
324 os.chmod(dest, 0664);
326 ######################################################################################
328 # FIXME: this is inherently nasty. Can't put this mapping in a conf
329 # file because the conf file depends on the archive.. doh. Maybe an
330 # archive independent conf file is needed.
333 res = socket.gethostbyaddr(socket.gethostname());
334 if res[0] == 'pandora.debian.org':
336 elif res[0] == 'auric.debian.org':
339 raise unknown_hostname_exc, res;
341 ######################################################################################
343 # FIXME: this isn't great either.
345 def which_conf_file ():
346 archive = where_am_i ();
347 if archive == 'non-US':
348 return '/org/non-us.debian.org/katie/katie.conf-non-US';
349 elif archive == 'ftp-master':
350 return '/org/ftp.debian.org/katie/katie.conf';
352 raise unknown_hostname_exc, archive
354 # FIXME: if the above isn't great, this can't be either :)
356 def which_apt_conf_file ():
357 archive = where_am_i ();
358 if archive == 'non-US':
359 return '/org/non-us.debian.org/katie/apt.conf-non-US';
360 elif archive == 'ftp-master':
361 return '/org/ftp.debian.org/katie/apt.conf';
363 raise unknown_hostname_exc, archive
365 ######################################################################################
367 # Escape characters which have meaning to SQL's regex comparison operator ('~')
368 # (woefully incomplete)
371 s = string.replace(s, '+', '\\\\+');
372 s = string.replace(s, '.', '\\\\.');
375 ######################################################################################
377 # Perform a substition of template
378 def TemplateSubst(Map,Template):
380 Template = string.replace(Template,x,Map[x]);
383 ######################################################################################
385 def fubar(msg, exit_code=1):
386 sys.stderr.write("E: %s\n" % (msg));
390 sys.stderr.write("W: %s\n" % (msg));
392 ######################################################################################
394 # Returns the user name with a laughable attempt at rfc822 conformancy
395 # (read: removing stray periods).
397 return string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
399 ######################################################################################
409 return ("%d%s" % (c, t))
411 ################################################################################
413 def cc_fix_changes (changes):
414 o = changes.get("architecture", "")
416 del changes["architecture"]
417 changes["architecture"] = {}
418 for j in string.split(o):
419 changes["architecture"][j] = 1
421 # Sort by 'have source', by source name, by source version number, by filename
423 def changes_compare (a, b):
425 a_changes = parse_changes(a, 0)
426 except changes_parse_error_exc, line:
430 b_changes = parse_changes(b, 0)
431 except changes_parse_error_exc, line:
434 cc_fix_changes (a_changes);
435 cc_fix_changes (b_changes);
437 # Sort by 'have source'
439 a_has_source = a_changes["architecture"].get("source")
440 b_has_source = b_changes["architecture"].get("source")
441 if a_has_source and not b_has_source:
443 elif b_has_source and not a_has_source:
446 # Sort by source name
448 a_source = a_changes.get("source");
449 b_source = b_changes.get("source");
450 q = cmp (a_source, b_source);
454 # Sort by source version
456 a_version = a_changes.get("version");
457 b_version = b_changes.get("version");
458 q = apt_pkg.VersionCompare(a_version, b_version);
462 # Fall back to sort by filename
466 ################################################################################