blob: b19a853251256f4682d3162da3eb245af3ad335a [file] [log] [blame]
# Copyright 2010-2023 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
__all__ = ["env_update"]
import errno
import glob
import stat
import subprocess
import time
import portage
from portage import os, _encodings, _unicode_decode, _unicode_encode
from portage.checksum import prelink_capable
from portage.data import ostype
from portage.exception import ParseError
from portage.localization import _
from portage.process import find_binary
from portage.util import (
atomic_ofstream,
ensure_dirs,
getconfig,
normalize_path,
writemsg,
)
from portage.util.listdir import listdir
from portage.dbapi.vartree import vartree
from portage.package.ebuild.config import config
def env_update(
makelinks=1,
target_root=None,
prev_mtimes=None,
contents=None,
env=None,
writemsg_level=None,
vardbapi=None,
):
"""
Parse /etc/env.d and use it to generate /etc/profile.env, csh.env,
ld.so.conf, and prelink.conf. Finally, run ldconfig. When ldconfig is
called, its -X option will be used in order to avoid potential
interference with installed soname symlinks that are required for
correct operation of FEATURES=preserve-libs for downgrade operations.
It's not necessary for ldconfig to create soname symlinks, since
portage will use NEEDED.ELF.2 data to automatically create them
after src_install if they happen to be missing.
@param makelinks: True if ldconfig should be called, False otherwise
@param target_root: root that is passed to the ldconfig -r option,
defaults to portage.settings["ROOT"].
@type target_root: String (Path)
"""
if vardbapi is None:
if isinstance(env, config):
vardbapi = vartree(settings=env).dbapi
else:
if target_root is None:
eprefix = portage.settings["EPREFIX"]
target_root = portage.settings["ROOT"]
target_eroot = portage.settings["EROOT"]
else:
eprefix = portage.const.EPREFIX
target_eroot = os.path.join(target_root, eprefix.lstrip(os.sep))
target_eroot = target_eroot.rstrip(os.sep) + os.sep
if hasattr(portage, "db") and target_eroot in portage.db:
vardbapi = portage.db[target_eroot]["vartree"].dbapi
else:
settings = config(
config_root=target_root, target_root=target_root, eprefix=eprefix
)
target_root = settings["ROOT"]
if env is None:
env = settings
vardbapi = vartree(settings=settings).dbapi
# Lock the config memory file to prevent symlink creation
# in merge_contents from overlapping with env-update.
vardbapi._fs_lock()
try:
return _env_update(
makelinks, target_root, prev_mtimes, contents, env, writemsg_level
)
finally:
vardbapi._fs_unlock()
def _env_update(makelinks, target_root, prev_mtimes, contents, env, writemsg_level):
if writemsg_level is None:
writemsg_level = portage.util.writemsg_level
if target_root is None:
target_root = portage.settings["ROOT"]
if prev_mtimes is None:
prev_mtimes = portage.mtimedb["ldpath"]
if env is None:
settings = portage.settings
else:
settings = env
eprefix = settings.get("EPREFIX", "")
eprefix_lstrip = eprefix.lstrip(os.sep)
eroot = (
normalize_path(os.path.join(target_root, eprefix_lstrip)).rstrip(os.sep)
+ os.sep
)
envd_dir = os.path.join(eroot, "etc", "env.d")
ensure_dirs(envd_dir, mode=0o755)
fns = listdir(envd_dir, EmptyOnError=1)
fns.sort()
templist = []
for x in fns:
if len(x) < 3:
continue
if not x[0].isdigit() or not x[1].isdigit():
continue
if x.startswith(".") or x.endswith("~") or x.endswith(".bak"):
continue
templist.append(x)
fns = templist
del templist
space_separated = {"CONFIG_PROTECT", "CONFIG_PROTECT_MASK"}
colon_separated = {
"ADA_INCLUDE_PATH",
"ADA_OBJECTS_PATH",
"CLASSPATH",
"INFODIR",
"INFOPATH",
"KDEDIRS",
"LDPATH",
"MANPATH",
"PATH",
"PKG_CONFIG_PATH",
"PRELINK_PATH",
"PRELINK_PATH_MASK",
"PYTHONPATH",
"ROOTPATH",
}
config_list = []
for x in fns:
file_path = os.path.join(envd_dir, x)
try:
myconfig = getconfig(file_path, expand=False)
except ParseError as e:
writemsg(f"!!! '{str(e)}'\n", noiselevel=-1)
del e
continue
if myconfig is None:
# broken symlink or file removed by a concurrent process
writemsg(f"!!! File Not Found: '{file_path}'\n", noiselevel=-1)
continue
config_list.append(myconfig)
if "SPACE_SEPARATED" in myconfig:
space_separated.update(myconfig["SPACE_SEPARATED"].split())
del myconfig["SPACE_SEPARATED"]
if "COLON_SEPARATED" in myconfig:
colon_separated.update(myconfig["COLON_SEPARATED"].split())
del myconfig["COLON_SEPARATED"]
env = {}
specials = {}
for var in space_separated:
mylist = []
for myconfig in config_list:
if var in myconfig:
for item in myconfig[var].split():
if item and not item in mylist:
mylist.append(item)
del myconfig[var] # prepare for env.update(myconfig)
if mylist:
env[var] = " ".join(mylist)
specials[var] = mylist
for var in colon_separated:
mylist = []
for myconfig in config_list:
if var in myconfig:
for item in myconfig[var].split(":"):
if item and not item in mylist:
mylist.append(item)
del myconfig[var] # prepare for env.update(myconfig)
if mylist:
env[var] = ":".join(mylist)
specials[var] = mylist
for myconfig in config_list:
"""Cumulative variables have already been deleted from myconfig so that
they won't be overwritten by this dict.update call."""
env.update(myconfig)
ldsoconf_path = os.path.join(eroot, "etc", "ld.so.conf")
try:
myld = open(
_unicode_encode(ldsoconf_path, encoding=_encodings["fs"], errors="strict"),
encoding=_encodings["content"],
errors="replace",
)
myldlines = myld.readlines()
myld.close()
oldld = []
for x in myldlines:
# each line has at least one char (a newline)
if x[:1] == "#":
continue
oldld.append(x[:-1])
except OSError as e:
if e.errno != errno.ENOENT:
raise
oldld = None
newld = specials["LDPATH"]
if oldld != newld:
# ld.so.conf needs updating and ldconfig needs to be run
myfd = atomic_ofstream(ldsoconf_path)
myfd.write("# ld.so.conf autogenerated by env-update; make all changes to\n")
myfd.write(f"# contents of {eprefix}/etc/env.d directory.\n")
for x in specials["LDPATH"]:
myfd.write(x + "\n")
myfd.close()
if eprefix:
# ldconfig needs ld.so.conf paths to be prefixed, but the bfd linker
# needs them unprefixed, so write an alternative ld.so.conf file for
# the latter. Other linkers do not use these files. See ldelf.c in
# binutils for precise bfd behavior, as well as bug #892549.
ldsoconf_path = os.path.join(eroot, "usr", "etc", "ld.so.conf")
ensure_dirs(os.path.dirname(ldsoconf_path), mode=0o755)
myfd = atomic_ofstream(ldsoconf_path)
myfd.write(
"# ld.so.conf autogenerated by env-update; make all changes to\n"
f"# contents of {eprefix}/etc/env.d directory.\n"
"# This file is only used by the bfd linker. The paths are not\n"
"# prefixed as this is automatically added by the linker.\n"
)
for x in specials["LDPATH"]:
if x.startswith(eprefix + os.path.sep):
myfd.write(x[len(eprefix) :] + "\n")
myfd.close()
potential_lib_dirs = set()
for lib_dir_glob in ("usr/lib*", "lib*"):
x = os.path.join(eroot, lib_dir_glob)
for y in glob.glob(
_unicode_encode(x, encoding=_encodings["fs"], errors="strict")
):
try:
y = _unicode_decode(y, encoding=_encodings["fs"], errors="strict")
except UnicodeDecodeError:
continue
if os.path.basename(y) != "libexec":
potential_lib_dirs.add(y[len(eroot) :])
# Update prelink.conf if we are prelink-enabled
if prelink_capable:
prelink_d = os.path.join(eroot, "etc", "prelink.conf.d")
ensure_dirs(prelink_d)
newprelink = atomic_ofstream(os.path.join(prelink_d, "portage.conf"))
newprelink.write(
"# prelink.conf autogenerated by env-update; make all changes to\n"
)
newprelink.write(f"# contents of {eprefix}/etc/env.d directory\n")
for x in sorted(potential_lib_dirs) + ["bin", "sbin"]:
newprelink.write(f"-l /{x}\n")
prelink_paths = set()
prelink_paths |= set(specials.get("LDPATH", []))
prelink_paths |= set(specials.get("PATH", []))
prelink_paths |= set(specials.get("PRELINK_PATH", []))
prelink_path_mask = specials.get("PRELINK_PATH_MASK", [])
for x in prelink_paths:
if not x:
continue
if x[-1:] != "/":
x += "/"
plmasked = 0
for y in prelink_path_mask:
if not y:
continue
if y[-1] != "/":
y += "/"
if y == x[0 : len(y)]:
plmasked = 1
break
if not plmasked:
newprelink.write(f"-h {x}\n")
for x in prelink_path_mask:
newprelink.write(f"-b {x}\n")
newprelink.close()
# Migration code path. If /etc/prelink.conf was generated by us, then
# point it to the new stuff until the prelink package re-installs.
prelink_conf = os.path.join(eroot, "etc", "prelink.conf")
try:
with open(
_unicode_encode(
prelink_conf, encoding=_encodings["fs"], errors="strict"
),
"rb",
) as f:
if (
f.readline()
== b"# prelink.conf autogenerated by env-update; make all changes to\n"
):
f = atomic_ofstream(prelink_conf)
f.write(f"-c {eprefix}/etc/prelink.conf.d/*.conf\n")
f.close()
except OSError as e:
if e.errno != errno.ENOENT:
raise
current_time = int(time.time())
mtime_changed = False
lib_dirs = set()
for lib_dir in set(specials["LDPATH"]) | potential_lib_dirs:
x = os.path.join(eroot, lib_dir.lstrip(os.sep))
try:
newldpathtime = os.stat(x)[stat.ST_MTIME]
lib_dirs.add(normalize_path(x))
except OSError as oe:
if oe.errno == errno.ENOENT:
try:
del prev_mtimes[x]
except KeyError:
pass
# ignore this path because it doesn't exist
continue
raise
if newldpathtime == current_time:
# Reset mtime to avoid the potential ambiguity of times that
# differ by less than 1 second.
newldpathtime -= 1
os.utime(x, (newldpathtime, newldpathtime))
prev_mtimes[x] = newldpathtime
mtime_changed = True
elif x in prev_mtimes:
if prev_mtimes[x] == newldpathtime:
pass
else:
prev_mtimes[x] = newldpathtime
mtime_changed = True
else:
prev_mtimes[x] = newldpathtime
mtime_changed = True
if makelinks and not mtime_changed and contents is not None:
libdir_contents_changed = False
for mypath, mydata in contents.items():
if mydata[0] not in ("obj", "sym"):
continue
head, tail = os.path.split(mypath)
if head in lib_dirs:
libdir_contents_changed = True
break
if not libdir_contents_changed:
makelinks = False
if (
"CHOST" in settings
and "CBUILD" in settings
and settings["CHOST"] != settings["CBUILD"]
):
ldconfig = find_binary(f"{settings['CHOST']}-ldconfig")
else:
ldconfig = os.path.join(eroot, "sbin", "ldconfig")
if ldconfig is None:
pass
elif not (os.access(ldconfig, os.X_OK) and os.path.isfile(ldconfig)):
ldconfig = None
# Only run ldconfig as needed
if makelinks and ldconfig:
# ldconfig has very different behaviour between FreeBSD and Linux
if ostype == "Linux" or ostype.lower().endswith("gnu"):
# We can't update links if we haven't cleaned other versions first, as
# an older package installed ON TOP of a newer version will cause ldconfig
# to overwrite the symlinks we just made. -X means no links. After 'clean'
# we can safely create links.
writemsg_level(_(f">>> Regenerating {eroot}etc/ld.so.cache...\n"))
ret = subprocess.run(
[ldconfig, "-X", "-r", target_root], cwd="/"
).returncode
elif ostype in ("FreeBSD", "DragonFly"):
writemsg_level(
_(f">>> Regenerating {target_root}var/run/ld-elf.so.hints...\n")
)
ret = subprocess.run(
[
ldconfig,
"-elf",
"-i",
"-f",
f"{target_root}var/run/ld-elf.so.hints",
f"{target_root}etc/ld.so.conf",
],
cwd="/",
).returncode
if ret > 0:
writemsg(f"!!! ldconfig failed with exit status {ret}\n", noiselevel=-1)
if ret < 0:
writemsg(f"!!! ldconfig was killed with signal {-ret}\n", noiselevel=-1)
del specials["LDPATH"]
notice = "# THIS FILE IS AUTOMATICALLY GENERATED BY env-update.\n"
notice += "# DO NOT EDIT THIS FILE."
penvnotice = notice + " CHANGES TO STARTUP PROFILES\n"
cenvnotice = penvnotice[:]
penvnotice += f"# GO INTO {eprefix}/etc/profile NOT {eprefix}/etc/profile.env\n\n"
cenvnotice += f"# GO INTO {eprefix}/etc/csh.cshrc NOT {eprefix}/etc/csh.env\n\n"
# create /etc/profile.env for bash support
profile_env_path = os.path.join(eroot, "etc", "profile.env")
with atomic_ofstream(profile_env_path) as outfile:
outfile.write(penvnotice)
env_keys = [x for x in env if x != "LDPATH"]
env_keys.sort()
for k in env_keys:
v = env[k]
if v.startswith("$") and not v.startswith("${"):
outfile.write(f"export {k}=$'{v[1:]}'\n")
else:
outfile.write(f"export {k}='{v}'\n")
# Create the systemd user environment configuration file
# /etc/environment.d/10-gentoo-env.conf with the
# environment configuration from /etc/env.d.
systemd_environment_dir = os.path.join(eroot, "etc", "environment.d")
os.makedirs(systemd_environment_dir, exist_ok=True)
systemd_gentoo_env_path = os.path.join(
systemd_environment_dir, "10-gentoo-env.conf"
)
with atomic_ofstream(systemd_gentoo_env_path) as systemd_gentoo_env:
senvnotice = notice + "\n\n"
systemd_gentoo_env.write(senvnotice)
for env_key in env_keys:
env_key_value = env[env_key]
# Skip variables with the empty string
# as value. Those sometimes appear in
# profile.env (e.g. "export GCC_SPECS=''"),
# but are invalid in systemd's syntax.
if not env_key_value:
continue
# Transform into systemd environment.d
# conf syntax, basically shell variable
# assignment (without "export ").
line = f"{env_key}={env_key_value}\n"
systemd_gentoo_env.write(line)
# create /etc/csh.env for (t)csh support
outfile = atomic_ofstream(os.path.join(eroot, "etc", "csh.env"))
outfile.write(cenvnotice)
for x in env_keys:
outfile.write(f"setenv {x} '{env[x]}'\n")
outfile.close()