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