Step1: build with busybox.

Signed-off-by: Chen Wang <wangchen20@iscas.ac.cn>
This commit is contained in:
2025-11-24 16:22:33 +08:00
committed by Chen Wang
commit d35682f2b7
163 changed files with 15660 additions and 0 deletions

167
support/scripts/apply-patches.sh Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env bash
# A little script I whipped up to make it easy to
# patch source trees and have sane error handling
# -Erik
#
# (c) 2002 Erik Andersen <andersen@codepoet.org>
#
# Parameters:
# - "-s", optional. Silent operation, don't print anything if there
# isn't any error.
# - the build directory, optional, default value is '.'. The place where are
# the package sources.
# - the patch directory, optional, default '../kernel-patches'. The place
# where are the scripts you want to apply.
# - other parameters are the patch name patterns, optional, default value is
# '*'. Pattern(s) describing the patch names you want to apply.
#
# The script will look recursively for patches from the patch directory. If a
# file named 'series' exists then the patches mentioned in it will be applied
# as plain patches, regardless of their file name. If no 'series' file exists,
# the script will look for file names matching pattern(s). If the name
# ends with '.tar.*', '.tbz2' or '.tgz', the file is considered as an archive
# and will be uncompressed into a directory named
# '.patches-name_of_the_archive-unpacked'. It's the turn of this directory to
# be scanned with '*' as pattern. Remember that scanning is recursive. Other
# files than series file and archives are considered as a patch.
#
# Once a patch is found, the script will try to apply it. If its name doesn't
# end with '.gz', '.bz', '.bz2', '.xz', '.zip', '.Z', '.diff*' or '.patch*',
# it will be skipped. If necessary, the patch will be uncompressed before being
# applied. The list of the patches applied is stored in '.applied_patches_list'
# file in the build directory.
set -e
silent=
if [ "$1" = "-s" ] ; then
# add option to be used by the patch tool
silent=-s
shift
fi
# Set directories from arguments, or use defaults.
builddir=${1-.}
patchdir=${2-../kernel-patches}
shift 2
patchpattern=${@-*}
export TAR=${TAR:-tar}
# use a well defined sorting order
export LC_COLLATE=C
if [ ! -d "${builddir}" ] ; then
echo "Aborting. '${builddir}' is not a directory."
exit 1
fi
if [ ! -d "${patchdir}" ] ; then
echo "Aborting. '${patchdir}' is not a directory."
exit 1
fi
# Remove any rejects present BEFORE patching - Because if there are
# any, even if patches are well applied, at the end it will complain
# about rejects in builddir.
find ${builddir}/ '(' -name '*.rej' -o -name '.*.rej' ')' -print0 | \
xargs -0 -r rm -f
function apply_patch {
path="${1%%/}"
patch="${2}"
case "${path}" in
/*) ;;
*) path="$PWD/${path}";;
esac
if [ "$3" ]; then
type="series"; uncomp="cat"
else
case "$patch" in
*.gz)
type="gzip"; uncomp="gunzip -dc"; ;;
*.bz)
type="bzip"; uncomp="bunzip -dc"; ;;
*.bz2)
type="bzip2"; uncomp="bunzip2 -dc"; ;;
*.xz)
type="xz"; uncomp="unxz -dc"; ;;
*.zip)
type="zip"; uncomp="unzip -d"; ;;
*.Z)
type="compress"; uncomp="uncompress -c"; ;;
*.diff*)
type="diff"; uncomp="cat"; ;;
*.patch*)
type="patch"; uncomp="cat"; ;;
*)
echo "Unsupported file type for ${path}/${patch}, skipping";
return 0
;;
esac
fi
if [ -z "$silent" ] ; then
echo ""
echo "Applying $patch using ${type}: "
fi
if [ ! -e "${path}/$patch" ] ; then
echo "Error: missing patch file ${path}/$patch"
exit 1
fi
existing="$(grep -E "/${patch}\$" ${builddir}/.applied_patches_list || true)"
if [ -n "${existing}" ]; then
echo "Error: duplicate filename '${patch}'"
echo "Conflicting files are:"
echo " already applied: ${existing}"
echo " to be applied : ${path}/${patch}"
exit 1
fi
echo "${path}/${patch}" >> ${builddir}/.applied_patches_list
${uncomp} "${path}/$patch" | patch -F0 -g0 -p1 --no-backup-if-mismatch -d "${builddir}" -t -N $silent
if [ $? != 0 ] ; then
echo "Patch failed! Please fix ${patch}!"
exit 1
fi
}
function scan_patchdir {
local path=$1
shift 1
patches=${@-*}
# If there is a series file, use it instead of using ls sort order
# to apply patches. Skip line starting with a dash.
if [ -e "${path}/series" ] ; then
# The format of a series file accepts a second field that is
# used to specify the number of directory components to strip
# when applying the patch, in the form -pN (N an integer >= 0)
# We assume this field to always be -p1 whether it is present
# or missing.
series_patches="`grep -Ev "^#" ${path}/series | cut -d ' ' -f1 2> /dev/null`"
for i in $series_patches; do
apply_patch "$path" "$i" series
done
else
for i in `cd $path; ls -d $patches 2> /dev/null` ; do
if [ -d "${path}/$i" ] ; then
scan_patchdir "${path}/$i"
elif echo "$i" | grep -q -E "\.tar(\..*)?$|\.tbz2?$|\.tgz$" ; then
unpackedarchivedir="$builddir/.patches-$(basename $i)-unpacked"
rm -rf "$unpackedarchivedir" 2> /dev/null
mkdir "$unpackedarchivedir"
${TAR} -C "$unpackedarchivedir" -xaf "${path}/$i"
scan_patchdir "$unpackedarchivedir"
else
apply_patch "$path" "$i"
fi
done
fi
}
touch ${builddir}/.applied_patches_list
scan_patchdir "$patchdir" "$patchpattern"
# Check for rejects...
if [ "`find $builddir/ '(' -name '*.rej' -o -name '.*.rej' ')' -print`" ] ; then
echo "Aborting. Reject files found."
exit 1
fi

96
support/scripts/check-bin-arch Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# List of hardcoded paths that should be ignored, as they may
# contain binaries for an architecture different from the
# architecture of the target.
declare -a IGNORES=(
# Skip firmware files, they could be ELF files for other
# architectures
"/lib/firmware"
"/usr/lib/firmware"
# Skip kernel modules
# When building a 32-bit userland on 64-bit architectures, the kernel
# and its modules may still be 64-bit. To keep the basic
# check-bin-arch logic simple, just skip this directory.
"/lib/modules"
"/usr/lib/modules"
# Skip files in /usr/share, several packages (qemu,
# pru-software-support) legitimately install ELF binaries that
# are not for the target architecture
"/usr/share"
# Skip files in {/usr,}/lib/grub, since it is possible to have
# it for a different architecture (e.g. i386 grub on x86_64).
"/lib/grub"
"/usr/lib/grub"
# Guile modules are ELF files, with a "None" machine
"/usr/lib/guile"
)
while getopts p:l:r:a:i: OPT ; do
case "${OPT}" in
p) package="${OPTARG}";;
l) pkg_list="${OPTARG}";;
r) readelf="${OPTARG}";;
a) arch_name="${OPTARG}";;
i)
# Ensure we do have single '/' as separators,
# and that we have a leading and a trailing one.
pattern="$(sed -r -e 's:/+:/:g; s:^/*:/:; s:/*$:/:;' <<<"${OPTARG}")"
IGNORES+=("${pattern}")
;;
:) error "option '%s' expects a mandatory argument\n" "${OPTARG}";;
\?) error "unknown option '%s'\n" "${OPTARG}";;
esac
done
if test -z "${package}" -o -z "${pkg_list}" -o -z "${readelf}" -o -z "${arch_name}" ; then
echo "Usage: $0 -p <pkg> -l <pkg-file-list> -r <readelf> -a <arch name> [-i PATH ...]"
exit 1
fi
exitcode=0
# Only split on new lines, for filenames-with-spaces
IFS="
"
while read f; do
for ignore in "${IGNORES[@]}"; do
if [[ "${f}" =~ ^"${ignore}" ]]; then
continue 2
fi
done
# Skip symlinks. Some symlinks may have absolute paths as
# target, pointing to host binaries while we're building.
if [[ -L "${TARGET_DIR}/${f}" ]]; then
continue
fi
# Get architecture using readelf. We pipe through 'head -1' so
# that when the file is a static library (.a), we only take
# into account the architecture of the first object file.
arch=$(LC_ALL=C ${readelf} -h "${TARGET_DIR}/${f}" 2>&1 | \
sed -r -e '/^ Machine: +(.+)/!d; s//\1/;' | head -1)
# If no architecture found, assume it was not an ELF file
if test "${arch}" = "" ; then
continue
fi
# Architecture is correct
if test "${arch}" = "${arch_name}" ; then
continue
fi
printf 'ERROR: architecture for "%s" is "%s", should be "%s"\n' \
"${f}" "${arch}" "${arch_name}"
exitcode=1
done < <( sed -r -e "/^${package},\.(.+)$/!d; s//\1/;" ${pkg_list} )
exit ${exitcode}

111
support/scripts/check-host-rpath Executable file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# This script scans $(HOST_DIR)/{bin,sbin} for all ELF files, and checks
# they have an RPATH to $(HOST_DIR)/lib if they need libraries from
# there.
# Override the user's locale so we are sure we can parse the output of
# readelf(1) and file(1)
export LC_ALL=C
main() {
local pkg="${1}"
local hostdir="${2}"
local perpackagedir="${3}"
local file ret
# Remove duplicate and trailing '/' for proper match
hostdir="$( sed -r -e 's:/+:/:g; s:/$::;' <<<"${hostdir}" )"
ret=0
while read file; do
is_elf "${file}" || continue
elf_needs_rpath "${file}" "${hostdir}" || continue
check_elf_has_rpath "${file}" "${hostdir}" "${perpackagedir}" && continue
if [ ${ret} -eq 0 ]; then
ret=1
printf "***\n"
printf "*** ERROR: package %s installs executables without proper RPATH:\n" "${pkg}"
fi
printf "*** %s\n" "${file}"
done < <( find "${hostdir}"/{bin,sbin} -type f 2>/dev/null )
return ${ret}
}
is_elf() {
local f="${1}"
readelf -l "${f}" 2>/dev/null \
|grep -E 'Requesting program interpreter:' >/dev/null 2>&1
}
# This function tells whether a given ELF executable (first argument)
# needs a RPATH pointing to the host library directory or not. It
# needs such an RPATH if at least of the libraries used by the ELF
# executable is available in the host library directory. This function
# returns 0 when a RPATH is needed, 1 otherwise.
#
# With per-package directory support, ${hostdir} will point to the
# current package per-package host directory, and this is where this
# function will check if the libraries needed by the executable are
# located (or not). In practice, the ELF executable RPATH may point to
# another package per-package host directory, but that is fine because
# if such an executable is within the current package per-package host
# directory, its libraries will also have been copied into the current
# package per-package host directory.
elf_needs_rpath() {
local file="${1}"
local hostdir="${2}"
local lib
while read lib; do
[ -e "${hostdir}/lib/${lib}" ] && return 0
done < <( readelf -d "${file}" 2>/dev/null \
|sed -r -e '/^.* \(NEEDED\) .*Shared library: \[(.+)\]$/!d;' \
-e 's//\1/;' \
)
return 1
}
# This function checks whether at least one of the RPATH of the given
# ELF executable (first argument) properly points to the host library
# directory (second argument), either through an absolute RPATH or a
# relative RPATH. In the context of per-package directory support,
# ${hostdir} (second argument) points to the current package host
# directory. However, it is perfectly valid for an ELF binary to have
# a RPATH pointing to another package per-package host directory,
# which is why such RPATH is also accepted (the per-package directory
# gets passed as third argument). Having a RPATH pointing to the host
# directory will make sure the ELF executable will find at runtime the
# shared libraries it depends on. This function returns 0 when a
# proper RPATH was found, or 1 otherwise.
check_elf_has_rpath() {
local file="${1}"
local hostdir="${2}"
local perpackagedir="${3}"
local rpath dir
while read rpath; do
for dir in ${rpath//:/ }; do
# Remove duplicate and trailing '/' for proper match
dir="$( sed -r -e 's:/+:/:g; s:/$::;' <<<"${dir}" )"
[ "${dir}" = "${hostdir}/lib" ] && return 0
[ "${dir}" = "\$ORIGIN/../lib" ] && return 0
# This check is done even for builds where
# BR2_PER_PACKAGE_DIRECTORIES is disabled. In this case,
# PER_PACKAGE_DIR and therefore ${perpackagedir} points to
# a non-existent directory, and this check will always be
# false.
[[ ${dir} =~ "${perpackagedir}/"[^/]+/host/lib ]] && return 0
done
done < <( readelf -d "${file}" 2>/dev/null \
|sed -r -e '/.* \(R(UN)?PATH\) +Library r(un)?path: \[(.+)\]$/!d' \
-e 's//\3/;' \
)
return 1
}
main "${@}"

View File

@@ -0,0 +1,69 @@
#!/bin/sh
# This script (and the embedded C code) will check that the actual
# headers version match the user told us they were:
#
# - if both versions are the same, all is well.
#
# - if the actual headers are older than the user told us, this is
# an error.
#
# - if the actual headers are more recent than the user told us, and
# we are doing a strict check, then this is an error.
#
# - if the actual headers are more recent than the user told us, and
# we are doing a loose check, then a warning is printed, but this is
# not an error.
BUILDDIR="${1}"
SYSROOT="${2}"
# Make sure we have enough version components
HDR_VER="${3}.0.0"
CHECK="${4}" # 'strict' or 'loose'
HDR_M="${HDR_VER%%.*}"
HDR_V="${HDR_VER#*.}"
HDR_m="${HDR_V%%.*}"
# Exit on any error, so we don't try to run an unexisting program if the
# compilation fails.
set -e
# Set the clean-up trap in advance to prevent a race condition in which we
# create the file but get a SIGTERM before setting it. Notice that we don't
# need to care about EXEC being empty, since 'rm -f ""' does nothing.
trap 'rm -f "${EXEC}"' EXIT
EXEC="$(mktemp -p "${BUILDDIR}" -t .check-headers.XXXXXX)"
# We do not want to account for the patch-level, since headers are
# not supposed to change for different patchlevels, so we mask it out.
# This only applies to kernels >= 3.0, but those are the only one
# we actually care about; we treat all 2.6.x kernels equally.
${HOSTCC} -imacros "${SYSROOT}/usr/include/linux/version.h" \
-x c -o "${EXEC}" - <<_EOF_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc __attribute__((unused)),
char** argv __attribute__((unused)))
{
int l = LINUX_VERSION_CODE & ~0xFF;
int h = KERNEL_VERSION(${HDR_M},${HDR_m},0);
if ((l >= h) && !strcmp("${CHECK}", "loose"))
return 0;
if (l != h) {
printf("Incorrect selection of kernel headers: ");
printf("expected %d.%d.x, got %d.%d.x\n", ${HDR_M}, ${HDR_m},
((LINUX_VERSION_CODE>>16) & 0xFF),
((LINUX_VERSION_CODE>>8) & 0xFF));
return 1;
}
return 0;
}
_EOF_
"${EXEC}"

193
support/scripts/fix-rpath Executable file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# Copyright (C) 2016 Samuel Martin <s.martin49@gmail.com>
# Copyright (C) 2017 Wolfgang Grandegger <wg@grandegger.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
usage() {
cat <<EOF >&2
Usage: ${0} TREE_KIND
Description:
This script scans a tree and sanitize ELF files' RPATH found in there.
Sanitization behaves the same whatever the kind of the processed tree,
but the resulting RPATH differs. The rpath sanitization is done using
"patchelf --make-rpath-relative".
Arguments:
TREE_KIND Kind of tree to be processed.
Allowed values: host, target, staging
Environment:
PATCHELF patchelf program to use
(default: HOST_DIR/bin/patchelf)
HOST_DIR host directory
STAGING_DIR staging directory
TARGET_DIR target directory
TOOLCHAIN_EXTERNAL_DOWNLOAD_INSTALL_DIR
(default HOST_DIR/opt/ext-toolchain)
PARALLEL_JOBS number of parallel jobs to run
Returns: 0 if success or 1 in case of error
EOF
}
: "${PATCHELF:=${HOST_DIR}/bin/patchelf}"
# ELF files should not be in these sub-directories
HOST_EXCLUDEPATHS="/share/terminfo"
STAGING_EXCLUDEPATHS="/usr/include /usr/share/terminfo"
TARGET_EXCLUDEPATHS="/lib/firmware"
declare -a sanitize_extra_args
rootdir=
patch_file() {
local file="${1}"
# check if it's an ELF file
rpath="$("${PATCHELF}" --print-rpath "${file}" 2>&1)"
if test $? -ne 0 ; then
return 0
fi
# make files writable if necessary
changed="$(chmod -c u+w "${file}")"
# With per-package directory support, most RPATH of host
# binaries will point to per-package directories. This won't
# work with the --make-rpath-relative ${rootdir} invocation as
# the per-package host directory is not within ${rootdir}. So,
# we rewrite all RPATHs pointing to per-package directories so
# that they point to the global host directory.
# shellcheck disable=SC2001 # ${var//search/replace} hard when search or replace have / in them
changed_rpath="$(echo "${rpath}" | sed "s@${PER_PACKAGE_DIR}/[^/]\+/host@${HOST_DIR}@g")"
if test "${rpath}" != "${changed_rpath}" ; then
"${PATCHELF}" --set-rpath "${changed_rpath}" "${file}"
fi
# call patchelf to sanitize the rpath
"${PATCHELF}" --make-rpath-relative "${rootdir}" "${sanitize_extra_args[@]}" "${file}"
# restore the original permission
test "${changed}" != "" && chmod u-w "${file}"
}
patch_files() {
while read -r -d $'\0' file; do
# for performance reasons, we want to do as little work as
# possible on non-ELF files. therefore, make sure that the
# file at least starts with the ELF magic before handing it of
# to patchelf, which will then perform the proper validation.
read -r -n4 magic <"${file}" && test "${magic}" != $'\x7fELF' && continue
patch_file "${file}"
done
}
main() {
local tree
local -a find_args
tree="${1}"
if ! "${PATCHELF}" --version > /dev/null 2>&1; then
echo "Error: can't execute patchelf utility '${PATCHELF}'"
exit 1
fi
case "${tree}" in
host)
rootdir="${HOST_DIR}"
# do not process the sysroot (only contains target binaries)
find_args+=( "-path" "${STAGING_DIR}" "-prune" "-o" )
# do not process the external toolchain installation directory to
# avoid breaking it.
test "${TOOLCHAIN_EXTERNAL_DOWNLOAD_INSTALL_DIR}" != "" && \
find_args+=( "-path" "${TOOLCHAIN_EXTERNAL_DOWNLOAD_INSTALL_DIR}" "-prune" "-o" )
for excludepath in ${HOST_EXCLUDEPATHS}; do
find_args+=( "-path" "${HOST_DIR}""${excludepath}" "-prune" "-o" )
done
# do not process the patchelf binary but a copy to work-around "file in use"
find_args+=( "-path" "${PATCHELF}" "-prune" "-o" )
cp "${PATCHELF}" "${PATCHELF}.__to_be_patched"
# we always want $ORIGIN-based rpaths to make it relocatable.
sanitize_extra_args+=( "--relative-to-file" )
;;
staging)
rootdir="${STAGING_DIR}"
# ELF files should not be in these sub-directories
for excludepath in ${STAGING_EXCLUDEPATHS}; do
find_args+=( "-path" "${STAGING_DIR}""${excludepath}" "-prune" "-o" )
done
# should be like for the target tree below
sanitize_extra_args+=( "--no-standard-lib-dirs" )
;;
target)
rootdir="${TARGET_DIR}"
for excludepath in ${TARGET_EXCLUDEPATHS}; do
find_args+=( "-path" "${TARGET_DIR}""${excludepath}" "-prune" "-o" )
done
# we don't want $ORIGIN-based rpaths but absolute paths without rootdir.
# we also want to remove rpaths pointing to /lib or /usr/lib.
sanitize_extra_args+=( "--no-standard-lib-dirs" )
;;
*)
usage
exit 1
;;
esac
work=$(mktemp --tmpdir -d fix-rpath.XXXXXXXX)
find_args+=( "-type" "f" "-print0" )
find "${rootdir}" "${find_args[@]}" \
| split -t '\0' -a4 -d -n "r/${PARALLEL_JOBS:-1}" "-" "${work}/part"
for part in "${work}/part"*; do
patch_files <"${part}" &
done
wait
rm -rf "${work}"
# Restore patched patchelf utility
test "${tree}" = "host" && mv "${PATCHELF}.__to_be_patched" "${PATCHELF}"
# ignore errors
return 0
}
main "${@}"

486
support/scripts/mkusers Executable file
View File

@@ -0,0 +1,486 @@
#!/usr/bin/env bash
set -e
myname="${0##*/}"
#----------------------------------------------------------------------------
# Configurable items
FIRST_USER_UID=1000
LAST_USER_UID=1999
FIRST_USER_GID=1000
LAST_USER_GID=1999
# use names from /etc/adduser.conf
FIRST_SYSTEM_UID=100
LAST_SYSTEM_UID=999
FIRST_SYSTEM_GID=100
LAST_SYSTEM_GID=999
# argument to automatically crease system/user id
AUTO_SYSTEM_ID=-1
AUTO_USER_ID=-2
# No more is configurable below this point
#----------------------------------------------------------------------------
#----------------------------------------------------------------------------
error() {
local fmt="${1}"
shift
printf "%s: " "${myname}" >&2
# shellcheck disable=SC2059 # fmt is the format passed to error()
printf "${fmt}" "${@}" >&2
}
fail() {
error "$@"
exit 1
}
#----------------------------------------------------------------------------
if [ ${#} -ne 2 ]; then
fail "usage: %s USERS_TABLE TARGET_DIR\n"
fi
USERS_TABLE="${1}"
TARGET_DIR="${2}"
shift 2
PASSWD="${TARGET_DIR}/etc/passwd"
SHADOW="${TARGET_DIR}/etc/shadow"
GROUP="${TARGET_DIR}/etc/group"
# /etc/gshadow is not part of the standard skeleton, so not everybody
# will have it, but some may have it, and its content must be in sync
# with /etc/group, so any use of gshadow must be conditional.
GSHADOW="${TARGET_DIR}/etc/gshadow"
# We can't simply source ${BR2_CONFIG} as it may contains constructs
# such as:
# BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig"
# which when sourced from a shell script will eventually try to execute
# a command named 'CONFIG_DIR', which is plain wrong for virtually every
# systems out there.
# So, we have to scan that file instead. Sigh... :-(
PASSWD_METHOD="sha-256"
# PASSWD_METHOD="$( sed -r -e '/^BR2_TARGET_GENERIC_PASSWD_METHOD="(.*)"$/!d;' \
# -e 's//\1/;' \
# "${BR2_CONFIG}" \
# )"
#----------------------------------------------------------------------------
get_uid() {
local username="${1}"
awk -F: -v username="${username}" \
'$1 == username { printf( "%d\n", $3 ); }' "${PASSWD}"
}
#----------------------------------------------------------------------------
get_ugid() {
local username="${1}"
awk -F: -v username="${username}" \
'$1 == username { printf( "%d\n", $4 ); }' "${PASSWD}"
}
#----------------------------------------------------------------------------
get_gid() {
local group="${1}"
awk -F: -v group="${group}" \
'$1 == group { printf( "%d\n", $3 ); }' "${GROUP}"
}
#----------------------------------------------------------------------------
get_members() {
local group="${1}"
awk -F: -v group="${group}" \
'$1 == group { printf( "%s\n", $4 ); }' "${GROUP}"
}
#----------------------------------------------------------------------------
get_username() {
local uid="${1}"
awk -F: -v uid="${uid}" \
'$3 == uid { printf( "%s\n", $1 ); }' "${PASSWD}"
}
#----------------------------------------------------------------------------
get_group() {
local gid="${1}"
awk -F: -v gid="${gid}" \
'$3 == gid { printf( "%s\n", $1 ); }' "${GROUP}"
}
#----------------------------------------------------------------------------
get_ugroup() {
local username="${1}"
local ugid
ugid="$( get_ugid "${username}" )"
if [ -n "${ugid}" ]; then
get_group "${ugid}"
fi
}
#----------------------------------------------------------------------------
# Sanity-check the new user/group:
# - check the gid is not already used for another group
# - check the group does not already exist with another gid
# - check the user does not already exist with another gid
# - check the uid is not already used for another user
# - check the user does not already exist with another uid
# - check the user does not already exist in another group
check_user_validity() {
local username="${1}"
local uid="${2}"
local group="${3}"
local gid="${4}"
local _uid _ugid _gid _username _group _ugroup
_group="$( get_group "${gid}" )"
_gid="$( get_gid "${group}" )"
_ugid="$( get_ugid "${username}" )"
_username="$( get_username "${uid}" )"
_uid="$( get_uid "${username}" )"
_ugroup="$( get_ugroup "${username}" )"
if [ "${username}" = "root" ]; then
fail "invalid username '%s\n'" "${username}"
fi
# shellcheck disable=SC2086 # gid is a non-empty int
# shellcheck disable=SC2166 # [ .. -o .. ] works well in this case
if [ ${gid} -lt -2 -o ${gid} -eq 0 ]; then
fail "invalid gid '%d' for '%s'\n" ${gid} "${username}"
elif [ ${gid} -ge 0 ]; then
# check the gid is not already used for another group
if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
fail "gid '%d' for '%s' is already used by group '%s'\n" \
${gid} "${username}" "${_group}"
fi
# check the group does not already exists with another gid
# Need to split the check in two, otherwise '[' complains it
# is missing arguments when _gid is empty
if [ -n "${_gid}" ] && [ ${_gid} -ne ${gid} ]; then
fail "group '%s' for '%s' already exists with gid '%d' (wants '%d')\n" \
"${group}" "${username}" ${_gid} ${gid}
fi
# check the user does not already exists with another gid
# Need to split the check in two, otherwise '[' complains it
# is missing arguments when _ugid is empty
if [ -n "${_ugid}" ] && [ ${_ugid} -ne ${gid} ]; then
fail "user '%s' already exists with gid '%d' (wants '%d')\n" \
"${username}" ${_ugid} ${gid}
fi
fi
# shellcheck disable=SC2086 # uid is a non-empty int
# shellcheck disable=SC2166 # [ .. -o .. ] works well in this case
if [ ${uid} -lt -2 -o ${uid} -eq 0 ]; then
fail "invalid uid '%d' for '%s'\n" ${uid} "${username}"
elif [ ${uid} -ge 0 ]; then
# check the uid is not already used for another user
if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
fail "uid '%d' for '%s' already used by user '%s'\n" \
${uid} "${username}" "${_username}"
fi
# check the user does not already exists with another uid
# Need to split the check in two, otherwise '[' complains it
# is missing arguments when _uid is empty
if [ -n "${_uid}" ] && [ ${_uid} -ne ${uid} ]; then
fail "user '%s' already exists with uid '%d' (wants '%d')\n" \
"${username}" ${_uid} ${uid}
fi
fi
# check the user does not already exist in another group
# shellcheck disable=SC2166 # [ .. -a .. ] works well in this case
if [ -n "${_ugroup}" -a "${_ugroup}" != "${group}" ]; then
fail "user '%s' already exists with group '%s' (wants '%s')\n" \
"${username}" "${_ugroup}" "${group}"
fi
return 0
}
#----------------------------------------------------------------------------
# Generate a unique GID for given group. If the group already exists,
# then simply report its current GID. Otherwise, generate the lowest GID
# that is:
# - not 0
# - comprised in [$2..$3]
# - not already used by a group
generate_gid() {
local group="${1}"
local mingid="${2}"
local maxgid="${3}"
local gid
gid="$( get_gid "${group}" )"
if [ -z "${gid}" ]; then
for(( gid=mingid; gid<=maxgid; gid++ )); do
if [ -z "$( get_group "${gid}" )" ]; then
break
fi
done
# shellcheck disable=SC2086 # gid and maxgid are non-empty ints
if [ ${gid} -gt ${maxgid} ]; then
fail "can not allocate a GID for group '%s'\n" "${group}"
fi
fi
printf "%d\n" "${gid}"
}
#----------------------------------------------------------------------------
# Add a group; if it does already exist, remove it first
add_one_group() {
local group="${1}"
local gid="${2}"
local members
# Generate a new GID if needed
# shellcheck disable=SC2086 # gid is a non-empty int
if [ ${gid} -eq ${AUTO_USER_ID} ]; then
gid="$( generate_gid "${group}" $FIRST_USER_GID $LAST_USER_GID )"
elif [ ${gid} -eq ${AUTO_SYSTEM_ID} ]; then
gid="$( generate_gid "${group}" $FIRST_SYSTEM_GID $LAST_SYSTEM_GID )"
fi
members=$(get_members "$group")
# Remove any previous instance of this group, and re-add the new one
sed -i --follow-symlinks -e '/^'"${group}"':.*/d;' "${GROUP}"
printf "%s:x:%d:%s\n" "${group}" "${gid}" "${members}" >>"${GROUP}"
# Ditto for /etc/gshadow if it exists
if [ -f "${GSHADOW}" ]; then
sed -i --follow-symlinks -e '/^'"${group}"':.*/d;' "${GSHADOW}"
printf "%s:*::\n" "${group}" >>"${GSHADOW}"
fi
}
#----------------------------------------------------------------------------
# Generate a unique UID for given username. If the username already exists,
# then simply report its current UID. Otherwise, generate the lowest UID
# that is:
# - not 0
# - comprised in [$2..$3]
# - not already used by a user
generate_uid() {
local username="${1}"
local minuid="${2}"
local maxuid="${3}"
local uid
uid="$( get_uid "${username}" )"
if [ -z "${uid}" ]; then
for(( uid=minuid; uid<=maxuid; uid++ )); do
if [ -z "$( get_username "${uid}" )" ]; then
break
fi
done
# shellcheck disable=SC2086 # uid is a non-empty int
if [ ${uid} -gt ${maxuid} ]; then
fail "can not allocate a UID for user '%s'\n" "${username}"
fi
fi
printf "%d\n" "${uid}"
}
#----------------------------------------------------------------------------
# Add given user to given group, if not already the case
add_user_to_group() {
local username="${1}"
local group="${2}"
local _f
for _f in "${GROUP}" "${GSHADOW}"; do
[ -f "${_f}" ] || continue
sed -r -i --follow-symlinks \
-e 's/^('"${group}"':.*:)(([^:]+,)?)'"${username}"'(,[^:]+*)?$/\1\2\4/;' \
-e 's/^('"${group}"':.*)$/\1,'"${username}"'/;' \
-e 's/,+/,/' \
-e 's/:,/:/' \
"${_f}"
done
}
#----------------------------------------------------------------------------
# Encode a password
encode_password() {
local passwd="${1}"
mkpasswd -m "${PASSWD_METHOD}" "${passwd}"
}
#----------------------------------------------------------------------------
# Add a user; if it does already exist, remove it first
add_one_user() {
local username="${1}"
local uid="${2}"
local group="${3}"
local gid="${4}"
local passwd="${5}"
local home="${6}"
local shell="${7}"
local groups="${8}"
local comment="${9}"
local _f _group _home _shell _gid _passwd
# First, sanity-check the user
check_user_validity "${username}" "${uid}" "${group}" "${gid}"
# Generate a new UID if needed
# shellcheck disable=SC2086 # uid is a non-empty int
if [ ${uid} -eq ${AUTO_USER_ID} ]; then
uid="$( generate_uid "${username}" $FIRST_USER_UID $LAST_USER_UID )"
elif [ ${uid} -eq ${AUTO_SYSTEM_ID} ]; then
uid="$( generate_uid "${username}" $FIRST_SYSTEM_UID $LAST_SYSTEM_UID )"
fi
# Remove any previous instance of this user
for _f in "${PASSWD}" "${SHADOW}"; do
sed -r -i --follow-symlinks -e '/^'"${username}"':.*/d;' "${_f}"
done
_gid="$( get_gid "${group}" )"
_shell="${shell}"
if [ "${shell}" = "-" ]; then
_shell="/bin/false"
fi
case "${home}" in
-) _home="/";;
/) fail "home can not explicitly be '/'\n";;
/*) _home="${home}";;
*) fail "home must be an absolute path\n";;
esac
case "${passwd}" in
-)
_passwd=""
;;
!=*)
_passwd='!'"$( encode_password "${passwd#!=}" )"
;;
=*)
_passwd="$( encode_password "${passwd#=}" )"
;;
*)
_passwd="${passwd}"
;;
esac
printf "%s:x:%d:%d:%s:%s:%s\n" \
"${username}" "${uid}" "${_gid}" \
"${comment}" "${_home}" "${_shell}" \
>>"${PASSWD}"
printf "%s:%s:::::::\n" \
"${username}" "${_passwd}" \
>>"${SHADOW}"
# Add the user to its additional groups
if [ "${groups}" != "-" ]; then
for _group in ${groups//,/ }; do
add_user_to_group "${username}" "${_group}"
done
fi
# If the user has a home, chown it
# (Note: stdout goes to the fakeroot-script)
if [ "${home}" != "-" ]; then
mkdir -p "${TARGET_DIR}/${home}"
printf "chown -h -R %d:%d '%s'\n" "${uid}" "${_gid}" "${TARGET_DIR}/${home}"
fi
}
#----------------------------------------------------------------------------
main() {
local username uid group gid passwd home shell groups comment
local line
local auto_id
local -a ENTRIES
# Some sanity checks
if [ ${FIRST_USER_UID} -le 0 ]; then
fail "FIRST_USER_UID must be >0 (currently %d)\n" ${FIRST_USER_UID}
fi
if [ ${FIRST_USER_GID} -le 0 ]; then
fail "FIRST_USER_GID must be >0 (currently %d)\n" ${FIRST_USER_GID}
fi
# Read in all the file in memory, exclude empty lines and comments
# mapfile reads all lines, even the last one if it is missing a \n
mapfile -t ENTRIES < <( sed -r -e 's/#.*//; /^[[:space:]]*$/d;' "${USERS_TABLE}" )
# We first create groups whose gid is positive, and then we create groups
# whose gid is automatic, so that, if a group is defined both with
# a specified gid and an automatic gid, we ensure the specified gid is
# used, rather than a different automatic gid is computed.
# First, create all the main groups which gid is *not* automatic
for line in "${ENTRIES[@]}"; do
read -r username uid group gid passwd home shell groups comment <<<"${line}"
# shellcheck disable=SC2086 # gid is a non-empty int
[ ${gid} -ge 0 ] || continue # Automatic gid
add_one_group "${group}" "${gid}"
done
# Then, create all the main groups which gid *is* automatic
for line in "${ENTRIES[@]}"; do
read -r username uid group gid passwd home shell groups comment <<<"${line}"
# shellcheck disable=SC2086 # gid is a non-empty int
[ ${gid} -lt 0 ] || continue # Non-automatic gid
add_one_group "${group}" "${gid}"
done
# Then, create all the additional groups
# If any additional group is already a main group, we should use
# the gid of that main group; otherwise, we can use any gid - a
# system gid if the uid is a system user (<= LAST_SYSTEM_UID),
# otherwise a user gid.
for line in "${ENTRIES[@]}"; do
read -r username uid group gid passwd home shell groups comment <<<"${line}"
if [ "${groups}" != "-" ]; then
# shellcheck disable=SC2086 # uid is a non-empty int
if [ ${uid} -le 0 ]; then
auto_id=${uid}
elif [ ${uid} -le ${LAST_SYSTEM_UID} ]; then
auto_id=${AUTO_SYSTEM_ID}
else
auto_id=${AUTO_USER_ID}
fi
for g in ${groups//,/ }; do
add_one_group "${g}" "${auto_id}"
done
fi
done
# When adding users, we do as for groups, in case two packages create
# the same user, one with an automatic uid, the other with a specified
# uid, to ensure the specified uid is used, rather than an incompatible
# uid be generated.
# Now, add users whose uid is *not* automatic
for line in "${ENTRIES[@]}"; do
read -r username uid group gid passwd home shell groups comment <<<"${line}"
[ "${username}" != "-" ] || continue # Magic string to skip user creation
# shellcheck disable=SC2086 # uid is a non-empty int
[ ${uid} -ge 0 ] || continue # Automatic uid
add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
"${home}" "${shell}" "${groups}" "${comment}"
done
# Finally, add users whose uid *is* automatic
for line in "${ENTRIES[@]}"; do
read -r username uid group gid passwd home shell groups comment <<<"${line}"
[ "${username}" != "-" ] || continue # Magic string to skip user creation
# shellcheck disable=SC2086 # uid is a non-empty int
[ ${uid} -lt 0 ] || continue # Non-automatic uid
add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
"${home}" "${shell}" "${groups}" "${comment}"
done
}
#----------------------------------------------------------------------------
main "${@}"