diff options
Diffstat (limited to 'nixpkgs/nixos/lib')
25 files changed, 3073 insertions, 0 deletions
diff --git a/nixpkgs/nixos/lib/build-vms.nix b/nixpkgs/nixos/lib/build-vms.nix new file mode 100644 index 000000000000..1bad63b9194c --- /dev/null +++ b/nixpkgs/nixos/lib/build-vms.nix @@ -0,0 +1,101 @@ +{ system +, # Use a minimal kernel? + minimal ? false +, # Ignored + config ? null + # Nixpkgs, for qemu, lib and more +, pkgs +, # NixOS configuration to add to the VMs + extraConfigurations ? [] +}: + +with pkgs.lib; +with import ../lib/qemu-flags.nix { inherit pkgs; }; + +rec { + + inherit pkgs; + + qemu = pkgs.qemu_test; + + + # Build a virtual network from an attribute set `{ machine1 = + # config1; ... machineN = configN; }', where `machineX' is the + # hostname and `configX' is a NixOS system configuration. Each + # machine is given an arbitrary IP address in the virtual network. + buildVirtualNetwork = + nodes: let nodesOut = mapAttrs (n: buildVM nodesOut) (assignIPAddresses nodes); in nodesOut; + + + buildVM = + nodes: configurations: + + import ./eval-config.nix { + inherit system; + modules = configurations ++ extraConfigurations; + baseModules = (import ../modules/module-list.nix) ++ + [ ../modules/virtualisation/qemu-vm.nix + ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs + { key = "no-manual"; documentation.nixos.enable = false; } + { key = "qemu"; system.build.qemu = qemu; } + { key = "nodes"; _module.args.nodes = nodes; } + ] ++ optional minimal ../modules/testing/minimal-kernel.nix; + }; + + + # Given an attribute set { machine1 = config1; ... machineN = + # configN; }, sequentially assign IP addresses in the 192.168.1.0/24 + # range to each machine, and set the hostname to the attribute name. + assignIPAddresses = nodes: + + let + + machines = attrNames nodes; + + machinesNumbered = zipLists machines (range 1 254); + + nodes_ = forEach machinesNumbered (m: nameValuePair m.fst + [ ( { config, nodes, ... }: + let + interfacesNumbered = zipLists config.virtualisation.vlans (range 1 255); + interfaces = forEach interfacesNumbered ({ fst, snd }: + nameValuePair "eth${toString snd}" { ipv4.addresses = + [ { address = "192.168.${toString fst}.${toString m.snd}"; + prefixLength = 24; + } ]; + }); + in + { key = "ip-address"; + config = + { networking.hostName = mkDefault m.fst; + + networking.interfaces = listToAttrs interfaces; + + networking.primaryIPAddress = + optionalString (interfaces != []) (head (head interfaces).value.ipv4.addresses).address; + + # Put the IP addresses of all VMs in this machine's + # /etc/hosts file. If a machine has multiple + # interfaces, use the IP address corresponding to + # the first interface (i.e. the first network in its + # virtualisation.vlans option). + networking.extraHosts = flip concatMapStrings machines + (m': let config = (getAttr m' nodes).config; in + optionalString (config.networking.primaryIPAddress != "") + ("${config.networking.primaryIPAddress} " + + optionalString (config.networking.domain != null) + "${config.networking.hostName}.${config.networking.domain} " + + "${config.networking.hostName}\n")); + + virtualisation.qemu.options = + forEach interfacesNumbered + ({ fst, snd }: qemuNICFlags snd fst m.snd); + }; + } + ) + (getAttr m.fst nodes) + ] ); + + in listToAttrs nodes_; + +} diff --git a/nixpkgs/nixos/lib/eval-config.nix b/nixpkgs/nixos/lib/eval-config.nix new file mode 100644 index 000000000000..77490ca3762a --- /dev/null +++ b/nixpkgs/nixos/lib/eval-config.nix @@ -0,0 +1,67 @@ +# From an end-user configuration file (`configuration.nix'), build a NixOS +# configuration object (`config') from which we can retrieve option +# values. + +# !!! Please think twice before adding to this argument list! +# Ideally eval-config.nix would be an extremely thin wrapper +# around lib.evalModules, so that modular systems that have nixos configs +# as subcomponents (e.g. the container feature, or nixops if network +# expressions are ever made modular at the top level) can just use +# types.submodule instead of using eval-config.nix +{ # !!! system can be set modularly, would be nice to remove + system ? builtins.currentSystem +, # !!! is this argument needed any more? The pkgs argument can + # be set modularly anyway. + pkgs ? null +, # !!! what do we gain by making this configurable? + baseModules ? import ../modules/module-list.nix +, # !!! See comment about args in lib/modules.nix + extraArgs ? {} +, # !!! See comment about args in lib/modules.nix + specialArgs ? {} +, modules +, # !!! See comment about check in lib/modules.nix + check ? true +, prefix ? [] +, lib ? import ../../lib +}: + +let extraArgs_ = extraArgs; pkgs_ = pkgs; + extraModules = let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH"; + in if e == "" then [] else [(import e)]; +in + +let + pkgsModule = rec { + _file = ./eval-config.nix; + key = _file; + config = { + # Explicit `nixpkgs.system` or `nixpkgs.localSystem` should override + # this. Since the latter defaults to the former, the former should + # default to the argument. That way this new default could propagate all + # they way through, but has the last priority behind everything else. + nixpkgs.system = lib.mkDefault system; + _module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_); + }; + }; + +in rec { + + # Merge the option definitions in all modules, forming the full + # system configuration. + inherit (lib.evalModules { + inherit prefix check; + modules = baseModules ++ extraModules ++ [ pkgsModule ] ++ modules; + args = extraArgs; + specialArgs = + { modulesPath = builtins.toString ../modules; } // specialArgs; + }) config options; + + # These are the extra arguments passed to every module. In + # particular, Nixpkgs is passed through the "pkgs" argument. + extraArgs = extraArgs_ // { + inherit baseModules extraModules modules; + }; + + inherit (config._module.args) pkgs; +} diff --git a/nixpkgs/nixos/lib/from-env.nix b/nixpkgs/nixos/lib/from-env.nix new file mode 100644 index 000000000000..6bd71e40e9a1 --- /dev/null +++ b/nixpkgs/nixos/lib/from-env.nix @@ -0,0 +1,4 @@ +# TODO: remove this file. There is lib.maybeEnv now +name: default: +let value = builtins.getEnv name; in +if value == "" then default else value diff --git a/nixpkgs/nixos/lib/make-channel.nix b/nixpkgs/nixos/lib/make-channel.nix new file mode 100644 index 000000000000..9b920b989fcf --- /dev/null +++ b/nixpkgs/nixos/lib/make-channel.nix @@ -0,0 +1,31 @@ +/* Build a channel tarball. These contain, in addition to the nixpkgs + * expressions themselves, files that indicate the version of nixpkgs + * that they represent. + */ +{ pkgs, nixpkgs, version, versionSuffix }: + +pkgs.releaseTools.makeSourceTarball { + name = "nixos-channel"; + + src = nixpkgs; + + officialRelease = false; # FIXME: fix this in makeSourceTarball + inherit version versionSuffix; + + buildInputs = [ pkgs.nix ]; + + distPhase = '' + rm -rf .git + echo -n $VERSION_SUFFIX > .version-suffix + echo -n ${nixpkgs.rev or nixpkgs.shortRev} > .git-revision + releaseName=nixos-$VERSION$VERSION_SUFFIX + mkdir -p $out/tarballs + cp -prd . ../$releaseName + chmod -R u+w ../$releaseName + ln -s . ../$releaseName/nixpkgs # hack to make ‘<nixpkgs>’ work + NIX_STATE_DIR=$TMPDIR nix-env -f ../$releaseName/default.nix -qaP --meta --xml \* > /dev/null + cd .. + chmod -R u+w $releaseName + tar cfJ $out/tarballs/$releaseName.tar.xz $releaseName + ''; +} diff --git a/nixpkgs/nixos/lib/make-disk-image.nix b/nixpkgs/nixos/lib/make-disk-image.nix new file mode 100644 index 000000000000..5e86ea479d51 --- /dev/null +++ b/nixpkgs/nixos/lib/make-disk-image.nix @@ -0,0 +1,247 @@ +{ pkgs +, lib + +, # The NixOS configuration to be installed onto the disk image. + config + +, # The size of the disk, in megabytes. + diskSize + + # The files and directories to be placed in the target file system. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. +, contents ? [] + +, # Type of partition table to use; either "legacy", "efi", or "none". + # For "efi" images, the GPT partition table is used and a mandatory ESP + # partition of reasonable size is created in addition to the root partition. + # If `installBootLoader` is true, GRUB will be installed in EFI mode. + # For "legacy", the msdos partition table is used and a single large root + # partition is created. If `installBootLoader` is true, GRUB will be + # installed in legacy mode. + # For "none", no partition table is created. Enabling `installBootLoader` + # most likely fails as GRUB will probably refuse to install. + partitionTableType ? "legacy" + +, # The root file system type. + fsType ? "ext4" + +, # Filesystem label + label ? "nixos" + +, # The initial NixOS configuration file to be copied to + # /etc/nixos/configuration.nix. + configFile ? null + +, # Shell code executed after the VM has finished. + postVM ? "" + +, name ? "nixos-disk-image" + +, # Disk image format, one of qcow2, qcow2-compressed, vpc, raw. + format ? "raw" +}: + +assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "none"; +# We use -E offset=X below, which is only supported by e2fsprogs +assert partitionTableType != "none" -> fsType == "ext4"; + +with lib; + +let format' = format; in let + + format = if format' == "qcow2-compressed" then "qcow2" else format'; + + compress = optionalString (format' == "qcow2-compressed") "-c"; + + filename = "nixos." + { + qcow2 = "qcow2"; + vpc = "vhd"; + raw = "img"; + }.${format}; + + rootPartition = { # switch-case + legacy = "1"; + efi = "2"; + }.${partitionTableType}; + + partitionDiskScript = { # switch-case + legacy = '' + parted --script $diskImage -- \ + mklabel msdos \ + mkpart primary ext4 1MiB -1 + ''; + efi = '' + parted --script $diskImage -- \ + mklabel gpt \ + mkpart ESP fat32 8MiB 256MiB \ + set 1 boot on \ + mkpart primary ext4 256MiB -1 + ''; + none = ""; + }.${partitionTableType}; + + nixpkgs = cleanSource pkgs.path; + + # FIXME: merge with channel.nix / make-channel.nix. + channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} '' + mkdir -p $out + cp -prd ${nixpkgs.outPath} $out/nixos + chmod -R u+w $out/nixos + if [ ! -e $out/nixos/nixpkgs ]; then + ln -s . $out/nixos/nixpkgs + fi + rm -rf $out/nixos/.git + echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix + ''; + + binPath = with pkgs; makeBinPath ( + [ rsync + utillinux + parted + e2fsprogs + lkl + config.system.build.nixos-install + config.system.build.nixos-enter + nix + ] ++ stdenv.initialPath); + + # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate + # image building logic. The comment right below this now appears in 4 different places in nixpkgs :) + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; }; + + prepareImage = '' + export PATH=${binPath} + + # Yes, mkfs.ext4 takes different units in different contexts. Fun. + sectorsToKilobytes() { + echo $(( ( "$1" * 512 ) / 1024 )) + } + + sectorsToBytes() { + echo $(( "$1" * 512 )) + } + + mkdir $out + diskImage=nixos.raw + truncate -s ${toString diskSize}M $diskImage + + ${partitionDiskScript} + + ${if partitionTableType != "none" then '' + # Get start & length of the root partition in sectors to $START and $SECTORS. + eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs) + + mkfs.${fsType} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K + '' else '' + mkfs.${fsType} -F -L ${label} $diskImage + ''} + + root="$PWD/root" + mkdir -p $root + + # Copy arbitrary other files into the image + # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of + # https://github.com/NixOS/nixpkgs/issues/23052. + set -f + sources_=(${concatStringsSep " " sources}) + targets_=(${concatStringsSep " " targets}) + set +f + + for ((i = 0; i < ''${#targets_[@]}; i++)); do + source="''${sources_[$i]}" + target="''${targets_[$i]}" + + if [[ "$source" =~ '*' ]]; then + # If the source name contains '*', perform globbing. + mkdir -p $root/$target + for fn in $source; do + rsync -a --no-o --no-g "$fn" $root/$target/ + done + else + mkdir -p $root/$(dirname $target) + if ! [ -e $root/$target ]; then + rsync -a --no-o --no-g $source $root/$target + else + echo "duplicate entry $target -> $source" + exit 1 + fi + fi + done + + export HOME=$TMPDIR + + # Provide a Nix database so that nixos-install can copy closures. + export NIX_STATE_DIR=$TMPDIR/state + nix-store --load-db < ${closureInfo}/registration + + echo "running nixos-install..." + nixos-install --root $root --no-bootloader --no-root-passwd \ + --system ${config.system.build.toplevel} --channel ${channelSources} --substituters "" + + echo "copying staging root to image..." + cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* / + ''; +in pkgs.vmTools.runInLinuxVM ( + pkgs.runCommand name + { preVM = prepareImage; + buildInputs = with pkgs; [ utillinux e2fsprogs dosfstools ]; + postVM = '' + ${if format == "raw" then '' + mv $diskImage $out/${filename} + '' else '' + ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename} + ''} + diskImage=$out/${filename} + ${postVM} + ''; + memSize = 1024; + } + '' + export PATH=${binPath}:$PATH + + rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"} + + # Some tools assume these exist + ln -s vda /dev/xvda + ln -s vda /dev/sda + + mountPoint=/mnt + mkdir $mountPoint + mount $rootDisk $mountPoint + + # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an + # '-E offset=X' option, so we can't do this outside the VM. + ${optionalString (partitionTableType == "efi") '' + mkdir -p /mnt/boot + mkfs.vfat -n ESP /dev/vda1 + mount /dev/vda1 /mnt/boot + ''} + + # Install a configuration.nix + mkdir -p /mnt/etc/nixos + ${optionalString (configFile != null) '' + cp ${configFile} /mnt/etc/nixos/configuration.nix + ''} + + # Set up core system link, GRUB, etc. + NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot + + # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images + rm -f $mountPoint/etc/machine-id + + umount -R /mnt + + # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal + # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic + # output, of course, but we can fix that when/if we start making images deterministic. + ${optionalString (fsType == "ext4") '' + tune2fs -T now -c 0 -i 0 $rootDisk + ''} + '' +) diff --git a/nixpkgs/nixos/lib/make-ext4-fs.nix b/nixpkgs/nixos/lib/make-ext4-fs.nix new file mode 100644 index 000000000000..932adcd97967 --- /dev/null +++ b/nixpkgs/nixos/lib/make-ext4-fs.nix @@ -0,0 +1,91 @@ +# Builds an ext4 image containing a populated /nix/store with the closure +# of store paths passed in the storePaths parameter, in addition to the +# contents of a directory that can be populated with commands. The +# generated image is sized to only fit its contents, with the expectation +# that a script resizes the filesystem at boot time. +{ pkgs +# List of derivations to be included +, storePaths +# Shell commands to populate the ./files directory. +# All files in that directory are copied to the root of the FS. +, populateImageCommands ? "" +, volumeLabel +, uuid ? "44444444-4444-4444-8888-888888888888" +, e2fsprogs +, libfaketime +, perl +, lkl +}: + +let + sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; }; +in + +pkgs.stdenv.mkDerivation { + name = "ext4-fs.img"; + + nativeBuildInputs = [e2fsprogs.bin libfaketime perl lkl]; + + buildCommand = + '' + ( + mkdir -p ./files + ${populateImageCommands} + ) + # Add the closures of the top-level store objects. + storePaths=$(cat ${sdClosureInfo}/store-paths) + + # Make a crude approximation of the size of the target image. + # If the script starts failing, increase the fudge factors here. + numInodes=$(find $storePaths ./files | wc -l) + numDataBlocks=$(du -s -c -B 4096 --apparent-size $storePaths ./files | tail -1 | awk '{ print int($1 * 1.03) }') + bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks)) + echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)" + + truncate -s $bytes $out + faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $out + + # Also include a manifest of the closures in a format suitable for nix-store --load-db. + cp ${sdClosureInfo}/registration nix-path-registration + cptofs -t ext4 -i $out nix-path-registration / + + # Create nix/store before copying paths + faketime -f "1970-01-01 00:00:01" mkdir -p nix/store + cptofs -t ext4 -i $out nix / + + echo "copying store paths to image..." + cptofs -t ext4 -i $out $storePaths /nix/store/ + + ( + echo "copying files to image..." + cd ./files + cptofs -t ext4 -i $out ./* / + ) + + # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build. + if ! fsck.ext4 -n -f $out; then + echo "--- Fsck failed for EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---" + cat errorlog + return 1 + fi + + ( + # Resizes **snugly** to its actual limits (or closer to) + free=$(dumpe2fs $out | grep '^Free blocks:') + blocksize=$(dumpe2fs $out | grep '^Block size:') + blocks=$(dumpe2fs $out | grep '^Block count:') + blocks=$((''${blocks##*:})) # format the number. + blocksize=$((''${blocksize##*:})) # format the number. + # System can't boot with 0 blocks free. + # Add 16MiB of free space + fudge=$(( 16 * 1024 * 1024 / blocksize )) + size=$(( blocks - ''${free##*:} + fudge )) + + echo "Resizing from $blocks blocks to $size blocks. (~ $((size*blocksize/1024/1024))MiB)" + EXT2FS_NO_MTAB_OK=yes resize2fs $out -f $size + ) + + # And a final fsck, because of the previous truncating. + fsck.ext4 -n -f $out + ''; +} diff --git a/nixpkgs/nixos/lib/make-iso9660-image.nix b/nixpkgs/nixos/lib/make-iso9660-image.nix new file mode 100644 index 000000000000..8cd19b6e1874 --- /dev/null +++ b/nixpkgs/nixos/lib/make-iso9660-image.nix @@ -0,0 +1,65 @@ +{ stdenv, closureInfo, xorriso, syslinux + +, # The file name of the resulting ISO image. + isoName ? "cd.iso" + +, # The files and directories to be placed in the ISO file system. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the CD. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the CD. + storeContents ? [] + +, # Whether this should be an El-Torito bootable CD. + bootable ? false + +, # Whether this should be an efi-bootable El-Torito CD. + efiBootable ? false + +, # Whether this should be an hybrid CD (bootable from USB as well as CD). + usbBootable ? false + +, # The path (in the ISO file system) of the boot image. + bootImage ? "" + +, # The path (in the ISO file system) of the efi boot image. + efiBootImage ? "" + +, # The path (outside the ISO file system) of the isohybrid-mbr image. + isohybridMbrImage ? "" + +, # Whether to compress the resulting ISO image with bzip2. + compressImage ? false + +, # The volume ID. + volumeID ? "" +}: + +assert bootable -> bootImage != ""; +assert efiBootable -> efiBootImage != ""; +assert usbBootable -> isohybridMbrImage != ""; + +stdenv.mkDerivation { + name = isoName; + builder = ./make-iso9660-image.sh; + buildInputs = [ xorriso syslinux ]; + + inherit isoName bootable bootImage compressImage volumeID efiBootImage efiBootable isohybridMbrImage usbBootable; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + objects = map (x: x.object) storeContents; + symlinks = map (x: x.symlink) storeContents; + + # For obtaining the closure of `storeContents'. + closureInfo = closureInfo { rootPaths = map (x: x.object) storeContents; }; +} diff --git a/nixpkgs/nixos/lib/make-iso9660-image.sh b/nixpkgs/nixos/lib/make-iso9660-image.sh new file mode 100644 index 000000000000..b7b1ab52a637 --- /dev/null +++ b/nixpkgs/nixos/lib/make-iso9660-image.sh @@ -0,0 +1,136 @@ +source $stdenv/setup + +sources_=($sources) +targets_=($targets) + +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +# Escape potential equal signs (=) with backslash (\=) +escapeEquals() { + echo "$1" | sed -e 's/\\/\\\\/g' -e 's/=/\\=/g' +} + +# Queues an file/directory to be placed on the ISO. +# An entry consists of a local source path (2) and +# a destination path on the ISO (1). +addPath() { + target="$1" + source="$2" + echo "$(escapeEquals "$target")=$(escapeEquals "$source")" >> pathlist +} + +stripSlash "$bootImage"; bootImage="$res" + + +if test -n "$bootable"; then + + # The -boot-info-table option modifies the $bootImage file, so + # find it in `contents' and make a copy of it (since the original + # is read-only in the Nix store...). + for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + if test "$res" = "$bootImage"; then + echo "copying the boot image ${sources_[$i]}" + cp "${sources_[$i]}" boot.img + chmod u+w boot.img + sources_[$i]=boot.img + fi + done + + isoBootFlags="-eltorito-boot ${bootImage} + -eltorito-catalog .boot.cat + -no-emul-boot -boot-load-size 4 -boot-info-table + --sort-weight 1 /isolinux" # Make sure isolinux is near the beginning of the ISO +fi + +if test -n "$usbBootable"; then + usbBootFlags="-isohybrid-mbr ${isohybridMbrImage}" +fi + +if test -n "$efiBootable"; then + efiBootFlags="-eltorito-alt-boot + -e $efiBootImage + -no-emul-boot + -isohybrid-gpt-basdat" +fi + +touch pathlist + + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + addPath "$res" "${sources_[$i]}" +done + + +# Add the closures of the top-level store objects. +for i in $(< $closureInfo/store-paths); do + addPath "${i:1}" "$i" +done + + +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +if [[ ${#objects[*]} != 0 ]]; then + cp $closureInfo/registration nix-path-registration + addPath "nix-path-registration" "nix-path-registration" +fi + + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + addPath "$symlink" "./$symlink" + fi +done + +mkdir -p $out/iso + +xorriso="xorriso + -as mkisofs + -iso-level 3 + -volid ${volumeID} + -appid nixos + -publisher nixos + -graft-points + -full-iso9660-filenames + ${isoBootFlags} + ${usbBootFlags} + ${efiBootFlags} + -r + -path-list pathlist + --sort-weight 0 / +" + +$xorriso -output $out/iso/$isoName + +if test -n "$usbBootable"; then + echo "Making image hybrid..." + if test -n "$efiBootable"; then + isohybrid --uefi $out/iso/$isoName + else + isohybrid $out/iso/$isoName + fi +fi + +if test -n "$compressImage"; then + echo "Compressing image..." + bzip2 $out/iso/$isoName +fi + +mkdir -p $out/nix-support +echo $system > $out/nix-support/system +echo "file iso $out/iso/$isoName" >> $out/nix-support/hydra-build-products diff --git a/nixpkgs/nixos/lib/make-options-doc/default.nix b/nixpkgs/nixos/lib/make-options-doc/default.nix new file mode 100644 index 000000000000..88e052106a28 --- /dev/null +++ b/nixpkgs/nixos/lib/make-options-doc/default.nix @@ -0,0 +1,164 @@ +/* Generate JSON, XML and DocBook documentation for given NixOS options. + + Minimal example: + + { pkgs, }: + + let + eval = import (pkgs.path + "/nixos/lib/eval-config.nix") { + baseModules = [ + ../module.nix + ]; + modules = []; + }; + in pkgs.nixosOptionsDoc { + options = eval.options; + } + +*/ +{ pkgs +, lib +, options +, transformOptions ? lib.id # function for additional tranformations of the options +, revision ? "" # Specify revision for the options +}: + +let + # Replace functions by the string <function> + substFunction = x: + if builtins.isAttrs x then lib.mapAttrs (name: substFunction) x + else if builtins.isList x then map substFunction x + else if lib.isFunction x then "<function>" + else x; + + optionsListDesc = lib.flip map optionsListVisible + (opt: transformOptions opt + // lib.optionalAttrs (opt ? example) { example = substFunction opt.example; } + // lib.optionalAttrs (opt ? default) { default = substFunction opt.default; } + // lib.optionalAttrs (opt ? type) { type = substFunction opt.type; } + // lib.optionalAttrs (opt ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages; } + ); + + # Generate DocBook documentation for a list of packages. This is + # what `relatedPackages` option of `mkOption` from + # ../../../lib/options.nix influences. + # + # Each element of `relatedPackages` can be either + # - a string: that will be interpreted as an attribute name from `pkgs`, + # - a list: that will be interpreted as an attribute path from `pkgs`, + # - an attrset: that can specify `name`, `path`, `package`, `comment` + # (either of `name`, `path` is required, the rest are optional). + genRelatedPackages = packages: + let + unpack = p: if lib.isString p then { name = p; } + else if lib.isList p then { path = p; } + else p; + describe = args: + let + title = args.title or null; + name = args.name or (lib.concatStringsSep "." args.path); + path = args.path or [ args.name ]; + package = args.package or (lib.attrByPath path (throw "Invalid package attribute path `${toString path}'") pkgs); + in "<listitem>" + + "<para><literal>${lib.optionalString (title != null) "${title} aka "}pkgs.${name} (${package.meta.name})</literal>" + + lib.optionalString (!package.meta.available) " <emphasis>[UNAVAILABLE]</emphasis>" + + ": ${package.meta.description or "???"}.</para>" + + lib.optionalString (args ? comment) "\n<para>${args.comment}</para>" + # Lots of `longDescription's break DocBook, so we just wrap them into <programlisting> + + lib.optionalString (package.meta ? longDescription) "\n<programlisting>${package.meta.longDescription}</programlisting>" + + "</listitem>"; + in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>"; + + # Custom "less" that pushes up all the things ending in ".enable*" + # and ".package*" + optionLess = a: b: + let + ise = lib.hasPrefix "enable"; + isp = lib.hasPrefix "package"; + cmp = lib.splitByAndCompare ise lib.compare + (lib.splitByAndCompare isp lib.compare lib.compare); + in lib.compareLists cmp a.loc b.loc < 0; + + # Remove invisible and internal options. + optionsListVisible = lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList options); + + # Customly sort option list for the man page. + optionsList = lib.sort optionLess optionsListDesc; + + # Convert the list of options into an XML file. + optionsXML = builtins.toFile "options.xml" (builtins.toXML optionsList); + + optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList); + + # TODO: declarations: link to github + singleAsciiDoc = name: value: '' + == ${name} + + ${value.description} + + [discrete] + === details + + Type:: ${value.type} + ${ if lib.hasAttr "default" value + then '' + Default:: + + + ---- + ${builtins.toJSON value.default} + ---- + '' + else "No Default:: {blank}" + } + ${ if value.readOnly + then "Read Only:: {blank}" + else "" + } + ${ if lib.hasAttr "example" value + then '' + Example:: + + + ---- + ${builtins.toJSON value.example} + ---- + '' + else "No Example:: {blank}" + } + ''; + +in rec { + inherit optionsNix; + + optionsAsciiDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleAsciiDoc optionsNix); + + optionsJSON = pkgs.runCommand "options.json" + { meta.description = "List of NixOS options in JSON format"; + } + '' + # Export list of options in different format. + dst=$out/share/doc/nixos + mkdir -p $dst + + cp ${builtins.toFile "options.json" (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix))} $dst/options.json + + mkdir -p $out/nix-support + echo "file json $dst/options.json" >> $out/nix-support/hydra-build-products + ''; # */ + + optionsDocBook = pkgs.runCommand "options-docbook.xml" {} '' + optionsXML=${optionsXML} + if grep /nixpkgs/nixos/modules $optionsXML; then + echo "The manual appears to depend on the location of Nixpkgs, which is bad" + echo "since this prevents sharing via the NixOS channel. This is typically" + echo "caused by an option default that refers to a relative path (see above" + echo "for hints about the offending path)." + exit 1 + fi + + ${pkgs.libxslt.bin}/bin/xsltproc \ + --stringparam revision '${revision}' \ + -o intermediate.xml ${./options-to-docbook.xsl} $optionsXML + ${pkgs.libxslt.bin}/bin/xsltproc \ + -o "$out" ${./postprocess-option-descriptions.xsl} intermediate.xml + ''; +} diff --git a/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl b/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl new file mode 100644 index 000000000000..72ac89d4ff62 --- /dev/null +++ b/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl @@ -0,0 +1,236 @@ +<?xml version="1.0"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:nixos="tag:nixos.org" + xmlns="http://docbook.org/ns/docbook" + extension-element-prefixes="str" + > + + <xsl:output method='xml' encoding="UTF-8" /> + + <xsl:param name="revision" /> + <xsl:param name="program" /> + + + <xsl:template match="/expr/list"> + <appendix xml:id="appendix-configuration-options"> + <title>Configuration Options</title> + <variablelist xml:id="configuration-variable-list"> + <xsl:for-each select="attrs"> + <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '<', '_'), '>', '_'), '?', '_'))" /> + <varlistentry> + <term xlink:href="#{$id}"> + <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute> + <option> + <xsl:value-of select="attr[@name = 'name']/string/@value" /> + </option> + </term> + + <listitem> + + <nixos:option-description> + <para> + <xsl:value-of disable-output-escaping="yes" + select="attr[@name = 'description']/string/@value" /> + </para> + </nixos:option-description> + + <xsl:if test="attr[@name = 'type']"> + <para> + <emphasis>Type:</emphasis> + <xsl:text> </xsl:text> + <xsl:value-of select="attr[@name = 'type']/string/@value"/> + <xsl:if test="attr[@name = 'readOnly']/bool/@value = 'true'"> + <xsl:text> </xsl:text> + <emphasis>(read only)</emphasis> + </xsl:if> + </para> + </xsl:if> + + <xsl:if test="attr[@name = 'default']"> + <para> + <emphasis>Default:</emphasis> + <xsl:text> </xsl:text> + <xsl:apply-templates select="attr[@name = 'default']" mode="top" /> + </para> + </xsl:if> + + <xsl:if test="attr[@name = 'example']"> + <para> + <emphasis>Example:</emphasis> + <xsl:text> </xsl:text> + <xsl:choose> + <xsl:when test="attr[@name = 'example']/attrs[attr[@name = '_type' and string[@value = 'literalExample']]]"> + <programlisting><xsl:value-of select="attr[@name = 'example']/attrs/attr[@name = 'text']/string/@value" /></programlisting> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates select="attr[@name = 'example']" mode="top" /> + </xsl:otherwise> + </xsl:choose> + </para> + </xsl:if> + + <xsl:if test="attr[@name = 'relatedPackages']"> + <para> + <emphasis>Related packages:</emphasis> + <xsl:text> </xsl:text> + <xsl:value-of disable-output-escaping="yes" + select="attr[@name = 'relatedPackages']/string/@value" /> + </para> + </xsl:if> + + <xsl:if test="count(attr[@name = 'declarations']/list/*) != 0"> + <para> + <emphasis>Declared by:</emphasis> + </para> + <xsl:apply-templates select="attr[@name = 'declarations']" /> + </xsl:if> + + <xsl:if test="count(attr[@name = 'definitions']/list/*) != 0"> + <para> + <emphasis>Defined by:</emphasis> + </para> + <xsl:apply-templates select="attr[@name = 'definitions']" /> + </xsl:if> + + </listitem> + + </varlistentry> + + </xsl:for-each> + + </variablelist> + </appendix> + </xsl:template> + + + <xsl:template match="*" mode="top"> + <xsl:choose> + <xsl:when test="string[contains(@value, '
')]"> +<programlisting> +<xsl:text>'' +</xsl:text><xsl:value-of select='str:replace(string/@value, "${", "''${")' /><xsl:text>''</xsl:text></programlisting> + </xsl:when> + <xsl:otherwise> + <literal><xsl:apply-templates /></literal> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + + <xsl:template match="null"> + <xsl:text>null</xsl:text> + </xsl:template> + + + <xsl:template match="string"> + <xsl:choose> + <xsl:when test="(contains(@value, '"') or contains(@value, '\')) and not(contains(@value, '
'))"> + <xsl:text>''</xsl:text><xsl:value-of select='str:replace(@value, "${", "''${")' /><xsl:text>''</xsl:text> + </xsl:when> + <xsl:otherwise> + <xsl:text>"</xsl:text><xsl:value-of select="str:replace(str:replace(str:replace(str:replace(@value, '\', '\\'), '"', '\"'), '
', '\n'), '$', '\$')" /><xsl:text>"</xsl:text> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + + <xsl:template match="int"> + <xsl:value-of select="@value" /> + </xsl:template> + + + <xsl:template match="bool[@value = 'true']"> + <xsl:text>true</xsl:text> + </xsl:template> + + + <xsl:template match="bool[@value = 'false']"> + <xsl:text>false</xsl:text> + </xsl:template> + + + <xsl:template match="list"> + [ + <xsl:for-each select="*"> + <xsl:apply-templates select="." /> + <xsl:text> </xsl:text> + </xsl:for-each> + ] + </xsl:template> + + + <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalExample']]]"> + <xsl:value-of select="attr[@name = 'text']/string/@value" /> + </xsl:template> + + + <xsl:template match="attrs"> + { + <xsl:for-each select="attr"> + <xsl:value-of select="@name" /> + <xsl:text> = </xsl:text> + <xsl:apply-templates select="*" /><xsl:text>; </xsl:text> + </xsl:for-each> + } + </xsl:template> + + + <xsl:template match="derivation"> + <replaceable>(build of <xsl:value-of select="attr[@name = 'name']/string/@value" />)</replaceable> + </xsl:template> + + <xsl:template match="attr[@name = 'declarations' or @name = 'definitions']"> + <simplelist> + <xsl:for-each select="list/string"> + <member><filename> + <!-- Hyperlink the filename either to the NixOS Subversion + repository (if it’s a module and we have a revision number), + or to the local filesystem. --> + <xsl:choose> + <xsl:when test="not(starts-with(@value, '/'))"> + <xsl:choose> + <xsl:when test="$revision = 'local'"> + <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/master/<xsl:value-of select="@value"/></xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/<xsl:value-of select="$revision"/>/<xsl:value-of select="@value"/></xsl:attribute> + </xsl:otherwise> + </xsl:choose> + </xsl:when> + <xsl:when test="$revision != 'local' and $program = 'nixops' and contains(@value, '/nix/')"> + <xsl:attribute name="xlink:href">https://github.com/NixOS/nixops/blob/<xsl:value-of select="$revision"/>/nix/<xsl:value-of select="substring-after(@value, '/nix/')"/></xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="xlink:href">file://<xsl:value-of select="@value"/></xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <!-- Print the filename and make it user-friendly by replacing the + /nix/store/<hash> prefix by the default location of nixos + sources. --> + <xsl:choose> + <xsl:when test="not(starts-with(@value, '/'))"> + <nixpkgs/<xsl:value-of select="@value"/>> + </xsl:when> + <xsl:when test="contains(@value, 'nixops') and contains(@value, '/nix/')"> + <nixops/<xsl:value-of select="substring-after(@value, '/nix/')"/>> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="@value" /> + </xsl:otherwise> + </xsl:choose> + </filename></member> + </xsl:for-each> + </simplelist> + </xsl:template> + + + <xsl:template match="function"> + <xsl:text>λ</xsl:text> + </xsl:template> + + +</xsl:stylesheet> diff --git a/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl b/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl new file mode 100644 index 000000000000..1201c7612c2e --- /dev/null +++ b/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl @@ -0,0 +1,115 @@ +<?xml version="1.0"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + xmlns:exsl="http://exslt.org/common" + xmlns:db="http://docbook.org/ns/docbook" + xmlns:nixos="tag:nixos.org" + extension-element-prefixes="str exsl"> + <xsl:output method='xml' encoding="UTF-8" /> + + <xsl:template match="@*|node()"> + <xsl:copy> + <xsl:apply-templates select="@*|node()" /> + </xsl:copy> + </xsl:template> + + <xsl:template name="break-up-description"> + <xsl:param name="input" /> + <xsl:param name="buffer" /> + + <!-- Every time we have two newlines following each other, we want to + break it into </para><para>. --> + <xsl:variable name="parbreak" select="'

