2024-06-30 08:12:46 +00:00
{
bashInteractive ,
buildPackages ,
cacert ,
callPackage ,
closureInfo ,
coreutils ,
e2fsprogs ,
proot ,
fakeNss ,
fakeroot ,
file ,
go ,
jq ,
jshon ,
lib ,
makeWrapper ,
moreutils ,
nix ,
nixosTests ,
pigz ,
rsync ,
runCommand ,
runtimeShell ,
shadow ,
skopeo ,
storeDir ? builtins . storeDir ,
substituteAll ,
symlinkJoin ,
tarsum ,
util-linux ,
vmTools ,
writeClosure ,
writeScript ,
writeShellScriptBin ,
writeText ,
writeTextDir ,
writePython3 ,
zstd ,
2024-05-02 00:46:19 +00:00
} :
let
2024-06-30 08:12:46 +00:00
inherit ( lib ) optionals optionalString ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
inherit ( lib ) escapeShellArgs toList ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
mkDbExtraCommand =
contents :
2024-05-02 00:46:19 +00:00
let
contentsList = if builtins . isList contents then contents else [ contents ] ;
in
''
echo " G e n e r a t i n g t h e n i x d a t a b a s e . . . "
echo " W a r n i n g : o n l y t h e d a t a b a s e o f t h e d e e p e s t N i x l a y e r i s l o a d e d . "
echo " I f y o u w a n t t o u s e n i x c o m m a n d s i n t h e c o n t a i n e r , i t w o u l d "
echo " b e b e t t e r t o o n l y h a v e o n e l a y e r t h a t c o n t a i n s a n i x s t o r e . "
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
2024-06-30 08:12:46 +00:00
$ { buildPackages . nix } /bin/nix-store - - load-db < $ {
closureInfo { rootPaths = contentsList ; }
} /registration
2024-05-02 00:46:19 +00:00
# Reset registration times to make the image reproducible
$ { buildPackages . sqlite } /bin/sqlite3 nix/var/nix/db/db.sqlite " U P D A T E V a l i d P a t h s S E T r e g i s t r a t i o n T i m e = ' ' ${ 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 = " c a t " ;
decompress = " c a t " ;
} ;
gz = {
ext = " . g z " ;
nativeInputs = [ pigz ] ;
compress = " p i g z - p $ N I X _ B U I L D _ C O R E S - n T R " ;
decompress = " p i g z - d - p $ N I X _ B U I L D _ C O R E S " ;
} ;
zstd = {
ext = " . z s t " ;
nativeInputs = [ zstd ] ;
compress = " z s t d - T $ N I X _ B U I L D _ C O R E S " ;
decompress = " z s t d - d - T $ N I X _ B U I L D _ C O R E S " ;
} ;
} ;
2024-06-30 08:12:46 +00:00
compressorForImage =
compressor : imageName :
compressors . ${ compressor }
or ( throw " i n d o c k e r i m a g e ${ imageName } : c o m p r e s s o r m u s t b e o n e o f : [ ${ toString builtins . attrNames compressors } ] " ) ;
2024-05-02 00:46:19 +00:00
in
rec {
examples = callPackage ./examples.nix {
2024-06-30 08:12:46 +00:00
inherit
buildImage
buildLayeredImage
fakeNss
pullImage
shadowSetup
buildImageWithNixDb
streamNixShellImage
;
2024-05-02 00:46:19 +00:00
} ;
tests = {
inherit ( nixosTests )
docker-tools
docker-tools-overlay
# requires remote builder
# docker-tools-cross
;
} ;
pullImage =
let
2024-06-30 08:12:46 +00:00
fixName =
name :
builtins . replaceStrings
[
" / "
" : "
]
[
" - "
" - "
]
name ;
2024-05-02 00:46:19 +00:00
in
2024-06-30 08:12:46 +00:00
{
imageName ,
2024-05-02 00:46:19 +00:00
# To find the digest of an image, you can use skopeo:
# see doc/functions.xml
2024-06-30 08:12:46 +00:00
imageDigest ,
sha256 ,
os ? " l i n u x " ,
# Image architecture, defaults to the architecture of the `hostPlatform` when unset
arch ? defaultArchitecture ,
2024-05-02 00:46:19 +00:00
# This is used to set name to the pulled image
2024-06-30 08:12:46 +00:00
finalImageName ? imageName ,
2024-05-02 00:46:19 +00:00
# This used to set a tag to the pulled image
2024-06-30 08:12:46 +00:00
finalImageTag ? " l a t e s t " ,
2024-05-02 00:46:19 +00:00
# This is used to disable TLS certificate verification, allowing access to http registries on (hopefully) trusted networks
2024-06-30 08:12:46 +00:00
tlsVerify ? true ,
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
name ? fixName " d o c k e r - i m a g e - ${ finalImageName } - ${ finalImageTag } . t a r " ,
2024-05-02 00:46:19 +00:00
} :
runCommand name
{
inherit imageDigest ;
imageName = finalImageName ;
imageTag = finalImageTag ;
impureEnvVars = lib . fetchers . proxyImpureEnvVars ;
outputHashMode = " f l a t " ;
outputHashAlgo = " s h a 2 5 6 " ;
outputHash = sha256 ;
nativeBuildInputs = [ skopeo ] ;
SSL_CERT_FILE = " ${ cacert . out } / e t c / s s l / c e r t s / c a - b u n d l e . c r t " ;
sourceURL = " d o c k e r : / / ${ imageName } @ ${ imageDigest } " ;
destNameTag = " ${ finalImageName } : ${ finalImageTag } " ;
2024-06-30 08:12:46 +00:00
}
''
skopeo \
- - insecure-policy \
- - tmpdir = $ TMPDIR \
- - override-os $ { os } \
- - override-arch $ { arch } \
copy \
- - src-tls-verify = $ { lib . boolToString tlsVerify } \
" $ s o u r c e U R L " " d o c k e r - a r c h i v e : / / $ o u t : $ d e s t N a m e T a g " \
| cat # pipe through cat to force-disable progress bar
'' ;
2024-05-02 00:46:19 +00:00
# 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 =
2024-06-30 08:12:46 +00:00
{
derivations ,
onlyDeps ? false ,
2024-05-02 00:46:19 +00:00
} :
2024-06-30 08:12:46 +00:00
runCommand " m e r g e - d r v s " { inherit derivations onlyDeps ; } ''
2024-05-02 00:46:19 +00:00
if [ [ - n " $ o n l y D e p s " ] ] ; then
echo $ derivations > $ out
exit 0
fi
mkdir $ out
for derivation in $ derivations ; do
echo " M e r g i n g $ d e r i v a t i o n . . . "
if [ [ - d " $ d e r i v a t i o n " ] ] ; then
# If it's a directory, copy all of its contents into $out.
cp - drf - - preserve = mode - f $ derivation /* $ o u t /
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 " r o o t : x : 0 : 0 : : / r o o t : ${ runtimeShell } " > /etc/passwd
echo " r o o t : ! x : : : : : : : " > /etc/shadow
fi
if [ [ ! - f /etc/group ] ] ; then
echo " r o o t : x : 0 : " > /etc/group
echo " r o o t : 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 =
2024-06-30 08:12:46 +00:00
{
name ,
fromImage ? null ,
fromImageName ? null ,
fromImageTag ? null ,
diskSize ? 1024 ,
buildVMMemorySize ? 512 ,
preMount ? " " ,
postMount ? " " ,
postUmount ? " " ,
2024-05-02 00:46:19 +00:00
} :
2024-06-30 08:12:46 +00:00
vmTools . runInLinuxVM (
runCommand name
{
preVM = vmTools . createEmptyImage {
size = diskSize ;
fullName = " d o c k e r - r u n - d i s k " ;
destination = " . / i m a g e " ;
} ;
inherit fromImage fromImageName fromImageTag ;
memSize = buildVMMemorySize ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
nativeBuildInputs = [
util-linux
e2fsprogs
jshon
rsync
jq
] ;
}
''
2024-05-02 00:46:19 +00:00
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 [ " $ l n " ] : - } " ] ] ; then
echo " $ l n " ; seen [ " $ l n " ] = 1
fi
done
}
if [ [ - n " $ f r o m I m a g e " ] ] ; then
echo " U n p a c k i n g b a s e i m a g e . . . "
mkdir image
tar - C image - xpf " $ f r o m I m a g e "
if [ [ - n " $ f r o m I m a g e N a m e " ] ] && [ [ - n " $ f r o m I m a g e T a g " ] ] ; then
parentID = " $ (
cat " i m a g e / m a n i f e s t . j s o n " |
jq - r ' . [ ] | select ( . RepoTags | contains ( [ $ desiredTag ] ) ) | rtrimstr ( " . j s o n " ) ' \
- - arg desiredTag " $ f r o m I m a g e N a m e : $ f r o m I m a g e T a g "
) "
else
echo " F r o m - i m a g e n a m e o r t a g w a s n ' t s e t . R e a d i n g t h e f i r s t I D . "
parentID = " $ ( c a t " image/manifest.json " | j q - r ' . [ 0 ] . C o n f i g | r t r i m s t r ( " . 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 " U n p a c k i n g l a y e r $ l a y e r T a r "
extractionID = $ ( ( extractionID + 1 ) )
mkdir - p image / $ extractionID/layer
tar - C image / $ extractionID/layer - xpf image / $ layerTar
rm image / $ layerTar
find image / $ extractionID/layer - name " . w h . * " - exec bash - c ' name = " $ ( b a s e n a m e { } | s e d " s / ^ . wh . // " ) " ; mknod " $ ( d i r n a m e { } ) / $ n a m e " c 0 0 ; rm { } ' \ ;
# Get the next lower directory and continue the loop.
lowerdir = image / $ extractionID/layer '' ${ lowerdir:+: } $l o w e r d i r
done
mkdir work
mkdir layer
mkdir mnt
$ { lib . optionalString ( preMount != " " ) ''
# Execute pre-mount steps
echo " E x e c u t i n g p r e - m o u n t s t e p s . . . "
$ { preMount }
'' }
if [ - n " $ l o w e r d i r " ] ; 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 " E x e c u t i n g p o s t - m o u n t s t e p s . . . "
$ { postMount }
'' }
umount mnt
(
cd layer
cmd = ' name = " $ ( b a s e n a m e { } ) " ; touch " $ ( d i r n a m e { } ) / . w h . $ n a m e " ; rm " { } " '
find . - type c - exec bash - c " $ c m d " \ ;
)
$ { postUmount }
2024-06-30 08:12:46 +00:00
''
) ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
exportImage =
{
name ? fromImage . name ,
fromImage ,
fromImageName ? null ,
fromImageTag ? null ,
diskSize ? 1024 ,
} :
2024-05-02 00:46:19 +00:00
runWithOverlay {
2024-06-30 08:12:46 +00:00
inherit
name
fromImage
fromImageName
fromImageTag
diskSize
;
2024-05-02 00:46:19 +00:00
postMount = ''
echo " P a c k i n g r a w i m a g e . . . "
tar - C mnt - - hard-dereference - - sort = name - - mtime = " @ $ S O U R C E _ D A T E _ E P O C H " - 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.
2024-06-30 08:12:46 +00:00
shellScript =
name : text :
2024-05-02 00:46:19 +00:00
writeScript name ''
#!${runtimeShell}
set - e
export PATH = $ { coreutils } /bin : /bin
$ { text }
'' ;
# Create a "layer" (set of files).
mkPureLayer =
{
# Name of the layer
2024-06-30 08:12:46 +00:00
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
2024-05-02 00:46:19 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
2024-06-30 08:12:46 +00:00
keepContentsDirlinks ? false ,
# Additional commands to run on the layer before it is tar'd up.
extraCommands ? " " ,
uid ? 0 ,
gid ? 0 ,
2024-05-02 00:46:19 +00:00
} :
runCommand " d o c k e r - l a y e r - ${ name } "
{
inherit baseJson extraCommands ;
contents = copyToRoot ;
2024-06-30 08:12:46 +00:00
nativeBuildInputs = [
jshon
rsync
tarsum
] ;
2024-05-02 00:46:19 +00:00
}
''
mkdir layer
if [ [ - n " $ c o n t e n t s " ] ] ; then
echo " A d d i n g c o n t e n t s . . . "
for item in $ contents ; do
echo " A d d i n g $ i t e m "
rsync - a $ { if keepContentsDirlinks then " K " else " k " } - - chown = 0 : 0 $ item / layer /
done
else
echo " N o c o n t e n t s t o a d d t o l a y e r . "
fi
chmod ug + w layer
if [ [ - n " $ e x t r a C o m m a n d s " ] ] ; then
( cd layer ; eval " $ e x t r a C o m m a n d s " )
fi
# Tar up the layer and throw it into 'layer.tar'.
echo " P a c k i n g l a y e r . . . "
mkdir $ out
tarhash = $ ( tar - C layer - - hard-dereference - - sort = name - - mtime = " @ $ S O U R C E _ D A T E _ E P O C H " - - 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 " $ t a r h a s h " - i checksum > $ out/json
# Indicate to docker that we're using schema version 1.0.
echo - n " 1 . 0 " > $ out/VERSION
echo " F i n i s h e d b u i l d i n g l a y e r ' ${ 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.
2024-06-30 08:12:46 +00:00
name ,
# Script to run as root. Bash.
runAsRoot ,
# Files to add to the layer. If null, an empty layer will be created.
2024-05-02 00:46:19 +00:00
# To add packages to /bin, use `buildEnv` or similar.
2024-06-30 08:12:46 +00:00
copyToRoot ? null ,
# When copying the contents into the image, preserve symlinks to
2024-05-02 00:46:19 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
2024-06-30 08:12:46 +00:00
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 ? " " ,
2024-05-02 00:46:19 +00:00
} :
# Generate an executable script from the `runAsRoot` text.
let
runAsRootScript = shellScript " r u n - a s - r o o t . s h " runAsRoot ;
extraCommandsScript = shellScript " e x t r a - c o m m a n d s . s h " extraCommands ;
in
runWithOverlay {
name = " d o c k e r - l a y e r - ${ name } " ;
2024-06-30 08:12:46 +00:00
inherit
fromImage
fromImageName
fromImageTag
diskSize
buildVMMemorySize
;
2024-05-02 00:46:19 +00:00
preMount = lib . optionalString ( copyToRoot != null && copyToRoot != [ ] ) ''
echo " A d d i n g c o n t e n t s . . . "
for item in $ { escapeShellArgs ( map ( c : " ${ c } " ) ( toList copyToRoot ) ) } ; do
echo " A d d i n g $ i t e m . . . "
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 " P a c k i n g l a y e r . . . "
mkdir - p $ out
tarhash = $ ( tar - C layer - - hard-dereference - - sort = name - - mtime = " @ $ S O U R C E _ D A T E _ E P O C H " - cf - . |
tee - p $ out/layer.tar |
$ { tarsum } /bin/tarsum )
cat $ { baseJson } | jshon - s " $ t a r h a s h " - i checksum > $ out/json
# Indicate to docker that we're using schema version 1.0.
echo - n " 1 . 0 " > $ out/VERSION
echo " F i n i s h e d b u i l d i n g l a y e r ' ${ name } ' "
'' ;
} ;
2024-06-30 08:12:46 +00:00
buildLayeredImage = lib . makeOverridable (
{
name ,
compressor ? " g z " ,
. . .
} @ args :
2024-05-02 00:46:19 +00:00
let
2024-06-30 08:12:46 +00:00
stream = streamLayeredImage ( builtins . removeAttrs args [ " c o m p r e s s o r " ] ) ;
2024-05-02 00:46:19 +00:00
compress = compressorForImage compressor name ;
in
2024-06-30 08:12:46 +00:00
runCommand " ${ baseNameOf name } . t a r ${ compress . ext } " {
inherit ( stream ) imageName ;
passthru = {
inherit ( stream ) imageTag ;
inherit stream ;
} ;
nativeBuildInputs = compress . nativeInputs ;
} " ${ stream } | ${ compress . compress } > $ o u t "
2024-05-02 00:46:19 +00:00
) ;
# 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.
2024-06-30 08:12:46 +00:00
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
2024-05-02 00:46:19 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
2024-06-30 08:12:46 +00:00
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 ? " 1 9 7 0 - 0 1 - 0 1 T 0 0 : 0 0 : 0 1 Z " ,
# Compressor to use. One of: none, gz, zstd.
compressor ? " g z " ,
# Deprecated.
contents ? null ,
2024-05-02 00:46:19 +00:00
} :
let
checked =
lib . warnIf ( contents != null )
" i n d o c k e r i m a g e ${ name } : T h e c o n t e n t s p a r a m e t e r i s d e p r e c a t e d . C h a n g e t o c o p y T o R o o t i f t h e c o n t e n t s a r e d e s i g n e d t o b e c o p i e d t o t h e r o o t f i l e s y s t e m , s u c h a s w h e n y o u u s e ` b u i l d E n v ` o r s i m i l a r b e t w e e n c o n t e n t s a n d y o u r p a c k a g e s . U s e c o p y T o R o o t = b u i l d E n v { . . . } ; o r s i m i l a r i f y o u i n t e n d t o a d d p a c k a g e s t o / b i n . "
2024-06-30 08:12:46 +00:00
lib . throwIf
( contents != null && copyToRoot != null )
" i n d o c k e r i m a g e ${ name } : Y o u c a n n o t s p e c i f y b o t h c o n t e n t s a n d c o p y T o R o o t . " ;
2024-05-02 00:46:19 +00:00
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
2024-06-30 08:12:46 +00:00
pure = writeText " ${ baseName } - c o n f i g . j s o n " (
builtins . toJSON {
inherit created config architecture ;
2024-05-02 00:46:19 +00:00
preferLocalBuild = true ;
2024-06-30 08:12:46 +00:00
os = " l i n u x " ;
2024-05-02 00:46:19 +00:00
}
2024-06-30 08:12:46 +00:00
) ;
impure =
runCommand " ${ baseName } - c o n f i g . j s o n "
{
nativeBuildInputs = [ jq ] ;
preferLocalBuild = true ;
}
''
jq " . c r e a t e d = \" $ ( T Z = u t c d a t e - - i s o - 8 6 0 1 = " seconds " ) \" " $ { pure } > $ out
'' ;
2024-05-02 00:46:19 +00:00
in
if created == " n o w " then impure else pure ;
compress = compressorForImage compressor name ;
layer =
2024-06-30 08:12:46 +00:00
if runAsRoot == null then
mkPureLayer {
name = baseName ;
inherit
baseJson
keepContentsDirlinks
extraCommands
uid
gid
;
copyToRoot = rootContents ;
}
else
2024-05-02 00:46:19 +00:00
mkRootLayer {
name = baseName ;
2024-06-30 08:12:46 +00:00
inherit
baseJson
fromImage
fromImageName
fromImageTag
keepContentsDirlinks
runAsRoot
diskSize
buildVMMemorySize
extraCommands
;
2024-05-02 00:46:19 +00:00
copyToRoot = rootContents ;
} ;
2024-06-30 08:12:46 +00:00
result =
runCommand " d o c k e r - i m a g e - ${ baseName } . t a r ${ 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 = " $ ( b a s e n a m e " $ out " ) "
outHash = $ ( echo " $ o u t N a m e " | cut - d - - f 1 )
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
imageTag = $ outHash
'' }
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# 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
}
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
mkdir image
touch baseFiles
baseEnvs = ' [ ] '
if [ [ - n " $ f r o m I m a g e " ] ] ; then
echo " U n p a c k i n g b a s e i m a g e . . . "
tar - C image - xpf " $ f r o m I m a g e "
# Store the layers and the environment variables from the base image
cat ./image/manifest.json | jq - r ' . [ 0 ] . Layers | . [ ] ' > layer-list
configName = " $ ( c a t . / i m a g e / m a n i f e s t . j s o n | j q - r ' . [ 0 ] . C o n f i g ' ) "
baseEnvs = " $ ( c a t " ./image / $ configName " | j q ' . c o n f i g . E n v / / [ ] ' ) "
# Extract the parentID from the manifest
if [ [ - n " $ f r o m I m a g e N a m e " ] ] && [ [ - n " $ f r o m I m a g e T a g " ] ] ; then
parentID = " $ (
cat " i m a g e / m a n i f e s t . j s o n " |
jq - r ' . [ ] | select ( . RepoTags | contains ( [ $ desiredTag ] ) ) | rtrimstr ( " . j s o n " ) ' \
- - arg desiredTag " $ f r o m I m a g e N a m e : $ f r o m I m a g e T a g "
) "
else
echo " F r o m - i m a g e n a m e o r t a g w a s n ' t s e t . R e a d i n g t h e f i r s t I D . "
parentID = " $ ( c a t " image/manifest.json " | j q - r ' . [ 0 ] . C o n f i g | r t r i m s t r ( " . json " ) ' ) "
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# Otherwise do not import the base image configuration and manifest
chmod a + w image image /* . j s o n
rm - f image /* . j s o n
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
for l in image /* / l a y e r . t a r ; d o
ls_tar $ l > > baseFiles
done
else
touch layer-list
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
chmod - R ug + rw image
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
mkdir temp
cp $ { layer } /* t e m p /
chmod ug + w temp /*
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
for dep in $ ( cat $ layerClosure ) ; do
find $ dep > > layerFiles
done
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
echo " A d d i n g l a y e r . . . "
# 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 " ^ / n i x / s t o r e $ " baseFiles ) - eq 0 ] ; then
mkdir - p nix/store
chmod - R 555 nix
echo " . / n i x " > > layerFiles
echo " . / n i x / s t o r e " > > layerFiles
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# 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 = " @ $ S O U R C E _ D A T E _ E P O C H " \
- - owner = 0 - - group = 0 - - no-recursion - - verbatim-files-from - - files-from newFiles
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
echo " A d d i n g m e t a . . . "
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# If we have a parentID, add it to the json metadata.
if [ [ - n " $ p a r e n t I D " ] ] ; then
cat temp/json | jshon - s " $ p a r e n t I D " - i parent > tmpjson
mv tmpjson temp/json
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# 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 " $ l a y e r I D " - 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 " $ l a y e r I D / l a y e r . t a r "
) | sponge layer-list
# Create image json and image manifest
imageJson = $ ( cat $ { baseJson } | jq ' . config . Env = $ baseenv + . config . Env' - - argjson baseenv " $ b a s e E n v s " )
imageJson = $ ( echo " $ i m a g e J s o n " | jq " . + { \" r o o t f s \" : { \" d i f f _ i d s \" : [ ] , \" t y p e \" : \" l a y e r s \" } } " )
manifestJson = $ ( jq - n " [ { \" R e p o T a g s \" : [ \" $ i m a g e N a m e : $ i m a g e T a g \" ] } ] " )
for layerTar in $ ( cat ./layer-list ) ; do
layerChecksum = $ ( sha256sum image / $ layerTar | cut - d ' ' - f1 )
imageJson = $ ( echo " $ i m a g e J s o n " | jq " . h i s t o r y | = . + [ { \" c r e a t e d \" : \" $ ( j q - r . c r e a t e d ${ baseJson } ) \" } ] " )
# diff_ids order is from the bottom-most to top-most layer
imageJson = $ ( echo " $ i m a g e J s o n " | jq " . r o o t f s . d i f f _ i d s | = . + [ \" s h a 2 5 6 : $ l a y e r C h e c k s u m \" ] " )
manifestJson = $ ( echo " $ m a n i f e s t J s o n " | jq " . [ 0 ] . L a y e r s | = . + [ \" $ l a y e r T a r \" ] " )
done
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
imageJsonChecksum = $ ( echo " $ i m a g e J s o n " | sha256sum | cut - d ' ' - f1 )
echo " $ i m a g e J s o n " > " i m a g e / $ i m a g e J s o n C h e c k s u m . j s o n "
manifestJson = $ ( echo " $ m a n i f e s t J s o n " | jq " . [ 0 ] . C o n f i g = \" $ i m a g e J s o n C h e c k s u m . j s o n \" " )
echo " $ m a n i f e s t J s o n " > image/manifest.json
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# Store the json under the name image/repositories.
jshon - n object \
- n object - s " $ l a y e r I D " - i " $ i m a g e T a g " \
- i " $ i m a g e N a m e " > image/repositories
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# Make the image read-only.
chmod - R a-w image
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
echo " C o o k i n g t h e i m a g e . . . "
tar - C image - - hard-dereference - - sort = name - - mtime = " @ $ S O U R C E _ D A T E _ E P O C H " - - owner = 0 - - group = 0 - - xform s:' ^ . / ' : : - c . | $ { compress . compress } > $ out
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
echo " F i n i s h e d . "
'' ;
2024-05-02 00:46:19 +00:00
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.
2024-06-30 08:12:46 +00:00
mergeImages =
images :
runCommand " m e r g e - d o c k e r - i m a g e s "
{
inherit images ;
nativeBuildInputs = [
file
jq
] ++ compressors . none . nativeInputs ++ compressors . gz . nativeInputs ++ compressors . zstd . nativeInputs ;
}
''
mkdir image inputs
# Extract images
repos = ( )
manifests = ( )
last_image_mime = " a p p l i c a t i o n / g z i p "
for item in $ images ; do
name = $ ( basename $ item )
mkdir inputs / $ name
last_image_mime = $ ( file - - mime-type - b $ item )
case $ last_image_mime in
" a p p l i c a t i o n / x - t a r " ) $ { compressors . none . decompress } ; ;
" a p p l i c a t i o n / z s t d " ) $ { compressors . zstd . decompress } ; ;
" a p p l i c a t i o n / g z i p " ) $ { compressors . gz . decompress } ; ;
* ) echo " e r r o r : u n e x p e c t e d l a y e r t y p e $ l a s t _ i m a g e _ m i m e " > & 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 /* / * i m a g e /
# 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 = " @ $ S O U R C E _ D A T E _ E P O C H " - - owner = 0 - - group = 0 - - xform s:' ^ . / ' : : - c . | (
case $ last_image_mime in
" a p p l i c a t i o n / x - t a r " ) $ { compressors . none . compress } ; ;
" a p p l i c a t i o n / z s t d " ) $ { compressors . zstd . compress } ; ;
" a p p l i c a t i o n / g z i p " ) $ { compressors . gz . compress } ; ;
# `*)` not needed; already checked.
esac
) > $ out
'' ;
2024-05-02 00:46:19 +00:00
# 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 " u s r - b i n - e n v " { } ''
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 " b i n - s h " { } ''
mkdir - p $ out/bin
ln - s $ { bashInteractive } /bin/bash $ out/bin/sh
'' ;
# This provides the ca bundle in common locations
caCertificates = runCommand " c a - c e r t i f i c a t e s " { } ''
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.
2024-06-30 08:12:46 +00:00
buildImageWithNixDb =
args @ {
copyToRoot ? contents ,
contents ? null ,
extraCommands ? " " ,
. . .
} :
( buildImage ( args // { extraCommands = ( mkDbExtraCommand copyToRoot ) + extraCommands ; } ) ) ;
2024-05-02 00:46:19 +00:00
# TODO: add the dependencies of the config json.
2024-06-30 08:12:46 +00:00
buildLayeredImageWithNixDb =
args @ {
contents ? null ,
extraCommands ? " " ,
. . .
} :
( buildLayeredImage ( args // { extraCommands = ( mkDbExtraCommand contents ) + extraCommands ; } ) ) ;
2024-05-02 00:46:19 +00:00
# Arguments are documented in ../../../doc/build-helpers/images/dockertools.section.md
streamLayeredImage = lib . makeOverridable (
{
2024-06-30 08:12:46 +00:00
name ,
tag ? null ,
fromImage ? null ,
contents ? [ ] ,
config ? { } ,
architecture ? defaultArchitecture ,
created ? " 1 9 7 0 - 0 1 - 0 1 T 0 0 : 0 0 : 0 1 Z " ,
uid ? 0 ,
gid ? 0 ,
uname ? " r o o t " ,
gname ? " r o o t " ,
maxLayers ? 100 ,
extraCommands ? " " ,
fakeRootCommands ? " " ,
enableFakechroot ? false ,
includeStorePaths ? true ,
passthru ? { } ,
2024-05-02 00:46:19 +00:00
} :
2024-06-30 08:12:46 +00:00
assert (
lib . assertMsg ( maxLayers > 1 )
" t h e m a x L a y e r s a r g u m e n t o f d o c k e r T o o l s . b u i l d L a y e r e d I m a g e f u n c t i o n m u s t b e g r e a t h e r t h a n 1 ( c u r r e n t v a l u e : ${ toString maxLayers } ) "
) ;
let
baseName = baseNameOf name ;
streamScript = writePython3 " s t r e a m " { } ./stream_layered_image.py ;
baseJson = writeText " ${ baseName } - b a s e . j s o n " (
builtins . toJSON {
2024-05-02 00:46:19 +00:00
inherit config architecture ;
os = " l i n u x " ;
2024-06-30 08:12:46 +00:00
}
) ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
contentsList = if builtins . isList contents then contents else [ contents ] ;
bind-paths = builtins . toString (
builtins . map ( path : " - - b i n d = ${ path } : ${ path } ! " ) [
2024-05-02 00:46:19 +00:00
" / d e v / "
" / p r o c / "
" / s y s / "
" ${ builtins . storeDir } / "
" $ o u t / l a y e r . t a r "
2024-06-30 08:12:46 +00:00
]
) ;
# 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 } - c u s t o m i s a t i o n - l a y e r " ;
paths = contentsList ;
inherit extraCommands fakeRootCommands ;
nativeBuildInputs = [ fakeroot ] ++ optionals enableFakechroot [ proot ] ;
postBuild = ''
mv $ out old_out
( cd old_out ; eval " $ e x t r a C o m m a n d s " )
mkdir $ out
$ {
if enableFakechroot then
''
proot - r $ PWD/old_out $ { bind-paths } - - pwd = / fakeroot bash - c '
source $ stdenv/setup
eval " $ f a k e R o o t C o m m a n d s "
tar \
- - sort name \
- - exclude = ./proc \
- - exclude = ./sys \
- - exclude = . ${ builtins . storeDir } \
- - numeric-owner - - mtime " @ $ S O U R C E _ D A T E _ E P O C H " \
- - hard-dereference \
- cf $ out/layer.tar .
'
''
else
''
fakeroot bash - c '
source $ stdenv/setup
cd old_out
eval " $ f a k e R o o t C o m m a n d s "
tar \
- - sort name \
- - numeric-owner - - mtime " @ $ S O U R C E _ D A T E _ E P O C H " \
- - hard-dereference \
- cf $ out/layer.tar .
'
''
}
sha256sum $ out/layer.tar \
| cut - f 1 - d ' ' \
> $ out/checksum
'' ;
} ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
closureRoots =
lib . optionals includeStorePaths # normally true
( [
baseJson
customisationLayer
] ) ;
overallClosure = writeText " c l o s u r e " ( lib . concatStringsSep " " closureRoots ) ;
# These derivations are only created as implementation details of docker-tools,
# so they'll be excluded from the created images.
unnecessaryDrvs = [
baseJson
overallClosure
customisationLayer
] ;
conf =
runCommand " ${ baseName } - c o n f . j s o n "
2024-05-02 00:46:19 +00:00
{
2024-06-30 08:12:46 +00:00
inherit
fromImage
maxLayers
created
uid
gid
uname
gname
;
2024-05-02 00:46:19 +00:00
imageName = lib . toLower name ;
preferLocalBuild = true ;
passthru . imageTag =
2024-06-30 08:12:46 +00:00
if tag != null then
tag
2024-05-02 00:46:19 +00:00
else
2024-06-30 08:12:46 +00:00
lib . head (
lib . strings . splitString " - " ( baseNameOf ( builtins . unsafeDiscardStringContext conf . outPath ) )
) ;
2024-05-02 00:46:19 +00:00
paths = buildPackages . referencesByPopularity overallClosure ;
nativeBuildInputs = [ jq ] ;
2024-06-30 08:12:46 +00:00
}
''
$ {
if ( tag == null ) then
''
outName = " $ ( b a s e n a m e " $ out " ) "
outHash = $ ( echo " $ o u t N a m e " | cut - d - - f 1 )
imageTag = $ outHash
''
else
''
imageTag = " ${ tag } "
''
}
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# convert "created" to iso format
if [ [ " $ c r e a t e d " != " n o w " ] ] ; then
created = " $ ( d a t e - I s e c o n d s - d " $ created " ) "
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
paths ( ) {
cat $ paths $ { lib . concatMapStringsSep " " ( path : " | ( g r e p - v ${ path } | | t r u e ) " ) unnecessaryDrvs }
}
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# Compute the number of layers that are already used by a potential
# 'fromImage' as well as the customization layer. Ensure that there is
# still at least one layer available to store the image contents.
usedLayers = 0
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# subtract number of base image layers
if [ [ - n " $ f r o m I m a g e " ] ] ; then
( ( usedLayers + = $ ( tar - xOf " $ f r o m I m a g e " manifest . json | jq ' . [ 0 ] . Layers | length' ) ) )
fi
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# one layer will be taken up by the customisation layer
( ( usedLayers + = 1 ) )
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
if ! ( ( $ usedLayers < $ maxLayers ) ) ; then
echo > & 2 " E r r o r : u s e d L a y e r s $ u s e d L a y e r s l a y e r s t o s t o r e ' f r o m I m a g e ' a n d " \
" ' e x t r a C o m m a n d s ' , b u t o n l y m a x L a y e r s = $ m a x L a y e r s w e r e " \
" a l l o w e d . A t l e a s t 1 l a y e r i s r e q u i r e d t o s t o r e c o n t e n t s . "
exit 1
fi
availableLayers = $ ( ( maxLayers - usedLayers ) )
# Create $maxLayers worth of Docker Layers, one layer per store path
# unless there are more paths than $maxLayers. In that case, create
# $maxLayers-1 for the most popular layers, and smush the remainaing
# store paths in to one final layer.
#
# The following code is fiddly w.r.t. ensuring every layer is
# created, and that no paths are missed. If you change the
# following lines, double-check that your code behaves properly
# when the number of layers equals:
# maxLayers-1, maxLayers, and maxLayers+1, 0
paths |
jq - sR '
rtrimstr ( " \n " ) | split ( " \n " )
| ( . [ : $ maxLayers-1 ] | map ( [ . ] ) ) + [ . [ $ maxLayers-1 : ] ]
| map ( select ( length > 0 ) )
' \
- - argjson maxLayers " $ a v a i l a b l e L a y e r s " > store_layers . json
# The index on $store_layers is necessary because the --slurpfile
# automatically reads the file as an array.
cat $ { baseJson } | jq '
. + {
" s t o r e _ d i r " : $ store_dir ,
" f r o m _ i m a g e " : $ from_image ,
" s t o r e _ l a y e r s " : $ store_layers [ 0 ] ,
" c u s t o m i s a t i o n _ l a y e r " , $ customisation_layer ,
" r e p o _ t a g " : $ repo_tag ,
" c r e a t e d " : $ created ,
" u i d " : $ uid ,
" g i d " : $ gid ,
" u n a m e " : $ uname ,
" g n a m e " : $ gname
}
' - - arg store_dir " ${ storeDir } " \
- - argjson from_image $ { if fromImage == null then " n u l l " else " ' \" ${ fromImage } \" ' " } \
- - slurpfile store_layers store_layers . json \
- - arg customisation_layer $ { customisationLayer } \
- - arg repo_tag " $ i m a g e N a m e : $ i m a g e T a g " \
- - arg created " $ c r e a t e d " \
- - arg uid " $ u i d " \
- - arg gid " $ g i d " \
- - arg uname " $ u n a m e " \
- - arg gname " $ g n a m e " |
tee $ out
'' ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
result =
runCommand " s t r e a m - ${ baseName } "
2024-05-02 00:46:19 +00:00
{
inherit ( conf ) imageName ;
preferLocalBuild = true ;
passthru = passthru // {
inherit ( conf ) imageTag ;
# 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 ] ;
2024-06-30 08:12:46 +00:00
}
''
makeWrapper $ { streamScript } $ out - - add-flags $ { conf }
'' ;
in
result
2024-05-02 00:46:19 +00:00
) ;
# This function streams a docker image that behaves like a nix-shell for a derivation
streamNixShellImage =
2024-06-30 08:12:46 +00:00
{
# The derivation whose environment this docker image should be based on
drv ,
# Image Name
name ? drv . name + " - e n v " ,
# Image tag, the Nix's output hash will be used if null
tag ? null ,
# User id to run the container as. Defaults to 1000, because many
2024-05-02 00:46:19 +00:00
# binaries don't like to be run as root
2024-06-30 08:12:46 +00:00
uid ? 1000 ,
# Group id to run the container as, see also uid
gid ? 1000 ,
# The home directory of the user
homeDirectory ? " / b u i l d " ,
# The path to the bash binary to use as the shell. See `NIX_BUILD_SHELL` in `man nix-shell`
shell ? bashInteractive + " / b i n / b a s h " ,
# Run this command in the environment of the derivation, in an interactive shell. See `--command` in `man nix-shell`
command ? null ,
# Same as `command`, but runs the command in a non-interactive shell instead. See `--run` in `man nix-shell`
run ? null ,
2024-05-02 00:46:19 +00:00
} :
2024-06-30 08:12:46 +00:00
assert lib . assertMsg ( ! ( drv . drvAttrs . __structuredAttrs or false ) )
" s t r e a m N i x S h e l l I m a g e : D o e s n o t w o r k w i t h t h e d e r i v a t i o n ${ drv . name } b e c a u s e i t u s e s _ _ s t r u c t u r e d A t t r s " ;
assert lib . assertMsg (
command == null || run == null
) " s t r e a m N i x S h e l l I m a g e : C a n ' t s p e c i f y b o t h c o m m a n d a n d r u n " ;
let
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# A binary that calls the command to build the derivation
builder = writeShellScriptBin " b u i l d D e r i v a t i o n " ''
exec $ { lib . escapeShellArg ( stringValue drv . drvAttrs . builder ) } $ { lib . escapeShellArgs ( map stringValue drv . drvAttrs . args ) }
'' ;
staticPath = " ${ dirOf shell } : ${ lib . makeBinPath [ builder ] } " ;
# https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L493-L526
rcfile = writeText " n i x - s h e l l - r c " ''
unset PATH
dontAddDisableDepTrack = 1
# TODO: https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L506
[ - e $ stdenv/setup ] && source $ stdenv/setup
PATH = $ { staticPath }: " $ P A T H "
SHELL = $ { lib . escapeShellArg shell }
BASH = $ { lib . escapeShellArg shell }
set + e
[ - n " $ P S 1 " - a - z " $ N I X _ S H E L L _ P R E S E R V E _ P R O M P T " ] && PS1 = ' \ n \ [ \ 033 [ 1 ; 3 2 m \ ] [ nix-shell : \ w ] \ $ \ [ \ 033 [ 0 m \ ] '
if [ " $ ( t y p e - t r u n H o o k ) " = 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
'' }
'' ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/globals.hh#L464-L465
sandboxBuildDir = " / b u i l d " ;
# This function closely mirrors what this Nix code does:
# https://github.com/NixOS/nix/blob/2.8.0/src/libexpr/primops.cc#L1102
# https://github.com/NixOS/nix/blob/2.8.0/src/libexpr/eval.cc#L1981-L2036
stringValue =
value :
# We can't just use `toString` on all derivation attributes because that
# would not put path literals in the closure. So we explicitly copy
# those into the store here
if builtins . typeOf value == " p a t h " then
" ${ value } "
else if builtins . typeOf value == " l i s t " then
toString ( map stringValue value )
else
toString value ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L992-L1004
drvEnv =
lib . mapAttrs' (
name : value :
let
str = stringValue value ;
in
if lib . elem name ( drv . drvAttrs . passAsFile or [ ] ) then
lib . nameValuePair " ${ name } P a t h " ( writeText " p a s s - a s - t e x t - ${ name } " str )
else
lib . nameValuePair name str
) drv . drvAttrs
# A mapping from output name to the nix store path where they should end up
# https://github.com/NixOS/nix/blob/2.8.0/src/libexpr/primops.cc#L1253
// lib . genAttrs drv . outputs ( output : builtins . unsafeDiscardStringContext drv . ${ output } . outPath ) ;
# Environment variables set in the image
envVars =
{
2024-05-02 00:46:19 +00:00
# Root certificates for internet access
SSL_CERT_FILE = " ${ cacert } / e t c / s s l / c e r t s / c a - b u n d l e . c r t " ;
NIX_SSL_CERT_FILE = " ${ cacert } / e t c / s s l / c e r t s / c a - b u n d l e . c r t " ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1027-L1030
# PATH = "/path-not-set";
# Allows calling bash and `buildDerivation` as the Cmd
PATH = staticPath ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1032-L1038
HOME = homeDirectory ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1040-L1044
NIX_STORE = storeDir ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1046-L1047
# TODO: Make configurable?
NIX_BUILD_CORES = " 1 " ;
2024-06-30 08:12:46 +00:00
}
// drvEnv
// {
2024-05-02 00:46:19 +00:00
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1008-L1010
NIX_BUILD_TOP = sandboxBuildDir ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1012-L1013
TMPDIR = sandboxBuildDir ;
TEMPDIR = sandboxBuildDir ;
TMP = sandboxBuildDir ;
TEMP = sandboxBuildDir ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1015-L1019
PWD = sandboxBuildDir ;
# https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1071-L1074
# 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.8.0/src/libstore/build/local-derivation-goal.cc#L1076-L1077
TERM = " x t e r m - 2 5 6 c o l o r " ;
} ;
2024-06-30 08:12:46 +00:00
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/ffe155abd36366a870482625543f9bf924a58281/src/libstore/build/local-derivation-goal.cc#L906-L910
# 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 = [
" n i x b l d : x : ${ toString uid } : ${ toString gid } : B u i l d u s e r : ${ homeDirectory } : / n o s h e l l "
] ;
extraGroupLines = [ " n i x b l d : ! : ${ 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 }
'' ;
2024-05-02 00:46:19 +00:00
2024-06-30 08:12:46 +00:00
# Run this image as the given uid/gid
config . User = " ${ toString uid } : ${ toString gid } " ;
config . Cmd =
# https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L185-L186
# https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L534-L536
if run == null then
[
shell
" - - r c f i l e "
rcfile
]
else
[
shell
rcfile
] ;
config . WorkingDir = sandboxBuildDir ;
config . Env = lib . mapAttrsToList ( name : value : " ${ name } = ${ value } " ) envVars ;
} ;
2024-05-02 00:46:19 +00:00
# Wrapper around streamNixShellImage to build an image from the result
2024-06-30 08:12:46 +00:00
buildNixShellImage =
{
drv ,
compressor ? " g z " ,
. . .
} @ args :
2024-05-02 00:46:19 +00:00
let
2024-06-30 08:12:46 +00:00
stream = streamNixShellImage ( builtins . removeAttrs args [ " c o m p r e s s o r " ] ) ;
2024-05-02 00:46:19 +00:00
compress = compressorForImage compressor drv . name ;
in
2024-06-30 08:12:46 +00:00
runCommand " ${ drv . name } - e n v . t a r ${ compress . ext } " {
inherit ( stream ) imageName ;
passthru = {
inherit ( stream ) imageTag ;
} ;
nativeBuildInputs = compress . nativeInputs ;
} " ${ stream } | ${ compress . compress } > $ o u t " ;
2024-05-02 00:46:19 +00:00
}