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