mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 15:45:53 +01:00
Dockerfile: Use rpmbuild
We were bit before by just doing a `COPY` of our binaries overtop of the base image because that doens't remove old files. Replace the pre-build approach with rpmbuild, and then change to do an rpm-based upgrade so that we fix that problem. Note that we still preserve incremental rebuilds by overriding some of the RPM build process. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
# Toplevel build bits
|
||||
!Makefile
|
||||
!Cargo.*
|
||||
# License and doc files needed for RPM
|
||||
!LICENSE-*
|
||||
!README.md
|
||||
# We do build manpages from markdown
|
||||
!docs/
|
||||
# We use the spec file
|
||||
|
||||
127
Dockerfile
127
Dockerfile
@@ -10,34 +10,26 @@ ARG base=quay.io/centos-bootc/centos-bootc:stream10
|
||||
FROM scratch as src
|
||||
COPY . /src
|
||||
|
||||
# And this image only captures contrib/packaging separately
|
||||
# to ensure we have more precise cache hits.
|
||||
FROM scratch as packaging
|
||||
COPY contrib/packaging /
|
||||
|
||||
FROM $base as base
|
||||
# We could inject other content here
|
||||
# Mark this as a test image (moved from --label build flag to fix layer caching)
|
||||
LABEL bootc.testimage="1"
|
||||
|
||||
# This image installs build deps, pulls in our source code, and installs updated
|
||||
# bootc binaries in /out. The intention is that the target rootfs is extracted from /out
|
||||
# back into a final stage (without the build deps etc) below.
|
||||
FROM base as build
|
||||
FROM base as buildroot
|
||||
# Flip this off to disable initramfs code
|
||||
ARG initramfs=1
|
||||
# This installs our package dependencies, and we want to cache it independently of the rest.
|
||||
# Basically we don't want changing a .rs file to blow out the cache of packages. So we only
|
||||
# copy files necessary
|
||||
COPY contrib/packaging /tmp/packaging
|
||||
RUN <<EORUN
|
||||
set -xeuo pipefail
|
||||
. /usr/lib/os-release
|
||||
case $ID in
|
||||
centos|rhel) dnf config-manager --set-enabled crb;;
|
||||
fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';;
|
||||
esac
|
||||
# Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174
|
||||
dnf -y distro-sync ostree{,-libs} systemd
|
||||
# Install base build requirements
|
||||
dnf -y builddep /tmp/packaging/bootc.spec
|
||||
# And extra packages
|
||||
grep -Ev -e '^#' /tmp/packaging/fedora-extra.txt | xargs dnf -y install
|
||||
rm /tmp/packaging -rf
|
||||
EORUN
|
||||
# Version for RPM build (optional, computed from git in Justfile)
|
||||
ARG pkgversion=
|
||||
# This installs our buildroot, and we want to cache it independently of the rest.
|
||||
# Basically we don't want changing a .rs file to blow out the cache of packages.
|
||||
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/install-buildroot
|
||||
# Now copy the rest of the source
|
||||
COPY --from=src /src /src
|
||||
WORKDIR /src
|
||||
@@ -45,15 +37,10 @@ WORKDIR /src
|
||||
# We aren't using the full recommendations there, just the simple bits.
|
||||
# First we download all of our Rust dependencies
|
||||
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch
|
||||
# Then on general principle all the stuff from the Makefile runs with no network
|
||||
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none <<EORUN
|
||||
set -xeuo pipefail
|
||||
make
|
||||
make install-all DESTDIR=/out
|
||||
if test "${initramfs:-}" = 1; then
|
||||
make install-initramfs-dracut DESTDIR=/out
|
||||
fi
|
||||
EORUN
|
||||
|
||||
FROM buildroot as build
|
||||
# Build RPM directly from source, using cached target directory
|
||||
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none RPM_VERSION=${pkgversion} /src/contrib/packaging/build-rpm
|
||||
|
||||
# This "build" includes our unit tests
|
||||
FROM build as units
|
||||
@@ -70,76 +57,14 @@ RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothom
|
||||
FROM base
|
||||
# See the Justfile for possible variants
|
||||
ARG variant
|
||||
RUN <<EORUN
|
||||
set -xeuo pipefail
|
||||
case "${variant}" in
|
||||
*-sdboot)
|
||||
dnf -y install systemd-boot-unsigned
|
||||
# And uninstall bootupd
|
||||
rpm -e bootupd
|
||||
rm /usr/lib/bootupd/updates -rf
|
||||
dnf clean all
|
||||
rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/*
|
||||
;;
|
||||
esac
|
||||
EORUN
|
||||
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-variant "${variant}"
|
||||
# Support overriding the rootfs at build time conveniently
|
||||
ARG rootfs=
|
||||
RUN <<EORUN
|
||||
set -xeuo pipefail
|
||||
# Do we have an explicit build-time override? Then write it.
|
||||
if test -n "$rootfs"; then
|
||||
cat > /usr/lib/bootc/install/80-rootfs-override.toml <<EOF
|
||||
[install.filesystem.root]
|
||||
type = "$rootfs"
|
||||
EOF
|
||||
else
|
||||
# Query the default rootfs
|
||||
base_rootfs=$(bootc install print-configuration | jq -r '.filesystem.root.type // ""')
|
||||
# No filesystem override set. If we're doing composefs, we need a FS that
|
||||
# supports fsverity. If btrfs is available we'll pick that, otherwise ext4.
|
||||
fs=
|
||||
case "${variant}" in
|
||||
composefs*)
|
||||
btrfs=$(grep -qEe '^CONFIG_BTRFS_FS' /usr/lib/modules/*/config && echo btrfs || true)
|
||||
fs=${btrfs:-ext4}
|
||||
;;
|
||||
*)
|
||||
# No explicit filesystem set and we're not using composefs. Default to xfs
|
||||
# with the rationale that we're trying to get filesystem coverage across
|
||||
# all the cases in general.
|
||||
if test -z "${base_rootfs}"; then
|
||||
fs=xfs
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
if test -n "$fs"; then
|
||||
cat > /usr/lib/bootc/install/80-ext4-composefs.toml <<EOF
|
||||
[install.filesystem.root]
|
||||
type = "${fs}"
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure we've flushed out prior state (i.e. files no longer shipped from the old version);
|
||||
# and yes, we may need to go to building an RPM in this Dockerfile by default.
|
||||
(set +x; rpm -ql bootc | while read line; do if test -f $line; then rm -v $line; fi; done)
|
||||
EORUN
|
||||
# Create a layer that is our new binaries
|
||||
COPY --from=build /out/ /
|
||||
# We have code in the initramfs so we always need to regenerate it
|
||||
RUN --network=none <<EORUN
|
||||
set -xeuo pipefail
|
||||
if test -x /usr/lib/bootc/initramfs-setup; then
|
||||
kver=$(cd /usr/lib/modules && echo *);
|
||||
env DRACUT_NO_XATTR=1 dracut -vf /usr/lib/modules/$kver/initramfs.img $kver
|
||||
fi
|
||||
# Only in this containerfile, inject a file which signifies
|
||||
# this comes from this development image. This can be used in
|
||||
# tests to know we're doing upstream CI.
|
||||
touch /usr/lib/.bootc-dev-stamp
|
||||
# And test our own linting
|
||||
## Workaround for https://github.com/bootc-dev/bootc/issues/1546
|
||||
rm -rf /root/buildinfo
|
||||
bootc container lint --fatal-warnings
|
||||
EORUN
|
||||
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}"
|
||||
# Install the RPM built in the build stage
|
||||
# This replaces the manual file deletion hack and COPY, ensuring proper package management
|
||||
# Use rpm -Uvh with --oldpackage to allow replacing with dev version
|
||||
COPY --from=build /out/*.rpm /tmp/
|
||||
RUN --mount=type=bind,from=packaging,target=/run/packaging --network=none /run/packaging/install-rpm-and-setup /tmp
|
||||
# Finally, testour own linting
|
||||
RUN bootc container lint --fatal-warnings
|
||||
|
||||
24
Justfile
24
Justfile
@@ -36,6 +36,30 @@ build:
|
||||
podman build {{base_buildargs}} -t localhost/bootc-bin {{buildargs}} .
|
||||
./tests/build-sealed {{variant}} localhost/bootc-bin localhost/bootc
|
||||
|
||||
# Build packages (e.g. RPM) using a container buildroot
|
||||
_packagecontainer:
|
||||
#!/bin/bash
|
||||
set -xeuo pipefail
|
||||
# Compute version from git (matching xtask.rs gitrev logic)
|
||||
if VERSION=$(git describe --tags --exact-match 2>/dev/null); then
|
||||
VERSION="${VERSION#v}"
|
||||
VERSION="${VERSION//-/.}"
|
||||
else
|
||||
COMMIT=$(git rev-parse HEAD | cut -c1-10)
|
||||
COMMIT_TS=$(git show -s --format=%ct)
|
||||
TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M)
|
||||
VERSION="${TIMESTAMP}.g${COMMIT}"
|
||||
fi
|
||||
echo "Building RPM with version: ${VERSION}"
|
||||
podman build {{base_buildargs}} {{buildargs}} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build .
|
||||
|
||||
# Build a packages (e.g. RPM) into target/
|
||||
# Any old packages will be removed.
|
||||
package: _packagecontainer
|
||||
mkdir -p target
|
||||
rm -vf target/*.rpm
|
||||
podman run --rm localhost/bootc-pkg tar -C /out/ -cf - . | tar -C target/ -xvf -
|
||||
|
||||
# This container image has additional testing content and utilities
|
||||
build-integration-test-image: build
|
||||
cd hack && podman build {{base_buildargs}} -t localhost/bootc-integration-bin -f Containerfile .
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
%bcond_without check
|
||||
%bcond_with tests
|
||||
%if 0%{?rhel} >= 9 || 0%{?fedora} > 41
|
||||
%bcond_without ostree_ext
|
||||
%else
|
||||
@@ -21,7 +22,8 @@
|
||||
%endif
|
||||
|
||||
Name: bootc
|
||||
Version: 1.1.5
|
||||
# Ensure this local build overrides anything else.
|
||||
Version: 99999.0.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Bootable container system
|
||||
|
||||
@@ -84,17 +86,49 @@ Recommends: podman
|
||||
%description -n system-reinstall-bootc
|
||||
This package provides a utility to simplify reinstalling the current system to a given bootc image.
|
||||
|
||||
%if %{with tests}
|
||||
%package tests
|
||||
Summary: Integration tests for bootc
|
||||
Requires: %{name} = %{version}-%{release}
|
||||
|
||||
%description tests
|
||||
This package contains the integration test suite for bootc.
|
||||
%endif
|
||||
|
||||
%global system_reinstall_bootc_install_podman_path %{_prefix}/lib/system-reinstall-bootc/install-podman
|
||||
|
||||
%if 0%{?container_build}
|
||||
# Source is already at /src, no subdirectory
|
||||
%global _buildsubdir .
|
||||
%endif
|
||||
|
||||
%prep
|
||||
%if ! 0%{?container_build}
|
||||
%autosetup -p1 -a1
|
||||
# Default -v vendor config doesn't support non-crates.io deps (i.e. git)
|
||||
cp .cargo/vendor-config.toml .
|
||||
%cargo_prep -N
|
||||
cat vendor-config.toml >> .cargo/config.toml
|
||||
rm vendor-config.toml
|
||||
%else
|
||||
# Container build: source already at _builddir (/src), nothing to extract
|
||||
# RPM's %mkbuilddir creates a subdirectory; symlink it back to the source
|
||||
cd ..
|
||||
rm -rf %{name}-%{version}-build
|
||||
ln -s . %{name}-%{version}-build
|
||||
cd %{name}-%{version}-build
|
||||
%endif
|
||||
|
||||
%build
|
||||
export SYSTEM_REINSTALL_BOOTC_INSTALL_PODMAN_PATH=%{system_reinstall_bootc_install_podman_path}
|
||||
%if 0%{?container_build}
|
||||
# Container build: use cargo directly with cached dependencies
|
||||
export CARGO_HOME=/var/roothome/.cargo
|
||||
cargo build -j%{_smp_build_ncpus} --release %{?with_rhsm:--features rhsm} \
|
||||
--bin=bootc --bin=system-reinstall-bootc \
|
||||
%{?with_tests:--bin tests-integration}
|
||||
make manpages
|
||||
%else
|
||||
# Build the main bootc binary
|
||||
%if %new_cargo_macros
|
||||
%cargo_build %{?with_rhsm:-f rhsm}
|
||||
@@ -104,7 +138,6 @@ rm vendor-config.toml
|
||||
|
||||
# Build the system reinstallation CLI binary
|
||||
%global cargo_args -p system-reinstall-bootc
|
||||
export SYSTEM_REINSTALL_BOOTC_INSTALL_PODMAN_PATH=%{system_reinstall_bootc_install_podman_path}
|
||||
%if %new_cargo_macros
|
||||
# In cargo-rpm-macros, the cargo_build macro does flag processing,
|
||||
# so we need to pass '--' to signify that cargo_args is not part
|
||||
@@ -118,18 +151,24 @@ export SYSTEM_REINSTALL_BOOTC_INSTALL_PODMAN_PATH=%{system_reinstall_bootc_insta
|
||||
%endif
|
||||
|
||||
make manpages
|
||||
%endif
|
||||
|
||||
%if ! 0%{?container_build}
|
||||
%cargo_vendor_manifest
|
||||
# https://pagure.io/fedora-rust/rust-packaging/issue/33
|
||||
sed -i -e '/https:\/\//d' cargo-vendor.txt
|
||||
%cargo_license_summary
|
||||
%{cargo_license} > LICENSE.dependencies
|
||||
%endif
|
||||
|
||||
%install
|
||||
%make_install INSTALL="install -p -c"
|
||||
%if %{with ostree_ext}
|
||||
make install-ostree-hooks DESTDIR=%{?buildroot}
|
||||
%endif
|
||||
%if %{with tests}
|
||||
install -D -m 0755 target/release/tests-integration %{buildroot}%{_bindir}/bootc-integration-tests
|
||||
%endif
|
||||
mkdir -p %{buildroot}/%{dirname:%{system_reinstall_bootc_install_podman_path}}
|
||||
cat >%{?buildroot}/%{system_reinstall_bootc_install_podman_path} <<EOF
|
||||
#!/bin/bash
|
||||
@@ -153,8 +192,10 @@ fi
|
||||
%files -f bootcdoclist.txt
|
||||
%license LICENSE-MIT
|
||||
%license LICENSE-APACHE
|
||||
%if ! 0%{?container_build}
|
||||
%license LICENSE.dependencies
|
||||
%license cargo-vendor.txt
|
||||
%endif
|
||||
%doc README.md
|
||||
%{_bindir}/bootc
|
||||
%{_prefix}/lib/bootc/
|
||||
@@ -169,5 +210,10 @@ fi
|
||||
%{_bindir}/system-reinstall-bootc
|
||||
%{system_reinstall_bootc_install_podman_path}
|
||||
|
||||
%if %{with tests}
|
||||
%files tests
|
||||
%{_bindir}/bootc-integration-tests
|
||||
%endif
|
||||
|
||||
%changelog
|
||||
%autochangelog
|
||||
|
||||
44
contrib/packaging/build-rpm
Executable file
44
contrib/packaging/build-rpm
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Build bootc RPM package from source
|
||||
set -xeuo pipefail
|
||||
|
||||
# Version can be passed via RPM_VERSION env var (set by Dockerfile ARG)
|
||||
# or defaults to the hardcoded value in the spec file
|
||||
VERSION="${RPM_VERSION:-}"
|
||||
|
||||
# Determine output directory (defaults to /out)
|
||||
OUTPUT_DIR="${1:-/out}"
|
||||
SRC_DIR="${2:-/src}"
|
||||
|
||||
if [ -n "${VERSION}" ]; then
|
||||
echo "Building RPM with version: ${VERSION}"
|
||||
else
|
||||
echo "Building RPM with version from spec file"
|
||||
fi
|
||||
|
||||
# Create temporary rpmbuild directories
|
||||
mkdir -p /tmp/rpmbuild/{RPMS,BUILDROOT,SPECS}
|
||||
|
||||
# If version is provided, create modified spec file; otherwise use original
|
||||
if [ -n "${VERSION}" ]; then
|
||||
sed "s/^Version:.*/Version: ${VERSION}/" \
|
||||
"${SRC_DIR}/contrib/packaging/bootc.spec" > /tmp/rpmbuild/SPECS/bootc.spec
|
||||
SPEC_FILE=/tmp/rpmbuild/SPECS/bootc.spec
|
||||
else
|
||||
SPEC_FILE="${SRC_DIR}/contrib/packaging/bootc.spec"
|
||||
fi
|
||||
|
||||
# Build RPM
|
||||
rpmbuild -bb \
|
||||
--define "_topdir /tmp/rpmbuild" \
|
||||
--define "_builddir ${SRC_DIR}" \
|
||||
--define "container_build 1" \
|
||||
--with tests \
|
||||
--nocheck \
|
||||
"${SPEC_FILE}"
|
||||
|
||||
# Copy built RPMs to output directory
|
||||
ARCH=$(uname -m)
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
cp /tmp/rpmbuild/RPMS/${ARCH}/*.rpm "${OUTPUT_DIR}/"
|
||||
rm -rf /tmp/rpmbuild
|
||||
46
contrib/packaging/configure-rootfs
Executable file
46
contrib/packaging/configure-rootfs
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Configure rootfs type for bootc installation
|
||||
set -xeuo pipefail
|
||||
|
||||
VARIANT="${1:-}"
|
||||
ROOTFS="${2:-}"
|
||||
|
||||
# Support overriding the rootfs at build time
|
||||
CONFIG_DIR="/usr/lib/bootc/install"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# Do we have an explicit build-time override? Then write it.
|
||||
if [ -n "$ROOTFS" ]; then
|
||||
cat > "${CONFIG_DIR}/80-rootfs-override.toml" <<EOF
|
||||
[install.filesystem.root]
|
||||
type = "$ROOTFS"
|
||||
EOF
|
||||
else
|
||||
# Query the default rootfs
|
||||
base_rootfs=$(bootc install print-configuration | jq -r '.filesystem.root.type // ""')
|
||||
|
||||
# No filesystem override set. If we're doing composefs, we need a FS that
|
||||
# supports fsverity. If btrfs is available we'll pick that, otherwise ext4.
|
||||
fs=
|
||||
case "${VARIANT}" in
|
||||
composefs*)
|
||||
btrfs=$(grep -qEe '^CONFIG_BTRFS_FS' /usr/lib/modules/*/config && echo btrfs || true)
|
||||
fs=${btrfs:-ext4}
|
||||
;;
|
||||
*)
|
||||
# No explicit filesystem set and we're not using composefs. Default to xfs
|
||||
# with the rationale that we're trying to get filesystem coverage across
|
||||
# all the cases in general.
|
||||
if [ -z "${base_rootfs}" ]; then
|
||||
fs=xfs
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$fs" ]; then
|
||||
cat > "${CONFIG_DIR}/80-ext4-composefs.toml" <<EOF
|
||||
[install.filesystem.root]
|
||||
type = "${fs}"
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
26
contrib/packaging/configure-variant
Executable file
26
contrib/packaging/configure-variant
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Configure system for a specific bootc variant
|
||||
set -xeuo pipefail
|
||||
|
||||
VARIANT="${1:-}"
|
||||
|
||||
if [ -z "$VARIANT" ]; then
|
||||
# No variant specified, nothing to do
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle variant-specific configuration
|
||||
case "${VARIANT}" in
|
||||
*-sdboot)
|
||||
# Install systemd-boot and remove bootupd
|
||||
dnf -y install systemd-boot-unsigned
|
||||
# Uninstall bootupd
|
||||
rpm -e bootupd
|
||||
rm -rf /usr/lib/bootupd/updates
|
||||
# Clean up package manager caches
|
||||
dnf clean all
|
||||
rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/*
|
||||
;;
|
||||
# Future variants can be added here
|
||||
# For Debian support, this could check package manager type and use apt instead
|
||||
esac
|
||||
@@ -5,3 +5,5 @@ rustfmt
|
||||
clippy
|
||||
git-core
|
||||
jq
|
||||
# We now always build a package in the container build
|
||||
rpm-build
|
||||
|
||||
15
contrib/packaging/install-buildroot
Executable file
15
contrib/packaging/install-buildroot
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Install buildroot requirements for Fedora derivatives
|
||||
set -xeuo pipefail
|
||||
cd $(dirname $0)
|
||||
. /usr/lib/os-release
|
||||
case $ID in
|
||||
centos|rhel) dnf config-manager --set-enabled crb;;
|
||||
fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';;
|
||||
esac
|
||||
# Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174
|
||||
dnf -y distro-sync ostree{,-libs} systemd
|
||||
# Install base build requirements
|
||||
dnf -y builddep bootc.spec
|
||||
# And extra packages
|
||||
grep -Ev -e '^#' fedora-extra.txt | xargs dnf -y install
|
||||
24
contrib/packaging/install-rpm-and-setup
Executable file
24
contrib/packaging/install-rpm-and-setup
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Install bootc RPM and perform post-installation setup
|
||||
set -xeuo pipefail
|
||||
|
||||
RPM_DIR="${1:-/tmp}"
|
||||
|
||||
# Install the RPM package
|
||||
# Use rpm -Uvh with --oldpackage to allow replacing with dev version
|
||||
rpm -Uvh --oldpackage "${RPM_DIR}"/*.rpm
|
||||
rm -f "${RPM_DIR}"/*.rpm
|
||||
|
||||
# Regenerate initramfs if we have initramfs-setup
|
||||
if [ -x /usr/lib/bootc/initramfs-setup ]; then
|
||||
kver=$(cd /usr/lib/modules && echo *)
|
||||
env DRACUT_NO_XATTR=1 dracut -vf /usr/lib/modules/$kver/initramfs.img $kver
|
||||
fi
|
||||
|
||||
# Only in this containerfile, inject a file which signifies
|
||||
# this comes from this development image. This can be used in
|
||||
# tests to know we're doing upstream CI.
|
||||
touch /usr/lib/.bootc-dev-stamp
|
||||
|
||||
# Workaround for https://github.com/bootc-dev/bootc/issues/1546
|
||||
rm -rf /root/buildinfo
|
||||
Reference in New Issue
Block a user