'" /> + + <!-- Similar to "(head:tail) = input" in Haskell. --> + <xsl:variable name="head" select="$input[1]" /> + <xsl:variable name="tail" select="$input[position() > 1]" /> + + <xsl:choose> + <xsl:when test="$head/self::text() and contains($head, $parbreak)"> + <!-- If the haystack provided to str:split() directly starts or + ends with $parbreak, it doesn't generate a <token/> for that, + so we are doing this here. --> + <xsl:variable name="splitted-raw"> + <xsl:if test="starts-with($head, $parbreak)"><token /></xsl:if> + <xsl:for-each select="str:split($head, $parbreak)"> + <token><xsl:value-of select="node()" /></token> + </xsl:for-each> + <!-- Something like ends-with($head, $parbreak), but there is + no ends-with() in XSLT, so we need to use substring(). --> + <xsl:if test=" + substring($head, string-length($head) - + string-length($parbreak) + 1) = $parbreak + "><token /></xsl:if> + </xsl:variable> + <xsl:variable name="splitted" + select="exsl:node-set($splitted-raw)/token" /> + <!-- The buffer we had so far didn't contain any text nodes that + contain a $parbreak, so we can put the buffer along with the + first token of $splitted into a para element. --> + <para xmlns="http://docbook.org/ns/docbook"> + <xsl:apply-templates select="exsl:node-set($buffer)" /> + <xsl:apply-templates select="$splitted[1]/node()" /> + </para> + <!-- We have already emitted the first splitted result, so the + last result is going to be set as the new $buffer later + because its contents may not be directly followed up by a + $parbreak. --> + <xsl:for-each select="$splitted[position() > 1 + and position() < last()]"> + <para xmlns="http://docbook.org/ns/docbook"> + <xsl:apply-templates select="node()" /> + </para> + </xsl:for-each> + <xsl:call-template name="break-up-description"> + <xsl:with-param name="input" select="$tail" /> + <xsl:with-param name="buffer" select="$splitted[last()]/node()" /> + </xsl:call-template> + </xsl:when> + <!-- Either non-text node or one without $parbreak, which we just + want to buffer and continue recursing. --> + <xsl:when test="$input"> + <xsl:call-template name="break-up-description"> + <xsl:with-param name="input" select="$tail" /> + <!-- This essentially appends $head to $buffer. --> + <xsl:with-param name="buffer"> + <xsl:if test="$buffer"> + <xsl:for-each select="exsl:node-set($buffer)"> + <xsl:apply-templates select="." /> + </xsl:for-each> + </xsl:if> + <xsl:apply-templates select="$head" /> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + <!-- No more $input, just put the remaining $buffer in a para. --> + <xsl:otherwise> + <para xmlns="http://docbook.org/ns/docbook"> + <xsl:apply-templates select="exsl:node-set($buffer)" /> + </para> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <xsl:template match="nixos:option-description"> + <xsl:choose> + <!-- + Only process nodes that are comprised of a single <para/> element, + because if that's not the case the description already contains + </para><para> in between and we need no further processing. + --> + <xsl:when test="count(db:para) > 1"> + <xsl:apply-templates select="node()" /> + </xsl:when> + <xsl:otherwise> + <xsl:call-template name="break-up-description"> + <xsl:with-param name="input" + select="exsl:node-set(db:para/node())" /> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + +</xsl:stylesheet> diff --git a/nixpkgs/nixos/lib/make-squashfs.nix b/nixpkgs/nixos/lib/make-squashfs.nix new file mode 100644 index 000000000000..ee76c9c5bf24 --- /dev/null +++ b/nixpkgs/nixos/lib/make-squashfs.nix @@ -0,0 +1,28 @@ +{ stdenv, squashfsTools, closureInfo + +, # The root directory of the squashfs filesystem is filled with the + # closures of the Nix store paths listed here. + storeContents ? [] +, # Compression parameters. + # For zstd compression you can use "zstd -Xcompression-level 6". + comp ? "xz -Xdict-size 100%" +}: + +stdenv.mkDerivation { + name = "squashfs.img"; + + nativeBuildInputs = [ squashfsTools ]; + + buildCommand = + '' + closureInfo=${closureInfo { rootPaths = storeContents; }} + + # Also include a manifest of the closures in a format suitable + # for nix-store --load-db. + cp $closureInfo/registration nix-path-registration + + # Generate the squashfs image. + mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \ + -keep-as-directory -all-root -b 1048576 -comp ${comp} + ''; +} diff --git a/nixpkgs/nixos/lib/make-system-tarball.nix b/nixpkgs/nixos/lib/make-system-tarball.nix new file mode 100644 index 000000000000..dee91a6ce3f4 --- /dev/null +++ b/nixpkgs/nixos/lib/make-system-tarball.nix @@ -0,0 +1,56 @@ +{ stdenv, closureInfo, pixz + +, # The file name of the resulting tarball + fileName ? "nixos-system-${stdenv.hostPlatform.system}" + +, # The files and directories to be placed in the tarball. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the tarball. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the tarball. + storeContents ? [] + + # Extra commands to be executed before archiving files +, extraCommands ? "" + + # Extra tar arguments +, extraArgs ? "" + # Command used for compression +, compressCommand ? "pixz" + # Extension for the compressed tarball +, compressionExtension ? ".xz" + # extra inputs, like the compressor to use +, extraInputs ? [ pixz ] +}: + +let + symlinks = map (x: x.symlink) storeContents; + objects = map (x: x.object) storeContents; +in + +stdenv.mkDerivation { + name = "tarball"; + builder = ./make-system-tarball.sh; + buildInputs = extraInputs; + + inherit fileName extraArgs extraCommands compressCommand; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + inherit symlinks objects; + + closureInfo = closureInfo { + rootPaths = objects; + }; + + extension = compressionExtension; +} diff --git a/nixpkgs/nixos/lib/make-system-tarball.sh b/nixpkgs/nixos/lib/make-system-tarball.sh new file mode 100644 index 000000000000..1a0017a1799a --- /dev/null +++ b/nixpkgs/nixos/lib/make-system-tarball.sh @@ -0,0 +1,57 @@ +source $stdenv/setup + +sources_=($sources) +targets_=($targets) + +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + mkdir -p "$(dirname "$res")" + cp -a "${sources_[$i]}" "$res" +done + + +# Add the closures of the top-level store objects. +chmod +w . +mkdir -p nix/store +for i in $(< $closureInfo/store-paths); do + cp -a "$i" "${i:1}" +done + + +# TODO tar ruxo +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +cp $closureInfo/registration nix-path-registration + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + fi +done + +$extraCommands + +mkdir -p $out/tarball + +rm env-vars + +time tar --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner -c * $extraArgs | $compressCommand > $out/tarball/$fileName.tar${extension} + +mkdir -p $out/nix-support +echo $system > $out/nix-support/system +echo "file system-tarball $out/tarball/$fileName.tar${extension}" > $out/nix-support/hydra-build-products diff --git a/nixpkgs/nixos/lib/qemu-flags.nix b/nixpkgs/nixos/lib/qemu-flags.nix new file mode 100644 index 000000000000..779f0377a512 --- /dev/null +++ b/nixpkgs/nixos/lib/qemu-flags.nix @@ -0,0 +1,25 @@ +# QEMU flags shared between various Nix expressions. +{ pkgs }: + +let + zeroPad = n: if n < 10 then "0${toString n}" else toString n; +in + +{ + + qemuNICFlags = nic: net: machine: + [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=52:54:00:12:${zeroPad net}:${zeroPad machine}" + "-netdev vde,id=vlan${toString nic},sock=$QEMU_VDE_SOCKET_${toString net}" + ]; + + qemuSerialDevice = if pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64 then "ttyS0" + else if pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64 then "ttyAMA0" + else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'"; + + qemuBinary = qemuPkg: { + "x86_64-linux" = "${qemuPkg}/bin/qemu-kvm -cpu kvm64"; + "armv7l-linux" = "${qemuPkg}/bin/qemu-system-arm -enable-kvm -machine virt -cpu host"; + "aarch64-linux" = "${qemuPkg}/bin/qemu-system-aarch64 -enable-kvm -machine virt,gic-version=host -cpu host"; + "x86_64-darwin" = "${qemuPkg}/bin/qemu-kvm -cpu kvm64"; + }.${pkgs.stdenv.hostPlatform.system} or "${qemuPkg}/bin/qemu-kvm"; +} diff --git a/nixpkgs/nixos/lib/test-driver/Logger.pm b/nixpkgs/nixos/lib/test-driver/Logger.pm new file mode 100644 index 000000000000..080310ea34e0 --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/Logger.pm @@ -0,0 +1,75 @@ +package Logger; + +use strict; +use Thread::Queue; +use XML::Writer; +use Encode qw(decode encode); +use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); + +sub new { + my ($class) = @_; + + my $logFile = defined $ENV{LOGFILE} ? "$ENV{LOGFILE}" : "/dev/null"; + my $log = new XML::Writer(OUTPUT => new IO::File(">$logFile")); + + my $self = { + log => $log, + logQueue => Thread::Queue->new() + }; + + $self->{log}->startTag("logfile"); + + bless $self, $class; + return $self; +} + +sub close { + my ($self) = @_; + $self->{log}->endTag("logfile"); + $self->{log}->end; +} + +sub drainLogQueue { + my ($self) = @_; + while (defined (my $item = $self->{logQueue}->dequeue_nb())) { + $self->{log}->dataElement("line", sanitise($item->{msg}), 'machine' => $item->{machine}, 'type' => 'serial'); + } +} + +sub maybePrefix { + my ($msg, $attrs) = @_; + $msg = $attrs->{machine} . ": " . $msg if defined $attrs->{machine}; + return $msg; +} + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + print STDERR maybePrefix("$msg\n", $attrs); + $self->{log}->startTag("nest"); + $self->{log}->dataElement("head", $msg, %{$attrs}); + my $now = clock_gettime(CLOCK_MONOTONIC); + $self->drainLogQueue(); + eval { &$coderef }; + my $res = $@; + $self->drainLogQueue(); + $self->log(sprintf("(%.2f seconds)", clock_gettime(CLOCK_MONOTONIC) - $now)); + $self->{log}->endTag("nest"); + die $@ if $@; +} + +sub sanitise { + my ($s) = @_; + $s =~ s/[[:cntrl:]\xff]//g; + $s = decode('UTF-8', $s, Encode::FB_DEFAULT); + return encode('UTF-8', $s, Encode::FB_CROAK); +} + +sub log { + my ($self, $msg, $attrs) = @_; + chomp $msg; + print STDERR maybePrefix("$msg\n", $attrs); + $self->drainLogQueue(); + $self->{log}->dataElement("line", $msg, %{$attrs}); +} + +1; diff --git a/nixpkgs/nixos/lib/test-driver/Machine.pm b/nixpkgs/nixos/lib/test-driver/Machine.pm new file mode 100644 index 000000000000..4d3d63cd2dbf --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/Machine.pm @@ -0,0 +1,734 @@ +package Machine; + +use strict; +use threads; +use Socket; +use IO::Handle; +use POSIX qw(dup2); +use FileHandle; +use Cwd; +use File::Basename; +use File::Path qw(make_path); +use File::Slurp; +use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); + + +my $showGraphics = defined $ENV{'DISPLAY'}; + +my $sharedDir; + + +sub new { + my ($class, $args) = @_; + + my $startCommand = $args->{startCommand}; + + my $name = $args->{name}; + if (!$name) { + $startCommand =~ /run-(.*)-vm$/ if defined $startCommand; + $name = $1 || "machine"; + } + + if (!$startCommand) { + # !!! merge with qemu-vm.nix. + my $netBackend = "-netdev user,id=net0"; + my $netFrontend = "-device virtio-net-pci,netdev=net0"; + + $netBackend .= "," . $args->{netBackendArgs} + if defined $args->{netBackendArgs}; + + $netFrontend .= "," . $args->{netFrontendArgs} + if defined $args->{netFrontendArgs}; + + $startCommand = + "qemu-kvm -m 384 $netBackend $netFrontend \$QEMU_OPTS "; + + if (defined $args->{hda}) { + if ($args->{hdaInterface} eq "scsi") { + $startCommand .= "-drive id=hda,file=" + . Cwd::abs_path($args->{hda}) + . ",werror=report,if=none " + . "-device scsi-hd,drive=hda "; + } else { + $startCommand .= "-drive file=" . Cwd::abs_path($args->{hda}) + . ",if=" . $args->{hdaInterface} + . ",werror=report "; + } + } + + $startCommand .= "-cdrom $args->{cdrom} " + if defined $args->{cdrom}; + $startCommand .= "-device piix3-usb-uhci -drive id=usbdisk,file=$args->{usb},if=none,readonly -device usb-storage,drive=usbdisk " + if defined $args->{usb}; + $startCommand .= "-bios $args->{bios} " + if defined $args->{bios}; + $startCommand .= $args->{qemuFlags} || ""; + } + + my $tmpDir = $ENV{'TMPDIR'} || "/tmp"; + unless (defined $sharedDir) { + $sharedDir = $tmpDir . "/xchg-shared"; + make_path($sharedDir, { mode => 0700, owner => $< }); + } + + my $allowReboot = 0; + $allowReboot = $args->{allowReboot} if defined $args->{allowReboot}; + + my $self = { + startCommand => $startCommand, + name => $name, + allowReboot => $allowReboot, + booted => 0, + pid => 0, + connected => 0, + socket => undef, + stateDir => "$tmpDir/vm-state-$name", + monitor => undef, + log => $args->{log}, + redirectSerial => $args->{redirectSerial} // 1, + }; + + mkdir $self->{stateDir}, 0700; + + bless $self, $class; + return $self; +} + + +sub log { + my ($self, $msg) = @_; + $self->{log}->log($msg, { machine => $self->{name} }); +} + + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + $self->{log}->nest($msg, $coderef, { %{$attrs || {}}, machine => $self->{name} }); +} + + +sub name { + my ($self) = @_; + return $self->{name}; +} + + +sub stateDir { + my ($self) = @_; + return $self->{stateDir}; +} + + +sub start { + my ($self) = @_; + return if $self->{booted}; + + $self->log("starting vm"); + + # Create a socket pair for the serial line input/output of the VM. + my ($serialP, $serialC); + socketpair($serialP, $serialC, PF_UNIX, SOCK_STREAM, 0) or die; + + # Create a Unix domain socket to which QEMU's monitor will connect. + my $monitorPath = $self->{stateDir} . "/monitor"; + unlink $monitorPath; + my $monitorS; + socket($monitorS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($monitorS, sockaddr_un($monitorPath)) or die "cannot bind monitor socket: $!"; + listen($monitorS, 1) or die; + + # Create a Unix domain socket to which the root shell in the guest will connect. + my $shellPath = $self->{stateDir} . "/shell"; + unlink $shellPath; + my $shellS; + socket($shellS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($shellS, sockaddr_un($shellPath)) or die "cannot bind shell socket: $!"; + listen($shellS, 1) or die; + + # Start the VM. + my $pid = fork(); + die if $pid == -1; + + if ($pid == 0) { + close $serialP; + close $monitorS; + close $shellS; + if ($self->{redirectSerial}) { + open NUL, "</dev/null" or die; + dup2(fileno(NUL), fileno(STDIN)); + dup2(fileno($serialC), fileno(STDOUT)); + dup2(fileno($serialC), fileno(STDERR)); + } + $ENV{TMPDIR} = $self->{stateDir}; + $ENV{SHARED_DIR} = $sharedDir; + $ENV{USE_TMPDIR} = 1; + $ENV{QEMU_OPTS} = + ($self->{allowReboot} ? "" : "-no-reboot ") . + "-monitor unix:./monitor -chardev socket,id=shell,path=./shell " . + "-device virtio-serial -device virtconsole,chardev=shell " . + "-device virtio-rng-pci " . + ($showGraphics ? "-serial stdio" : "-nographic") . " " . ($ENV{QEMU_OPTS} || ""); + chdir $self->{stateDir} or die; + exec $self->{startCommand}; + die "running VM script: $!"; + } + + # Process serial line output. + close $serialC; + + threads->create(\&processSerialOutput, $self, $serialP)->detach; + + sub processSerialOutput { + my ($self, $serialP) = @_; + while (<$serialP>) { + chomp; + s/\r$//; + print STDERR $self->{name}, "# $_\n"; + $self->{log}->{logQueue}->enqueue({msg => $_, machine => $self->{name}}); # !!! + } + } + + eval { + local $SIG{CHLD} = sub { die "QEMU died prematurely\n"; }; + + # Wait until QEMU connects to the monitor. + accept($self->{monitor}, $monitorS) or die; + + # Wait until QEMU connects to the root shell socket. QEMU + # does so immediately; this doesn't mean that the root shell + # has connected yet inside the guest. + accept($self->{socket}, $shellS) or die; + $self->{socket}->autoflush(1); + }; + die "$@" if $@; + + $self->waitForMonitorPrompt; + + $self->log("QEMU running (pid $pid)"); + + $self->{pid} = $pid; + $self->{booted} = 1; +} + + +# Send a command to the monitor and wait for it to finish. TODO: QEMU +# also has a JSON-based monitor interface now, but it doesn't support +# all commands yet. We should use it once it does. +sub sendMonitorCommand { + my ($self, $command) = @_; + $self->log("sending monitor command: $command"); + syswrite $self->{monitor}, "$command\n"; + return $self->waitForMonitorPrompt; +} + + +# Wait until the monitor sends "(qemu) ". +sub waitForMonitorPrompt { + my ($self) = @_; + my $res = ""; + my $s; + while (sysread($self->{monitor}, $s, 1024)) { + $res .= $s; + last if $res =~ s/\(qemu\) $//; + } + return $res; +} + + +# Call the given code reference repeatedly, with 1 second intervals, +# until it returns 1 or a timeout is reached. +sub retry { + my ($coderef) = @_; + my $n; + for ($n = 899; $n >=0; $n--) { + return if &$coderef($n); + sleep 1; + } + die "action timed out after $n seconds"; +} + + +sub connect { + my ($self) = @_; + return if $self->{connected}; + + $self->nest("waiting for the VM to finish booting", sub { + + $self->start; + + my $now = clock_gettime(CLOCK_MONOTONIC); + local $SIG{ALRM} = sub { die "timed out waiting for the VM to connect\n"; }; + alarm 600; + readline $self->{socket} or die "the VM quit before connecting\n"; + alarm 0; + + $self->log("connected to guest root shell"); + # We're interested in tracking how close we are to `alarm`. + $self->log(sprintf("(connecting took %.2f seconds)", clock_gettime(CLOCK_MONOTONIC) - $now)); + $self->{connected} = 1; + + }); +} + + +sub waitForShutdown { + my ($self) = @_; + return unless $self->{booted}; + + $self->nest("waiting for the VM to power off", sub { + waitpid $self->{pid}, 0; + $self->{pid} = 0; + $self->{booted} = 0; + $self->{connected} = 0; + }); +} + + +sub isUp { + my ($self) = @_; + return $self->{booted} && $self->{connected}; +} + + +sub execute_ { + my ($self, $command) = @_; + + $self->connect; + + print { $self->{socket} } ("( $command ); echo '|!=EOF' \$?\n"); + + my $out = ""; + + while (1) { + my $line = readline($self->{socket}); + die "connection to VM lost unexpectedly" unless defined $line; + #$self->log("got line: $line"); + if ($line =~ /^(.*)\|\!\=EOF\s+(\d+)$/) { + $out .= $1; + $self->log("exit status $2"); + return ($2, $out); + } + $out .= $line; + } +} + + +sub execute { + my ($self, $command) = @_; + my @res; + $self->nest("running command: $command", sub { + @res = $self->execute_($command); + }); + return @res; +} + + +sub succeed { + my ($self, @commands) = @_; + + my $res; + foreach my $command (@commands) { + $self->nest("must succeed: $command", sub { + my ($status, $out) = $self->execute_($command); + if ($status != 0) { + $self->log("output: $out"); + die "command `$command' did not succeed (exit code $status)\n"; + } + $res .= $out; + }); + } + + return $res; +} + + +sub mustSucceed { + succeed @_; +} + + +sub waitUntilSucceeds { + my ($self, $command) = @_; + $self->nest("waiting for success: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status == 0; + }; + }); +} + + +sub waitUntilFails { + my ($self, $command) = @_; + $self->nest("waiting for failure: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status != 0; + }; + }); +} + + +sub fail { + my ($self, $command) = @_; + $self->nest("must fail: $command", sub { + my ($status, $out) = $self->execute_($command); + die "command `$command' unexpectedly succeeded" + if $status == 0; + }); +} + + +sub mustFail { + fail @_; +} + + +sub getUnitInfo { + my ($self, $unit, $user) = @_; + my ($status, $lines) = $self->systemctl("--no-pager show \"$unit\"", $user); + return undef if $status != 0; + my $info = {}; + foreach my $line (split '\n', $lines) { + $line =~ /^([^=]+)=(.*)$/ or next; + $info->{$1} = $2; + } + return $info; +} + +sub systemctl { + my ($self, $q, $user) = @_; + if ($user) { + $q =~ s/'/\\'/g; + return $self->execute("su -l $user -c \$'XDG_RUNTIME_DIR=/run/user/`id -u` systemctl --user $q'"); + } + + return $self->execute("systemctl $q"); +} + +# Fail if the given systemd unit is not in the "active" state. +sub requireActiveUnit { + my ($self, $unit) = @_; + $self->nest("checking if unit ‘$unit’ has reached state 'active'", sub { + my $info = $self->getUnitInfo($unit); + my $state = $info->{ActiveState}; + if ($state ne "active") { + die "Expected unit ‘$unit’ to to be in state 'active' but it is in state ‘$state’\n"; + }; + }); +} + +# Wait for a systemd unit to reach the "active" state. +sub waitForUnit { + my ($self, $unit, $user) = @_; + $self->nest("waiting for unit ‘$unit’", sub { + retry sub { + my $info = $self->getUnitInfo($unit, $user); + my $state = $info->{ActiveState}; + die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; + if ($state eq "inactive") { + # If there are no pending jobs, then assume this unit + # will never reach active state. + my ($status, $jobs) = $self->systemctl("list-jobs --full 2>&1", $user); + if ($jobs =~ /No jobs/) { # FIXME: fragile + # Handle the case where the unit may have started + # between the previous getUnitInfo() and + # list-jobs. + my $info2 = $self->getUnitInfo($unit); + die "unit ‘$unit’ is inactive and there are no pending jobs\n" + if $info2->{ActiveState} eq $state; + } + } + return 1 if $state eq "active"; + }; + }); +} + + +sub waitForJob { + my ($self, $jobName) = @_; + return $self->waitForUnit($jobName); +} + + +# Wait until the specified file exists. +sub waitForFile { + my ($self, $fileName) = @_; + $self->nest("waiting for file ‘$fileName’", sub { + retry sub { + my ($status, $out) = $self->execute("test -e $fileName"); + return 1 if $status == 0; + } + }); +} + +sub startJob { + my ($self, $jobName, $user) = @_; + $self->systemctl("start $jobName", $user); + # FIXME: check result +} + +sub stopJob { + my ($self, $jobName, $user) = @_; + $self->systemctl("stop $jobName", $user); +} + + +# Wait until the machine is listening on the given TCP port. +sub waitForOpenPort { + my ($self, $port) = @_; + $self->nest("waiting for TCP port $port", sub { + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status == 0; + } + }); +} + + +# Wait until the machine is not listening on the given TCP port. +sub waitForClosedPort { + my ($self, $port) = @_; + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status != 0; + } +} + + +sub shutdown { + my ($self) = @_; + return unless $self->{booted}; + + print { $self->{socket} } ("poweroff\n"); + + $self->waitForShutdown; +} + + +sub crash { + my ($self) = @_; + return unless $self->{booted}; + + $self->log("forced crash"); + + $self->sendMonitorCommand("quit"); + + $self->waitForShutdown; +} + + +# Make the machine unreachable by shutting down eth1 (the multicast +# interface used to talk to the other VMs). We keep eth0 up so that +# the test driver can continue to talk to the machine. +sub block { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 off"); +} + + +# Make the machine reachable. +sub unblock { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 on"); +} + + +# Take a screenshot of the X server on :0.0. +sub screenshot { + my ($self, $filename) = @_; + my $dir = $ENV{'out'} || Cwd::abs_path("."); + $filename = "$dir/${filename}.png" if $filename =~ /^\w+$/; + my $tmp = "${filename}.ppm"; + my $name = basename($filename); + $self->nest("making screenshot ‘$name’", sub { + $self->sendMonitorCommand("screendump $tmp"); + system("pnmtopng $tmp > ${filename}") == 0 + or die "cannot convert screenshot"; + unlink $tmp; + }, { image => $name } ); +} + +# Get the text of TTY<n> +sub getTTYText { + my ($self, $tty) = @_; + + my ($status, $out) = $self->execute("fold -w\$(stty -F /dev/tty${tty} size | awk '{print \$2}') /dev/vcs${tty}"); + return $out; +} + +# Wait until TTY<n>'s text matches a particular regular expression +sub waitUntilTTYMatches { + my ($self, $tty, $regexp) = @_; + + $self->nest("waiting for $regexp to appear on tty $tty", sub { + retry sub { + my ($retries_remaining) = @_; + if ($retries_remaining == 0) { + $self->log("Last chance to match /$regexp/ on TTY$tty, which currently contains:"); + $self->log($self->getTTYText($tty)); + } + + return 1 if $self->getTTYText($tty) =~ /$regexp/; + } + }); +} + +# Debugging: Dump the contents of the TTY<n> +sub dumpTTYContents { + my ($self, $tty) = @_; + + $self->execute("fold -w 80 /dev/vcs${tty} | systemd-cat"); +} + +# Take a screenshot and return the result as text using optical character +# recognition. +sub getScreenText { + my ($self) = @_; + + system("command -v tesseract &> /dev/null") == 0 + or die "getScreenText used but enableOCR is false"; + + my $text; + $self->nest("performing optical character recognition", sub { + my $tmpbase = Cwd::abs_path(".")."/ocr"; + my $tmpin = $tmpbase."in.ppm"; + + $self->sendMonitorCommand("screendump $tmpin"); + + my $magickArgs = "-filter Catrom -density 72 -resample 300 " + . "-contrast -normalize -despeckle -type grayscale " + . "-sharpen 1 -posterize 3 -negate -gamma 100 " + . "-blur 1x65535"; + my $tessArgs = "-c debug_file=/dev/null --psm 11 --oem 2"; + + $text = `convert $magickArgs $tmpin tiff:- | tesseract - - $tessArgs`; + my $status = $? >> 8; + unlink $tmpin; + + die "OCR failed with exit code $status" if $status != 0; + }); + return $text; +} + + +# Wait until a specific regexp matches the textual contents of the screen. +sub waitForText { + my ($self, $regexp) = @_; + $self->nest("waiting for $regexp to appear on the screen", sub { + retry sub { + my ($retries_remaining) = @_; + if ($retries_remaining == 0) { + $self->log("Last chance to match /$regexp/ on the screen, which currently contains:"); + $self->log($self->getScreenText); + } + + return 1 if $self->getScreenText =~ /$regexp/; + } + }); +} + + +# Wait until it is possible to connect to the X server. Note that +# testing the existence of /tmp/.X11-unix/X0 is insufficient. +sub waitForX { + my ($self, $regexp) = @_; + $self->nest("waiting for the X11 server", sub { + retry sub { + my ($status, $out) = $self->execute("journalctl -b SYSLOG_IDENTIFIER=systemd | grep 'Reached target Current graphical'"); + return 0 if $status != 0; + ($status, $out) = $self->execute("[ -e /tmp/.X11-unix/X0 ]"); + return 1 if $status == 0; + } + }); +} + + +sub getWindowNames { + my ($self) = @_; + my $res = $self->mustSucceed( + q{xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'}); + return split /\n/, $res; +} + + +sub waitForWindow { + my ($self, $regexp) = @_; + $self->nest("waiting for a window to appear", sub { + retry sub { + my @names = $self->getWindowNames; + + my ($retries_remaining) = @_; + if ($retries_remaining == 0) { + $self->log("Last chance to match /$regexp/ on the the window list, which currently contains:"); + $self->log(join(", ", @names)); + } + + foreach my $n (@names) { + return 1 if $n =~ /$regexp/; + } + } + }); +} + + +sub copyFileFromHost { + my ($self, $from, $to) = @_; + my $s = `cat $from` or die; + $s =~ s/'/'\\''/g; + $self->mustSucceed("echo '$s' > $to"); +} + + +my %charToKey = ( + 'A' => "shift-a", 'N' => "shift-n", '-' => "0x0C", '_' => "shift-0x0C", '!' => "shift-0x02", + 'B' => "shift-b", 'O' => "shift-o", '=' => "0x0D", '+' => "shift-0x0D", '@' => "shift-0x03", + 'C' => "shift-c", 'P' => "shift-p", '[' => "0x1A", '{' => "shift-0x1A", '#' => "shift-0x04", + 'D' => "shift-d", 'Q' => "shift-q", ']' => "0x1B", '}' => "shift-0x1B", '$' => "shift-0x05", + 'E' => "shift-e", 'R' => "shift-r", ';' => "0x27", ':' => "shift-0x27", '%' => "shift-0x06", + 'F' => "shift-f", 'S' => "shift-s", '\'' => "0x28", '"' => "shift-0x28", '^' => "shift-0x07", + 'G' => "shift-g", 'T' => "shift-t", '`' => "0x29", '~' => "shift-0x29", '&' => "shift-0x08", + 'H' => "shift-h", 'U' => "shift-u", '\\' => "0x2B", '|' => "shift-0x2B", '*' => "shift-0x09", + 'I' => "shift-i", 'V' => "shift-v", ',' => "0x33", '<' => "shift-0x33", '(' => "shift-0x0A", + 'J' => "shift-j", 'W' => "shift-w", '.' => "0x34", '>' => "shift-0x34", ')' => "shift-0x0B", + 'K' => "shift-k", 'X' => "shift-x", '/' => "0x35", '?' => "shift-0x35", + 'L' => "shift-l", 'Y' => "shift-y", ' ' => "spc", + 'M' => "shift-m", 'Z' => "shift-z", "\n" => "ret", +); + + +sub sendKeys { + my ($self, @keys) = @_; + foreach my $key (@keys) { + $key = $charToKey{$key} if exists $charToKey{$key}; + $self->sendMonitorCommand("sendkey $key"); + } +} + + +sub sendChars { + my ($self, $chars) = @_; + $self->nest("sending keys ‘$chars’", sub { + $self->sendKeys(split //, $chars); + }); +} + + +# Sleep N seconds (in virtual guest time, not real time). +sub sleep { + my ($self, $time) = @_; + $self->succeed("sleep $time"); +} + + +# Forward a TCP port on the host to a TCP port on the guest. Useful +# during interactive testing. +sub forwardPort { + my ($self, $hostPort, $guestPort) = @_; + $hostPort = 8080 unless defined $hostPort; + $guestPort = 80 unless defined $guestPort; + $self->sendMonitorCommand("hostfwd_add tcp::$hostPort-:$guestPort"); +} + + +1; diff --git a/nixpkgs/nixos/lib/test-driver/log2html.xsl b/nixpkgs/nixos/lib/test-driver/log2html.xsl new file mode 100644 index 000000000000..0485412b4c8e --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/log2html.xsl @@ -0,0 +1,135 @@ +<?xml version="1.0"?> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + + <xsl:output method='html' encoding="UTF-8" + doctype-public="-//W3C//DTD HTML 4.01//EN" + doctype-system="http://www.w3.org/TR/html4/strict.dtd" /> + + <xsl:template match="logfile"> + <html> + <head> + <script type="text/javascript" src="jquery.min.js"></script> + <script type="text/javascript" src="jquery-ui.min.js"></script> + <script type="text/javascript" src="treebits.js" /> + <link rel="stylesheet" href="logfile.css" type="text/css" /> + <title>Log File</title> + </head> + <body> + <h1>VM build log</h1> + <p> + <a href="javascript:" class="logTreeExpandAll">Expand all</a> | + <a href="javascript:" class="logTreeCollapseAll">Collapse all</a> + </p> + <ul class='toplevel'> + <xsl:for-each select='line|nest'> + <li> + <xsl:apply-templates select='.'/> + </li> + </xsl:for-each> + </ul> + + <xsl:if test=".//*[@image]"> + <h1>Screenshots</h1> + <ul class="vmScreenshots"> + <xsl:for-each select='.//*[@image]'> + <li><a href="{@image}"><xsl:value-of select="@image" /></a></li> + </xsl:for-each> + </ul> + </xsl:if> + + </body> + </html> + </xsl:template> + + + <xsl:template match="nest"> + + <!-- The tree should be collapsed by default if all children are + unimportant or if the header is unimportant. --> + <xsl:variable name="collapsed" select="not(./head[@expanded]) and count(.//*[@error]) = 0"/> + + <xsl:variable name="style"><xsl:if test="$collapsed">display: none;</xsl:if></xsl:variable> + + <xsl:if test="line|nest"> + <a href="javascript:" class="logTreeToggle"> + <xsl:choose> + <xsl:when test="$collapsed"><xsl:text>+</xsl:text></xsl:when> + <xsl:otherwise><xsl:text>-</xsl:text></xsl:otherwise> + </xsl:choose> + </a> + <xsl:text> </xsl:text> + </xsl:if> + + <xsl:apply-templates select='head'/> + + <!-- Be careful to only generate <ul>s if there are <li>s, otherwise it’s malformed. --> + <xsl:if test="line|nest"> + + <ul class='nesting' style="{$style}"> + <xsl:for-each select='line|nest'> + + <!-- Is this the last line? If so, mark it as such so that it + can be rendered differently. --> + <xsl:variable name="class"><xsl:choose><xsl:when test="position() != last()">line</xsl:when><xsl:otherwise>lastline</xsl:otherwise></xsl:choose></xsl:variable> + + <li class='{$class}'> + <span class='lineconn' /> + <span class='linebody'> + <xsl:apply-templates select='.'/> + </span> + </li> + </xsl:for-each> + </ul> + </xsl:if> + + </xsl:template> + + + <xsl:template match="head|line"> + <code> + <xsl:if test="@error"> + <xsl:attribute name="class">errorLine</xsl:attribute> + </xsl:if> + <xsl:if test="@warning"> + <xsl:attribute name="class">warningLine</xsl:attribute> + </xsl:if> + <xsl:if test="@priority = 3"> + <xsl:attribute name="class">prio3</xsl:attribute> + </xsl:if> + + <xsl:if test="@type = 'serial'"> + <xsl:attribute name="class">serial</xsl:attribute> + </xsl:if> + + <xsl:if test="@machine"> + <xsl:choose> + <xsl:when test="@type = 'serial'"> + <span class="machine"><xsl:value-of select="@machine"/># </span> + </xsl:when> + <xsl:otherwise> + <span class="machine"><xsl:value-of select="@machine"/>: </span> + </xsl:otherwise> + </xsl:choose> + </xsl:if> + + <xsl:choose> + <xsl:when test="@image"> + <a href="{@image}"><xsl:apply-templates/></a> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates/> + </xsl:otherwise> + </xsl:choose> + </code> + </xsl:template> + + + <xsl:template match="storeref"> + <em class='storeref'> + <span class='popup'><xsl:apply-templates/></span> + <span class='elided'>/...</span><xsl:apply-templates select='name'/><xsl:apply-templates select='path'/> + </em> + </xsl:template> + +</xsl:stylesheet> diff --git a/nixpkgs/nixos/lib/test-driver/logfile.css b/nixpkgs/nixos/lib/test-driver/logfile.css new file mode 100644 index 000000000000..a54d8504a867 --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/logfile.css @@ -0,0 +1,129 @@ +body { + font-family: sans-serif; + background: white; +} + +h1 +{ + color: #005aa0; + font-size: 180%; +} + +a { + text-decoration: none; +} + + +ul.nesting, ul.toplevel { + padding: 0; + margin: 0; +} + +ul.toplevel { + list-style-type: none; +} + +.line, .head { + padding-top: 0em; +} + +ul.nesting li.line, ul.nesting li.lastline { + position: relative; + list-style-type: none; +} + +ul.nesting li.line { + padding-left: 2.0em; +} + +ul.nesting li.lastline { + padding-left: 2.1em; /* for the 0.1em border-left in .lastline > .lineconn */ +} + +li.line { + border-left: 0.1em solid #6185a0; +} + +li.line > span.lineconn, li.lastline > span.lineconn { + position: absolute; + height: 0.65em; + left: 0em; + width: 1.5em; + border-bottom: 0.1em solid #6185a0; +} + +li.lastline > span.lineconn { + border-left: 0.1em solid #6185a0; +} + + +em.storeref { + color: #500000; + position: relative; + width: 100%; +} + +em.storeref:hover { + background-color: #eeeeee; +} + +*.popup { + display: none; +/* background: url('http://losser.st-lab.cs.uu.nl/~mbravenb/menuback.png') repeat; */ + background: #ffffcd; + border: solid #555555 1px; + position: absolute; + top: 0em; + left: 0em; + margin: 0; + padding: 0; + z-index: 100; +} + +em.storeref:hover span.popup { + display: inline; + width: 40em; +} + + +.logTreeToggle { + text-decoration: none; + font-family: monospace; + font-size: larger; +} + +.errorLine { + color: #ff0000; + font-weight: bold; +} + +.warningLine { + color: darkorange; + font-weight: bold; +} + +.prio3 { + font-style: italic; +} + +code { + white-space: pre-wrap; +} + +.serial { + color: #56115c; +} + +.machine { + color: #002399; + font-style: italic; +} + +ul.vmScreenshots { + padding-left: 1em; +} + +ul.vmScreenshots li { + font-family: monospace; + list-style: square; +} diff --git a/nixpkgs/nixos/lib/test-driver/test-driver.pl b/nixpkgs/nixos/lib/test-driver/test-driver.pl new file mode 100644 index 000000000000..a3354fb0e1eb --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/test-driver.pl @@ -0,0 +1,191 @@ +#! /somewhere/perl -w + +use strict; +use Machine; +use Term::ReadLine; +use IO::File; +use IO::Pty; +use Logger; +use Cwd; +use POSIX qw(_exit dup2); +use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); + +$SIG{PIPE} = 'IGNORE'; # because Unix domain sockets may die unexpectedly + +STDERR->autoflush(1); + +my $log = new Logger; + + +# Start vde_switch for each network required by the test. +my %vlans; +foreach my $vlan (split / /, $ENV{VLANS} || "") { + next if defined $vlans{$vlan}; + # Start vde_switch as a child process. We don't run it in daemon + # mode because we want the child process to be cleaned up when we + # die. Since we have to make sure that the control socket is + # ready, we send a dummy command to vde_switch (via stdin) and + # wait for a reply. Note that vde_switch requires stdin to be a + # TTY, so we create one. + $log->log("starting VDE switch for network $vlan"); + my $socket = Cwd::abs_path "./vde$vlan.ctl"; + my $pty = new IO::Pty; + my ($stdoutR, $stdoutW); pipe $stdoutR, $stdoutW; + my $pid = fork(); die "cannot fork" unless defined $pid; + if ($pid == 0) { + dup2(fileno($pty->slave), 0); + dup2(fileno($stdoutW), 1); + exec "vde_switch -s $socket --dirmode 0700" or _exit(1); + } + close $stdoutW; + print $pty "version\n"; + readline $stdoutR or die "cannot start vde_switch"; + $ENV{"QEMU_VDE_SOCKET_$vlan"} = $socket; + $vlans{$vlan} = $pty; + die unless -e "$socket/ctl"; +} + + +my %vms; +my $context = ""; + +sub createMachine { + my ($args) = @_; + my $vm = Machine->new({%{$args}, log => $log, redirectSerial => ($ENV{USE_SERIAL} // "0") ne "1"}); + $vms{$vm->name} = $vm; + $context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; "; + return $vm; +} + +foreach my $vmScript (@ARGV) { + my $vm = createMachine({startCommand => $vmScript}); +} + + +sub startAll { + $log->nest("starting all VMs", sub { + $_->start foreach values %vms; + }); +} + + +# Wait until all VMs have terminated. +sub joinAll { + $log->nest("waiting for all VMs to finish", sub { + $_->waitForShutdown foreach values %vms; + }); +} + + +# In interactive tests, this allows the non-interactive test script to +# be executed conveniently. +sub testScript { + eval "$context $ENV{testScript};\n"; + warn $@ if $@; +} + + +my $nrTests = 0; +my $nrSucceeded = 0; + + +sub subtest { + my ($name, $coderef) = @_; + $log->nest("subtest: $name", sub { + $nrTests++; + eval { &$coderef }; + if ($@) { + $log->log("error: $@", { error => 1 }); + } else { + $nrSucceeded++; + } + }); +} + + +sub runTests { + if (defined $ENV{tests}) { + $log->nest("running the VM test script", sub { + eval "$context $ENV{tests}"; + if ($@) { + $log->log("error: $@", { error => 1 }); + die $@; + } + }, { expanded => 1 }); + } else { + my $term = Term::ReadLine->new('nixos-vm-test'); + $term->ReadHistory; + while (defined ($_ = $term->readline("> "))) { + eval "$context $_\n"; + warn $@ if $@; + } + $term->WriteHistory; + } + + # Copy the kernel coverage data for each machine, if the kernel + # has been compiled with coverage instrumentation. + $log->nest("collecting coverage data", sub { + foreach my $vm (values %vms) { + my $gcovDir = "/sys/kernel/debug/gcov"; + + next unless $vm->isUp(); + + my ($status, $out) = $vm->execute("test -e $gcovDir"); + next if $status != 0; + + # Figure out where to put the *.gcda files so that the + # report generator can find the corresponding kernel + # sources. + my $kernelDir = $vm->mustSucceed("echo \$(dirname \$(readlink -f /run/current-system/kernel))/.build/linux-*"); + chomp $kernelDir; + my $coverageDir = "/tmp/xchg/coverage-data/$kernelDir"; + + # Copy all the *.gcda files. + $vm->execute("for d in $gcovDir/nix/store/*/.build/linux-*; do for i in \$(cd \$d && find -name '*.gcda'); do echo \$i; mkdir -p $coverageDir/\$(dirname \$i); cp -v \$d/\$i $coverageDir/\$i; done; done"); + } + }); + + $log->nest("syncing", sub { + foreach my $vm (values %vms) { + next unless $vm->isUp(); + $vm->execute("sync"); + } + }); + + if ($nrTests != 0) { + $log->log("$nrSucceeded out of $nrTests tests succeeded", + ($nrSucceeded < $nrTests ? { error => 1 } : { })); + } +} + + +# Create an empty raw virtual disk with the given name and size (in +# MiB). +sub createDisk { + my ($name, $size) = @_; + system("qemu-img create -f raw $name ${size}M") == 0 + or die "cannot create image of size $size"; +} + + +END { + $log->nest("cleaning up", sub { + foreach my $vm (values %vms) { + if ($vm->{pid}) { + $log->log("killing " . $vm->{name} . " (pid " . $vm->{pid} . ")"); + kill 9, $vm->{pid}; + } + } + }); + $log->close(); +} + +my $now1 = clock_gettime(CLOCK_MONOTONIC); + +runTests; + +my $now2 = clock_gettime(CLOCK_MONOTONIC); + +printf STDERR "test script finished in %.2fs\n", $now2 - $now1; + +exit ($nrSucceeded < $nrTests ? 1 : 0); diff --git a/nixpkgs/nixos/lib/test-driver/treebits.js b/nixpkgs/nixos/lib/test-driver/treebits.js new file mode 100644 index 000000000000..9754093dfd07 --- /dev/null +++ b/nixpkgs/nixos/lib/test-driver/treebits.js @@ -0,0 +1,30 @@ +$(document).ready(function() { + + /* When a toggle is clicked, show or hide the subtree. */ + $(".logTreeToggle").click(function() { + if ($(this).siblings("ul:hidden").length != 0) { + $(this).siblings("ul").show(); + $(this).text("-"); + } else { + $(this).siblings("ul").hide(); + $(this).text("+"); + } + }); + + /* Implementation of the expand all link. */ + $(".logTreeExpandAll").click(function() { + $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() { + $(this).siblings("ul").show(); + $(this).text("-"); + }); + }); + + /* Implementation of the collapse all link. */ + $(".logTreeCollapseAll").click(function() { + $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() { + $(this).siblings("ul").hide(); + $(this).text("+"); + }); + }); + +}); diff --git a/nixpkgs/nixos/lib/testing.nix b/nixpkgs/nixos/lib/testing.nix new file mode 100644 index 000000000000..767068771036 --- /dev/null +++ b/nixpkgs/nixos/lib/testing.nix @@ -0,0 +1,269 @@ +{ system +, pkgs ? import ../.. { inherit system config; } + # Use a minimal kernel? +, minimal ? false + # Ignored +, config ? {} + # Modules to add to each VM +, extraConfigurations ? [] }: + +with import ./build-vms.nix { inherit system pkgs minimal extraConfigurations; }; +with pkgs; + +let + jquery-ui = callPackage ./testing/jquery-ui.nix { }; + jquery = callPackage ./testing/jquery.nix { }; + +in rec { + + inherit pkgs; + + + testDriver = stdenv.mkDerivation { + name = "nixos-test-driver"; + + buildInputs = [ makeWrapper perl ]; + + dontUnpack = true; + + preferLocalBuild = true; + + installPhase = + '' + mkdir -p $out/bin + cp ${./test-driver/test-driver.pl} $out/bin/nixos-test-driver + chmod u+x $out/bin/nixos-test-driver + + libDir=$out/${perl.libPrefix} + mkdir -p $libDir + cp ${./test-driver/Machine.pm} $libDir/Machine.pm + cp ${./test-driver/Logger.pm} $libDir/Logger.pm + + wrapProgram $out/bin/nixos-test-driver \ + --prefix PATH : "${lib.makeBinPath [ qemu_test vde2 netpbm coreutils ]}" \ + --prefix PERL5LIB : "${with perlPackages; makePerlPath [ TermReadLineGnu XMLWriter IOTty FileSlurp ]}:$out/${perl.libPrefix}" + ''; + }; + + + # Run an automated test suite in the given virtual network. + # `driver' is the script that runs the network. + runTests = driver: + stdenv.mkDerivation { + name = "vm-test-run-${driver.testName}"; + + requiredSystemFeatures = [ "kvm" "nixos-test" ]; + + buildInputs = [ libxslt ]; + + buildCommand = + '' + mkdir -p $out/nix-support + + LOGFILE=$out/log.xml tests='eval $ENV{testScript}; die $@ if $@;' ${driver}/bin/nixos-test-driver + + # Generate a pretty-printed log. + xsltproc --output $out/log.html ${./test-driver/log2html.xsl} $out/log.xml + ln -s ${./test-driver/logfile.css} $out/logfile.css + ln -s ${./test-driver/treebits.js} $out/treebits.js + ln -s ${jquery}/js/jquery.min.js $out/ + ln -s ${jquery-ui}/js/jquery-ui.min.js $out/ + + touch $out/nix-support/hydra-build-products + echo "report testlog $out log.html" >> $out/nix-support/hydra-build-products + + for i in */xchg/coverage-data; do + mkdir -p $out/coverage-data + mv $i $out/coverage-data/$(dirname $(dirname $i)) + done + ''; + }; + + + makeTest = + { testScript + , makeCoverageReport ? false + , enableOCR ? false + , name ? "unnamed" + , ... + } @ t: + + let + # A standard store path to the vm monitor is built like this: + # /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor + # The max filename length of a unix domain socket is 108 bytes. + # This means $name can at most be 50 bytes long. + maxTestNameLen = 50; + testNameLen = builtins.stringLength name; + + testDriverName = with builtins; + if testNameLen > maxTestNameLen then + abort ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " + + "it's currently ${toString testNameLen} characters long.") + else + "nixos-test-driver-${name}"; + + nodes = buildVirtualNetwork ( + t.nodes or (if t ? machine then { machine = t.machine; } else { })); + + testScript' = + # Call the test script with the computed nodes. + if lib.isFunction testScript + then testScript { inherit nodes; } + else testScript; + + vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes); + + vms = map (m: m.config.system.build.vm) (lib.attrValues nodes); + + ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; }; + + imagemagick_tiff = imagemagick_light.override { inherit libtiff; }; + + # Generate onvenience wrappers for running the test driver + # interactively with the specified network, and for starting the + # VMs from the command line. + driver = runCommand testDriverName + { buildInputs = [ makeWrapper]; + testScript = testScript'; + preferLocalBuild = true; + testName = name; + } + '' + mkdir -p $out/bin + echo "$testScript" > $out/test-script + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/ + vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done)) + wrapProgram $out/bin/nixos-test-driver \ + --add-flags "''${vms[*]}" \ + ${lib.optionalString enableOCR + "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \ + --run "export testScript=\"\$(cat $out/test-script)\"" \ + --set VLANS '${toString vlans}' + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms + wrapProgram $out/bin/nixos-run-vms \ + --add-flags "''${vms[*]}" \ + ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \ + --set tests 'startAll; joinAll;' \ + --set VLANS '${toString vlans}' \ + ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"} + ''; # " + + passMeta = drv: drv // lib.optionalAttrs (t ? meta) { + meta = (drv.meta or {}) // t.meta; + }; + + test = passMeta (runTests driver); + report = passMeta (releaseTools.gcovReport { coverageRuns = [ test ]; }); + + nodeNames = builtins.attrNames nodes; + invalidNodeNames = lib.filter + (node: builtins.match "^[A-z_][A-z0-9_]+$" node == null) nodeNames; + + in + if lib.length invalidNodeNames > 0 then + throw '' + Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})! + All machines are referenced as perl variables in the testing framework which will break the + script when special characters are used. + + Please stick to alphanumeric chars and underscores as separation. + '' + else + (if makeCoverageReport then report else test) // { + inherit nodes driver test; + }; + + runInMachine = + { drv + , machine + , preBuild ? "" + , postBuild ? "" + , ... # ??? + }: + let + vm = buildVM { } + [ machine + { key = "run-in-machine"; + networking.hostName = "client"; + nix.readOnlyStore = false; + virtualisation.writableStore = false; + } + ]; + + buildrunner = writeText "vm-build" '' + source $1 + + ${coreutils}/bin/mkdir -p $TMPDIR + cd $TMPDIR + + exec $origBuilder $origArgs + ''; + + testScript = '' + startAll; + $client->waitForUnit("multi-user.target"); + ${preBuild} + $client->succeed("env -i ${bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2"); + ${postBuild} + $client->succeed("sync"); # flush all data before pulling the plug + ''; + + vmRunCommand = writeText "vm-run" '' + xchg=vm-state-client/xchg + ${coreutils}/bin/mkdir $out + ${coreutils}/bin/mkdir -p $xchg + + for i in $passAsFile; do + i2=''${i}Path + _basename=$(${coreutils}/bin/basename ''${!i2}) + ${coreutils}/bin/cp ''${!i2} $xchg/$_basename + eval $i2=/tmp/xchg/$_basename + ${coreutils}/bin/ls -la $xchg + done + + unset i i2 _basename + export | ${gnugrep}/bin/grep -v '^xchg=' > $xchg/saved-env + unset xchg + + export tests='${testScript}' + ${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm + ''; # */ + + in + lib.overrideDerivation drv (attrs: { + requiredSystemFeatures = [ "kvm" ]; + builder = "${bash}/bin/sh"; + args = ["-e" vmRunCommand]; + origArgs = attrs.args; + origBuilder = attrs.builder; + }); + + + runInMachineWithX = { require ? [], ... } @ args: + let + client = + { ... }: + { + inherit require; + virtualisation.memorySize = 1024; + services.xserver.enable = true; + services.xserver.displayManager.slim.enable = false; + services.xserver.displayManager.auto.enable = true; + services.xserver.windowManager.default = "icewm"; + services.xserver.windowManager.icewm.enable = true; + services.xserver.desktopManager.default = "none"; + }; + in + runInMachine ({ + machine = client; + preBuild = + '' + $client->waitForX; + ''; + } // args); + + + simpleTest = as: (makeTest as).test; + +} diff --git a/nixpkgs/nixos/lib/testing/jquery-ui.nix b/nixpkgs/nixos/lib/testing/jquery-ui.nix new file mode 100644 index 000000000000..e65107a3c2fb --- /dev/null +++ b/nixpkgs/nixos/lib/testing/jquery-ui.nix @@ -0,0 +1,24 @@ +{ stdenv, fetchurl, unzip }: + +stdenv.mkDerivation rec { + name = "jquery-ui-1.11.4"; + + src = fetchurl { + url = "http://jqueryui.com/resources/download/${name}.zip"; + sha256 = "0ciyaj1acg08g8hpzqx6whayq206fvf4whksz2pjgxlv207lqgjh"; + }; + + buildInputs = [ unzip ]; + + installPhase = + '' + mkdir -p "$out/js" + cp -rv . "$out/js" + ''; + + meta = { + homepage = http://jqueryui.com/; + description = "A library of JavaScript widgets and effects"; + platforms = stdenv.lib.platforms.all; + }; +} diff --git a/nixpkgs/nixos/lib/testing/jquery.nix b/nixpkgs/nixos/lib/testing/jquery.nix new file mode 100644 index 000000000000..e272f66a5765 --- /dev/null +++ b/nixpkgs/nixos/lib/testing/jquery.nix @@ -0,0 +1,36 @@ +{ stdenv, fetchurl, compressed ? true }: + +with stdenv.lib; + +stdenv.mkDerivation rec { + name = "jquery-1.11.3"; + + src = if compressed then + fetchurl { + url = "http://code.jquery.com/${name}.min.js"; + sha256 = "1f4glgxxn3jnvry3dpzmazj3207baacnap5w20gr2xlk789idfgc"; + } + else + fetchurl { + url = "http://code.jquery.com/${name}.js"; + sha256 = "1v956yf5spw0156rni5z77hzqwmby7ajwdcd6mkhb6zvl36awr90"; + }; + + dontUnpack = true; + + installPhase = + '' + mkdir -p "$out/js" + cp -v "$src" "$out/js/jquery.js" + ${optionalString compressed '' + (cd "$out/js" && ln -s jquery.js jquery.min.js) + ''} + ''; + + meta = with stdenv.lib; { + description = "JavaScript library designed to simplify the client-side scripting of HTML"; + homepage = http://jquery.com/; + license = licenses.mit; + platforms = platforms.all; + }; +} diff --git a/nixpkgs/nixos/lib/utils.nix b/nixpkgs/nixos/lib/utils.nix new file mode 100644 index 000000000000..b68e55a40b90 --- /dev/null +++ b/nixpkgs/nixos/lib/utils.nix @@ -0,0 +1,27 @@ +pkgs: with pkgs.lib; + +rec { + + # Check whenever fileSystem is needed for boot + fsNeededForBoot = fs: fs.neededForBoot + || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]; + + # Check whenever `b` depends on `a` as a fileSystem + fsBefore = a: b: a.mountPoint == b.device + || hasPrefix "${a.mountPoint}${optionalString (!(hasSuffix "/" a.mountPoint)) "/"}" b.mountPoint; + + # Escape a path according to the systemd rules, e.g. /dev/xyzzy + # becomes dev-xyzzy. FIXME: slow. + escapeSystemdPath = s: + replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"] + (if hasPrefix "/" s then substring 1 (stringLength s) s else s); + + # Returns a system path for a given shell package + toShellPath = shell: + if types.shellPackage.check shell then + "/run/current-system/sw${shell.shellPath}" + else if types.package.check shell then + throw "${shell} is not a shell package" + else + shell; +} |