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