dockerTools streamNixShellImage implements a facsimile of the environment created by the Nix build process. In order to keep track of the Nix features implemented, the streamNixShellImage code has comments containing hyperlinks indicating the corresponding Nix C++ code in the GitHub repository. As Nix 2.32.0 has now been released [1], this patch updates the GitHub links to correspond to the code in the new version. [1] https://discourse.nixos.org/t/nix-2-32-0-released/70528
1425 lines
47 KiB
Nix
1425 lines
47 KiB
Nix
{
|
|
bashInteractive,
|
|
buildPackages,
|
|
cacert,
|
|
callPackage,
|
|
closureInfo,
|
|
coreutils,
|
|
devShellTools,
|
|
e2fsprogs,
|
|
proot,
|
|
fakeNss,
|
|
fakeroot,
|
|
file,
|
|
go,
|
|
jq,
|
|
jshon,
|
|
lib,
|
|
makeWrapper,
|
|
moreutils,
|
|
nix,
|
|
nixosTests,
|
|
pigz,
|
|
rsync,
|
|
runCommand,
|
|
runtimeShell,
|
|
shadow,
|
|
skopeo,
|
|
stdenv,
|
|
storeDir ? builtins.storeDir,
|
|
symlinkJoin,
|
|
tarsum,
|
|
util-linux,
|
|
vmTools,
|
|
writeClosure,
|
|
writeScript,
|
|
writeShellScriptBin,
|
|
writeText,
|
|
writeTextDir,
|
|
writePython3,
|
|
zstd,
|
|
}:
|
|
|
|
let
|
|
inherit (lib)
|
|
optionals
|
|
optionalString
|
|
;
|
|
|
|
inherit (lib)
|
|
escapeShellArgs
|
|
toList
|
|
;
|
|
|
|
inherit (devShellTools)
|
|
valueToString
|
|
;
|
|
|
|
mkDbExtraCommand =
|
|
contents:
|
|
let
|
|
contentsList = if builtins.isList contents then contents else [ contents ];
|
|
in
|
|
''
|
|
echo "Generating the nix database..."
|
|
echo "Warning: only the database of the deepest Nix layer is loaded."
|
|
echo " If you want to use nix commands in the container, it would"
|
|
echo " be better to only have one layer that contains a nix store."
|
|
|
|
export NIX_REMOTE=local?root=$PWD
|
|
# A user is required by nix
|
|
# https://github.com/NixOS/nix/blob/9348f9291e5d9e4ba3c4347ea1b235640f54fd79/src/libutil/util.cc#L478
|
|
export USER=nobody
|
|
${buildPackages.nix}/bin/nix-store --load-db < ${
|
|
closureInfo { rootPaths = contentsList; }
|
|
}/registration
|
|
# Reset registration times to make the image reproducible
|
|
${buildPackages.sqlite}/bin/sqlite3 nix/var/nix/db/db.sqlite "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
|
|
|
mkdir -p nix/var/nix/gcroots/docker/
|
|
for i in ${lib.concatStringsSep " " contentsList}; do
|
|
ln -s $i nix/var/nix/gcroots/docker/$(basename $i)
|
|
done;
|
|
'';
|
|
|
|
# The OCI Image specification recommends that configurations use values listed
|
|
# in the Go Language document for GOARCH.
|
|
# Reference: https://github.com/opencontainers/image-spec/blob/master/config.md#properties
|
|
# For the mapping from Nixpkgs system parameters to GOARCH, we can reuse the
|
|
# mapping from the go package.
|
|
defaultArchitecture = go.GOARCH;
|
|
|
|
compressors = {
|
|
none = {
|
|
ext = "";
|
|
nativeInputs = [ ];
|
|
compress = "cat";
|
|
decompress = "cat";
|
|
};
|
|
gz = {
|
|
ext = ".gz";
|
|
nativeInputs = [ pigz ];
|
|
compress = "pigz -p$NIX_BUILD_CORES -nTR";
|
|
decompress = "pigz -d -p$NIX_BUILD_CORES";
|
|
};
|
|
zstd = {
|
|
ext = ".zst";
|
|
nativeInputs = [ zstd ];
|
|
compress = "zstd -T$NIX_BUILD_CORES";
|
|
decompress = "zstd -d -T$NIX_BUILD_CORES";
|
|
};
|
|
};
|
|
|
|
compressorForImage =
|
|
compressor: imageName:
|
|
compressors.${compressor}
|
|
or (throw "in docker image ${imageName}: compressor must be one of: [${toString builtins.attrNames compressors}]");
|
|
|
|
in
|
|
rec {
|
|
examples = callPackage ./examples.nix {
|
|
inherit
|
|
buildImage
|
|
buildLayeredImage
|
|
fakeNss
|
|
pullImage
|
|
shadowSetup
|
|
buildImageWithNixDb
|
|
streamNixShellImage
|
|
;
|
|
};
|
|
|
|
tests = {
|
|
inherit (nixosTests)
|
|
docker-tools
|
|
docker-tools-overlay
|
|
# requires remote builder
|
|
# docker-tools-cross
|
|
;
|
|
};
|
|
|
|
pullImage =
|
|
let
|
|
fixName = name: builtins.replaceStrings [ "/" ":" ] [ "-" "-" ] name;
|
|
in
|
|
lib.fetchers.withNormalizedHash { } (
|
|
{
|
|
imageName,
|
|
# To find the digest of an image, you can use skopeo:
|
|
# see doc/functions.xml
|
|
imageDigest,
|
|
outputHash,
|
|
outputHashAlgo,
|
|
os ? "linux",
|
|
# Image architecture, defaults to the architecture of the `hostPlatform` when unset
|
|
arch ? defaultArchitecture,
|
|
# This is used to set name to the pulled image
|
|
finalImageName ? imageName,
|
|
# This used to set a tag to the pulled image
|
|
finalImageTag ? "latest",
|
|
# This is used to disable TLS certificate verification, allowing access to http registries on (hopefully) trusted networks
|
|
tlsVerify ? true,
|
|
|
|
name ? fixName "docker-image-${finalImageName}-${finalImageTag}.tar",
|
|
}:
|
|
|
|
runCommand name
|
|
{
|
|
inherit imageDigest;
|
|
imageName = finalImageName;
|
|
imageTag = finalImageTag;
|
|
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
|
|
|
|
inherit outputHash outputHashAlgo;
|
|
outputHashMode = "flat";
|
|
|
|
nativeBuildInputs = [ skopeo ];
|
|
SSL_CERT_FILE = "${cacert.out}/etc/ssl/certs/ca-bundle.crt";
|
|
|
|
sourceURL = "docker://${imageName}@${imageDigest}";
|
|
destNameTag = "${finalImageName}:${finalImageTag}";
|
|
}
|
|
''
|
|
skopeo \
|
|
--insecure-policy \
|
|
--tmpdir=$TMPDIR \
|
|
--override-os ${os} \
|
|
--override-arch ${arch} \
|
|
copy \
|
|
--src-tls-verify=${lib.boolToString tlsVerify} \
|
|
"$sourceURL" "docker-archive://$out:$destNameTag" \
|
|
| cat # pipe through cat to force-disable progress bar
|
|
''
|
|
);
|
|
|
|
# We need to sum layer.tar, not a directory, hence tarsum instead of nix-hash.
|
|
# And we cannot untar it, because then we cannot preserve permissions etc.
|
|
inherit tarsum; # pkgs.dockerTools.tarsum
|
|
|
|
# buildEnv creates symlinks to dirs, which is hard to edit inside the overlay VM
|
|
mergeDrvs =
|
|
{
|
|
derivations,
|
|
onlyDeps ? false,
|
|
}:
|
|
runCommand "merge-drvs"
|
|
{
|
|
inherit derivations onlyDeps;
|
|
}
|
|
''
|
|
if [[ -n "$onlyDeps" ]]; then
|
|
echo $derivations > $out
|
|
exit 0
|
|
fi
|
|
|
|
mkdir $out
|
|
for derivation in $derivations; do
|
|
echo "Merging $derivation..."
|
|
if [[ -d "$derivation" ]]; then
|
|
# If it's a directory, copy all of its contents into $out.
|
|
cp -drf --preserve=mode -f $derivation/* $out/
|
|
else
|
|
# Otherwise treat the derivation as a tarball and extract it
|
|
# into $out.
|
|
tar -C $out -xpf $drv || true
|
|
fi
|
|
done
|
|
'';
|
|
|
|
# Helper for setting up the base files for managing users and
|
|
# groups, only if such files don't exist already. It is suitable for
|
|
# being used in a runAsRoot script.
|
|
shadowSetup = ''
|
|
export PATH=${shadow}/bin:$PATH
|
|
mkdir -p /etc/pam.d
|
|
if [[ ! -f /etc/passwd ]]; then
|
|
echo "root:x:0:0::/root:${runtimeShell}" > /etc/passwd
|
|
echo "root:!x:::::::" > /etc/shadow
|
|
fi
|
|
if [[ ! -f /etc/group ]]; then
|
|
echo "root:x:0:" > /etc/group
|
|
echo "root:x::" > /etc/gshadow
|
|
fi
|
|
if [[ ! -f /etc/pam.d/other ]]; then
|
|
cat > /etc/pam.d/other <<EOF
|
|
account sufficient pam_unix.so
|
|
auth sufficient pam_rootok.so
|
|
password requisite pam_unix.so nullok yescrypt
|
|
session required pam_unix.so
|
|
EOF
|
|
fi
|
|
if [[ ! -f /etc/login.defs ]]; then
|
|
touch /etc/login.defs
|
|
fi
|
|
'';
|
|
|
|
# Run commands in a virtual machine.
|
|
runWithOverlay =
|
|
{
|
|
name,
|
|
fromImage ? null,
|
|
fromImageName ? null,
|
|
fromImageTag ? null,
|
|
diskSize ? 1024,
|
|
buildVMMemorySize ? 512,
|
|
preMount ? "",
|
|
postMount ? "",
|
|
postUmount ? "",
|
|
}:
|
|
vmTools.runInLinuxVM (
|
|
runCommand name
|
|
{
|
|
preVM = vmTools.createEmptyImage {
|
|
size = diskSize;
|
|
fullName = "docker-run-disk";
|
|
destination = "./image";
|
|
};
|
|
inherit fromImage fromImageName fromImageTag;
|
|
memSize = buildVMMemorySize;
|
|
|
|
nativeBuildInputs = [
|
|
util-linux
|
|
e2fsprogs
|
|
jshon
|
|
rsync
|
|
jq
|
|
];
|
|
}
|
|
''
|
|
mkdir disk
|
|
mkfs /dev/${vmTools.hd}
|
|
mount /dev/${vmTools.hd} disk
|
|
cd disk
|
|
|
|
function dedup() {
|
|
declare -A seen
|
|
while read ln; do
|
|
if [[ -z "''${seen["$ln"]:-}" ]]; then
|
|
echo "$ln"; seen["$ln"]=1
|
|
fi
|
|
done
|
|
}
|
|
|
|
if [[ -n "$fromImage" ]]; then
|
|
echo "Unpacking base image..."
|
|
mkdir image
|
|
tar -C image -xpf "$fromImage"
|
|
|
|
if [[ -n "$fromImageName" ]] && [[ -n "$fromImageTag" ]]; then
|
|
parentID="$(
|
|
cat "image/manifest.json" |
|
|
jq -r '.[] | select(.RepoTags | contains([$desiredTag])) | rtrimstr(".json")' \
|
|
--arg desiredTag "$fromImageName:$fromImageTag"
|
|
)"
|
|
else
|
|
echo "From-image name or tag wasn't set. Reading the first ID."
|
|
parentID="$(cat "image/manifest.json" | jq -r '.[0].Config | rtrimstr(".json")')"
|
|
fi
|
|
|
|
# In case of repeated layers, unpack only the last occurrence of each
|
|
cat ./image/manifest.json | jq -r '.[0].Layers | .[]' | tac | dedup | tac > layer-list
|
|
else
|
|
touch layer-list
|
|
fi
|
|
|
|
# Unpack all of the parent layers into the image.
|
|
lowerdir=""
|
|
extractionID=0
|
|
for layerTar in $(cat layer-list); do
|
|
echo "Unpacking layer $layerTar"
|
|
extractionID=$((extractionID + 1))
|
|
|
|
mkdir -p image/$extractionID/layer
|
|
tar -C image/$extractionID/layer -xpf image/$layerTar
|
|
rm image/$layerTar
|
|
|
|
find image/$extractionID/layer -name ".wh.*" -exec bash -c 'name="$(basename {}|sed "s/^.wh.//")"; mknod "$(dirname {})/$name" c 0 0; rm {}' \;
|
|
|
|
# Get the next lower directory and continue the loop.
|
|
lowerdir=image/$extractionID/layer''${lowerdir:+:}$lowerdir
|
|
done
|
|
|
|
mkdir work
|
|
mkdir layer
|
|
mkdir mnt
|
|
|
|
${lib.optionalString (preMount != "") ''
|
|
# Execute pre-mount steps
|
|
echo "Executing pre-mount steps..."
|
|
${preMount}
|
|
''}
|
|
|
|
if [ -n "$lowerdir" ]; then
|
|
mount -t overlay overlay -olowerdir=$lowerdir,workdir=work,upperdir=layer mnt
|
|
else
|
|
mount --bind layer mnt
|
|
fi
|
|
|
|
${lib.optionalString (postMount != "") ''
|
|
# Execute post-mount steps
|
|
echo "Executing post-mount steps..."
|
|
${postMount}
|
|
''}
|
|
|
|
umount mnt
|
|
|
|
(
|
|
cd layer
|
|
cmd='name="$(basename {})"; touch "$(dirname {})/.wh.$name"; rm "{}"'
|
|
find . -type c -exec bash -c "$cmd" \;
|
|
)
|
|
|
|
${postUmount}
|
|
''
|
|
);
|
|
|
|
exportImage =
|
|
{
|
|
name ? fromImage.name,
|
|
fromImage,
|
|
fromImageName ? null,
|
|
fromImageTag ? null,
|
|
diskSize ? 1024,
|
|
}:
|
|
runWithOverlay {
|
|
inherit
|
|
name
|
|
fromImage
|
|
fromImageName
|
|
fromImageTag
|
|
diskSize
|
|
;
|
|
|
|
postMount = ''
|
|
echo "Packing raw image..."
|
|
mkdir -p $out
|
|
tar -C mnt --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" -cf $out/layer.tar .
|
|
'';
|
|
|
|
postUmount = ''
|
|
mv $out/layer.tar .
|
|
rm -rf $out
|
|
mv layer.tar $out
|
|
'';
|
|
};
|
|
|
|
# Create an executable shell script which has the coreutils in its
|
|
# PATH. Since root scripts are executed in a blank environment, even
|
|
# things like `ls` or `echo` will be missing.
|
|
shellScript =
|
|
name: text:
|
|
writeScript name ''
|
|
#!${runtimeShell}
|
|
set -e
|
|
export PATH=${coreutils}/bin:/bin
|
|
${text}
|
|
'';
|
|
|
|
# Create a "layer" (set of files).
|
|
mkPureLayer =
|
|
{
|
|
# Name of the layer
|
|
name,
|
|
# JSON containing configuration and metadata for this layer.
|
|
baseJson,
|
|
# Files to add to the layer.
|
|
copyToRoot ? null,
|
|
# When copying the contents into the image, preserve symlinks to
|
|
# directories (see `rsync -K`). Otherwise, transform those symlinks
|
|
# into directories.
|
|
keepContentsDirlinks ? false,
|
|
# Additional commands to run on the layer before it is tar'd up.
|
|
extraCommands ? "",
|
|
uid ? 0,
|
|
gid ? 0,
|
|
}:
|
|
runCommand "docker-layer-${name}"
|
|
{
|
|
inherit baseJson extraCommands;
|
|
contents = copyToRoot;
|
|
nativeBuildInputs = [
|
|
jshon
|
|
rsync
|
|
tarsum
|
|
];
|
|
}
|
|
''
|
|
mkdir layer
|
|
if [[ -n "$contents" ]]; then
|
|
echo "Adding contents..."
|
|
for item in $contents; do
|
|
echo "Adding $item"
|
|
rsync -a${if keepContentsDirlinks then "K" else "k"} --chown=0:0 $item/ layer/
|
|
done
|
|
else
|
|
echo "No contents to add to layer."
|
|
fi
|
|
|
|
chmod ug+w layer
|
|
|
|
if [[ -n "$extraCommands" ]]; then
|
|
(cd layer; eval "$extraCommands")
|
|
fi
|
|
|
|
# Tar up the layer and throw it into 'layer.tar'.
|
|
echo "Packing layer..."
|
|
mkdir $out
|
|
tarhash=$(tar -C layer --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=${toString uid} --group=${toString gid} -cf - . | tee -p $out/layer.tar | tarsum)
|
|
|
|
# Add a 'checksum' field to the JSON, with the value set to the
|
|
# checksum of the tarball.
|
|
cat ${baseJson} | jshon -s "$tarhash" -i checksum > $out/json
|
|
|
|
# Indicate to docker that we're using schema version 1.0.
|
|
echo -n "1.0" > $out/VERSION
|
|
|
|
echo "Finished building layer '${name}'"
|
|
'';
|
|
|
|
# Make a "root" layer; required if we need to execute commands as a
|
|
# privileged user on the image. The commands themselves will be
|
|
# performed in a virtual machine sandbox.
|
|
mkRootLayer =
|
|
{
|
|
# Name of the image.
|
|
name,
|
|
# Script to run as root. Bash.
|
|
runAsRoot,
|
|
# Files to add to the layer. If null, an empty layer will be created.
|
|
# To add packages to /bin, use `buildEnv` or similar.
|
|
copyToRoot ? null,
|
|
# When copying the contents into the image, preserve symlinks to
|
|
# directories (see `rsync -K`). Otherwise, transform those symlinks
|
|
# into directories.
|
|
keepContentsDirlinks ? false,
|
|
# JSON containing configuration and metadata for this layer.
|
|
baseJson,
|
|
# Existing image onto which to append the new layer.
|
|
fromImage ? null,
|
|
# Name of the image we're appending onto.
|
|
fromImageName ? null,
|
|
# Tag of the image we're appending onto.
|
|
fromImageTag ? null,
|
|
# How much disk to allocate for the temporary virtual machine.
|
|
diskSize ? 1024,
|
|
# How much memory to allocate for the temporary virtual machine.
|
|
buildVMMemorySize ? 512,
|
|
# Commands (bash) to run on the layer; these do not require sudo.
|
|
extraCommands ? "",
|
|
}:
|
|
# Generate an executable script from the `runAsRoot` text.
|
|
let
|
|
runAsRootScript = shellScript "run-as-root.sh" runAsRoot;
|
|
extraCommandsScript = shellScript "extra-commands.sh" extraCommands;
|
|
in
|
|
runWithOverlay {
|
|
name = "docker-layer-${name}";
|
|
|
|
inherit
|
|
fromImage
|
|
fromImageName
|
|
fromImageTag
|
|
diskSize
|
|
buildVMMemorySize
|
|
;
|
|
|
|
preMount = lib.optionalString (copyToRoot != null && copyToRoot != [ ]) ''
|
|
echo "Adding contents..."
|
|
for item in ${escapeShellArgs (map (c: "${c}") (toList copyToRoot))}; do
|
|
echo "Adding $item..."
|
|
rsync -a${if keepContentsDirlinks then "K" else "k"} --chown=0:0 $item/ layer/
|
|
done
|
|
|
|
chmod ug+w layer
|
|
'';
|
|
|
|
postMount = ''
|
|
mkdir -p mnt/{dev,proc,sys,tmp} mnt${storeDir}
|
|
|
|
# Mount /dev, /sys and the nix store as shared folders.
|
|
mount --rbind /dev mnt/dev
|
|
mount --rbind /sys mnt/sys
|
|
mount --rbind ${storeDir} mnt${storeDir}
|
|
|
|
# Execute the run as root script. See 'man unshare' for
|
|
# details on what's going on here; basically this command
|
|
# means that the runAsRootScript will be executed in a nearly
|
|
# completely isolated environment.
|
|
#
|
|
# Ideally we would use --mount-proc=mnt/proc or similar, but this
|
|
# doesn't work. The workaround is to setup proc after unshare.
|
|
# See: https://github.com/karelzak/util-linux/issues/648
|
|
unshare -imnpuf --mount-proc sh -c 'mount --rbind /proc mnt/proc && chroot mnt ${runAsRootScript}'
|
|
|
|
# Unmount directories and remove them.
|
|
umount -R mnt/dev mnt/sys mnt${storeDir}
|
|
rmdir --ignore-fail-on-non-empty \
|
|
mnt/dev mnt/proc mnt/sys mnt${storeDir} \
|
|
mnt$(dirname ${storeDir})
|
|
'';
|
|
|
|
postUmount = ''
|
|
(cd layer; ${extraCommandsScript})
|
|
|
|
echo "Packing layer..."
|
|
mkdir -p $out
|
|
tarhash=$(tar -C layer --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" -cf - . |
|
|
tee -p $out/layer.tar |
|
|
${tarsum}/bin/tarsum)
|
|
|
|
cat ${baseJson} | jshon -s "$tarhash" -i checksum > $out/json
|
|
# Indicate to docker that we're using schema version 1.0.
|
|
echo -n "1.0" > $out/VERSION
|
|
|
|
echo "Finished building layer '${name}'"
|
|
'';
|
|
};
|
|
|
|
buildLayeredImage = lib.makeOverridable (
|
|
{
|
|
name,
|
|
compressor ? "gz",
|
|
...
|
|
}@args:
|
|
let
|
|
stream = streamLayeredImage (removeAttrs args [ "compressor" ]);
|
|
compress = compressorForImage compressor name;
|
|
in
|
|
runCommand "${baseNameOf name}.tar${compress.ext}" {
|
|
inherit (stream) imageName;
|
|
passthru = {
|
|
inherit (stream) imageTag;
|
|
inherit stream;
|
|
};
|
|
nativeBuildInputs = compress.nativeInputs;
|
|
} "${stream} | ${compress.compress} > $out"
|
|
);
|
|
|
|
# 1. extract the base image
|
|
# 2. create the layer
|
|
# 3. add layer deps to the layer itself, diffing with the base image
|
|
# 4. compute the layer id
|
|
# 5. put the layer in the image
|
|
# 6. repack the image
|
|
buildImage = lib.makeOverridable (
|
|
args@{
|
|
# Image name.
|
|
name,
|
|
# Image tag, when null then the nix output hash will be used.
|
|
tag ? null,
|
|
# Parent image, to append to.
|
|
fromImage ? null,
|
|
# Name of the parent image; will be read from the image otherwise.
|
|
fromImageName ? null,
|
|
# Tag of the parent image; will be read from the image otherwise.
|
|
fromImageTag ? null,
|
|
# Files to put on the image (a nix store path or list of paths).
|
|
copyToRoot ? null,
|
|
# When copying the contents into the image, preserve symlinks to
|
|
# directories (see `rsync -K`). Otherwise, transform those symlinks
|
|
# into directories.
|
|
keepContentsDirlinks ? false,
|
|
# Docker config; e.g. what command to run on the container.
|
|
config ? null,
|
|
# Image architecture, defaults to the architecture of the `hostPlatform` when unset
|
|
architecture ? defaultArchitecture,
|
|
# Optional bash script to run on the files prior to fixturizing the layer.
|
|
extraCommands ? "",
|
|
uid ? 0,
|
|
gid ? 0,
|
|
# Optional bash script to run as root on the image when provisioning.
|
|
runAsRoot ? null,
|
|
# Size of the virtual machine disk to provision when building the image.
|
|
diskSize ? 1024,
|
|
# Size of the virtual machine memory to provision when building the image.
|
|
buildVMMemorySize ? 512,
|
|
# Time of creation of the image.
|
|
created ? "1970-01-01T00:00:01Z",
|
|
# Compressor to use. One of: none, gz, zstd.
|
|
compressor ? "gz",
|
|
# Populate the nix database in the image with the dependencies of `copyToRoot`.
|
|
includeNixDB ? false,
|
|
# Deprecated.
|
|
contents ? null,
|
|
}:
|
|
|
|
let
|
|
checked =
|
|
lib.warnIf (contents != null)
|
|
"in docker image ${name}: The contents parameter is deprecated. Change to copyToRoot if the contents are designed to be copied to the root filesystem, such as when you use `buildEnv` or similar between contents and your packages. Use copyToRoot = buildEnv { ... }; or similar if you intend to add packages to /bin."
|
|
lib.throwIf
|
|
(contents != null && copyToRoot != null)
|
|
"in docker image ${name}: You can not specify both contents and copyToRoot.";
|
|
|
|
rootContents = if copyToRoot == null then contents else copyToRoot;
|
|
|
|
baseName = baseNameOf name;
|
|
|
|
# Create a JSON blob of the configuration. Set the date to unix zero.
|
|
baseJson =
|
|
let
|
|
pure = writeText "${baseName}-config.json" (
|
|
builtins.toJSON {
|
|
inherit created config architecture;
|
|
preferLocalBuild = true;
|
|
os = "linux";
|
|
}
|
|
);
|
|
impure =
|
|
runCommand "${baseName}-config.json"
|
|
{
|
|
nativeBuildInputs = [ jq ];
|
|
preferLocalBuild = true;
|
|
}
|
|
''
|
|
jq ".created = \"$(TZ=utc date --iso-8601="seconds")\"" ${pure} > $out
|
|
'';
|
|
in
|
|
if created == "now" then impure else pure;
|
|
|
|
compress = compressorForImage compressor name;
|
|
|
|
# TODO: add the dependencies of the config json.
|
|
extraCommandsWithDB =
|
|
if includeNixDB then (mkDbExtraCommand rootContents) + extraCommands else extraCommands;
|
|
|
|
layer =
|
|
if runAsRoot == null then
|
|
mkPureLayer {
|
|
name = baseName;
|
|
inherit
|
|
baseJson
|
|
keepContentsDirlinks
|
|
uid
|
|
gid
|
|
;
|
|
extraCommands = extraCommandsWithDB;
|
|
copyToRoot = rootContents;
|
|
}
|
|
else
|
|
mkRootLayer {
|
|
name = baseName;
|
|
inherit
|
|
baseJson
|
|
fromImage
|
|
fromImageName
|
|
fromImageTag
|
|
keepContentsDirlinks
|
|
runAsRoot
|
|
diskSize
|
|
buildVMMemorySize
|
|
;
|
|
extraCommands = extraCommandsWithDB;
|
|
copyToRoot = rootContents;
|
|
};
|
|
result =
|
|
runCommand "docker-image-${baseName}.tar${compress.ext}"
|
|
{
|
|
nativeBuildInputs = [
|
|
jshon
|
|
jq
|
|
moreutils
|
|
]
|
|
++ compress.nativeInputs;
|
|
# Image name must be lowercase
|
|
imageName = lib.toLower name;
|
|
imageTag = lib.optionalString (tag != null) tag;
|
|
inherit fromImage baseJson;
|
|
layerClosure = writeClosure [ layer ];
|
|
passthru.buildArgs = args;
|
|
passthru.layer = layer;
|
|
passthru.imageTag =
|
|
if tag != null then
|
|
tag
|
|
else
|
|
lib.head (
|
|
lib.strings.splitString "-" (baseNameOf (builtins.unsafeDiscardStringContext result.outPath))
|
|
);
|
|
}
|
|
''
|
|
${lib.optionalString (tag == null) ''
|
|
outName="$(basename "$out")"
|
|
outHash=$(echo "$outName" | cut -d - -f 1)
|
|
|
|
imageTag=$outHash
|
|
''}
|
|
|
|
# Print tar contents:
|
|
# 1: Interpreted as relative to the root directory
|
|
# 2: With no trailing slashes on directories
|
|
# This is useful for ensuring that the output matches the
|
|
# values generated by the "find" command
|
|
ls_tar() {
|
|
for f in $(tar -tf $1 | xargs realpath -ms --relative-to=.); do
|
|
if [[ "$f" != "." ]]; then
|
|
echo "/$f"
|
|
fi
|
|
done
|
|
}
|
|
|
|
mkdir image
|
|
touch baseFiles
|
|
baseEnvs='[]'
|
|
if [[ -n "$fromImage" ]]; then
|
|
echo "Unpacking base image..."
|
|
tar -C image -xpf "$fromImage"
|
|
|
|
# Store the layers and the environment variables from the base image
|
|
cat ./image/manifest.json | jq -r '.[0].Layers | .[]' > layer-list
|
|
configName="$(cat ./image/manifest.json | jq -r '.[0].Config')"
|
|
baseEnvs="$(cat "./image/$configName" | jq '.config.Env // []')"
|
|
|
|
# Extract the parentID from the manifest
|
|
if [[ -n "$fromImageName" ]] && [[ -n "$fromImageTag" ]]; then
|
|
parentID="$(
|
|
cat "image/manifest.json" |
|
|
jq -r '.[] | select(.RepoTags | contains([$desiredTag])) | rtrimstr(".json")' \
|
|
--arg desiredTag "$fromImageName:$fromImageTag"
|
|
)"
|
|
else
|
|
echo "From-image name or tag wasn't set. Reading the first ID."
|
|
parentID="$(cat "image/manifest.json" | jq -r '.[0].Config | rtrimstr(".json")')"
|
|
fi
|
|
|
|
# Otherwise do not import the base image configuration and manifest
|
|
chmod a+w image image/*.json
|
|
rm -f image/*.json
|
|
|
|
for l in image/*/layer.tar; do
|
|
ls_tar $l >> baseFiles
|
|
done
|
|
else
|
|
touch layer-list
|
|
fi
|
|
|
|
chmod -R ug+rw image
|
|
|
|
mkdir temp
|
|
cp ${layer}/* temp/
|
|
chmod ug+w temp/*
|
|
|
|
for dep in $(cat $layerClosure); do
|
|
find $dep >> layerFiles
|
|
done
|
|
|
|
echo "Adding layer..."
|
|
# Record the contents of the tarball with ls_tar.
|
|
ls_tar temp/layer.tar >> baseFiles
|
|
|
|
# Append nix/store directory to the layer so that when the layer is loaded in the
|
|
# image /nix/store has read permissions for non-root users.
|
|
# nix/store is added only if the layer has /nix/store paths in it.
|
|
if [ $(wc -l < $layerClosure) -gt 1 ] && [ $(grep -c -e "^/nix/store$" baseFiles) -eq 0 ]; then
|
|
mkdir -p nix/store
|
|
chmod -R 555 nix
|
|
echo "./nix" >> layerFiles
|
|
echo "./nix/store" >> layerFiles
|
|
fi
|
|
|
|
# Get the files in the new layer which were *not* present in
|
|
# the old layer, and record them as newFiles.
|
|
comm <(sort -n baseFiles|uniq) \
|
|
<(sort -n layerFiles|uniq|grep -v ${layer}) -1 -3 > newFiles
|
|
# Append the new files to the layer.
|
|
tar -rpf temp/layer.tar --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" \
|
|
--owner=0 --group=0 --no-recursion --verbatim-files-from --files-from newFiles
|
|
|
|
echo "Adding meta..."
|
|
|
|
# If we have a parentID, add it to the json metadata.
|
|
if [[ -n "$parentID" ]]; then
|
|
cat temp/json | jshon -s "$parentID" -i parent > tmpjson
|
|
mv tmpjson temp/json
|
|
fi
|
|
|
|
# Take the sha256 sum of the generated json and use it as the layer ID.
|
|
# Compute the size and add it to the json under the 'Size' field.
|
|
layerID=$(sha256sum temp/json|cut -d ' ' -f 1)
|
|
size=$(stat --printf="%s" temp/layer.tar)
|
|
cat temp/json | jshon -s "$layerID" -i id -n $size -i Size > tmpjson
|
|
mv tmpjson temp/json
|
|
|
|
# Use the temp folder we've been working on to create a new image.
|
|
mv temp image/$layerID
|
|
|
|
# Add the new layer ID to the end of the layer list
|
|
(
|
|
cat layer-list
|
|
# originally this used `sed -i "1i$layerID" layer-list`, but
|
|
# would fail if layer-list was completely empty.
|
|
echo "$layerID/layer.tar"
|
|
) | sponge layer-list
|
|
|
|
# Create image json and image manifest
|
|
imageJson=$(cat ${baseJson} | jq '.config.Env = $baseenv + .config.Env' --argjson baseenv "$baseEnvs")
|
|
imageJson=$(echo "$imageJson" | jq ". + {\"rootfs\": {\"diff_ids\": [], \"type\": \"layers\"}}")
|
|
manifestJson=$(jq -n "[{\"RepoTags\":[\"$imageName:$imageTag\"]}]")
|
|
|
|
for layerTar in $(cat ./layer-list); do
|
|
layerChecksum=$(sha256sum image/$layerTar | cut -d ' ' -f1)
|
|
imageJson=$(echo "$imageJson" | jq ".history |= . + [{\"created\": \"$(jq -r .created ${baseJson})\"}]")
|
|
# diff_ids order is from the bottom-most to top-most layer
|
|
imageJson=$(echo "$imageJson" | jq ".rootfs.diff_ids |= . + [\"sha256:$layerChecksum\"]")
|
|
manifestJson=$(echo "$manifestJson" | jq ".[0].Layers |= . + [\"$layerTar\"]")
|
|
done
|
|
|
|
imageJsonChecksum=$(echo "$imageJson" | sha256sum | cut -d ' ' -f1)
|
|
echo "$imageJson" > "image/$imageJsonChecksum.json"
|
|
manifestJson=$(echo "$manifestJson" | jq ".[0].Config = \"$imageJsonChecksum.json\"")
|
|
echo "$manifestJson" > image/manifest.json
|
|
|
|
# Store the json under the name image/repositories.
|
|
jshon -n object \
|
|
-n object -s "$layerID" -i "$imageTag" \
|
|
-i "$imageName" > image/repositories
|
|
|
|
# Make the image read-only.
|
|
chmod -R a-w image
|
|
|
|
echo "Cooking the image..."
|
|
tar -C image --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --xform s:'^./':: -c . | ${compress.compress} > $out
|
|
|
|
echo "Finished."
|
|
'';
|
|
|
|
in
|
|
checked result
|
|
);
|
|
|
|
# Merge the tarballs of images built with buildImage into a single
|
|
# tarball that contains all images. Running `docker load` on the resulting
|
|
# tarball will load the images into the docker daemon.
|
|
mergeImages =
|
|
images:
|
|
runCommand "merge-docker-images"
|
|
{
|
|
inherit images;
|
|
nativeBuildInputs = [
|
|
file
|
|
jq
|
|
]
|
|
++ compressors.none.nativeInputs
|
|
++ compressors.gz.nativeInputs
|
|
++ compressors.zstd.nativeInputs;
|
|
}
|
|
''
|
|
mkdir image inputs
|
|
# Extract images
|
|
repos=()
|
|
manifests=()
|
|
last_image_mime="application/gzip"
|
|
for item in $images; do
|
|
name=$(basename $item)
|
|
mkdir inputs/$name
|
|
|
|
last_image_mime=$(file --mime-type -b $item)
|
|
case $last_image_mime in
|
|
"application/x-tar") ${compressors.none.decompress};;
|
|
"application/zstd") ${compressors.zstd.decompress};;
|
|
"application/gzip") ${compressors.gz.decompress};;
|
|
*) echo "error: unexpected layer type $last_image_mime" >&2; exit 1;;
|
|
esac < $item | tar -xC inputs/$name
|
|
|
|
if [ -f inputs/$name/repositories ]; then
|
|
repos+=(inputs/$name/repositories)
|
|
fi
|
|
if [ -f inputs/$name/manifest.json ]; then
|
|
manifests+=(inputs/$name/manifest.json)
|
|
fi
|
|
done
|
|
# Copy all layers from input images to output image directory
|
|
cp -R --update=none inputs/*/* image/
|
|
# Merge repositories objects and manifests
|
|
jq -s add "''${repos[@]}" > repositories
|
|
jq -s add "''${manifests[@]}" > manifest.json
|
|
# Replace output image repositories and manifest with merged versions
|
|
mv repositories image/repositories
|
|
mv manifest.json image/manifest.json
|
|
# Create tarball and gzip
|
|
tar -C image --hard-dereference --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --xform s:'^./':: -c . | (
|
|
case $last_image_mime in
|
|
"application/x-tar") ${compressors.none.compress};;
|
|
"application/zstd") ${compressors.zstd.compress};;
|
|
"application/gzip") ${compressors.gz.compress};;
|
|
# `*)` not needed; already checked.
|
|
esac
|
|
) > $out
|
|
'';
|
|
|
|
# Provide a /etc/passwd and /etc/group that contain root and nobody.
|
|
# Useful when packaging binaries that insist on using nss to look up
|
|
# username/groups (like nginx).
|
|
# /bin/sh is fine to not exist, and provided by another shim.
|
|
inherit fakeNss; # alias
|
|
|
|
# This provides a /usr/bin/env, for shell scripts using the
|
|
# "#!/usr/bin/env executable" shebang.
|
|
usrBinEnv = runCommand "usr-bin-env" { } ''
|
|
mkdir -p $out/usr/bin
|
|
ln -s ${coreutils}/bin/env $out/usr/bin
|
|
'';
|
|
|
|
# This provides /bin/sh, pointing to bashInteractive.
|
|
# The use of bashInteractive here is intentional to support cases like `docker run -it <image_name>`, so keep these use cases in mind if making any changes to how this works.
|
|
binSh = runCommand "bin-sh" { } ''
|
|
mkdir -p $out/bin
|
|
ln -s ${bashInteractive}/bin/bash $out/bin/sh
|
|
'';
|
|
|
|
# This provides the ca bundle in common locations
|
|
caCertificates = runCommand "ca-certificates" { } ''
|
|
mkdir -p $out/etc/ssl/certs $out/etc/pki/tls/certs
|
|
# Old NixOS compatibility.
|
|
ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/ssl/certs/ca-bundle.crt
|
|
# NixOS canonical location + Debian/Ubuntu/Arch/Gentoo compatibility.
|
|
ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/ssl/certs/ca-certificates.crt
|
|
# CentOS/Fedora compatibility.
|
|
ln -s ${cacert}/etc/ssl/certs/ca-bundle.crt $out/etc/pki/tls/certs/ca-bundle.crt
|
|
'';
|
|
|
|
# Build an image and populate its nix database with the provided
|
|
# contents. The main purpose is to be able to use nix commands in
|
|
# the container.
|
|
# Be careful since this doesn't work well with multilayer.
|
|
# TODO: add the dependencies of the config json.
|
|
buildImageWithNixDb = args: buildImage (args // { includeNixDB = true; });
|
|
|
|
buildLayeredImageWithNixDb = args: buildLayeredImage (args // { includeNixDB = true; });
|
|
|
|
# Arguments are documented in ../../../doc/build-helpers/images/dockertools.section.md
|
|
streamLayeredImage = lib.makeOverridable (
|
|
{
|
|
name,
|
|
tag ? null,
|
|
fromImage ? null,
|
|
contents ? [ ],
|
|
config ? { },
|
|
architecture ? defaultArchitecture,
|
|
created ? "1970-01-01T00:00:01Z",
|
|
mtime ? "1970-01-01T00:00:01Z",
|
|
uid ? 0,
|
|
gid ? 0,
|
|
uname ? "root",
|
|
gname ? "root",
|
|
maxLayers ? 100,
|
|
extraCommands ? "",
|
|
fakeRootCommands ? "",
|
|
enableFakechroot ? false,
|
|
includeStorePaths ? true,
|
|
includeNixDB ? false,
|
|
passthru ? { },
|
|
# Pipeline used to produce docker layers. If not set, popularity contest
|
|
# algorithm is used. If set, maxLayers is ignored as the author of the
|
|
# pipeline can use one of the available functions (like "limit_layers")
|
|
# to control the amount of layers.
|
|
# See: pkgs/build-support/flatten-references-graph/src/flatten_references_graph/pipe.py
|
|
# for available functions, and it's test for how to use them.
|
|
# WARNING!! this interface is highly experimental and subject to change.
|
|
layeringPipeline ? null,
|
|
# Enables debug logging for the layering pipeline.
|
|
debug ? false,
|
|
}:
|
|
assert (
|
|
lib.assertMsg (layeringPipeline == null -> maxLayers > 1)
|
|
"the maxLayers argument of dockerTools.buildLayeredImage function must be greather than 1 (current value: ${toString maxLayers})"
|
|
);
|
|
assert (
|
|
lib.assertMsg (enableFakechroot -> !stdenv.hostPlatform.isDarwin) ''
|
|
cannot use `enableFakechroot` because `proot` is not portable to Darwin. Workarounds:
|
|
- use `fakeRootCommands` with the restricted `fakeroot` environment
|
|
- cross-compile your packages
|
|
- run your packages in a virtual machine
|
|
Discussion: https://github.com/NixOS/nixpkgs/issues/327311''
|
|
);
|
|
let
|
|
baseName = baseNameOf name;
|
|
|
|
streamScript = writePython3 "stream" { } ./stream_layered_image.py;
|
|
baseJson = writeText "${baseName}-base.json" (
|
|
builtins.toJSON {
|
|
inherit config architecture;
|
|
os = "linux";
|
|
}
|
|
);
|
|
|
|
contentsList = if builtins.isList contents then contents else [ contents ];
|
|
bind-paths = toString (
|
|
map (path: "--bind=${path}:${path}!") [
|
|
"/dev/"
|
|
"/proc/"
|
|
"/sys/"
|
|
"${builtins.storeDir}/"
|
|
"$NIX_BUILD_TOP"
|
|
"$out/layer.tar"
|
|
]
|
|
);
|
|
|
|
# We store the customisation layer as a tarball, to make sure that
|
|
# things like permissions set on 'extraCommands' are not overridden
|
|
# by Nix. Then we precompute the sha256 for performance.
|
|
customisationLayer = symlinkJoin {
|
|
name = "${baseName}-customisation-layer";
|
|
paths = contentsList;
|
|
extraCommands = (lib.optionalString includeNixDB (mkDbExtraCommand contents)) + extraCommands;
|
|
inherit fakeRootCommands;
|
|
nativeBuildInputs = [
|
|
fakeroot
|
|
]
|
|
++ optionals enableFakechroot [
|
|
proot
|
|
];
|
|
postBuild = ''
|
|
mv $out old_out
|
|
(cd old_out; eval "$extraCommands" )
|
|
|
|
mkdir $out
|
|
${
|
|
if enableFakechroot then
|
|
''
|
|
proot -r $PWD/old_out ${bind-paths} --pwd=/ fakeroot bash -e -c '
|
|
if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi
|
|
source $stdenv/setup
|
|
eval "$fakeRootCommands"
|
|
tar \
|
|
--sort name \
|
|
--exclude=./dev \
|
|
--exclude=./proc \
|
|
--exclude=./sys \
|
|
--exclude=.${builtins.storeDir} \
|
|
--exclude=".$NIX_BUILD_TOP" \
|
|
--numeric-owner --mtime "@$SOURCE_DATE_EPOCH" \
|
|
--hard-dereference \
|
|
-cf $out/layer.tar .
|
|
'
|
|
''
|
|
else
|
|
''
|
|
fakeroot bash -e -c '
|
|
if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi
|
|
source $stdenv/setup
|
|
cd old_out
|
|
eval "$fakeRootCommands"
|
|
tar \
|
|
--sort name \
|
|
--numeric-owner --mtime "@$SOURCE_DATE_EPOCH" \
|
|
--hard-dereference \
|
|
-cf $out/layer.tar .
|
|
'
|
|
''
|
|
}
|
|
sha256sum $out/layer.tar \
|
|
| cut -f 1 -d ' ' \
|
|
> $out/checksum
|
|
'';
|
|
};
|
|
|
|
closureRoots = optionals includeStorePaths [
|
|
baseJson
|
|
customisationLayer
|
|
];
|
|
|
|
excludePaths = [
|
|
baseJson
|
|
customisationLayer
|
|
];
|
|
|
|
layersJsonFile =
|
|
if layeringPipeline == null then
|
|
buildPackages.dockerAutoLayer {
|
|
inherit
|
|
closureRoots
|
|
debug
|
|
excludePaths
|
|
fromImage
|
|
maxLayers
|
|
;
|
|
}
|
|
else
|
|
buildPackages.dockerMakeLayers {
|
|
inherit closureRoots debug excludePaths;
|
|
pipeline = layeringPipeline;
|
|
};
|
|
|
|
conf =
|
|
runCommand "${baseName}-conf.json"
|
|
{
|
|
inherit
|
|
fromImage
|
|
created
|
|
mtime
|
|
uid
|
|
gid
|
|
uname
|
|
gname
|
|
layersJsonFile
|
|
;
|
|
imageName = lib.toLower name;
|
|
preferLocalBuild = true;
|
|
passthru.imageTag =
|
|
if tag != null then
|
|
tag
|
|
else
|
|
lib.head (
|
|
lib.strings.splitString "-" (baseNameOf (builtins.unsafeDiscardStringContext conf.outPath))
|
|
);
|
|
nativeBuildInputs = [ jq ];
|
|
}
|
|
''
|
|
${
|
|
if (tag == null) then
|
|
''
|
|
outName="$(basename "$out")"
|
|
outHash=$(echo "$outName" | cut -d - -f 1)
|
|
|
|
imageTag=$outHash
|
|
''
|
|
else
|
|
''
|
|
imageTag="${tag}"
|
|
''
|
|
}
|
|
|
|
# convert "created" and "mtime" to iso format
|
|
if [[ "$created" != "now" ]]; then
|
|
created="$(date -Iseconds -d "$created")"
|
|
fi
|
|
if [[ "$mtime" != "now" ]]; then
|
|
mtime="$(date -Iseconds -d "$mtime")"
|
|
fi
|
|
|
|
jq '
|
|
. + {
|
|
"store_dir": $store_dir,
|
|
"from_image": $from_image,
|
|
"store_layers": $store_layers[0],
|
|
"customisation_layer", $customisation_layer,
|
|
"repo_tag": $repo_tag,
|
|
"created": $created,
|
|
"mtime": $mtime,
|
|
"uid": $uid,
|
|
"gid": $gid,
|
|
"uname": $uname,
|
|
"gname": $gname
|
|
}
|
|
' --arg store_dir "${storeDir}" \
|
|
--argjson from_image ${if fromImage == null then "null" else "'\"${fromImage}\"'"} \
|
|
--slurpfile store_layers "$layersJsonFile" \
|
|
--arg customisation_layer ${customisationLayer} \
|
|
--arg repo_tag "$imageName:$imageTag" \
|
|
--arg created "$created" \
|
|
--arg mtime "$mtime" \
|
|
--arg uid "$uid" \
|
|
--arg gid "$gid" \
|
|
--arg uname "$uname" \
|
|
--arg gname "$gname" \
|
|
${baseJson} \
|
|
| tee $out
|
|
'';
|
|
|
|
result =
|
|
runCommand "stream-${baseName}"
|
|
{
|
|
inherit conf;
|
|
inherit (conf) imageName;
|
|
inherit streamScript;
|
|
preferLocalBuild = true;
|
|
passthru = passthru // {
|
|
inherit (conf) imageTag;
|
|
inherit conf;
|
|
inherit streamScript;
|
|
|
|
# Distinguish tarballs and exes at the Nix level so functions that
|
|
# take images can know in advance how the image is supposed to be used.
|
|
isExe = true;
|
|
};
|
|
nativeBuildInputs = [ makeWrapper ];
|
|
}
|
|
''
|
|
makeWrapper $streamScript $out --add-flags $conf
|
|
'';
|
|
in
|
|
result
|
|
);
|
|
|
|
# This function streams a docker image that behaves like a nix-shell for a derivation
|
|
# Docs: doc/build-helpers/images/dockertools.section.md
|
|
# Tests: nixos/tests/docker-tools-nix-shell.nix
|
|
streamNixShellImage =
|
|
{
|
|
drv,
|
|
name ? drv.name + "-env",
|
|
tag ? null,
|
|
uid ? 1000,
|
|
gid ? 1000,
|
|
homeDirectory ? "/build",
|
|
shell ? bashInteractive + "/bin/bash",
|
|
command ? null,
|
|
run ? null,
|
|
}:
|
|
assert lib.assertMsg (!(drv.drvAttrs.__structuredAttrs or false))
|
|
"streamNixShellImage: Does not work with the derivation ${drv.name} because it uses __structuredAttrs";
|
|
assert lib.assertMsg (
|
|
command == null || run == null
|
|
) "streamNixShellImage: Can't specify both command and run";
|
|
let
|
|
|
|
# A binary that calls the command to build the derivation
|
|
builder = writeShellScriptBin "buildDerivation" ''
|
|
exec ${lib.escapeShellArg (valueToString drv.drvAttrs.builder)} ${lib.escapeShellArgs (map valueToString drv.drvAttrs.args)}
|
|
'';
|
|
|
|
staticPath = "${dirOf shell}:${lib.makeBinPath [ builder ]}";
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L617-L651
|
|
rcfile = writeText "nix-shell-rc" ''
|
|
unset PATH
|
|
dontAddDisableDepTrack=1
|
|
# TODO: https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L628
|
|
[ -e $stdenv/setup ] && source $stdenv/setup
|
|
PATH=${staticPath}:"$PATH"
|
|
SHELL=${lib.escapeShellArg shell}
|
|
BASH=${lib.escapeShellArg shell}
|
|
set +e
|
|
[ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '
|
|
if [ "$(type -t runHook)" = function ]; then
|
|
runHook shellHook
|
|
fi
|
|
unset NIX_ENFORCE_PURITY
|
|
shopt -u nullglob
|
|
shopt -s execfail
|
|
${optionalString (command != null || run != null) ''
|
|
${optionalString (command != null) command}
|
|
${optionalString (run != null) run}
|
|
exit
|
|
''}
|
|
'';
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/include/nix/store/globals.hh#L778-L788
|
|
sandboxBuildDir = "/build";
|
|
|
|
drvEnv =
|
|
devShellTools.unstructuredDerivationInputEnv { inherit (drv) drvAttrs; }
|
|
// devShellTools.derivationOutputEnv {
|
|
outputList = drv.outputs;
|
|
outputMap = drv;
|
|
};
|
|
|
|
# Environment variables set in the image
|
|
envVars = {
|
|
|
|
# Root certificates for internet access
|
|
SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
|
|
NIX_SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1001-L1004
|
|
# PATH = "/path-not-set";
|
|
# Allows calling bash and `buildDerivation` as the Cmd
|
|
PATH = staticPath;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1006-L1012
|
|
HOME = homeDirectory;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1014-L1018
|
|
NIX_STORE = storeDir;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1020-L1021
|
|
# TODO: Make configurable?
|
|
NIX_BUILD_CORES = "1";
|
|
|
|
}
|
|
// drvEnv
|
|
// {
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1035-L1037
|
|
NIX_BUILD_TOP = sandboxBuildDir;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1039-L1040
|
|
TMPDIR = sandboxBuildDir;
|
|
TEMPDIR = sandboxBuildDir;
|
|
TMP = sandboxBuildDir;
|
|
TEMP = sandboxBuildDir;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1042-L1046
|
|
PWD = sandboxBuildDir;
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1079-L1082
|
|
# We don't set it here because the output here isn't handled in any special way
|
|
# NIX_LOG_FD = "2";
|
|
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/derivation-builder.cc#L1084-L1085
|
|
TERM = "xterm-256color";
|
|
};
|
|
|
|
in
|
|
streamLayeredImage {
|
|
inherit name tag;
|
|
contents = [
|
|
binSh
|
|
usrBinEnv
|
|
(fakeNss.override {
|
|
# Allows programs to look up the build user's home directory
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/libstore/unix/build/linux-derivation-builder.cc#L409-L416
|
|
# Slightly differs however: We use the passed-in homeDirectory instead of sandboxBuildDir.
|
|
# We're doing this because it's arguably a bug in Nix that sandboxBuildDir is used here: https://github.com/NixOS/nix/issues/6379
|
|
extraPasswdLines = [
|
|
"nixbld:x:${toString uid}:${toString gid}:Build user:${homeDirectory}:/noshell"
|
|
];
|
|
extraGroupLines = [
|
|
"nixbld:!:${toString gid}:"
|
|
];
|
|
})
|
|
];
|
|
|
|
fakeRootCommands = ''
|
|
# Effectively a single-user installation of Nix, giving the user full
|
|
# control over the Nix store. Needed for building the derivation this
|
|
# shell is for, but also in case one wants to use Nix inside the
|
|
# image
|
|
mkdir -p ./nix/{store,var/nix} ./etc/nix
|
|
chown -R ${toString uid}:${toString gid} ./nix ./etc/nix
|
|
|
|
# Gives the user control over the build directory
|
|
mkdir -p .${sandboxBuildDir}
|
|
chown -R ${toString uid}:${toString gid} .${sandboxBuildDir}
|
|
'';
|
|
|
|
# Run this image as the given uid/gid
|
|
config.User = "${toString uid}:${toString gid}";
|
|
config.Cmd =
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L240-L241
|
|
# https://github.com/NixOS/nix/blob/2.32.0/src/nix/nix-build/nix-build.cc#L659
|
|
if run == null then
|
|
[
|
|
shell
|
|
"--rcfile"
|
|
rcfile
|
|
]
|
|
else
|
|
[
|
|
shell
|
|
rcfile
|
|
];
|
|
config.WorkingDir = sandboxBuildDir;
|
|
config.Env = lib.mapAttrsToList (name: value: "${name}=${value}") envVars;
|
|
};
|
|
|
|
# Wrapper around streamNixShellImage to build an image from the result
|
|
# Docs: doc/build-helpers/images/dockertools.section.md
|
|
# Tests: nixos/tests/docker-tools-nix-shell.nix
|
|
buildNixShellImage =
|
|
{
|
|
drv,
|
|
compressor ? "gz",
|
|
...
|
|
}@args:
|
|
let
|
|
stream = streamNixShellImage (removeAttrs args [ "compressor" ]);
|
|
compress = compressorForImage compressor drv.name;
|
|
in
|
|
runCommand "${drv.name}-env.tar${compress.ext}" {
|
|
inherit (stream) imageName;
|
|
passthru = { inherit (stream) imageTag; };
|
|
nativeBuildInputs = compress.nativeInputs;
|
|
} "${stream} | ${compress.compress} > $out";
|
|
}
|