-#!@BASH@
+#!@PYTHON@
# Copyright 1999-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Id$
-# This script tries to quickly create a Gentoo binary package using the
-# VDB_PATH/category/pkg/* files
-#
-# Resulting tbz2 file will be created in ${PKGDIR} ...
-# default is /usr/portage/packages/All/
-
-# not in prefix
-#if [[ ${UID} != "0" ]] ; then
-# echo "You must run this as root"
-# exit 1
-#fi
-
-# We need to ensure a sane umask for the packages that will be created.
-umask 022
-
-# We need this portage cruft to be before --help output because
-# we utilize some of these vars in the usage info :/
-eval $(portageq envvar -v NOCOLOR PKGDIR PORTAGE_BIN_PATH PORTAGE_NICENESS \
- PORTAGE_PYM_PATH PORTAGE_TMPDIR ROOT)
-export PKGDIR PORTAGE_TMPDIR ROOT
-
-[[ -n ${PORTAGE_NICENESS} ]] && renice $PORTAGE_NICENESS $$ > /dev/null
-
-# Make sure the xpak module is in PYTHONPATH
-export PYTHONPATH=${PORTAGE_PYM_PATH}
-export PORTAGE_DB=$(portageq vdb_path)
-
-version() {
- local svnrev='$Rev$'
- svnrev=${svnrev#* }
- echo "quickpkg-${svnrev% *}"
- exit 0
-}
-usage() {
- cat <<-EOF
- Usage: quickpkg [options] <list of pkgs>
-
- Options:
- -C, --nocolor Disable color output
- -x, --debug Run with shell debug turned on
- -V, --version Print version and exit
- -h, --help This cruft output
-
- A pkg can be of the form:
- - ${PORTAGE_DB}/<CATEGORY>/<PKG-VERSION>/
- - single depend-type atom ...
- if portage can emerge it, quickpkg can make a package
- for exact definitions of depend atoms, see ebuild(5)
-
- Examples:
- quickpkg ${PORTAGE_DB}/net-www/apache-1.3.27-r1
- package up apache, just version 1.3.27-r1
- quickpkg apache
- package up apache, all versions of apache installed
- quickpkg =apache-1.3.27-r1
- quickpkg =apache-1.3.27-r1
- EOF
- if [[ -n $1 ]] ; then
- echo ""
- echo "Unknown arguments: $*" 1>&2
- exit 1
- else
- exit 0
- fi
-}
-
-SET_X="no"
-while [[ -n $1 ]] ; do
- case $1 in
- -C|--nocolor) export NOCOLOR="true";;
- -x|--debug) SET_X="yes";;
- -V|--version) version;;
- -h|--help) usage;;
- -*) usage "$1";;
- *) break;;
- esac
- shift
-done
-[[ ${SET_X} == "yes" ]] && set -x
-
-source "${PORTAGE_BIN_PATH}/isolated-functions.sh"
-
-# here we make a package given a little info
-# $1 = package-name w/version
-# $2 = category
-do_pkg() {
- mkdir -p "${PORTAGE_TMPDIR}/binpkgs" || exit 1
- chmod 0750 "${PORTAGE_TMPDIR}/binpkgs"
- MYDIR="${PORTAGE_TMPDIR}/binpkgs/$1"
- SRCDIR="${PORTAGE_DB}/$2/$1"
- LOG="${PORTAGE_TMPDIR}/binpkgs/$1-quickpkglog"
-
- ebegin "Building package for $1"
- (
- # clean up temp directory
- rm -rf "${MYDIR}"
-
- # get pkg info files
- mkdir -p "${MYDIR}"/temp
- cp "${SRCDIR}"/* "${MYDIR}"/temp/
- [ -d "${PKGDIR}"/All ] || mkdir -p "${PKGDIR}"/All
- local pkg_dest="${PKGDIR}/All/${1}.tbz2"
- local pkg_tmp="${PKGDIR}/All/${1}.tbz2.$$"
-
- # create filelist and a basic tbz2
- gawk '{
- if ($1 != "dir") {
- if ($1 == "obj")
- NF=NF-2
- else if ($1 == "sym")
- NF=NF-3
- }
- print
- }' "${SRCDIR}"/CONTENTS | cut -f2- -d" " - | sed -e 's:^/:./:' | \
- while read f; do
- [ -d "${ROOT}/${f}" ] && [ -h "${ROOT}/${f}" ] && continue
- echo "$f"
- done > "${MYDIR}"/filelist
- tar vjcf "${pkg_tmp}" -C "${ROOT}" --files-from="${MYDIR}"/filelist --no-recursion
-
- # join together the basic tbz2 and the pkg info files
- python -c "import portage.xpak; t=portage.xpak.tbz2('${pkg_tmp}'); t.recompose('${MYDIR}/temp')"
-
- # move the final binary package to PKGDIR
- mv -f "${pkg_tmp}" "${pkg_dest}"
- [ -d "${PKGDIR}/$2" ] || mkdir -p "${PKGDIR}/$2"
- ( cd "${PKGDIR}/$2" && ln -s ../All/$1.tbz2 )
-
- # cleanup again
- rm -rf "${MYDIR}"
- ) >& "${LOG}"
-
- if [ -e "${PKGDIR}/All/$1.tbz2" ] ; then
- rm -f "${LOG}"
- PKGSTATS="${PKGSTATS}"$'\n'"$(einfo $1: $(du -h "${PKGDIR}/All/$1.tbz2" | gawk '{print $1}'))"
- eend 0
- else
- cat ${LOG}
- PKGSTATS="${PKGSTATS}"$'\n'"$(ewarn $1: not created)"
- eend 1
- fi
-}
-
-# here we parse the parameters given to use on the cmdline
-export PKGERROR=""
-export PKGSTATS=""
-for x in "$@" ; do
-
- # they gave us full path
- if [ -e "${x}"/CONTENTS ] ; then
- x=$(readlink -f $x)
- pkg=$(echo ${x} | cut -d/ -f6)
- cat=$(echo ${x} | cut -d/ -f5)
- do_pkg "${pkg}" "${cat}"
-
- # lets figure out what they want
- else
- DIRLIST=$(portageq match "${ROOT}" "${x}")
- if [ -z "${DIRLIST}" ] ; then
- eerror "Could not find anything to match '${x}'; skipping"
- export PKGERROR="${PKGERROR} ${x}"
+import errno, signal, sys, os
+
+def quickpkg_main(options, args, eout):
+ from portage import dblink, dep_expand, catsplit, isvalidatom, xpak
+ from portage.util import ensure_dirs
+ from portage.exception import InvalidData
+ from portage.dbapi.vartree import tar_contents
+ import tarfile
+ import portage
+ root = portage.settings["ROOT"]
+ trees = portage.db[root]
+ vartree = trees["vartree"]
+ vardb = vartree.dbapi
+ bintree = trees["bintree"]
+ if not os.access(bintree.pkgdir, os.W_OK):
+ eout.eerror("No write access to '%s'" % bintree.pkgdir)
+ return errno.EACCES
+ successes = []
+ missing = []
+ for arg in args:
+ try:
+ atom = dep_expand(arg, mydb=vardb, settings=vartree.settings)
+ except ValueError, e:
+ # Multiple matches thrown from cpv_expand
+ eout.eerror("Please use a more specific atom: %s" % \
+ " ".join(e.args[0]))
+ del e
+ missing.append(arg)
continue
- fi
-
- for d in ${DIRLIST} ; do
- pkg=$(echo ${d} | cut -d/ -f2)
- cat=$(echo ${d} | cut -d/ -f1)
- if [ -f "${PORTAGE_DB}/${cat}/${pkg}/CONTENTS" ] ; then
- do_pkg ${pkg} ${cat}
- elif [ -d "${PORTAGE_DB}/${cat}/${pkg}" ] ; then
- ewarn "Package '${cat}/${pkg}' was injected; skipping"
- else
- eerror "Unhandled case (${cat}/${pkg}) !"
- eerror "Please file a bug at http://bugs.gentoo.org/"
- exit 10
- fi
- done
- fi
-
-done
-
-if [ -z "${PKGSTATS}" ] ; then
- eerror "No packages found"
- exit 1
-else
- echo $'\n'"$(einfo Packages now in ${PKGDIR}:)${PKGSTATS}"
-fi
-if [ ! -z "${PKGERROR}" ] ; then
- ewarn "The following packages could not be found:"
- ewarn "${PKGERROR}"
- exit 2
-fi
-
-exit 0
+ except InvalidData, e:
+ eout.eerror("Invalid atom: %s" % str(e))
+ del e
+ missing.append(arg)
+ continue
+ if not isvalidatom(atom):
+ eout.eerror("Invalid atom: %s" % atom)
+ missing.append(arg)
+ continue
+ matches = vardb.match(atom)
+ pkgs_for_arg = 0
+ for cpv in matches:
+ bintree.prevent_collision(cpv)
+ cat, pkg = catsplit(cpv)
+ dblnk = dblink(cat, pkg, root,
+ vartree.settings, treetype="vartree",
+ vartree=vartree)
+ dblnk.lockdb()
+ try:
+ if not dblnk.exists():
+ # unmerged by a concurrent process
+ continue
+ eout.ebegin("Building package for %s" % cpv)
+ pkgs_for_arg += 1
+ contents = dblnk.getcontents()
+ xpdata = xpak.xpak(dblnk.dbdir)
+ binpkg_tmpfile = os.path.join(bintree.pkgdir,
+ cpv + ".tbz2." + str(os.getpid()))
+ ensure_dirs(os.path.dirname(binpkg_tmpfile))
+ tar = tarfile.open(binpkg_tmpfile, "w:bz2")
+ tar_contents(contents, root, tar)
+ tar.close()
+ xpak.tbz2(binpkg_tmpfile).recompose_mem(xpdata)
+ finally:
+ dblnk.unlockdb()
+ bintree.inject(cpv, filename=binpkg_tmpfile)
+ binpkg_path = bintree.getname(cpv)
+ try:
+ s = os.stat(binpkg_path)
+ except OSError, e:
+ # Sanity check, shouldn't happen normally.
+ eout.eend(1)
+ eout.eerror(str(e))
+ del e
+ eout.eerror("Failed to create package: '%s'" % binpkg_path)
+ else:
+ eout.eend(0)
+ successes.append((cpv, s.st_size))
+ if not pkgs_for_arg:
+ eout.eerror("Could not find anything " + \
+ "to match '%s'; skipping" % arg)
+ missing.append(arg)
+ if not successes:
+ eout.eerror("No packages found")
+ return 1
+ print
+ eout.einfo("Packages now in '%s':" % bintree.pkgdir)
+ import math
+ units = {10:'K', 20:'M', 30:'G', 40:'T',
+ 50:'P', 60:'E', 70:'Z', 80:'Y'}
+ for cpv, size in successes:
+ if not size:
+ # avoid OverflowError in math.log()
+ size_str = "0"
+ else:
+ power_of_2 = math.log(size, 2)
+ power_of_2 = 10*int(power_of_2/10)
+ unit = units.get(power_of_2)
+ if unit:
+ size = float(size)/(2**power_of_2)
+ size_str = "%.1f" % size
+ if len(size_str) > 4:
+ # emulate `du -h`, don't show too many sig figs
+ size_str = str(int(size))
+ size_str += unit
+ else:
+ size_str = str(size)
+ eout.einfo("%s: %s" % (cpv, size_str))
+ if missing:
+ print
+ eout.ewarn("The following packages could not be found:")
+ eout.ewarn(" ".join(missing))
+ return 2
+ return os.EX_OK
+
+if __name__ == "__main__":
+ usage = "Usage: quickpkg [options] <list of package atoms>"
+ from optparse import OptionParser
+ parser = OptionParser(usage=usage)
+ options, args = parser.parse_args(sys.argv[1:])
+ if not args:
+ parser.error("no packages atoms given")
+ # We need to ensure a sane umask for the packages that will be created.
+ old_umask = os.umask(022)
+ from portage.output import get_term_size, EOutput
+ eout = EOutput()
+ def sigwinch_handler(signum, frame):
+ lines, eout.term_columns = get_term_size()
+ signal.signal(signal.SIGWINCH, sigwinch_handler)
+ try:
+ retval = quickpkg_main(options, args, eout)
+ finally:
+ os.umask(old_umask)
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
+ sys.exit(retval)
\fBeconf\fR \fI[configure options]\fR
This is used as a replacement for configure. Performs:
.nf
-configure \\
+${\fIECONF_SOURCE\fR:-.}/configure \\
\-\-prefix=/usr \\
\-\-host=${CHOST} \\
\-\-mandir=/usr/share/man \\
important informational messages. This variable is unset by default.
If it is set to any value pauses are ignored.
.TP
+\fBEXTRA_ECONF\fR = \fI[configure options string]\fR
+Contains additional options that \fBeconf\fR will append to configure
+script arguments (see \fBebuild\fR(5)).
+.TP
\fBFEATURES\fR = \fI"sandbox ccache"\fR
Defines actions portage takes by default. These options should
not be changed by anyone but developers and/or maintainers. 'sandbox' is an important
This variable defines the directory in which per\-ebuild logs are kept.
Logs are created only when this is set and writable.
.TP
-\fBPORTAGE_BINHOST\fR = \fI"ftp://login:pass@grp.mirror.site/pub/grp/i686/athlon\-xp"\fR
+\fBPORTAGE_BINHOST\fR = \fI"ftp://login:pass@grp.mirror.site/pub/grp/i686/athlon\-xp/"\fR
This is the host from which portage will grab prebuilt\-binary packages.
The list is a single entry specifying the full address of the directory
serving the tbz2's for your system. This is only used when running with
the get binary pkg options are given to \fBemerge\fR. Review \fBemerge\fR(1)
-for more information. Note that it should point to the 'All' directory on
-the host that creates the binary packages and not to the root of the \fBPKGDIR\fR.
+for more information. For versions of portage less that 2.2, this variable
+should point to the 'All' directory on the host that creates the binary
+packages and not to the root of the \fBPKGDIR\fR. Starting with portage 2.2,
+it should point to a directory containing a 'Packages' index file. If
+${PORTAGE_BINHOST}/Packages does not exist then portage will attempt to use
+the older protocol.
+.TP
+\fBPORTAGE_BINHOST_HEADER_URI\fR = \fI"ftp://login:pass@grp.mirror.site/pub/grp/i686/athlon\-xp/"\fR
+This variable only makes sense on a system that will serve as a binhost and
+build packages for clients. It defines the URI header field for the package
+index file which is located at ${PKGDIR}/Packages. Clients that have
+\fBPORTAGE_BINHOST\fR properly configured will be able to fetch the index and
+use the URI header field as a base URI for fetching binary packages. If the URI
+header field is not defined then the client will use it's ${PORTAGE_BINHOST}
+setting as the base URI.
.TP
\fBPORTAGE_COMPRESS\fR = \fI"bzip2"\fR
This variable contains the command used to compress documentation during the
self.blocker_parents.setdefault(
tuple(mybigkey), set()).add(myparent)
return 1
- if not arg and myroot == self.target_root:
- arg = portage.best_match_to_list(mykey, self.args_keys)
+
# select the correct /var database that we'll be checking against
vardbapi = self.trees[myroot]["vartree"].dbapi
portdb = self.trees[myroot]["porttree"].dbapi
mydbapi = self.trees[myroot][self.pkg_tree_map[mytype]].dbapi
+ if not arg and myroot == self.target_root:
+ cpv_slot = "%s:%s" % (mykey, mydbapi.aux_get(mykey, ["SLOT"])[0])
+ arg = portage.best_match_to_list(cpv_slot, self.args_keys)
+
if myuse is None:
self.pkgsettings[myroot].setcpv(mykey, mydb=portdb)
myuse = self.pkgsettings[myroot]["USE"].split()
+# Copyright 1998-2007 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
+
from portage.dep import dep_getslot, dep_getkey, match_from_list
from portage.locks import unlockfile
from portage.output import red
+# Copyright 1998-2007 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
from portage.dep import isvalidatom, isjustname, dep_getkey, match_from_list
from portage.dbapi.virtual import fakedbapi
from portage.exception import InvalidPackageName, InvalidAtom
self.populated = 0
self.tree = {}
self._remote_has_index = False
+ self._remote_base_uri = None
self._remotepkgs = None # remote metadata indexed by cpv
self.remotepkgs = {} # indexed by tbz2 name (deprecated)
self.invalids = []
self._pkg_paths = {}
self._all_directory = os.path.isdir(
os.path.join(self.pkgdir, "All"))
+ self._pkgindex_version = 0
+ self._pkgindex_hashes = ["MD5","SHA1"]
self._pkgindex_file = os.path.join(self.pkgdir, "Packages")
self._pkgindex_keys = set(["CPV", "SLOT", "MTIME", "SIZE"])
self._pkgindex_header_keys = set(["ACCEPT_KEYWORDS", "CBUILD",
other_cat = portage.xpak.tbz2(dest_path).getfile("CATEGORY")
if other_cat:
other_cat = other_cat.strip()
- self._move_from_all(other_cat + "/" + mypkg)
+ other_cpv = other_cat + "/" + mypkg
+ self._move_from_all(other_cpv)
+ self.inject(other_cpv)
"""The file may or may not exist. Move it if necessary and update
internal state for future calls to getname()."""
self._move_to_all(cpv)
+ if os.path.exists(full_path):
+ self.inject(cpv)
def _move_to_all(self, cpv):
"""If the file exists, move it. Whether or not it exists, update state
dirs.sort()
dirs.insert(0, "All")
pkgindex = portage.getbinpkg.PackageIndex()
- header = pkgindex.header
- metadata = pkgindex.packages
pf_index = None
try:
f = open(self._pkgindex_file)
finally:
f.close()
del f
+ if not self._pkgindex_version_supported(pkgindex):
+ pkgindex = portage.getbinpkg.PackageIndex()
+ header = pkgindex.header
+ metadata = pkgindex.packages
update_pkgindex = False
for mydir in dirs:
for myfile in listdir(os.path.join(self.pkgdir, mydir)):
# no timestamp in the header, something's wrong
pkgindex = None
else:
- if local_timestamp != remote_timestamp:
+ if not self._pkgindex_version_supported(rmt_idx):
+ writemsg("\n\n!!! Binhost package index version" + \
+ " is not supported: '%s'\n" % \
+ rmt_idx.header.get("VERSION"), noiselevel=-1)
+ pkgindex = None
+ elif local_timestamp != remote_timestamp:
rmt_idx.readBody(f)
pkgindex = rmt_idx
finally:
if pkgindex:
self._remotepkgs = pkgindex.packages
self._remote_has_index = True
+ self._remote_base_uri = pkgindex.header.get("URI", base_url)
self.remotepkgs = {}
for cpv in self._remotepkgs:
self.dbapi.cpv_inject(cpv)
noiselevel=-1)
return
slot = slot.strip()
- from portage.checksum import perform_md5
- md5 = perform_md5(full_path)
+ from portage.checksum import perform_multiple_checksums
+ digests = perform_multiple_checksums(
+ full_path, hashes=self._pkgindex_hashes)
self.dbapi.cpv_inject(cpv)
self.dbapi._aux_cache.pop(cpv, None)
finally:
f.close()
del f
- d = {}
+ if not self._pkgindex_version_supported(pkgindex):
+ pkgindex = portage.getbinpkg.PackageIndex()
+ d = digests
d["CPV"] = cpv
d["SLOT"] = slot
d["MTIME"] = str(long(s.st_mtime))
d["SIZE"] = str(s.st_size)
- d["MD5"] = str(md5)
rel_path = self._pkg_paths[cpv]
# record location if it's non-default
if rel_path != cpv + ".tbz2":
def _update_pkgindex_header(self, header):
portdir = normalize_path(os.path.realpath(self.settings["PORTDIR"]))
- profiles_base = os.path.join(portdir, "profiles")
+ profiles_base = os.path.join(portdir, "profiles") + os.path.sep
profile_path = normalize_path(os.path.realpath(self.settings.profile_path))
- profile_path = profile_path.lstrip(profiles_base)
+ if profile_path.startswith(profiles_base):
+ profile_path = profile_path[len(profiles_base):]
header["PROFILE"] = profile_path
+ header["VERSION"] = str(self._pkgindex_version)
+ base_uri = self.settings.get("PORTAGE_BINHOST_HEADER_URI")
+ if base_uri:
+ header["URI"] = base_uri
+ else:
+ header.pop("URI", None)
for k in self._pkgindex_header_keys:
v = self.settings.get(k, None)
if v:
else:
header.pop(k, None)
+ def _pkgindex_version_supported(self, pkgindex):
+ version = pkgindex.header.get("VERSION")
+ if version:
+ try:
+ if int(version) <= self._pkgindex_version:
+ return True
+ except ValueError:
+ pass
+ return False
+
def exists_specific(self, cpv):
if not self.populated:
self.populate()
except (OSError, IOError):
pass
from urlparse import urljoin
- base_url = self.settings["PORTAGE_BINHOST"]
+ base_url = self._remote_base_uri
fcmd = self.settings["RESUMECOMMAND"]
if self._remote_has_index:
rel_url = self._remotepkgs[pkgname].get("PATH")
if success and "strict" in self.settings.features:
metadata = self._remotepkgs[pkgname]
digests = {}
- if "MD5" in metadata:
- digests["MD5"] = self._remotepkgs[pkgname]["MD5"]
+ from portage.checksum import hashfunc_map, verify_all
+ for k in hashfunc_map:
+ v = metadata.get(k)
+ if not v:
+ continue
+ digests[k] = v
if "SIZE" in metadata:
try:
digests["size"] = long(self._remotepkgs[pkgname]["SIZE"])
writemsg("!!! Malformed SIZE attribute in remote " + \
"metadata for '%s'\n" % pkgname)
if digests:
- from portage.checksum import verify_all
ok, reason = verify_all(tbz2_path, digests)
if not ok:
raise portage.exception.DigestException(
+# Copyright 1998-2007 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
from portage.cache.cache_errors import CacheError
from portage.const import REPO_NAME_LOC
from portage.data import portage_gid, secpass
+# Copyright 1998-2007 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
from portage.checksum import perform_md5
from portage.const import CACHE_PATH, CONFIG_MEMORY_FILE, PORTAGE_BIN_PATH, \
PRIVATE_PATH, VDB_PATH, EPREFIX
def isregular(self):
"Is this a regular package (does it have a CATEGORY file? A dblink can be virtual *and* regular)"
return os.path.exists(os.path.join(self.dbdir, "CATEGORY"))
+
+def tar_contents(contents, root, tar, onProgress=None):
+ from portage import normalize_path
+ root = normalize_path(root).rstrip(os.path.sep) + os.path.sep
+ id_strings = {}
+ maxval = len(contents)
+ curval = 0
+ if onProgress:
+ onProgress(maxval, 0)
+ paths = contents.keys()
+ paths.sort()
+ for path in paths:
+ curval += 1
+ try:
+ lst = os.lstat(path)
+ except OSError, e:
+ if e.errno != errno.ENOENT:
+ raise
+ del e
+ if onProgress:
+ onProgress(maxval, curval)
+ continue
+ contents_type = contents[path][0]
+ if path.startswith(root):
+ arcname = path[len(root):]
+ else:
+ raise ValueError("invalid root argument: '%s'" % root)
+ live_path = path
+ if 'dir' == contents_type and \
+ not stat.S_ISDIR(lst.st_mode) and \
+ os.path.isdir(live_path):
+ # Even though this was a directory in the original ${D}, it exists
+ # as a symlink to a directory in the live filesystem. It must be
+ # recorded as a real directory in the tar file to ensure that tar
+ # can properly extract it's children.
+ live_path = os.path.realpath(live_path)
+ tarinfo = tar.gettarinfo(live_path, arcname)
+ # store numbers instead of real names like tar's --numeric-owner
+ tarinfo.uname = id_strings.setdefault(tarinfo.uid, str(tarinfo.uid))
+ tarinfo.gname = id_strings.setdefault(tarinfo.gid, str(tarinfo.gid))
+
+ if stat.S_ISREG(lst.st_mode):
+ f = file(path)
+ try:
+ tar.addfile(tarinfo, f)
+ finally:
+ f.close()
+ else:
+ tar.addfile(tarinfo)
+ if onProgress:
+ onProgress(maxval, curval)
+# Copyright 1998-2007 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
+
from portage.dbapi import dbapi
from portage import cpv_getkey