3 # Copyright (c) 2007 Red Hat, Inc.
4 # Copyright (c) 2009, 2010, 2011 Intel, Inc.
6 # This program is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the Free
8 # Software Foundation; version 2 of the License
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc., 59
17 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 from mic.utils import errors, misc, runner, fs_related as fs
28 import pykickstart.commands as kscommands
29 import pykickstart.constants as ksconstants
30 import pykickstart.errors as kserrors
31 import pykickstart.parser as ksparser
32 import pykickstart.version as ksversion
33 from pykickstart.handlers.control import commandMap
34 from pykickstart.handlers.control import dataMap
36 import custom_commands.desktop as desktop
37 import custom_commands.moblinrepo as moblinrepo
38 import custom_commands.micboot as micboot
40 def read_kickstart(path):
41 """Parse a kickstart file and return a KickstartParser instance.
43 This is a simple utility function which takes a path to a kickstart file,
44 parses it and returns a pykickstart KickstartParser instance which can
45 be then passed to an ImageCreator constructor.
47 If an error occurs, a CreatorError exception is thrown.
50 #version = ksversion.makeVersion()
51 #ks = ksparser.KickstartParser(version)
53 using_version = ksversion.DEVEL
54 commandMap[using_version]["desktop"] = desktop.Moblin_Desktop
55 commandMap[using_version]["repo"] = moblinrepo.Moblin_Repo
56 commandMap[using_version]["bootloader"] = micboot.Moblin_Bootloader
57 dataMap[using_version]["RepoData"] = moblinrepo.Moblin_RepoData
58 superclass = ksversion.returnClassForVersion(version=using_version)
60 class KSHandlers(superclass):
61 def __init__(self, mapping={}):
62 superclass.__init__(self, mapping=commandMap[using_version])
64 ks = ksparser.KickstartParser(KSHandlers())
67 ks.readKickstart(path)
68 except kserrors.KickstartError, e:
69 raise errors.KsError("'%s': %s" % (path, str(e)))
70 except kserrors.KickstartParseError, e:
71 raise errors.KsError("'%s': %s" % (path, str(e)))
74 def build_name(kscfg, prefix = None, suffix = None, maxlen = None):
75 """Construct and return an image name string.
77 This is a utility function to help create sensible name and fslabel
78 strings. The name is constructed using the sans-prefix-and-extension
79 kickstart filename and the supplied prefix and suffix.
81 If the name exceeds the maxlen length supplied, the prefix is first dropped
82 and then the kickstart filename portion is reduced until it fits. In other
83 words, the suffix takes precedence over the kickstart portion and the
84 kickstart portion takes precedence over the prefix.
86 kscfg -- a path to a kickstart file
87 prefix -- a prefix to prepend to the name; defaults to None, which causes
89 suffix -- a suffix to append to the name; defaults to None, which causes
90 a YYYYMMDDHHMM suffix to be used
91 maxlen -- the maximum length for the returned string; defaults to None,
92 which means there is no restriction on the name length
94 Note, if maxlen is less then the len(suffix), you get to keep both pieces.
97 name = os.path.basename(kscfg)
105 suffix = time.strftime("%Y%m%d%H%M")
107 if name.startswith(prefix):
108 name = name[len(prefix):]
110 ret = prefix + name + "-" + suffix
111 if not maxlen is None and len(ret) > maxlen:
112 ret = name[:maxlen - len(suffix) - 1] + "-" + suffix
116 class KickstartConfig(object):
117 """A base class for applying kickstart configurations to a system."""
118 def __init__(self, instroot):
119 self.instroot = instroot
121 def path(self, subpath):
122 return self.instroot + subpath
124 def _check_sysconfig(self):
125 if not os.path.exists(self.path("/etc/sysconfig")):
126 fs.makedirs(self.path("/etc/sysconfig"))
129 os.chroot(self.instroot)
132 def call(self, args):
133 if not os.path.exists("%s/%s" %(self.instroot, args[0])):
134 msger.warning("%s/%s" %(self.instroot, args[0]))
135 raise errors.KsError("Unable to run %s!" %(args))
136 subprocess.call(args, preexec_fn = self.chroot)
141 class LanguageConfig(KickstartConfig):
142 """A class to apply a kickstart language configuration to a system."""
143 def apply(self, kslang):
144 self._check_sysconfig()
146 f = open(self.path("/etc/sysconfig/i18n"), "w+")
147 f.write("LANG=\"" + kslang.lang + "\"\n")
150 class KeyboardConfig(KickstartConfig):
151 """A class to apply a kickstart keyboard configuration to a system."""
152 def apply(self, kskeyboard):
155 # should this impact the X keyboard config too?
156 # or do we want to make X be able to do this mapping?
158 #k = rhpl.keyboard.Keyboard()
159 #if kskeyboard.keyboard:
160 # k.set(kskeyboard.keyboard)
161 #k.write(self.instroot)
164 class TimezoneConfig(KickstartConfig):
165 """A class to apply a kickstart timezone configuration to a system."""
166 def apply(self, kstimezone):
167 self._check_sysconfig()
168 tz = kstimezone.timezone or "America/New_York"
169 utc = str(kstimezone.isUtc)
171 f = open(self.path("/etc/sysconfig/clock"), "w+")
172 f.write("ZONE=\"" + tz + "\"\n")
173 f.write("UTC=" + utc + "\n")
175 tz_source = self.path("/usr/share/zoneinfo/%s" % (tz))
176 tz_dest = self.path("/etc/localtime")
178 shutil.copyfile(tz_source, tz_dest)
179 except (IOError, OSError), (errno, msg):
180 raise errors.KickstartError("Error copying timezone info from "
182 % (tz_source, tz_dest, msg))
185 class AuthConfig(KickstartConfig):
186 """A class to apply a kickstart authconfig configuration to a system."""
187 def apply(self, ksauthconfig):
188 auth = ksauthconfig.authconfig or "--useshadow --enablemd5"
189 args = ["/usr/share/authconfig/authconfig.py", "--update", "--nostart"]
190 self.call(args + auth.split())
192 class FirewallConfig(KickstartConfig):
193 """A class to apply a kickstart firewall configuration to a system."""
194 def apply(self, ksfirewall):
196 # FIXME: should handle the rest of the options
198 if not os.path.exists(self.path("/usr/sbin/lokkit")):
200 if ksfirewall.enabled:
203 status = "--disabled"
205 self.call(["/usr/sbin/lokkit",
206 "-f", "--quiet", "--nostart", status])
208 class RootPasswordConfig(KickstartConfig):
209 """A class to apply a kickstart root password configuration to a system."""
211 self.call(["/usr/bin/passwd", "-d", "root"])
213 def set_encrypted(self, password):
214 self.call(["/usr/sbin/usermod", "-p", password, "root"])
216 def set_unencrypted(self, password):
217 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
218 if not os.path.exists("%s/%s" %(self.instroot, p)):
219 raise errors.KsError("Unable to set unencrypted password due "
222 p1 = subprocess.Popen(["/bin/echo", "root:%s" %password],
223 stdout = subprocess.PIPE,
224 preexec_fn = self.chroot)
225 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
227 stdout = subprocess.PIPE,
228 preexec_fn = self.chroot)
231 def apply(self, ksrootpw):
232 if ksrootpw.isCrypted:
233 self.set_encrypted(ksrootpw.password)
234 elif ksrootpw.password != "":
235 self.set_unencrypted(ksrootpw.password)
239 class UserConfig(KickstartConfig):
240 def set_empty_passwd(self, user):
241 self.call(["/usr/bin/passwd", "-d", user])
243 def set_encrypted_passwd(self, user, password):
244 self.call(["/usr/sbin/usermod", "-p", "%s" % password, user])
246 def set_unencrypted_passwd(self, user, password):
247 for p in ("/bin/echo", "/usr/sbin/chpasswd"):
248 if not os.path.exists("%s/%s" %(self.instroot, p)):
249 raise errors.KsError("Unable to set unencrypted password due "
252 p1 = subprocess.Popen(["/bin/echo", "%s:%s" %(user, password)],
253 stdout = subprocess.PIPE,
254 preexec_fn = self.chroot)
255 p2 = subprocess.Popen(["/usr/sbin/chpasswd", "-m"],
257 stdout = subprocess.PIPE,
258 preexec_fn = self.chroot)
261 def addUser(self, userconfig):
262 args = [ "/usr/sbin/useradd" ]
263 if userconfig.groups:
264 args += [ "--groups", string.join(userconfig.groups, ",") ]
266 args.append(userconfig.name)
267 dev_null = os.open("/dev/null", os.O_WRONLY)
268 subprocess.call(args,
271 preexec_fn = self.chroot)
273 if userconfig.password not in (None, ""):
274 if userconfig.isCrypted:
275 self.set_encrypted_passwd(userconfig.name,
278 self.set_unencrypted_passwd(userconfig.name,
281 self.set_empty_passwd(userconfig.name)
283 raise errors.KsError("Invalid kickstart command: %s" \
284 % userconfig.__str__())
286 def apply(self, user):
287 for userconfig in user.userList:
289 self.addUser(userconfig)
293 class ServicesConfig(KickstartConfig):
294 """A class to apply a kickstart services configuration to a system."""
295 def apply(self, ksservices):
296 if not os.path.exists(self.path("/sbin/chkconfig")):
298 for s in ksservices.enabled:
299 self.call(["/sbin/chkconfig", s, "on"])
300 for s in ksservices.disabled:
301 self.call(["/sbin/chkconfig", s, "off"])
303 class XConfig(KickstartConfig):
304 """A class to apply a kickstart X configuration to a system."""
305 def apply(self, ksxconfig):
306 if ksxconfig.startX and os.path.exists(self.path("/etc/inittab")):
307 f = open(self.path("/etc/inittab"), "rw+")
309 buf = buf.replace("id:3:initdefault", "id:5:initdefault")
313 if ksxconfig.defaultdesktop:
314 self._check_sysconfig()
315 f = open(self.path("/etc/sysconfig/desktop"), "w")
316 f.write("DESKTOP="+ksxconfig.defaultdesktop+"\n")
319 class DesktopConfig(KickstartConfig):
320 """A class to apply a kickstart desktop configuration to a system."""
321 def apply(self, ksdesktop):
322 if ksdesktop.defaultdesktop:
323 self._check_sysconfig()
324 f = open(self.path("/etc/sysconfig/desktop"), "w")
325 f.write("DESKTOP="+ksdesktop.defaultdesktop+"\n")
327 if os.path.exists(self.path("/etc/gdm/custom.conf")):
328 f = open(self.path("/etc/skel/.dmrc"), "w")
329 f.write("[Desktop]\n")
330 f.write("Session="+ksdesktop.defaultdesktop.lower()+"\n")
332 if ksdesktop.session:
333 if os.path.exists(self.path("/etc/sysconfig/uxlaunch")):
334 f = open(self.path("/etc/sysconfig/uxlaunch"), "a+")
335 f.write("session="+ksdesktop.session.lower()+"\n")
337 if ksdesktop.autologinuser:
338 self._check_sysconfig()
339 f = open(self.path("/etc/sysconfig/desktop"), "a+")
340 f.write("AUTOLOGIN_USER=" + ksdesktop.autologinuser + "\n")
342 if os.path.exists(self.path("/etc/gdm/custom.conf")):
343 f = open(self.path("/etc/gdm/custom.conf"), "w")
344 f.write("[daemon]\n")
345 f.write("AutomaticLoginEnable=true\n")
346 f.write("AutomaticLogin=" + ksdesktop.autologinuser + "\n")
349 class MoblinRepoConfig(KickstartConfig):
350 """A class to apply a kickstart desktop configuration to a system."""
351 def __create_repo_section(self, repo, type, fd):
354 reposuffix = {"base":"", "debuginfo":"-debuginfo", "source":"-source"}
355 reponame = repo.name + reposuffix[type]
358 baseurl = repo.baseurl
360 mirrorlist = repo.mirrorlist
362 elif type == "debuginfo":
364 if repo.baseurl.endswith("/"):
365 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
367 baseurl = os.path.dirname(repo.baseurl)
371 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
372 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
373 mirrorlist += "debug" + "-" + variant
375 elif type == "source":
377 if repo.baseurl.endswith("/"):
378 baseurl = os.path.dirname(
380 os.path.dirname(repo.baseurl)))
382 baseurl = os.path.dirname(os.path.dirname(repo.baseurl))
386 variant = repo.mirrorlist[repo.mirrorlist.find("$"):]
387 mirrorlist = repo.mirrorlist[0:repo.mirrorlist.find("$")]
388 mirrorlist += "source" + "-" + variant
390 fd.write("[" + reponame + "]\n")
391 fd.write("name=" + reponame + "\n")
392 fd.write("failovermethod=priority\n")
394 fd.write("baseurl=" + baseurl + "\n")
396 fd.write("mirrorlist=" + mirrorlist + "\n")
397 """ Skip saving proxy settings """
399 # fd.write("proxy=" + repo.proxy + "\n")
400 #if repo.proxy_username:
401 # fd.write("proxy_username=" + repo.proxy_username + "\n")
402 #if repo.proxy_password:
403 # fd.write("proxy_password=" + repo.proxy_password + "\n")
405 fd.write("gpgkey=" + repo.gpgkey + "\n")
406 fd.write("gpgcheck=1\n")
408 fd.write("gpgcheck=0\n")
409 if type == "source" or type == "debuginfo" or repo.disable:
410 fd.write("enabled=0\n")
412 fd.write("enabled=1\n")
415 def __create_repo_file(self, repo, repodir):
416 fs.makedirs(self.path(repodir))
417 f = open(self.path(repodir + "/" + repo.name + ".repo"), "w")
418 self.__create_repo_section(repo, "base", f)
420 self.__create_repo_section(repo, "debuginfo", f)
422 self.__create_repo_section(repo, "source", f)
425 def apply(self, ksrepo, repodata):
426 for repo in ksrepo.repoList:
428 #self.__create_repo_file(repo, "/etc/yum.repos.d")
429 self.__create_repo_file(repo, "/etc/zypp/repos.d")
430 """ Import repo gpg keys """
432 for repo in repodata:
435 "--root=%s" % self.instroot,
439 class RPMMacroConfig(KickstartConfig):
440 """A class to apply the specified rpm macros to the filesystem"""
444 if not os.path.exists(self.path("/etc/rpm")):
445 os.mkdir(self.path("/etc/rpm"))
446 f = open(self.path("/etc/rpm/macros.imgcreate"), "w+")
448 f.write("%_excludedocs 1\n")
449 f.write("%__file_context_path %{nil}\n")
450 if inst_langs(ks) != None:
451 f.write("%_install_langs ")
452 f.write(inst_langs(ks))
456 class NetworkConfig(KickstartConfig):
457 """A class to apply a kickstart network configuration to a system."""
458 def write_ifcfg(self, network):
459 p = self.path("/etc/sysconfig/network-scripts/ifcfg-" + network.device)
464 f.write("DEVICE=%s\n" % network.device)
465 f.write("BOOTPROTO=%s\n" % network.bootProto)
467 if network.bootProto.lower() == "static":
469 f.write("IPADDR=%s\n" % network.ip)
471 f.write("NETMASK=%s\n" % network.netmask)
474 f.write("ONBOOT=on\n")
476 f.write("ONBOOT=off\n")
479 f.write("ESSID=%s\n" % network.essid)
482 if network.ethtool.find("autoneg") == -1:
483 network.ethtool = "autoneg off " + network.ethtool
484 f.write("ETHTOOL_OPTS=%s\n" % network.ethtool)
486 if network.bootProto.lower() == "dhcp":
488 f.write("DHCP_HOSTNAME=%s\n" % network.hostname)
489 if network.dhcpclass:
490 f.write("DHCP_CLASSID=%s\n" % network.dhcpclass)
493 f.write("MTU=%s\n" % network.mtu)
497 def write_wepkey(self, network):
498 if not network.wepkey:
501 p = self.path("/etc/sysconfig/network-scripts/keys-" + network.device)
504 f.write("KEY=%s\n" % network.wepkey)
507 def write_sysconfig(self, useipv6, hostname, gateway):
508 path = self.path("/etc/sysconfig/network")
512 f.write("NETWORKING=yes\n")
515 f.write("NETWORKING_IPV6=yes\n")
517 f.write("NETWORKING_IPV6=no\n")
520 f.write("HOSTNAME=%s\n" % hostname)
522 f.write("HOSTNAME=localhost.localdomain\n")
525 f.write("GATEWAY=%s\n" % gateway)
529 def write_hosts(self, hostname):
531 if hostname and hostname != "localhost.localdomain":
532 localline += hostname + " "
533 l = hostname.split(".")
535 localline += l[0] + " "
536 localline += "localhost.localdomain localhost"
538 path = self.path("/etc/hosts")
541 f.write("127.0.0.1\t\t%s\n" % localline)
542 f.write("::1\t\tlocalhost6.localdomain6 localhost6\n")
545 def write_resolv(self, nodns, nameservers):
546 if nodns or not nameservers:
549 path = self.path("/etc/resolv.conf")
553 for ns in (nameservers):
555 f.write("nameserver %s\n" % ns)
559 def apply(self, ksnet):
560 fs.makedirs(self.path("/etc/sysconfig/network-scripts"))
568 for network in ksnet.network:
569 if not network.device:
570 raise errors.KsError("No --device specified with "
571 "network kickstart command")
573 if (network.onboot and network.bootProto.lower() != "dhcp" and
574 not (network.ip and network.netmask)):
575 raise errors.KsError("No IP address and/or netmask "
576 "specified with static "
577 "configuration for '%s'" %
580 self.write_ifcfg(network)
581 self.write_wepkey(network)
589 hostname = network.hostname
591 gateway = network.gateway
593 if network.nameserver:
594 nameservers = network.nameserver.split(",")
596 self.write_sysconfig(useipv6, hostname, gateway)
597 self.write_hosts(hostname)
598 self.write_resolv(nodns, nameservers)
601 def get_image_size(ks, default = None):
603 for p in ks.handler.partition.partitions:
604 if p.mountpoint == "/" and p.size:
607 return int(__size) * 1024L * 1024L
611 def get_image_fstype(ks, default = None):
612 for p in ks.handler.partition.partitions:
613 if p.mountpoint == "/" and p.fstype:
617 def get_image_fsopts(ks, default = None):
618 for p in ks.handler.partition.partitions:
619 if p.mountpoint == "/" and p.fsopts:
625 if isinstance(ks.handler.device, kscommands.device.FC3_Device):
626 devices.append(ks.handler.device)
628 devices.extend(ks.handler.device.deviceList)
631 for device in devices:
632 if not device.moduleName:
634 modules.extend(device.moduleName.split(":"))
638 def get_timeout(ks, default = None):
639 if not hasattr(ks.handler.bootloader, "timeout"):
641 if ks.handler.bootloader.timeout is None:
643 return int(ks.handler.bootloader.timeout)
645 def get_kernel_args(ks, default = "ro liveimg"):
646 if not hasattr(ks.handler.bootloader, "appendLine"):
648 if ks.handler.bootloader.appendLine is None:
650 return "%s %s" %(default, ks.handler.bootloader.appendLine)
652 def get_menu_args(ks, default = ""):
653 if not hasattr(ks.handler.bootloader, "menus"):
655 if ks.handler.bootloader.menus in (None, ""):
657 return "%s" % ks.handler.bootloader.menus
659 def get_default_kernel(ks, default = None):
660 if not hasattr(ks.handler.bootloader, "default"):
662 if not ks.handler.bootloader.default:
664 return ks.handler.bootloader.default
666 def get_repos(ks, repo_urls = {}):
668 for repo in ks.handler.repo.repoList:
670 if hasattr(repo, "includepkgs"):
671 inc.extend(repo.includepkgs)
674 if hasattr(repo, "excludepkgs"):
675 exc.extend(repo.excludepkgs)
677 baseurl = repo.baseurl
678 mirrorlist = repo.mirrorlist
680 if repo.name in repo_urls:
681 baseurl = repo_urls[repo.name]
684 if repos.has_key(repo.name):
685 msger.warning("Overriding already specified repo %s" %(repo.name,))
688 if hasattr(repo, "proxy"):
690 proxy_username = None
691 if hasattr(repo, "proxy_username"):
692 proxy_username = repo.proxy_username
693 proxy_password = None
694 if hasattr(repo, "proxy_password"):
695 proxy_password = repo.proxy_password
696 if hasattr(repo, "debuginfo"):
697 debuginfo = repo.debuginfo
698 if hasattr(repo, "source"):
700 if hasattr(repo, "gpgkey"):
702 if hasattr(repo, "disable"):
703 disable = repo.disable
705 if hasattr(repo, "ssl_verify"):
706 ssl_verify = repo.ssl_verify == "yes"
708 if hasattr(repo, "cost"):
711 if hasattr(repo, "priority"):
712 priority = repo.priority
714 repos[repo.name] = (repo.name, baseurl, mirrorlist, inc, exc,
715 proxy, proxy_username, proxy_password, debuginfo,
716 source, gpgkey, disable, ssl_verify, cost, priority)
718 return repos.values()
720 def convert_method_to_repo(ks):
722 ks.handler.repo.methodToRepo()
723 except (AttributeError, kserrors.KickstartError):
726 def get_packages(ks, required = []):
727 return ks.handler.packages.packageList + required
729 def get_groups(ks, required = []):
730 return ks.handler.packages.groupList + required
732 def get_excluded(ks, required = []):
733 return ks.handler.packages.excludedList + required
735 def get_partitions(ks, required = []):
736 return ks.handler.partition.partitions
738 def ignore_missing(ks):
739 return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE
741 def exclude_docs(ks):
742 return ks.handler.packages.excludeDocs
745 if hasattr(ks.handler.packages, "instLange"):
746 return ks.handler.packages.instLange
747 elif hasattr(ks.handler.packages, "instLangs"):
748 return ks.handler.packages.instLangs
751 def get_post_scripts(ks):
753 for s in ks.handler.scripts:
754 if s.type != ksparser.KS_SCRIPT_POST:
759 def add_repo(ks, repostr):
760 args = repostr.split()
761 repoobj = ks.handler.repo.parse(args[1:])
762 if repoobj and repoobj not in ks.handler.repo.repoList:
763 ks.handler.repo.repoList.append(repoobj)
765 def remove_all_repos(ks):
766 while len(ks.handler.repo.repoList) != 0:
767 del ks.handler.repo.repoList[0]
769 def remove_duplicate_repos(ks):
773 if len(ks.handler.repo.repoList) < 2:
775 if i >= len(ks.handler.repo.repoList) - 1:
777 name = ks.handler.repo.repoList[i].name
778 baseurl = ks.handler.repo.repoList[i].baseurl
779 if j < len(ks.handler.repo.repoList):
780 if (ks.handler.repo.repoList[j].name == name or \
781 ks.handler.repo.repoList[j].baseurl == baseurl):
782 del ks.handler.repo.repoList[j]
785 if j >= len(ks.handler.repo.repoList):
792 def resolve_groups(creatoropts, repometadata):
794 if 'zypp' == creatoropts['pkgmgr']:
796 ks = creatoropts['ks']
798 for repo in repometadata:
799 """ Mustn't replace group with package list if repo is ready for the
800 corresponding package manager.
803 if iszypp and repo["patterns"]:
805 if not iszypp and repo["comps"]:
808 # But we also must handle such cases, use zypp but repo only has comps,
809 # use yum but repo only has patterns, use zypp but use_comps is true,
810 # use yum but use_comps is false.
812 if iszypp and repo["comps"]:
813 groupfile = repo["comps"]
814 get_pkglist_handler = misc.get_pkglist_in_comps
815 if not iszypp and repo["patterns"]:
816 groupfile = repo["patterns"]
817 get_pkglist_handler = misc.get_pkglist_in_patterns
822 if i >= len(ks.handler.packages.groupList):
824 pkglist = get_pkglist_handler(
825 ks.handler.packages.groupList[i].name,
828 del ks.handler.packages.groupList[i]
830 if pkg not in ks.handler.packages.packageList:
831 ks.handler.packages.packageList.append(pkg)