‘dput.dcut’: Convert ‘print’ statements to ‘sys.stdout.write’ calls.
[dput.git] / dput / dcut.py
blob205e2a092368d3cbf9e12f7871642b934739bfef
1 #! /usr/bin/python2
2 # -*- coding: utf-8; -*-
4 # dput/dcut.py
5 # Part of ‘dput’, a Debian package upload toolkit.
7 # Copyright © 2015 Ben Finney <ben+debian@benfinney.id.au>
8 # Copyright © 2008–2013 Y Giridhar Appaji Nag <appaji@debian.org>
9 # Copyright © 2004–2009 Thomas Viehmann <tv@beamnet.de>
10 # Copyright © 2000–2004 Christian Kurz <shorty@debian.org>
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.
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.
22 # You should have received a copy of the GNU General Public License along
23 # with this program; if not, write to the Free Software Foundation, Inc.,
24 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 """ dcut — Debian command upload tool. """
28 import sys
29 import os
30 import tempfile
31 import shutil
32 import string
33 import textwrap
34 import time
35 import subprocess
36 import pwd
38 from .helper import dputhelper
39 from . import dput
42 validcommands = ('rm', 'cancel', 'reschedule')
45 def make_usage_message():
46 """ Make the program usage help message. """
47 text = textwrap.dedent("""\
48 Usage: %s [options] [host] command [, command]
49 Supported options (see man page for long forms):
50 -c file Config file to parse.
51 -d Enable debug messages.
52 -h Display this help message.
53 -s Simulate the commands file creation only.
54 -v Display version information.
55 -m maintaineraddress
56 Use maintainer information in "Uploader:" field.
57 -k keyid
58 Use this keyid for signing.
59 -O file Write commands to file.
60 -U file Upload specified commands file (presently no checks).
61 -i changes
62 Upload a commands file to remove files listed in .changes.
63 Supported commands: mv, rm
64 (No paths or command-line options allowed on ftp-master.)
65 """) % (dputhelper.get_progname(sys.argv))
66 return text
69 def getoptions():
70 # seed some defaults
71 options = {
72 'debug': 0, 'simulate': 0, 'config': None, 'host': None,
73 'uploader': None, 'keyid': None, 'passive': 0,
74 'filetocreate': None, 'filetoupload': None, 'changes': None}
75 progname = dputhelper.get_progname(sys.argv)
76 version = dputhelper.get_distribution_version()
78 # enable debugging very early
79 if ('-d' in sys.argv[1:] or '--debug' in sys.argv[1:]):
80 options['debug'] = 1
81 sys.stdout.write("D: %s %s\n" % (progname, version))
83 # check environment for maintainer
84 if options['debug']:
85 sys.stdout.write(
86 "D: trying to get maintainer email from environment\n")
88 if 'DEBEMAIL' in os.environ:
89 if os.environ['DEBEMAIL'].find('<') < 0:
90 options['uploader'] = os.environ.get("DEBFULLNAME", '')
91 if options['uploader']:
92 options['uploader'] += ' '
93 options['uploader'] += '<%s>' % (os.environ['DEBEMAIL'])
94 else:
95 options['uploader'] = os.environ['DEBEMAIL']
96 if options['debug']:
97 sys.stdout.write(
98 "D: Uploader from env: %s\n" % (options['uploader']))
99 elif 'EMAIL' in os.environ:
100 if os.environ['EMAIL'].find('<') < 0:
101 options['uploader'] = os.environ.get("DEBFULLNAME", '')
102 if options['uploader']:
103 options['uploader'] += ' '
104 options['uploader'] += '<%s>' % (os.environ['EMAIL'])
105 else:
106 options['uploader'] = os.environ['EMAIL']
107 if options['debug']:
108 sys.stdout.write(
109 "D: Uploader from env: %s\n" % (options['uploader']))
110 else:
111 if options['debug']:
112 sys.stdout.write("D: Guessing uploader\n")
113 pwrec = pwd.getpwuid(os.getuid())
114 username = pwrec[0]
115 fullname = pwrec[4].split(',')[0]
116 try:
117 hostname = open('/etc/mailname').read().strip()
118 except IOError:
119 hostname = ''
120 if not hostname:
121 if options['debug']:
122 sys.stdout.write(
123 "D: Guessing uploader: /etc/mailname was a failure\n")
124 hostname_subprocess = subprocess.Popen(
125 "/bin/hostname --fqdn",
126 shell=True, stdout=subprocess.PIPE)
127 hostname = hostname_subprocess.stdout.read().strip()
128 if hostname:
129 options['uploader'] = (
130 "%s <%s@%s>" % (fullname, username, hostname))
131 if options['debug']:
132 sys.stdout.write(
133 "D: Guessed uploader: %s\n" % (options['uploader']))
134 else:
135 if options['debug']:
136 sys.stdout.write("D: Couldn't guess uploader\n")
137 # parse command line arguments
138 (opts, arguments) = dputhelper.getopt(
139 sys.argv[1:],
140 'c:dDhsvm:k:PU:O:i:', [
141 'config=', 'debug',
142 'help', 'simulate', 'version', 'host=',
143 'maintainteraddress=', 'keyid=',
144 'passive', 'upload=', 'output=', 'input='
147 for (option, arg) in opts:
148 if options['debug']:
149 sys.stdout.write(
150 'D: processing arg "%s", option "%s"\n' % (option, arg))
151 if option in ('-h', '--help'):
152 sys.stdout.write(make_usage_message())
153 sys.exit(0)
154 elif option in ('-v', '--version'):
155 sys.stdout.write("%s %s\n" % (progname, version))
156 sys.exit(0)
157 elif option in ('-d', '--debug'):
158 options['debug'] = 1
159 elif option in ('-c', '--config'):
160 options['config'] = arg
161 elif option in ('-m', '--maintaineraddress'):
162 options['uploader'] = arg
163 elif option in ('-k', '--keyid'):
164 options['keyid'] = arg
165 elif option in ('-s', '--simulate'):
166 options['simulate'] = 1
167 elif option in ('-P', '--passive'):
168 options['passive'] = 1
169 elif option in ('-U', '--upload'):
170 options['filetoupload'] = arg
171 elif option in ('-O', '--output'):
172 options['filetocreate'] = arg
173 elif option == '--host':
174 options['host'] = arg
175 elif option in ('-i', '--input'):
176 options['changes'] = arg
177 else:
178 sys.stderr.write(
179 "%s internal error: Option %s, argument %s unknown\n"
180 % (progname, option, arg))
181 sys.exit(1)
183 if not options['host'] and arguments and arguments[0] not in validcommands:
184 options['host'] = arguments[0]
185 if options['debug']:
186 sys.stdout.write(
187 'D: first argument "%s" treated as host\n'
188 % (options['host']))
189 del arguments[0]
191 # we don't create command files without uploader
192 if (
193 not options['uploader']
194 and (options['filetoupload'] or options['changes'])):
195 sys.stderr.write(
196 "%s error: command file cannot be created"
197 " without maintainer email\n"
198 % progname)
199 sys.stderr.write(
200 '%s please set $DEBEMAIL, $EMAIL'
201 ' or use the "-m" option\n'
202 % (len(progname) * ' '))
203 sys.exit(1)
205 return options, arguments
208 def parse_queuecommands(arguments, options, config):
209 commands = []
210 # want to consume a copy of arguments
211 arguments = arguments[:]
212 arguments.append(0)
213 curarg = []
214 while arguments:
215 if arguments[0] in validcommands:
216 curarg = [arguments[0]]
217 if arguments[0] == 'rm':
218 if len(arguments) > 1 and arguments[1] == '--nosearchdirs':
219 del arguments[1]
220 else:
221 curarg.append('--searchdirs')
222 else:
223 if not curarg and arguments[0] != 0:
224 sys.stderr.write(
225 'Error: Could not parse commands at "%s"\n'
226 % (arguments[0]))
227 sys.exit(1)
228 if str(arguments[0])[-1] in (',', ';', 0):
229 curarg.append(arguments[0][0:-1])
230 arguments[0] = ','
231 if arguments[0] in (',', ';', 0) and curarg:
232 # TV-TODO: syntax check for #args etc.
233 if options['debug']:
234 sys.stdout.write(
235 'D: Successfully parsed command "%s"\n'
236 % (' '.join(curarg)))
237 commands.append(' '.join(curarg))
238 curarg = []
239 else:
240 # TV-TODO: maybe syntax check the arguments here
241 curarg.append(arguments[0])
242 del arguments[0]
243 if not commands:
244 sys.stderr.write("Error: no arguments given, see dcut -h\n")
245 sys.exit(1)
246 return commands
249 def write_commands(commands, options, config, tempdir):
250 progname = dputhelper.get_progname(sys.argv)
251 if options['filetocreate']:
252 filename = options['filetocreate']
253 else:
254 translationorig = (
255 str('').join(map(chr, range(256)))
256 + string.ascii_letters + string.digits)
257 translationdest = 256 * '_' + string.ascii_letters + string.digits
258 translationmap = {
259 ord(orig_char): ord(dest_char)
260 for (orig_char, dest_char)
261 in zip(translationorig, translationdest)}
262 uploadpartforname = options['uploader'].translate(translationmap)
263 filename = (
264 progname + '.%s.%d.%d.commands' %
265 (uploadpartforname, int(time.time()), os.getpid()))
266 if tempdir:
267 filename = os.path.join(tempdir, filename)
268 f = open(filename, "w")
269 f.write("Uploader: %s\n" % options['uploader'])
270 f.write("Commands:\n %s\n\n" % ('\n '.join(commands)))
271 f.close()
272 debsign_cmdline = ['debsign']
273 debsign_cmdline.append('-m%s' % options['uploader'])
274 if options['keyid']:
275 debsign_cmdline.append('-k%s' % options['keyid'])
276 debsign_cmdline.append('%s' % filename)
277 if options['debug']:
278 sys.stdout.write("D: calling debsign: %s\n" % debsign_cmdline)
279 try:
280 subprocess.check_call(debsign_cmdline)
281 except subprocess.CalledProcessError:
282 sys.stderr.write("Error: debsign failed.\n")
283 sys.exit(1)
284 return filename
287 def upload_stolen_from_dput_main(
288 host, upload_methods, config, debug, simulate,
289 files_to_upload, ftp_passive_mode):
290 # Messy, yes. But it isn't referenced by the upload method anyway.
291 if config.get(host, 'method') == 'local':
292 fqdn = 'localhost'
293 else:
294 fqdn = config.get(host, 'fqdn')
296 # Check the upload methods that we have as default and per host
297 if debug:
298 sys.stdout.write(
299 "D: Default Method: %s\n" % config.get('DEFAULT', 'method'))
300 if config.get('DEFAULT', 'method') not in upload_methods:
301 sys.stderr.write(
302 "Unknown upload method: %s\n"
303 % config.get('DEFAULT', 'method'))
304 sys.exit(1)
305 if debug:
306 sys.stdout.write("D: Host Method: %s\n" % config.get(host, 'method'))
307 if config.get(host, 'method') not in upload_methods:
308 sys.stderr.write(
309 "Unknown upload method: %s\n" % config.get(host, 'method'))
310 sys.exit(1)
312 # Inspect the Config and set appropriate upload method
313 if not config.get(host, 'method'):
314 method = config.get('DEFAULT', 'method')
315 else:
316 method = config.get(host, 'method')
318 # Check now the login and redefine it if needed
319 if (
320 config.has_option(host, 'login')
321 and config.get(host, 'login') != 'username'):
322 login = config.get(host, 'login')
323 elif (
324 config.has_option('DEFAULT', 'login')
325 and config.get('DEFAULT', 'login') != 'username'):
326 login = config.get('DEFAULT', 'login')
327 else:
328 # Try to get the login from the enviroment
329 if 'USER' in os.environ:
330 login = os.environ['USER']
331 else:
332 sys.stdout.write("$USER not set, will use login information.\n")
333 # Else use the current username
334 login = pwd.getpwuid(os.getuid())[0]
335 if debug:
336 sys.stdout.write("D: User-ID: %s\n" % os.getuid())
337 if debug:
338 sys.stdout.write(
339 "D: Neither host %s nor default login used. Using %s\n"
340 % (host, login))
341 if debug:
342 sys.stdout.write("D: Login to use: %s\n" % login)
344 incoming = config.get(host, 'incoming')
345 # Do the actual upload
346 if not simulate:
347 if debug:
348 sys.stdout.write("D: FQDN: %s\n" % fqdn)
349 sys.stdout.write("D: Login: %s\n" % login)
350 sys.stdout.write("D: Incoming: %s\n" % incoming)
351 if method == 'ftp':
352 ftp_mode = config.getboolean(host, 'passive_ftp')
353 if ftp_passive_mode == 1:
354 ftp_mode = 1
355 if ftp_mode == 1:
356 if debug:
357 if ftp_passive_mode == 1:
358 sys.stdout.write("D: Using passive ftp\n")
359 else:
360 sys.stdout.write("D: Using active ftp\n")
361 upload_methods[method](
362 fqdn, login, incoming,
363 files_to_upload, debug, ftp_mode)
364 elif method == 'scp':
365 if debug and config.getboolean(host, 'scp_compress'):
366 sys.stdout.write("D: Setting compression for scp\n")
367 scp_compress = config.getboolean(host, 'scp_compress')
368 ssh_config_options = [
369 y for y in (
370 x.strip() for x in
371 config.get(host, 'ssh_config_options').split('\n'))
372 if y]
373 upload_methods[method](
374 fqdn, login, incoming,
375 files_to_upload, debug, scp_compress, ssh_config_options)
376 else:
377 upload_methods[method](
378 fqdn, login, incoming,
379 files_to_upload, debug, 0)
380 # Or just simulate it.
381 else:
382 for file in files_to_upload:
383 sys.stderr.write(
384 "Uploading with %s: %s to %s:%s\n"
385 % (method, file, fqdn, incoming))
386 subprocess.call("cat %s" % file, shell=True)
389 def dcut():
390 options, arguments = getoptions()
391 if options['debug']:
392 sys.stdout.write('D: calling dput.read_configs\n')
393 config = dput.read_configs(options['config'], options['debug'])
394 if (
395 not options['host']
396 and config.has_option('DEFAULT', 'default_host_main')):
397 options['host'] = config.get('DEFAULT', 'default_host_main')
398 if options['debug']:
399 sys.stdout.write(
400 'D: Using host "%s" (default_host_main)\n'
401 % (options['host']))
402 if not options['host']:
403 options['host'] = 'ftp-master'
404 if options['debug']:
405 sys.stdout.write(
406 'D: Using host "%s" (hardcoded)\n'
407 % (options['host']))
408 tempdir = None
409 filename = None
410 progname = dputhelper.get_progname(sys.argv)
411 try:
412 if not (options['filetoupload'] or options['filetocreate']):
413 tempdir = tempfile.mkdtemp(prefix=progname + '.')
414 if not options['filetocreate']:
415 if not options['host']:
416 sys.stdout.write(
417 "Error: No host specified"
418 " and no default found in config\n")
419 sys.exit(1)
420 if not config.has_section(options['host']):
421 sys.stdout.write(
422 "No host %s found in config\n" % (options['host']))
423 sys.exit(1)
424 else:
425 if config.has_option(options['host'], 'allow_dcut'):
426 dcut_allowed = config.getboolean(
427 options['host'], 'allow_dcut')
428 else:
429 dcut_allowed = config.getboolean('DEFAULT', 'allow_dcut')
430 if not dcut_allowed:
431 sys.stdout.write(
432 'Error: dcut is not supported'
433 ' for this upload queue.\n')
434 sys.exit(1)
435 if options['filetoupload']:
436 if arguments:
437 sys.stdout.write(
438 'Error: cannot take commands'
439 ' when uploading existing file,\n'
440 ' "%s" found\n' % (' '.join(arguments)))
441 sys.exit(1)
442 commands = None
443 filename = options['filetoupload']
444 if not filename.endswith(".commands"):
445 sys.stdout.write(
446 'Error: I\'m insisting on the .commands extension,'
447 ' which\n'
448 ' "%s" doesnt seem to have.\n' % filename)
449 # TV-TODO: check file to be readable?
450 elif options['changes']:
451 parse_changes = dput.parse_changes
452 removecommands = create_commands(options, config, parse_changes)
453 filename = write_commands(removecommands, options, config, tempdir)
454 else:
455 commands = parse_queuecommands(arguments, options, config)
456 filename = write_commands(commands, options, config, tempdir)
457 if not options['filetocreate']:
458 dput.import_upload_functions()
459 upload_methods = dput.import_upload_functions()
460 upload_stolen_from_dput_main(
461 options['host'], upload_methods, config,
462 options['debug'], options['simulate'],
463 [filename], options['passive'])
464 finally:
465 # we use sys.exit, so we need to clean up here
466 if tempdir:
467 shutil.rmtree(tempdir)
470 def create_commands(options, config, parse_changes):
471 """ Get the removal commands from a package changes file.
473 Parse the specified ‘foo.changes’ file and returns commands to
474 remove files named in it.
477 changes_file = options['changes']
478 if options['debug']:
479 sys.stdout.write(
480 "D: Parsing changes file (%s) for files to remove\n"
481 % changes_file)
482 try:
483 chg_fd = open(changes_file, 'r')
484 except IOError:
485 sys.stdout.write("Can't open changes file: %s\n" % changes_file)
486 sys.exit(1)
487 the_changes = parse_changes(chg_fd)
488 chg_fd.close
489 removecommands = ['rm --searchdirs ' + os.path.basename(changes_file)]
490 for file in the_changes['files'].strip().split('\n'):
491 # filename only
492 fn = file.split()[4]
493 rm = 'rm --searchdirs ' + fn
494 if options['debug']:
495 sys.stdout.write("D: Will remove %s with '%s'\n" % (fn, rm))
496 removecommands.append(rm)
497 return removecommands
500 if __name__ == "__main__":
501 try:
502 dcut()
503 except dputhelper.DputException as e:
504 sys.stderr.write("%s\n" % e)
505 sys.exit(1)
508 # Local variables:
509 # coding: utf-8
510 # mode: python
511 # End:
512 # vim: fileencoding=utf-8 filetype=python :