2024-05-13 21:24:10 +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-05-13 21:24:10 +00:00
inherit ( lib ) optionals optionalString ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
inherit ( lib ) escapeShellArgs toList ;
2024-05-02 00:46:19 +00:00
mkDbExtraCommand = contents :
let
2024-05-13 21:24:10 +00:00
contentsList =
if builtins . isList contents then contents else [ contents ] ;
in ''
2024-05-02 00:46:19 +00:00
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-05-13 21:24:10 +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-05-13 21:24:10 +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
2024-05-13 21:24:10 +00:00
in rec {
2024-05-02 00:46:19 +00:00
examples = callPackage ./examples.nix {
2024-05-13 21:24:10 +00:00
inherit buildImage buildLayeredImage fakeNss pullImage shadowSetup
buildImageWithNixDb streamNixShellImage ;
2024-05-02 00:46:19 +00:00
} ;
tests = {
inherit ( nixosTests )
2024-05-13 21:24:10 +00:00
docker-tools docker-tools-overlay
2024-05-02 00:46:19 +00:00
# requires remote builder
# docker-tools-cross
2024-05-13 21:24:10 +00:00
;
2024-05-02 00:46:19 +00:00
} ;
pullImage =
2024-05-13 21:24:10 +00:00
let fixName = name : builtins . replaceStrings [ " / " " : " ] [ " - " " - " ] name ;
in { imageName
# To find the digest of an image, you can use skopeo:
# see doc/functions.xml
, imageDigest , sha256 , os ? " l i n u x "
2024-05-02 00:46:19 +00:00
, # Image architecture, defaults to the architecture of the `hostPlatform` when unset
2024-05-13 21:24:10 +00:00
arch ? defaultArchitecture
2024-05-02 00:46:19 +00:00
# This is used to set name to the pulled image
, finalImageName ? imageName
# This used to set a tag to the pulled image
, finalImageTag ? " l a t e s t "
# This is used to disable TLS certificate verification, allowing access to http registries on (hopefully) trusted networks
, tlsVerify ? true
2024-05-13 21:24:10 +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
2024-05-13 21:24:10 +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 ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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 " ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
sourceURL = " d o c k e r : / / ${ imageName } @ ${ imageDigest } " ;
destNameTag = " ${ finalImageName } : ${ finalImageTag } " ;
} ''
2024-05-02 00:46:19 +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
'' ;
# 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
2024-05-13 21:24:10 +00:00
mergeDrvs = { derivations , onlyDeps ? false }:
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.
2024-05-13 21:24:10 +00:00
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 = " 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-05-13 21:24:10 +00:00
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 [ " $ l n " ] : - } " ] ] ; then
echo " $ l n " ; seen [ " $ l n " ] = 1
2024-05-02 00:46:19 +00:00
fi
2024-05-13 21:24:10 +00:00
done
}
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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 ) )
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
mkdir - p image / $ extractionID/layer
tar - C image / $ extractionID/layer - xpf image / $ layerTar
rm image / $ layerTar
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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 { } ' \ ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# Get the next lower directory and continue the loop.
lowerdir = image / $ extractionID/layer '' ${ lowerdir:+: } $l o w e r d i r
done
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
mkdir work
mkdir layer
mkdir mnt
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
$ { 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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
$ { 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 }
'' }
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
umount mnt
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
(
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 " \ ;
)
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
$ { postUmount }
'' ) ;
exportImage = { name ? fromImage . name , fromImage , fromImageName ? null
, fromImageTag ? null , diskSize ? 1024 } :
2024-05-02 00:46:19 +00:00
runWithOverlay {
inherit name fromImage fromImageName fromImageTag diskSize ;
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.
shellScript = name : text :
writeScript name ''
#!${runtimeShell}
set - e
export PATH = $ { coreutils } /bin : /bin
$ { text }
'' ;
# Create a "layer" (set of files).
2024-05-13 21:24:10 +00:00
mkPureLayer = {
# Name of the layer
name , # JSON containing configuration and metadata for this layer.
baseJson , # Files to add to the layer.
copyToRoot ? null
2024-05-02 00:46:19 +00:00
, # When copying the contents into the image, preserve symlinks to
2024-05-13 21:24:10 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
keepContentsDirlinks ? false
2024-05-02 00:46:19 +00:00
, # Additional commands to run on the layer before it is tar'd up.
2024-05-13 21:24:10 +00:00
extraCommands ? " " , uid ? 0 , gid ? 0 } :
runCommand " d o c k e r - l a y e r - ${ name } " {
inherit baseJson extraCommands ;
contents = copyToRoot ;
nativeBuildInputs = [ jshon rsync tarsum ] ;
} ''
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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
chmod ug + w layer
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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 )
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# Indicate to docker that we're using schema version 1.0.
echo - n " 1 . 0 " > $ out/VERSION
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
echo " F i n i s h e d b u i l d i n g l a y e r ' ${ name } ' "
'' ;
2024-05-02 00:46:19 +00:00
# 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.
2024-05-13 21:24:10 +00:00
mkRootLayer = {
# Name of the image.
name , # Script to run as root. Bash.
runAsRoot
2024-05-02 00:46:19 +00:00
, # Files to add to the layer. If null, an empty layer will be created.
2024-05-13 21:24:10 +00:00
# To add packages to /bin, use `buildEnv` or similar.
copyToRoot ? null
2024-05-02 00:46:19 +00:00
, # When copying the contents into the image, preserve symlinks to
2024-05-13 21:24:10 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
keepContentsDirlinks ? false
2024-05-02 00:46:19 +00:00
, # JSON containing configuration and metadata for this layer.
2024-05-13 21:24:10 +00:00
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
2024-05-02 00:46:19 +00:00
, # How much disk to allocate for the temporary virtual machine.
2024-05-13 21:24:10 +00:00
diskSize ? 1024
2024-05-02 00:46:19 +00:00
, # How much memory to allocate for the temporary virtual machine.
2024-05-13 21:24:10 +00:00
buildVMMemorySize ? 512
2024-05-02 00:46:19 +00:00
, # Commands (bash) to run on the layer; these do not require sudo.
2024-05-13 21:24:10 +00:00
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 ;
2024-05-13 21:24:10 +00:00
in runWithOverlay {
2024-05-02 00:46:19 +00:00
name = " d o c k e r - l a y e r - ${ name } " ;
inherit fromImage fromImageName fromImageTag diskSize buildVMMemorySize ;
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 . . . "
2024-05-13 21:24:10 +00:00
rsync - a $ {
if keepContentsDirlinks then " K " else " k "
} - - chown = 0 : 0 $ item / layer /
2024-05-02 00:46:19 +00:00
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-05-13 21:24:10 +00:00
buildLayeredImage = lib . makeOverridable
( { name , compressor ? " g z " , . . . } @ args :
let
stream =
streamLayeredImage ( builtins . removeAttrs args [ " c o m p r e s s o r " ] ) ;
compress = compressorForImage compressor name ;
in runCommand " ${ baseNameOf name } . t a r ${ compress . ext } " {
2024-05-02 00:46:19 +00:00
inherit ( stream ) imageName ;
2024-05-13 21:24:10 +00:00
passthru = {
inherit ( stream ) imageTag ;
inherit stream ;
} ;
2024-05-02 00:46:19 +00:00
nativeBuildInputs = compress . nativeInputs ;
2024-05-13 21:24:10 +00:00
} " ${ 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
2024-05-13 21:24:10 +00:00
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
2024-05-02 00:46:19 +00:00
, # Name of the parent image; will be read from the image otherwise.
2024-05-13 21:24:10 +00:00
fromImageName ? null
2024-05-02 00:46:19 +00:00
, # Tag of the parent image; will be read from the image otherwise.
2024-05-13 21:24:10 +00:00
fromImageTag ? null
2024-05-02 00:46:19 +00:00
, # Files to put on the image (a nix store path or list of paths).
2024-05-13 21:24:10 +00:00
copyToRoot ? null
2024-05-02 00:46:19 +00:00
, # When copying the contents into the image, preserve symlinks to
2024-05-13 21:24:10 +00:00
# directories (see `rsync -K`). Otherwise, transform those symlinks
# into directories.
keepContentsDirlinks ? false
2024-05-02 00:46:19 +00:00
, # Docker config; e.g. what command to run on the container.
2024-05-13 21:24:10 +00:00
config ? null
2024-05-02 00:46:19 +00:00
, # Image architecture, defaults to the architecture of the `hostPlatform` when unset
2024-05-13 21:24:10 +00:00
architecture ? defaultArchitecture
2024-05-02 00:46:19 +00:00
, # Optional bash script to run on the files prior to fixturizing the layer.
2024-05-13 21:24:10 +00:00
extraCommands ? " " , uid ? 0 , gid ? 0
2024-05-02 00:46:19 +00:00
, # Optional bash script to run as root on the image when provisioning.
2024-05-13 21:24:10 +00:00
runAsRoot ? null
2024-05-02 00:46:19 +00:00
, # Size of the virtual machine disk to provision when building the image.
2024-05-13 21:24:10 +00:00
diskSize ? 1024
2024-05-02 00:46:19 +00:00
, # Size of the virtual machine memory to provision when building the image.
2024-05-13 21:24:10 +00:00
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 "
2024-05-02 00:46:19 +00:00
, # Compressor to use. One of: none, gz, zstd.
2024-05-13 21:24:10 +00:00
compressor ? " g z " , # Deprecated.
contents ? null , } :
2024-05-02 00:46:19 +00:00
let
2024-05-13 21:24:10 +00:00
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 . "
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.
2024-05-13 21:24:10 +00:00
baseJson = let
pure = writeText " ${ baseName } - c o n f i g . j s o n " ( builtins . toJSON {
inherit created config architecture ;
preferLocalBuild = true ;
os = " l i n u x " ;
} ) ;
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
'' ;
in if created == " n o w " then impure else pure ;
2024-05-02 00:46:19 +00:00
compress = compressorForImage compressor name ;
2024-05-13 21:24:10 +00:00
layer = if runAsRoot == null then
mkPureLayer {
name = baseName ;
inherit baseJson keepContentsDirlinks extraCommands uid gid ;
copyToRoot = rootContents ;
}
else
mkRootLayer {
name = baseName ;
inherit baseJson fromImage fromImageName fromImageTag
keepContentsDirlinks runAsRoot diskSize buildVMMemorySize
extraCommands ;
copyToRoot = rootContents ;
} ;
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 ) ) ) ;
} ''
2024-05-02 00:46:19 +00:00
$ { 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 )
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 " $ 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
# 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
for l in image /* / l a y e r . t a r ; d o
ls_tar $ l > > baseFiles
done
else
touch layer-list
fi
chmod - R ug + rw image
mkdir temp
cp $ { layer } /* t e m p /
chmod ug + w temp /*
for dep in $ ( cat $ layerClosure ) ; do
find $ dep > > layerFiles
done
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
# 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
echo " A d d i n g m e t a . . . "
# 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
# 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
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
# 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
# Make the image read-only.
chmod - R a-w image
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
echo " F i n i s h e d . "
'' ;
2024-05-13 21:24:10 +00:00
in checked result ) ;
2024-05-02 00:46:19 +00:00
# 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-05-13 21:24:10 +00:00
mergeImages = images :
runCommand " m e r g e - d o c k e r - i m a g e s " {
2024-05-02 00:46:19 +00:00
inherit images ;
2024-05-13 21:24:10 +00:00
nativeBuildInputs = [ file jq ] ++ compressors . none . nativeInputs
++ compressors . gz . nativeInputs ++ compressors . zstd . nativeInputs ;
2024-05-02 00:46:19 +00:00
} ''
2024-05-13 21:24:10 +00:00
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-05-13 21:24:10 +00:00
buildImageWithNixDb =
args @ { copyToRoot ? contents , contents ? null , extraCommands ? " " , . . . }:
( buildImage ( args // {
2024-05-02 00:46:19 +00:00
extraCommands = ( mkDbExtraCommand copyToRoot ) + extraCommands ;
2024-05-13 21:24:10 +00:00
} ) ) ;
2024-05-02 00:46:19 +00:00
# TODO: add the dependencies of the config json.
2024-05-13 21:24:10 +00:00
buildLayeredImageWithNixDb =
args @ { contents ? null , extraCommands ? " " , . . . }:
( buildLayeredImage ( args // {
2024-05-02 00:46:19 +00:00
extraCommands = ( mkDbExtraCommand contents ) + extraCommands ;
2024-05-13 21:24:10 +00:00
} ) ) ;
2024-05-02 00:46:19 +00:00
# Arguments are documented in ../../../doc/build-helpers/images/dockertools.section.md
2024-05-13 21:24:10 +00:00
streamLayeredImage = lib . makeOverridable ( { 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 ? { } , } :
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 ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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 {
inherit config architecture ;
os = " l i n u x " ;
} ) ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +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-05-13 21:24:10 +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 .
'
2024-05-02 00:46:19 +00:00
'' e l s e ''
2024-05-13 21:24:10 +00:00
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 .
'
2024-05-02 00:46:19 +00:00
'' }
2024-05-13 21:24:10 +00:00
sha256sum $ out/layer.tar \
| cut - f 1 - d ' ' \
> $ out/checksum
'' ;
} ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +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 " {
inherit fromImage maxLayers created uid gid uname gname ;
imageName = lib . toLower name ;
preferLocalBuild = true ;
passthru . imageTag = if tag != null then
tag
else
lib . head ( lib . strings . splitString " - "
( baseNameOf ( builtins . unsafeDiscardStringContext conf . outPath ) ) ) ;
paths = buildPackages . referencesByPopularity overallClosure ;
nativeBuildInputs = [ jq ] ;
} ''
$ { 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 )
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
imageTag = $ outHash
'' e l s e ''
imageTag = " ${ tag } "
'' }
# 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
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-05-13 21:24:10 +00:00
}
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +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-05-13 21:24:10 +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-05-13 21:24:10 +00:00
# one layer will be taken up by the customisation layer
( ( usedLayers + = 1 ) )
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +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 ) )
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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
'' ;
result = runCommand " s t r e a m - ${ baseName } " {
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 ] ;
} ''
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 =
{ # The derivation whose environment this docker image should be based on
2024-05-13 21:24:10 +00:00
drv , # Image Name
name ? drv . name + " - e n v "
2024-05-02 00:46:19 +00:00
, # Image tag, the Nix's output hash will be used if null
2024-05-13 21:24:10 +00:00
tag ? null
2024-05-02 00:46:19 +00:00
, # User id to run the container as. Defaults to 1000, because many
2024-05-13 21:24:10 +00:00
# binaries don't like to be run as root
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 "
2024-05-02 00:46:19 +00:00
, # The path to the bash binary to use as the shell. See `NIX_BUILD_SHELL` in `man nix-shell`
2024-05-13 21:24:10 +00:00
shell ? bashInteractive + " / b i n / b a s h "
2024-05-02 00:46:19 +00:00
, # Run this command in the environment of the derivation, in an interactive shell. See `--command` in `man nix-shell`
2024-05-13 21:24:10 +00:00
command ? null
2024-05-02 00:46:19 +00:00
, # Same as `command`, but runs the command in a non-interactive shell instead. See `--run` in `man nix-shell`
2024-05-13 21:24:10 +00:00
run ? null } :
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-05-13 21:24:10 +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 )
}
'' ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
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-05-13 21:24:10 +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 ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# 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 ) ;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
# Environment variables set in the image
envVars = {
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +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 " ;
} // drvEnv // {
# 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-05-02 00:46:19 +00:00
} ;
2024-05-13 21:24:10 +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
} : Build user:$ { homeDirectory }: /noshell "
] ;
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 }
'' ;
# 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
buildNixShellImage = { drv , compressor ? " g z " , . . . } @ args :
let
2024-05-13 21:24:10 +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 ;
2024-05-13 21:24:10 +00:00
in 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
}