]> git.ipfire.org Git - people/stevee/pakfire.git/blame - python/pakfire/packages/packager.py
QA: Remove remove-emtpy dirs check which is redundant.
[people/stevee/pakfire.git] / python / pakfire / packages / packager.py
CommitLineData
47a4cb89 1#!/usr/bin/python
b792d887
MT
2###############################################################################
3# #
4# Pakfire - The IPFire package management system #
5# Copyright (C) 2011 Pakfire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
47a4cb89 21
c07a3ca7
MT
22import collections
23import fnmatch
47a4cb89 24import glob
c07a3ca7 25import hashlib
47a4cb89
MT
26import os
27import progressbar
8c617c20 28import re
be4a3422 29import shutil
47a4cb89
MT
30import sys
31import tarfile
32import tempfile
c07a3ca7 33import time
1317485d 34import uuid
ce9ffa40 35import zlib
47a4cb89 36
8b6bc023
MT
37import logging
38log = logging.getLogger("pakfire")
39
6387e526 40import pakfire.lzma as lzma
102c5ee7 41import pakfire.util as util
c1fbb0b7 42
47a4cb89
MT
43from pakfire.constants import *
44from pakfire.i18n import _
45
ebd961c3 46from file import BinaryPackage, InnerTarFile, InnerTarFileXz, SourcePackage
47a4cb89 47
47a4cb89 48class Packager(object):
ebd961c3
MT
49 payload_compression = None
50
c07a3ca7 51 def __init__(self, pakfire, pkg):
47a4cb89
MT
52 self.pakfire = pakfire
53 self.pkg = pkg
8c617c20 54
c07a3ca7
MT
55 self.files = []
56 self.tmpfiles = []
8c617c20 57
c07a3ca7
MT
58 def __del__(self):
59 for file in self.tmpfiles:
60 if not os.path.exists(file):
61 continue
8c617c20 62
8b6bc023 63 log.debug("Removing tmpfile: %s" % file)
8c617c20 64
5dda54e4
MT
65 if os.path.isdir(file):
66 util.rm(file)
67 else:
68 os.remove(file)
69
70 def mktemp(self, directory=False):
5dda54e4 71 if directory:
ebd961c3 72 filename = os.path.join("/", LOCAL_TMP_PATH, util.random_string())
5dda54e4 73 os.makedirs(filename)
ebd961c3
MT
74 else:
75 f = tempfile.NamedTemporaryFile(mode="w", delete=False)
76 f.close()
77
78 filename = f.name
5dda54e4 79
c07a3ca7 80 self.tmpfiles.append(filename)
4496b160 81
c07a3ca7 82 return filename
47a4cb89 83
c07a3ca7
MT
84 def save(self, filename):
85 # Create a new tar archive.
86 tar = tarfile.TarFile(filename, mode="w", format=tarfile.PAX_FORMAT)
47a4cb89 87
c07a3ca7
MT
88 # Add package formation information.
89 # Must always be the first file in the archive.
90 formatfile = self.create_package_format()
91 tar.add(formatfile, arcname="pakfire-format")
47a4cb89 92
c07a3ca7
MT
93 # XXX make sure all files belong to the root user
94
95 # Create checksum file.
96 chksumsfile = self.mktemp()
97 chksums = open(chksumsfile, "w")
47a4cb89 98
c07a3ca7
MT
99 # Add all files to tar file.
100 for arcname, filename in self.files:
101 tar.add(filename, arcname=arcname)
47a4cb89 102
c07a3ca7
MT
103 # Calculating the hash sum of the added file
104 # and store it in the chksums file.
105 f = open(filename)
106 h = hashlib.sha512()
107 while True:
108 buf = f.read(BUFFER_SIZE)
109 if not buf:
110 break
111
112 h.update(buf)
113 f.close()
114
115 chksums.write("%-10s %s\n" % (arcname, h.hexdigest()))
116
117 # Close checksum file and attach it to the end.
118 chksums.close()
119 tar.add(chksumsfile, "chksums")
120
121 # Close the tar file.
47a4cb89
MT
122 tar.close()
123
c07a3ca7
MT
124 def add(self, filename, arcname=None):
125 if not arcname:
126 arcname = os.path.basename(filename)
127
8b6bc023 128 log.debug("Adding %s (as %s) to tarball." % (filename, arcname))
c07a3ca7
MT
129 self.files.append((arcname, filename))
130
131 def create_package_format(self):
132 filename = self.mktemp()
133
134 f = open(filename, "w")
135 f.write("%s\n" % PACKAGE_FORMAT)
136 f.close()
137
138 return filename
139
140 def create_filelist(self, datafile):
141 filelist = self.mktemp()
142
143 f = open(filelist, "w")
ebd961c3
MT
144
145 if self.payload_compression == "xz":
146 datafile = InnerTarFileXz.open(datafile)
147 else:
148 datafile = InnerTarFile.open(datafile)
c07a3ca7 149
3ce6a8ad
MT
150 while True:
151 m = datafile.next()
152 if not m:
153 break
154
8b6bc023 155 log.debug(" %s %-8s %-8s %s %6s %s" % \
c07a3ca7
MT
156 (tarfile.filemode(m.mode), m.uname, m.gname,
157 "%d-%02d-%02d %02d:%02d:%02d" % time.localtime(m.mtime)[:6],
158 util.format_size(m.size), m.name))
c07a3ca7
MT
159
160 f.write("%(name)-40s %(type)1s %(size)-10d %(uname)-10s %(gname)-10s %(mode)-6d %(mtime)-12d" \
b41705e4 161 % m.get_info(tarfile.ENCODING, "strict"))
677ff42a 162
c07a3ca7
MT
163 # Calculate SHA512 hash of regular files.
164 if m.isreg():
165 mobj = datafile.extractfile(m)
166 h = hashlib.sha512()
47a4cb89 167
c07a3ca7
MT
168 while True:
169 buf = mobj.read(BUFFER_SIZE)
170 if not buf:
171 break
172 h.update(buf)
d507be4d 173
c07a3ca7 174 mobj.close()
cabf1fbe
MT
175 f.write(" %s" % h.hexdigest())
176 else:
177 f.write(" -")
c07a3ca7 178
cabf1fbe
MT
179 caps = m.pax_headers.get("PAKFIRE.capabilities", None)
180 if caps:
181 f.write(" %s" % caps)
c07a3ca7 182 else:
cabf1fbe
MT
183 f.write(" -")
184
185 f.write("\n")
3e4a9b06 186
8b6bc023 187 log.info("")
c07a3ca7
MT
188
189 datafile.close()
190 f.close()
191
192 return filelist
193
194 def run(self):
195 raise NotImplementedError
d507be4d 196
97d7d682
MT
197 def getsize(self, filename):
198 if tarfile.is_tarfile(filename):
199 return os.path.getsize(filename)
200
201 size = 0
202 f = lzma.LZMAFile(filename)
203
204 while True:
205 buf = f.read(BUFFER_SIZE)
206 if not buf:
207 break
208
209 size += len(buf)
210 f.close()
211
212 return size
213
c07a3ca7
MT
214
215class BinaryPackager(Packager):
ebd961c3
MT
216 payload_compression = "xz"
217
5dda54e4 218 def __init__(self, pakfire, pkg, builder, buildroot):
c07a3ca7
MT
219 Packager.__init__(self, pakfire, pkg)
220
5dda54e4 221 self.builder = builder
c07a3ca7
MT
222 self.buildroot = buildroot
223
224 def create_metafile(self, datafile):
225 info = collections.defaultdict(lambda: "")
226
5dda54e4
MT
227 # Extract datafile in temporary directory and scan for dependencies.
228 tmpdir = self.mktemp(directory=True)
229
ebd961c3
MT
230 if self.payload_compression == "xz":
231 tarfile = InnerTarFileXz.open(datafile)
232 else:
233 tarfile = InnerTarFile.open(datafile)
234
5dda54e4
MT
235 tarfile.extractall(path=tmpdir)
236 tarfile.close()
237
238 # Run the dependency tracker.
239 self.pkg.track_dependencies(self.builder, tmpdir)
240
c07a3ca7
MT
241 # Generic package information including Pakfire information.
242 info.update({
243 "pakfire_version" : PAKFIRE_VERSION,
c62d93f1 244 "uuid" : self.pkg.uuid,
1b59091e 245 "type" : "binary",
c07a3ca7
MT
246 })
247
248 # Include distribution information.
249 info.update(self.pakfire.distro.info)
250 info.update(self.pkg.info)
251
252 # Update package information for string formatting.
253 info.update({
5dda54e4
MT
254 "groups" : " ".join(self.pkg.groups),
255 "prerequires" : "\n".join([PACKAGE_INFO_DEPENDENCY_LINE % d \
256 for d in self.pkg.prerequires]),
257 "requires" : "\n".join([PACKAGE_INFO_DEPENDENCY_LINE % d \
258 for d in self.pkg.requires]),
259 "provides" : "\n".join([PACKAGE_INFO_DEPENDENCY_LINE % d \
260 for d in self.pkg.provides]),
261 "conflicts" : "\n".join([PACKAGE_INFO_DEPENDENCY_LINE % d \
262 for d in self.pkg.conflicts]),
263 "obsoletes" : "\n".join([PACKAGE_INFO_DEPENDENCY_LINE % d \
264 for d in self.pkg.obsoletes]),
c07a3ca7
MT
265 })
266
267 # Format description.
268 description = [PACKAGE_INFO_DESCRIPTION_LINE % l \
269 for l in util.text_wrap(self.pkg.description, length=80)]
270 info["description"] = "\n".join(description)
271
272 # Build information.
273 info.update({
274 # Package it built right now.
275 "build_time" : int(time.time()),
276 "build_id" : uuid.uuid4(),
277 })
278
279 # Installed size (equals size of the uncompressed tarball).
280 info.update({
97d7d682 281 "inst_size" : self.getsize(datafile),
c07a3ca7
MT
282 })
283
284 metafile = self.mktemp()
285
286 f = open(metafile, "w")
287 f.write(PACKAGE_INFO % info)
288 f.close()
289
290 return metafile
291
292 def create_datafile(self):
715a78e9
MT
293 includes = []
294 excludes = []
295
c07a3ca7
MT
296 # List of all patterns, which grows.
297 patterns = self.pkg.files
298
299 for pattern in patterns:
715a78e9
MT
300 # Check if we are running in include or exclude mode.
301 if pattern.startswith("!"):
302 files = excludes
303
c07a3ca7 304 # Strip the ! character.
715a78e9 305 pattern = pattern[1:]
715a78e9
MT
306 else:
307 files = includes
308
c07a3ca7 309 # Expand file to point to chroot.
47a4cb89
MT
310 if pattern.startswith("/"):
311 pattern = pattern[1:]
c07a3ca7 312 pattern = os.path.join(self.buildroot, pattern)
47a4cb89
MT
313
314 # Recognize the type of the pattern. Patterns could be a glob
315 # pattern that is expanded here or just a directory which will
316 # be included recursively.
75bb74a7
MT
317 if "*" in pattern or "?" in pattern or ("[" in pattern and "]" in pattern):
318 _patterns = glob.glob(pattern)
319 else:
320 _patterns = [pattern,]
321
322 for pattern in _patterns:
e70f166b
MT
323 # Try to stat the pattern. If that is not successful, we cannot go on.
324 try:
325 os.lstat(pattern)
326 except OSError:
75bb74a7 327 continue
47a4cb89 328
26639c54
MT
329 # Add directories recursively but skip those symlinks
330 # that point to a directory.
331 if os.path.isdir(pattern) and not os.path.islink(pattern):
c07a3ca7
MT
332 # Add directory itself.
333 files.append(pattern)
334
47a4cb89 335 for dir, subdirs, _files in os.walk(pattern):
c07a3ca7
MT
336 for subdir in subdirs:
337 if subdir in ORPHAN_DIRECTORIES:
338 continue
339
340 subdir = os.path.join(dir, subdir)
341 files.append(subdir)
342
47a4cb89
MT
343 for file in _files:
344 file = os.path.join(dir, file)
345 files.append(file)
346
26639c54 347 # All other files are just added.
47a4cb89
MT
348 else:
349 files.append(pattern)
350
c00c6f33
MT
351 # ...
352 orphan_directories = [os.path.join(self.buildroot, d) for d in ORPHAN_DIRECTORIES]
353
715a78e9
MT
354 files = []
355 for file in includes:
58d47ee6
MT
356 # Skip if file is already in the file set or
357 # marked to be excluded from this archive.
358 if file in excludes or file in files:
715a78e9
MT
359 continue
360
c00c6f33
MT
361 # Skip orphan directories.
362 if file in orphan_directories and not os.listdir(file):
8b6bc023 363 log.debug("Found an orphaned directory: %s" % file)
c00c6f33
MT
364 continue
365
715a78e9 366 files.append(file)
c00c6f33
MT
367
368 while True:
369 file = os.path.dirname(file)
370
371 if file == self.buildroot:
372 break
373
374 if not file in files:
375 files.append(file)
376
47a4cb89
MT
377 files.sort()
378
c07a3ca7
MT
379 # Load progressbar.
380 message = "%-10s : %s" % (_("Packaging"), self.pkg.friendly_name)
381 pb = util.make_progress(message, len(files), eta=False)
382
383 datafile = self.mktemp()
ebd961c3
MT
384 if self.payload_compression == "xz":
385 tar = InnerTarFileXz.open(datafile, mode="w")
386 else:
387 tar = InnerTarFile.open(datafile, mode="w")
c07a3ca7
MT
388
389 # All files in the tarball are relative to this directory.
390 basedir = self.buildroot
0bab8cdb 391
c07a3ca7
MT
392 i = 0
393 for file in files:
394 if pb:
395 i += 1
396 pb.update(i)
47a4cb89 397
c07a3ca7
MT
398 # Never package /.
399 if os.path.normpath(file) == os.path.normpath(basedir):
8c617c20
MT
400 continue
401
c00c6f33 402 # Name of the file in the archive.
c07a3ca7 403 arcname = "/%s" % os.path.relpath(file, basedir)
49e0d33b 404
c07a3ca7
MT
405 # Add file to tarball.
406 tar.add(file, arcname=arcname, recursive=False)
407
408 # Remove all packaged files.
409 for file in reversed(files):
c07a3ca7
MT
410 # It's okay if we cannot remove directories,
411 # when they are not empty.
412 if os.path.isdir(file):
413 try:
414 os.rmdir(file)
415 except OSError:
416 continue
0bab8cdb 417 else:
dcb5bc82
MT
418 try:
419 os.unlink(file)
420 except OSError:
421 pass
c07a3ca7
MT
422
423 while True:
424 file = os.path.dirname(file)
425
426 if not file.startswith(basedir):
427 break
428
429 try:
430 os.rmdir(file)
431 except OSError:
432 break
433
434 # Close the tarfile.
435 tar.close()
436
437 # Finish progressbar.
438 if pb:
439 pb.finish()
440
441 return datafile
0bab8cdb 442
c07a3ca7
MT
443 def create_scriptlets(self):
444 scriptlets = []
0bab8cdb 445
08e95360
MT
446 # Collect all prerequires for the scriptlets.
447 prerequires = []
448
c07a3ca7
MT
449 for scriptlet_name in SCRIPTS:
450 scriptlet = self.pkg.get_scriptlet(scriptlet_name)
0bab8cdb 451
c07a3ca7
MT
452 if not scriptlet:
453 continue
454
455 # Write script to a file.
456 scriptlet_file = self.mktemp()
457
458 if scriptlet["lang"] == "bin":
459 path = lang["path"]
460 try:
461 f = open(path, "b")
462 except OSError:
463 raise Exception, "Cannot open script file: %s" % lang["path"]
464
465 s = open(scriptlet_file, "wb")
466
467 while True:
468 buf = f.read(BUFFER_SIZE)
469 if not buf:
470 break
471
472 s.write(buf)
473
474 f.close()
475 s.close()
9a0737c7 476
c07a3ca7
MT
477 elif scriptlet["lang"] == "shell":
478 s = open(scriptlet_file, "w")
479
480 # Write shell script to file.
481 s.write("#!/bin/sh -e\n\n")
482 s.write(scriptlet["scriptlet"])
483 s.write("\n\nexit 0\n")
484
485 s.close()
556b4305 486
08e95360
MT
487 if scriptlet_name in SCRIPTS_PREREQUIRES:
488 # Shell scripts require a shell to be executed.
489 prerequires.append("/bin/sh")
490
491 prerequires += self.builder.find_prerequires(scriptlet_file)
492
0bab8cdb 493 else:
c07a3ca7 494 raise Exception, "Unknown scriptlet language: %s" % scriptlet["lang"]
0bab8cdb 495
c07a3ca7 496 scriptlets.append((scriptlet_name, scriptlet_file))
47a4cb89 497
08e95360
MT
498 # Cleanup prerequires.
499 self.pkg.update_prerequires(prerequires)
0bab8cdb 500
c07a3ca7 501 return scriptlets
47a4cb89 502
c07a3ca7 503 def create_configs(self, datafile):
ebd961c3
MT
504 if self.payload_compression == "xz":
505 datafile = InnerTarFileXz.open(datafile)
506 else:
507 datafile = InnerTarFile.open(datafile)
c07a3ca7
MT
508
509 members = datafile.getmembers()
510
511 configfiles = []
512 configdirs = []
513
514 # Find all directories in the config file list.
515 for file in self.pkg.configfiles:
516 if file.startswith("/"):
517 file = file[1:]
518
519 for member in members:
520 if member.name == file and member.isdir():
521 configdirs.append(file)
47a4cb89 522
c07a3ca7
MT
523 for configdir in configdirs:
524 for member in members:
525 if not member.isdir() and member.name.startswith(configdir):
526 configfiles.append(member.name)
ce9ffa40 527
c07a3ca7
MT
528 for pattern in self.pkg.configfiles:
529 if pattern.startswith("/"):
530 pattern = pattern[1:]
531
532 for member in members:
533 if not fnmatch.fnmatch(member.name, pattern):
534 continue
535
536 if member.name in configfiles:
537 continue
538
539 configfiles.append(member.name)
ce9ffa40 540
c07a3ca7
MT
541 # Sort list alphabetically.
542 configfiles.sort()
47a4cb89 543
c07a3ca7 544 configsfile = self.mktemp()
8c617c20 545
c07a3ca7
MT
546 f = open(configsfile, "w")
547 for file in configfiles:
548 f.write("%s\n" % file)
47a4cb89 549 f.close()
d507be4d 550
c07a3ca7 551 return configsfile
d507be4d 552
75bb74a7 553 def run(self, resultdir):
c07a3ca7
MT
554 # Add all files to this package.
555 datafile = self.create_datafile()
556
557 # Get filelist from datafile.
558 filelist = self.create_filelist(datafile)
559 configs = self.create_configs(datafile)
560
561 # Create script files.
562 scriptlets = self.create_scriptlets()
563
564 metafile = self.create_metafile(datafile)
565
c07a3ca7
MT
566 # Add files to the tar archive in correct order.
567 self.add(metafile, "info")
568 self.add(filelist, "filelist")
569 self.add(configs, "configs")
570 self.add(datafile, "data.img")
571
572 for scriptlet_name, scriptlet_file in scriptlets:
573 self.add(scriptlet_file, "scriptlets/%s" % scriptlet_name)
574
575 # Build the final package.
576 tempfile = self.mktemp()
577 self.save(tempfile)
578
75bb74a7
MT
579 # Add architecture information to path.
580 resultdir = "%s/%s" % (resultdir, self.pkg.arch)
c07a3ca7 581
75bb74a7
MT
582 if not os.path.exists(resultdir):
583 os.makedirs(resultdir)
c07a3ca7 584
75bb74a7 585 resultfile = os.path.join(resultdir, self.pkg.package_filename)
8b6bc023 586 log.info("Saving package to %s" % resultfile)
75bb74a7
MT
587 try:
588 os.link(tempfile, resultfile)
589 except OSError:
590 shutil.copy2(tempfile, resultfile)
d507be4d 591
56f5e5ff
MT
592 return BinaryPackage(self.pakfire, self.pakfire.repos.dummy, resultfile)
593
d507be4d
MT
594
595class SourcePackager(Packager):
ebd961c3
MT
596 payload_compression = None
597
c07a3ca7
MT
598 def create_metafile(self, datafile):
599 info = collections.defaultdict(lambda: "")
600
601 # Generic package information including Pakfire information.
602 info.update({
603 "pakfire_version" : PAKFIRE_VERSION,
1b59091e 604 "type" : "source",
c07a3ca7
MT
605 })
606
607 # Include distribution information.
608 info.update(self.pakfire.distro.info)
609 info.update(self.pkg.info)
610
8b88b494 611 # Size is the size of the (uncompressed) datafile.
97d7d682 612 info["inst_size"] = self.getsize(datafile)
8b88b494 613
c07a3ca7
MT
614 # Update package information for string formatting.
615 requires = [PACKAGE_INFO_DEPENDENCY_LINE % r for r in self.pkg.requires]
616 info.update({
617 "groups" : " ".join(self.pkg.groups),
618 "requires" : "\n".join(requires),
619 })
620
621 # Format description.
622 description = [PACKAGE_INFO_DESCRIPTION_LINE % l \
623 for l in util.text_wrap(self.pkg.description, length=80)]
624 info["description"] = "\n".join(description)
625
626 # Build information.
627 info.update({
628 # Package it built right now.
629 "build_time" : int(time.time()),
630 "build_id" : uuid.uuid4(),
631 })
632
1b59091e
MT
633 # Arches equals supported arches.
634 info["arch"] = self.pkg.supported_arches
635
c07a3ca7
MT
636 # Set UUID
637 # XXX replace this by the payload hash
638 info.update({
639 "uuid" : uuid.uuid4(),
640 })
641
642 metafile = self.mktemp()
643
644 f = open(metafile, "w")
645 f.write(PACKAGE_INFO % info)
646 f.close()
647
648 return metafile
649
650 def create_datafile(self):
7d40ac70
MT
651 # Create a list of all files that have to be put into the
652 # package.
653 files = []
c07a3ca7 654
7d40ac70 655 # Download all files that go into the package.
c07a3ca7 656 for file in self.pkg.download():
7d40ac70
MT
657 assert os.path.getsize(file), "Don't package empty files"
658 files.append(("files/%s" % os.path.basename(file), file))
c07a3ca7
MT
659
660 # Add all files in the package directory.
7d40ac70
MT
661 for file in self.pkg.files:
662 files.append((os.path.relpath(file, self.pkg.path), file))
663
664 # Add files in alphabetical order.
665 files.sort()
666
667 # Load progressbar.
668 message = "%-10s : %s" % (_("Packaging"), self.pkg.friendly_name)
669 pb = util.make_progress(message, len(files), eta=False)
670
671 filename = self.mktemp()
ebd961c3
MT
672 if self.payload_compression == "xz":
673 datafile = InnerTarFileXz.open(filename, mode="w")
674 else:
675 datafile = InnerTarFile.open(filename, mode="w")
7d40ac70
MT
676
677 i = 0
678 for arcname, file in files:
679 if pb:
680 i += 1
681 pb.update(i)
c07a3ca7 682
7d40ac70 683 datafile.add(file, arcname)
c07a3ca7
MT
684 datafile.close()
685
7d40ac70
MT
686 if pb:
687 pb.finish()
688
c07a3ca7
MT
689 return filename
690
7d40ac70
MT
691 def run(self, resultdir):
692 # Create resultdir if it does not exist yet.
693 if not os.path.exists(resultdir):
694 os.makedirs(resultdir)
c07a3ca7 695
8b6bc023 696 log.info(_("Building source package %s:") % self.pkg.package_filename)
c07a3ca7 697
7d40ac70
MT
698 # The filename where this source package is saved at.
699 target_filename = os.path.join(resultdir, self.pkg.package_filename)
700
c07a3ca7
MT
701 # Add datafile to package.
702 datafile = self.create_datafile()
703
704 # Create filelist out of data.
705 filelist = self.create_filelist(datafile)
706
707 # Create metadata.
708 metafile = self.create_metafile(datafile)
709
710 # Add files to the tar archive in correct order.
711 self.add(metafile, "info")
712 self.add(filelist, "filelist")
713 self.add(datafile, "data.img")
714
715 # Build the final tarball.
7d40ac70
MT
716 try:
717 self.save(target_filename)
718 except:
719 # Remove the target file when anything went wrong.
720 os.unlink(target_filename)
721 raise
d507be4d 722
7d40ac70 723 return target_filename