diff --git a/pkgs/build-support/build-fhsenv-bubblewrap/buildFHSEnv.nix b/pkgs/build-support/build-fhsenv-bubblewrap/buildFHSEnv.nix index 7dcb9f0baf1b..291af512a411 100644 --- a/pkgs/build-support/build-fhsenv-bubblewrap/buildFHSEnv.nix +++ b/pkgs/build-support/build-fhsenv-bubblewrap/buildFHSEnv.nix @@ -33,18 +33,15 @@ # follows: # /lib32 will include 32bit libraries from multiPkgs # /lib64 will include 64bit libraries from multiPkgs and targetPkgs -# /lib will link to /lib32 +# /lib will link to /lib64 let - inherit (stdenv.hostPlatform) is64bit; - name = if (args ? pname && args ? version) then "${args.pname}-${args.version}" else args.name; # "use of glibc_multi is only supported on x86_64-linux" isMultiBuild = multiArch && stdenv.system == "x86_64-linux"; - isTargetBuild = !isMultiBuild; # list of packages (usually programs) which match the host's architecture # (which includes stuff from multiPkgs) @@ -60,7 +57,7 @@ let baseTargetPaths = with pkgs; [ glibcLocales (if isMultiBuild then glibc_multi else glibc) - (toString gcc.cc.lib) + gcc.cc.lib bashInteractiveFHS coreutils less @@ -77,7 +74,7 @@ let xz ]; baseMultiPaths = with pkgsi686Linux; [ - (toString gcc.cc.lib) + gcc.cc.lib ]; ldconfig = writeShellScriptBin "ldconfig" '' @@ -85,190 +82,139 @@ let exec ${if isMultiBuild then pkgsi686Linux.glibc.bin else pkgs.glibc.bin}/bin/ldconfig -f /etc/ld.so.conf -C /etc/ld.so.cache "$@" ''; - etcProfile = writeText "profile" '' - export PS1='${name}-fhsenv:\u@\h:\w\$ ' - export LOCALE_ARCHIVE='/usr/lib/locale/locale-archive' - export PATH="/run/wrappers/bin:/usr/bin:/usr/sbin:$PATH" - export TZDIR='/etc/zoneinfo' + etcProfile = pkgs.writeTextFile { + name = "${name}-fhsenv-profile"; + destination = "/etc/profile"; + text = '' + export PS1='${name}-fhsenv:\u@\h:\w\$ ' + export LOCALE_ARCHIVE='/usr/lib/locale/locale-archive' + export PATH="/run/wrappers/bin:/usr/bin:/usr/sbin:$PATH" + export TZDIR='/etc/zoneinfo' - # XDG_DATA_DIRS is used by pressure-vessel (steam proton) and vulkan loaders to find the corresponding icd - export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/run/opengl-driver/share:/run/opengl-driver-32/share + # XDG_DATA_DIRS is used by pressure-vessel (steam proton) and vulkan loaders to find the corresponding icd + export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/run/opengl-driver/share:/run/opengl-driver-32/share - # Following XDG spec [1], XDG_DATA_DIRS should default to "/usr/local/share:/usr/share". - # In nix, it is commonly set without containing these values, so we add them as fallback. - # - # [1] - case ":$XDG_DATA_DIRS:" in - *:/usr/local/share:*) ;; - *) export XDG_DATA_DIRS="$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/usr/local/share" ;; - esac - case ":$XDG_DATA_DIRS:" in - *:/usr/share:*) ;; - *) export XDG_DATA_DIRS="$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/usr/share" ;; - esac + # Following XDG spec [1], XDG_DATA_DIRS should default to "/usr/local/share:/usr/share". + # In nix, it is commonly set without containing these values, so we add them as fallback. + # + # [1] + case ":$XDG_DATA_DIRS:" in + *:/usr/local/share:*) ;; + *) export XDG_DATA_DIRS="$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/usr/local/share" ;; + esac + case ":$XDG_DATA_DIRS:" in + *:/usr/share:*) ;; + *) export XDG_DATA_DIRS="$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}/usr/share" ;; + esac - # Force compilers and other tools to look in default search paths - unset NIX_ENFORCE_PURITY - export NIX_BINTOOLS_WRAPPER_TARGET_HOST_${stdenv.cc.suffixSalt}=1 - export NIX_CC_WRAPPER_TARGET_HOST_${stdenv.cc.suffixSalt}=1 - export NIX_CFLAGS_COMPILE='-idirafter /usr/include' - export NIX_CFLAGS_LINK='-L/usr/lib -L/usr/lib32' - export NIX_LDFLAGS='-L/usr/lib -L/usr/lib32' - export PKG_CONFIG_PATH=/usr/lib/pkgconfig - export ACLOCAL_PATH=/usr/share/aclocal + # Force compilers and other tools to look in default search paths + unset NIX_ENFORCE_PURITY + export NIX_BINTOOLS_WRAPPER_TARGET_HOST_${stdenv.cc.suffixSalt}=1 + export NIX_CC_WRAPPER_TARGET_HOST_${stdenv.cc.suffixSalt}=1 + export NIX_CFLAGS_COMPILE='-idirafter /usr/include' + export NIX_CFLAGS_LINK='-L/usr/lib -L/usr/lib32' + export NIX_LDFLAGS='-L/usr/lib -L/usr/lib32' + export PKG_CONFIG_PATH=/usr/lib/pkgconfig + export ACLOCAL_PATH=/usr/share/aclocal - # GStreamer searches for plugins relative to its real binary's location - # https://gitlab.freedesktop.org/gstreamer/gstreamer/-/commit/bd97973ce0f2c5495bcda5cccd4f7ef7dcb7febc - export GST_PLUGIN_SYSTEM_PATH_1_0=/usr/lib/gstreamer-1.0:/usr/lib32/gstreamer-1.0 + # GStreamer searches for plugins relative to its real binary's location + # https://gitlab.freedesktop.org/gstreamer/gstreamer/-/commit/bd97973ce0f2c5495bcda5cccd4f7ef7dcb7febc + export GST_PLUGIN_SYSTEM_PATH_1_0=/usr/lib/gstreamer-1.0:/usr/lib32/gstreamer-1.0 - ${profile} + ${profile} + ''; + }; + + ensureGsettingsSchemasIsDirectory = runCommandLocal "fhsenv-ensure-gsettings-schemas-directory" {} '' + mkdir -p $out/share/glib-2.0/schemas + touch $out/share/glib-2.0/schemas/.keep ''; - # Compose /etc for the fhs environment - etcPkg = runCommandLocal "${name}-fhs-etc" { } '' - mkdir -p $out/etc - pushd $out/etc + # Shamelessly stolen (and cleaned up) from original buildEnv. + # Should be semantically equivalent, except we also take + # a list of default extra outputs that will be installed + # for derivations that don't explicitly specify one. + # Note that this is not the same as `extraOutputsToInstall`, + # as that will apply even to derivations with an output + # explicitly specified, so this does change the behavior + # very slightly for that particular edge case. + pickOutputs = let + pickOutputsOne = outputs: drv: + let + isSpecifiedOutput = drv.outputSpecified or false; + outputsToInstall = drv.meta.outputsToInstall or null; + pickedOutputs = if isSpecifiedOutput || outputsToInstall == null + then [ drv ] + else map (out: drv.${out} or null) (outputsToInstall ++ outputs); + extraOutputs = map (out: drv.${out} or null) extraOutputsToInstall; + cleanOutputs = lib.filter (o: o != null) (pickedOutputs ++ extraOutputs); + in { + paths = cleanOutputs; + priority = drv.meta.priority or lib.meta.defaultPriority; + }; + in paths: outputs: map (pickOutputsOne outputs) paths; - # environment variables - ln -s ${etcProfile} profile + paths = let + basePaths = [ + etcProfile + # ldconfig wrapper must come first so it overrides the original ldconfig + ldconfig + # magic package that just creates a directory, to ensure that + # the entire directory can't be a symlink, as we will write + # compiled schemas to it + ensureGsettingsSchemasIsDirectory + ] ++ baseTargetPaths ++ targetPaths; + in pickOutputs basePaths ["out" "lib" "bin"]; + + paths32 = lib.optionals isMultiBuild ( + let + basePaths = baseMultiPaths ++ multiPaths; + in pickOutputs basePaths ["out" "lib"] + ); + + allPaths = paths ++ paths32; + + rootfs-builder = pkgs.rustPlatform.buildRustPackage { + name = "fhs-rootfs-bulder"; + src = ./rootfs-builder; + cargoLock.lockFile = ./rootfs-builder/Cargo.lock; + doCheck = false; + }; + + rootfs = pkgs.runCommand "${name}-fhsenv-rootfs" { + __structuredAttrs = true; + exportReferencesGraph.graph = lib.concatMap (p: p.paths) allPaths; + inherit paths paths32 isMultiBuild includeClosures; + nativeBuildInputs = [ pkgs.jq ]; + } '' + ${rootfs-builder}/bin/rootfs-builder + + # create a bunch of symlinks for usrmerge + ln -s /usr/bin $out/bin + ln -s /usr/sbin $out/sbin + ln -s /usr/lib $out/lib + ln -s /usr/lib32 $out/lib32 + ln -s /usr/lib64 $out/lib64 + ln -s /usr/lib64 $out/usr/lib + + # symlink 32-bit ld-linux so it's visible in /lib + if [ -e $out/usr/lib32/ld-linux.so.2 ]; then + ln -s /usr/lib32/ld-linux.so.2 $out/usr/lib64/ld-linux.so.2 + fi # symlink /etc/mtab -> /proc/mounts (compat for old userspace progs) - ln -s /proc/mounts mtab - ''; + ln -s /proc/mounts $out/etc/mtab - # Composes a /usr-like directory structure - staticUsrProfileTarget = buildEnv { - name = "${name}-usr-target"; - # ldconfig wrapper must come first so it overrides the original ldconfig - paths = [ etcPkg ldconfig ] ++ baseTargetPaths ++ targetPaths; - extraOutputsToInstall = [ "out" "lib" "bin" ] ++ extraOutputsToInstall; - ignoreCollisions = true; - postBuild = '' - if [[ -d $out/share/gsettings-schemas/ ]]; then - # Recreate the standard schemas directory if its a symlink to make it writable - if [[ -L $out/share/glib-2.0 ]]; then - target=$(readlink $out/share/glib-2.0) - rm $out/share/glib-2.0 - mkdir $out/share/glib-2.0 - ln -fsr $target/* $out/share/glib-2.0 - fi - - if [[ -L $out/share/glib-2.0/schemas ]]; then - target=$(readlink $out/share/glib-2.0/schemas) - rm $out/share/glib-2.0/schemas - mkdir $out/share/glib-2.0/schemas - ln -fsr $target/* $out/share/glib-2.0/schemas - fi - - mkdir -p $out/share/glib-2.0/schemas - - for d in $out/share/gsettings-schemas/*; do - # Force symlink, in case there are duplicates - ln -fsr $d/glib-2.0/schemas/*.xml $out/share/glib-2.0/schemas - ln -fsr $d/glib-2.0/schemas/*.gschema.override $out/share/glib-2.0/schemas - done - - # and compile them - ${pkgs.glib.dev}/bin/glib-compile-schemas $out/share/glib-2.0/schemas - fi - ''; - inherit includeClosures; - }; - - staticUsrProfileMulti = buildEnv { - name = "${name}-usr-multi"; - paths = baseMultiPaths ++ multiPaths; - extraOutputsToInstall = [ "out" "lib" ] ++ extraOutputsToInstall; - ignoreCollisions = true; - inherit includeClosures; - }; - - # setup library paths only for the targeted architecture - setupLibDirsTarget = '' - # link content of targetPaths - cp -rsHf ${staticUsrProfileTarget}/lib lib - ln -s lib lib${if is64bit then "64" else "32"} - ''; - - # setup /lib, /lib32 and /lib64 - setupLibDirsMulti = '' - mkdir -m0755 lib32 - mkdir -m0755 lib64 - ln -s lib64 lib - - # copy glibc stuff - cp -rsHf ${staticUsrProfileTarget}/lib/32/* lib32/ - chmod u+w -R lib32/ - - # copy content of multiPaths (32bit libs) - if [ -d ${staticUsrProfileMulti}/lib ]; then - cp -rsHf ${staticUsrProfileMulti}/lib/* lib32/ - chmod u+w -R lib32/ + if [[ -d $out/usr/share/gsettings-schemas/ ]]; then + for d in $out/usr/share/gsettings-schemas/*; do + # Force symlink, in case there are duplicates + ln -fsr $d/glib-2.0/schemas/*.xml $out/usr/share/glib-2.0/schemas + ln -fsr $d/glib-2.0/schemas/*.gschema.override $out/usr/share/glib-2.0/schemas + done + ${pkgs.glib.dev}/bin/glib-compile-schemas $out/usr/share/glib-2.0/schemas fi - # copy content of targetPaths (64bit libs) - cp -rsHf ${staticUsrProfileTarget}/lib/* lib64/ - chmod u+w -R lib64/ - - # symlink 32-bit ld-linux.so - ln -Lsf ${staticUsrProfileTarget}/lib/32/ld-linux.so.2 lib/ + ${extraBuildCommands} + ${lib.optionalString isMultiBuild extraBuildCommandsMulti} ''; - - setupLibDirs = if isTargetBuild - then setupLibDirsTarget - else setupLibDirsMulti; - - # the target profile is the actual profile that will be used for the fhs - setupTargetProfile = '' - mkdir -m0755 usr - pushd usr - - ${setupLibDirs} - - '' + lib.optionalString isMultiBuild '' - if [ -d "${staticUsrProfileMulti}/share" ]; then - cp -rLf ${staticUsrProfileMulti}/share share - fi - '' + '' - if [ -d "${staticUsrProfileTarget}/share" ]; then - if [ -d share ]; then - chmod -R 755 share - cp -rLTf ${staticUsrProfileTarget}/share share - else - cp -rsHf ${staticUsrProfileTarget}/share share - fi - fi - for i in bin sbin include; do - if [ -d "${staticUsrProfileTarget}/$i" ]; then - cp -rsHf "${staticUsrProfileTarget}/$i" "$i" - fi - done - cd .. - - for i in etc opt; do - if [ -d "${staticUsrProfileTarget}/$i" ]; then - cp -rsHf "${staticUsrProfileTarget}/$i" "$i" - fi - done - for i in usr/{bin,sbin,lib,lib32,lib64}; do - if [ -d "$i" ]; then - ln -s "$i" - fi - done - - popd - ''; - -in runCommandLocal "${name}-fhs" { - inherit nativeBuildInputs; - passthru = { - inherit args baseTargetPaths targetPaths baseMultiPaths ldconfig isMultiBuild; - }; -} '' - mkdir -p $out - pushd $out - - ${setupTargetProfile} - ${extraBuildCommands} - ${lib.optionalString isMultiBuild extraBuildCommandsMulti} -'' +in rootfs diff --git a/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.lock b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.lock new file mode 100644 index 000000000000..7f9606804dc9 --- /dev/null +++ b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.lock @@ -0,0 +1,249 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "goblin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ab3f32d1d77146981dea5d6b1e8fe31eedcb7013e5e00d6ccd1259a4b4d923" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rootfs-builder" +version = "0.1.0" +dependencies = [ + "anyhow", + "goblin", + "serde", + "serde_json", + "walkdir", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.toml b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.toml new file mode 100644 index 000000000000..a6bfc71603fd --- /dev/null +++ b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rootfs-builder" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "*" +serde = { version = "*", features = ["derive"] } +serde_json = "*" +walkdir = "*" +goblin = "*" diff --git a/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/src/main.rs b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/src/main.rs new file mode 100644 index 000000000000..f765a96d3e08 --- /dev/null +++ b/pkgs/build-support/build-fhsenv-bubblewrap/rootfs-builder/src/main.rs @@ -0,0 +1,293 @@ +#![deny(clippy::pedantic)] + +use std::{ + collections::{hash_map::Entry, HashMap}, + env, + fs::{self, File}, + io::{BufReader, Read}, + os::unix::fs as ufs, + path::{Path, PathBuf}, +}; + +use anyhow::{anyhow, Context}; +use goblin::{Hint, Object}; +use serde::Deserialize; +use walkdir::WalkDir; + +#[derive(Debug, Deserialize)] +struct RefGraphNode { + path: PathBuf, + references: Vec, +} + +#[derive(Debug, Deserialize)] +struct InputDrv { + paths: Vec, + priority: i64, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct StructuredAttrsRoot { + graph: Vec, + paths: Vec, + paths32: Vec, + include_closures: bool, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct PriorityKey { + implicit: bool, + group_priority: i64, + priority: i64, + root_index: usize, +} + +fn build_reference_map(refs: Vec) -> HashMap> { + refs.into_iter() + .map(|mut gn| { + gn.references.retain_mut(|x| x != &gn.path); + (gn.path, gn.references) + }) + .collect() +} + +fn build_priority_keys(roots: &[InputDrv], group_priority: i64) -> HashMap { + let mut roots_map = HashMap::new(); + + for (idx, path) in roots.iter().enumerate() { + for subpath in &path.paths { + let priority = PriorityKey { + group_priority, + priority: path.priority, + root_index: idx, + implicit: false, + }; + + roots_map.entry(subpath.clone()).or_insert(priority); + } + } + + roots_map +} + +fn extend_to_closure( + roots: HashMap, + refs: &HashMap>, +) -> anyhow::Result> { + let mut path_map = HashMap::new(); + + for (root, priority) in roots { + path_map.insert(root.clone(), priority); + + let mut priority = priority; + priority.implicit = true; + + let mut stack = vec![root.clone()]; + + while let Some(next) = stack.pop() { + match path_map.entry(next.clone()) { + Entry::Occupied(mut occupied_entry) => { + let old_priority: &PriorityKey = occupied_entry.get(); + if old_priority > &priority { + occupied_entry.insert(priority); + } + } + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(priority); + } + } + + stack.extend_from_slice(refs.get(&next).ok_or(anyhow!("encountered unknown path"))?); + } + } + + Ok(path_map) +} + +#[derive(Clone, Debug)] +struct CandidatePath { + root: PathBuf, + relative: PathBuf, + priority: PriorityKey, +} + +fn collect_candidate_paths( + paths: HashMap, + mapper: impl Fn(&Path, &Path) -> Option, +) -> anyhow::Result>> { + let mut candidates: HashMap<_, Vec<_>> = HashMap::new(); + + for (path, priority) in paths { + for entry in WalkDir::new(&path).follow_links(true) { + let entry: PathBuf = match entry { + Ok(ent) => { + // we don't care about directory structure + if ent.file_type().is_dir() { + continue; + } + + ent.path().into() + } + Err(e) => { + match e.io_error() { + // could be a broken symlink, that's fine, we still want to handle those + Some(_) => e + .path() + .ok_or_else(|| anyhow!("I/O error when walking {path:?}"))? + .into(), + None => { + // symlink loop + continue; + } + } + } + }; + + let relative = entry.strip_prefix(&path)?.to_owned(); + if let Some(mapped) = mapper(&path, &relative) { + candidates.entry(mapped).or_default().push(CandidatePath { + root: path.clone(), + relative, + priority, + }); + } + } + } + + Ok(candidates) +} + +fn remap_native_path(root: &Path, p: &Path) -> Option { + if p.starts_with("bin/") || p.starts_with("sbin/") { + return Some(PathBuf::from("usr/").join(p)); + } + + // glibc-multilib special case + if let Ok(no_lib32) = p.strip_prefix("lib/32/") { + return Some(PathBuf::from("usr/lib32/").join(no_lib32)); + } + + remap_multilib_path(root, p) +} + +fn is_32_bit(path: &Path) -> bool { + // Be as pessimistic as possible, at least for now. + let Ok(mut f) = File::open(path) else { + return false; + }; + + let Ok(Hint::Elf(hint)) = goblin::peek(&mut f) else { + return false; + }; + + if let Some(is64) = hint.is_64 { + return !is64; + } + + let mut buf = vec![]; + let Ok(_) = f.read_to_end(&mut buf) else { + return false; + }; + + let Ok(Object::Elf(e)) = goblin::Object::parse(&buf) else { + return false; + }; + + !e.is_64 +} + +fn remap_multilib_path(root: &Path, p: &Path) -> Option { + if let Ok(no_lib) = p.strip_prefix("lib/") { + let full = root.join(p); + + let libdir = if is_32_bit(&full) { "lib32" } else { "lib64" }; + + return Some(PathBuf::from("usr/").join(libdir).join(no_lib)); + } + + if p.starts_with("etc/") || p.starts_with("opt/") { + return Some(p.into()); + } + + if p.starts_with("share/") || p.starts_with("include/") { + return Some(PathBuf::from("usr/").join(p)); + } + + None +} + +fn build_plan( + paths: HashMap, + paths32: HashMap, +) -> anyhow::Result> { + let candidates_native = collect_candidate_paths(paths, remap_native_path)?; + let candidates_32 = collect_candidate_paths(paths32, remap_multilib_path)?; + + let mut all_candidates: HashMap<_, Vec<_>> = HashMap::new(); + + for map in [candidates_native, candidates_32] { + for (path, candidates) in map { + all_candidates + .entry(path) + .or_default() + .extend_from_slice(&candidates); + } + } + + let mut final_plan: HashMap = HashMap::new(); + for (path, candidates) in all_candidates { + let best = candidates + .into_iter() + .min_by_key(|&CandidatePath { priority, .. }| priority) + .ok_or(anyhow!("candidate list empty"))?; + final_plan.insert(path, best.root.join(best.relative)); + } + + Ok(final_plan) +} + +fn build_env(out: &Path, plan: HashMap) -> anyhow::Result<()> { + fs::create_dir_all(out)?; + + for (dest, src) in plan { + let full_dest = out.join(&dest); + let dest_dir = full_dest + .parent() + .ok_or(anyhow!("destination directory is root")) + .with_context(|| { + format!("When trying to determine destination directory for {full_dest:?}") + })?; + fs::create_dir_all(dest_dir) + .with_context(|| format!("When trying to create directory {dest_dir:?}"))?; + ufs::symlink(&src, &full_dest) + .with_context(|| format!("When symlinking {src:?} to {full_dest:?}"))?; + } + + Ok(()) +} + +fn main() -> anyhow::Result<()> { + let filename = env::var("NIX_ATTRS_JSON_FILE")?; + + let reader = File::open(filename)?; + let buf_reader = BufReader::new(reader); + + let attrs: StructuredAttrsRoot = serde_json::from_reader(buf_reader)?; + + let mut paths = build_priority_keys(&attrs.paths, 1); + let mut paths32 = build_priority_keys(&attrs.paths32, 2); + + if attrs.include_closures { + let refs = build_reference_map(attrs.graph); + + paths = extend_to_closure(paths, &refs)?; + paths32 = extend_to_closure(paths32, &refs)?; + }; + + let plan = build_plan(paths, paths32)?; + + let out_dir = env::var("out")?; + + build_env(&PathBuf::from(out_dir), plan) +}