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