f61e189ad3
This is effectively a rewrite of `overrideSDK`. It was required because `wrapGAppsHook` propagates `depsTargetTarget` with the expectation that it will effectively be `buildInputs` when the hook is itself used as a `nativeBuildInput`. This propagates Gtk, which itself propagates the default Dariwn SDK, making it effectively impossible to override the SDK when a package depends on Gtk and uses `wrapGAppsHook`. This rewrite implements the following improvements: * Cross-compilation should be supported correctly (untested); * Supports public and private frameworks; * Supports SDK `libs`; * Remaps instead of replacing extra (native) build inputs in the stdenv; * Updates any Darwin framework references in `nix-support`; and * It updates `xcodebuild` regardless of which input its in. The implementation avoids recursion for performance reasons. Instead, it enumerates transitive dependencies and walks the list from the leaf packages backwards to the parent packages.
437 lines
16 KiB
Nix
437 lines
16 KiB
Nix
# The basic algorithm is to rewrite the propagated inputs of a package and any of its
|
||
# own propagated inputs recursively to replace references from the default SDK with
|
||
# those from the requested SDK version. This is done across all propagated inputs to
|
||
# avoid making assumptions about how those inputs are being used.
|
||
#
|
||
# For example, packages may propagate target-target dependencies with the expectation that
|
||
# they will be just build inputs when the package itself is used as a native build input.
|
||
#
|
||
# To support this use case and operate without regard to the original package set,
|
||
# `overrideSDK` creates a mapping from the default SDK in all package categories to the
|
||
# requested SDK. If the lookup fails, it is assumed the package is not part of the SDK.
|
||
# Non-SDK packages are processed per the algorithm described above.
|
||
{
|
||
lib,
|
||
stdenvNoCC,
|
||
extendMkDerivationArgs,
|
||
pkgsBuildBuild,
|
||
pkgsBuildHost,
|
||
pkgsBuildTarget,
|
||
pkgsHostHost,
|
||
pkgsHostTarget,
|
||
pkgsTargetTarget,
|
||
}@args:
|
||
|
||
let
|
||
# Takes a mapping from a package to its replacement and transforms it into a list of
|
||
# mappings by output (e.g., a package with three outputs produces a list of size 3).
|
||
expandOutputs =
|
||
mapping:
|
||
map (output: {
|
||
name = builtins.unsafeDiscardStringContext (lib.getOutput output mapping.original);
|
||
value = lib.getOutput output mapping.replacement;
|
||
}) mapping.original.outputs;
|
||
|
||
# Produces a list of mappings from the default SDK to the new SDK (`sdk`).
|
||
# `attr` indicates which SDK path to remap (e.g., `libs` remaps `apple_sdk.libs`).
|
||
#
|
||
# TODO: Update once the SDKs have been refactored to a common pattern to better handle
|
||
# frameworks that are not present in both SDKs. Currently, they’re dropped.
|
||
mkMapping =
|
||
attr: pkgs: sdk:
|
||
lib.foldlAttrs (
|
||
mappings: name: pkg:
|
||
let
|
||
# Avoid evaluation failures due to missing or throwing
|
||
# frameworks (such as QuickTime in the 11.0 SDK).
|
||
maybeReplacement = builtins.tryEval sdk.${attr}.${name} or { success = false; };
|
||
in
|
||
if maybeReplacement.success then
|
||
mappings
|
||
++ expandOutputs {
|
||
original = pkg;
|
||
replacement = maybeReplacement.value;
|
||
}
|
||
else
|
||
mappings
|
||
) [ ] pkgs.darwin.apple_sdk.${attr};
|
||
|
||
# Produces a list of overrides for the given package set, SDK, and version.
|
||
# If you want to manually specify a mapping, this is where you should do it.
|
||
mkOverrides =
|
||
pkgs: sdk: version:
|
||
lib.concatMap expandOutputs [
|
||
# Libsystem needs to match the one used by the SDK or weird errors happen.
|
||
{
|
||
original = pkgs.darwin.apple_sdk.Libsystem;
|
||
replacement = sdk.Libsystem;
|
||
}
|
||
# Make sure darwin.CF is mapped to the correct version for the SDK.
|
||
{
|
||
original = pkgs.darwin.CF;
|
||
replacement = sdk.frameworks.CoreFoundation;
|
||
}
|
||
# libobjc needs to be handled specially because it’s not actually in the SDK.
|
||
{
|
||
original = pkgs.darwin.libobjc;
|
||
replacement = sdk.objc4;
|
||
}
|
||
# Unfortunately, this is not consistent between Darwin SDKs in nixpkgs, so
|
||
# try both versions to map between them.
|
||
{
|
||
original = pkgs.darwin.apple_sdk.sdk or pkgs.darwin.apple_sdk.MacOSX-SDK;
|
||
replacement = sdk.sdk or sdk.MacOSX-SDK;
|
||
}
|
||
# Remap the SDK root. This is used by clang to set the SDK version when
|
||
# linking. This behavior is automatic by clang and can’t be overriden.
|
||
# Otherwise, without the SDK root set, the SDK version will be inferred to
|
||
# be the same as the deployment target, which is not usually what you want.
|
||
{
|
||
original = pkgs.darwin.apple_sdk.sdkRoot;
|
||
replacement = sdk.sdkRoot;
|
||
}
|
||
# Override xcodebuild because it hardcodes the SDK version.
|
||
# TODO: Make xcodebuild defer to the SDK root set in the stdenv.
|
||
{
|
||
original = pkgs.xcodebuild;
|
||
replacement = pkgs.xcodebuild.override {
|
||
# Do the override manually to avoid an infinite recursion.
|
||
stdenv = pkgs.stdenv.override (old: {
|
||
buildPlatform = mkPlatform version old.buildPlatform;
|
||
hostPlatform = mkPlatform version old.hostPlatform;
|
||
targetPlatform = mkPlatform version old.targetPlatform;
|
||
|
||
allowedRequisites = null;
|
||
cc = mkCC sdk.Libsystem old.cc;
|
||
});
|
||
};
|
||
}
|
||
];
|
||
|
||
mkBintools =
|
||
Libsystem: bintools:
|
||
if bintools ? override then
|
||
bintools.override { libc = Libsystem; }
|
||
else
|
||
let
|
||
# `override` isn’t available, so bintools has to be rewrapped with the new libc.
|
||
# Most of the required arguments can be recovered except for `postLinkSignHook`
|
||
# and `signingUtils`, which have to be scrapped from the original’s `postFixup`.
|
||
# This isn’t ideal, but it works.
|
||
postFixup = lib.splitString "\n" bintools.postFixup;
|
||
|
||
postLinkSignHook = lib.pipe postFixup [
|
||
(lib.findFirst (lib.hasPrefix "echo 'source") null)
|
||
(builtins.match "^echo 'source (.*-post-link-sign-hook)' >> \\$out/nix-support/post-link-hook$")
|
||
lib.head
|
||
];
|
||
|
||
signingUtils = lib.pipe postFixup [
|
||
(lib.findFirst (lib.hasPrefix "export signingUtils") null)
|
||
(builtins.match "^export signingUtils=(.*)$")
|
||
lib.head
|
||
];
|
||
|
||
newBintools = pkgsBuildTarget.wrapBintoolsWith {
|
||
inherit (bintools) name;
|
||
|
||
buildPackages = { };
|
||
libc = Libsystem;
|
||
|
||
inherit lib;
|
||
|
||
coreutils = bintools.coreutils_bin;
|
||
gnugrep = bintools.gnugrep_bin;
|
||
|
||
inherit (bintools) bintools;
|
||
|
||
inherit postLinkSignHook signingUtils;
|
||
};
|
||
in
|
||
lib.getOutput bintools.outputName newBintools;
|
||
|
||
mkCC =
|
||
Libsystem: cc:
|
||
if cc ? override then
|
||
cc.override {
|
||
bintools = mkBintools Libsystem cc.bintools;
|
||
libc = Libsystem;
|
||
}
|
||
else
|
||
builtins.throw "CC has no override: ${cc}";
|
||
|
||
mkPlatform =
|
||
version: platform:
|
||
platform
|
||
// lib.optionalAttrs platform.isDarwin { inherit (version) darwinMinVersion darwinSdkVersion; };
|
||
|
||
# Creates a stub package. Unchanged files from the original package are symlinked
|
||
# into the package. The contents of `nix-support` are updated to reference any
|
||
# replaced packages.
|
||
#
|
||
# Note: `env` is an attrset containing `outputs` and `dependencies`.
|
||
# `dependencies` is a regex passed to sed and must be `passAsFile`.
|
||
mkProxyPackage =
|
||
name: env:
|
||
stdenvNoCC.mkDerivation {
|
||
inherit name;
|
||
|
||
inherit (env) outputs replacements sourceOutputs;
|
||
|
||
# Take advantage of the fact that replacements and sourceOutputs will be passed
|
||
# via JSON and parsed into environment variables.
|
||
__structuredAttrs = true;
|
||
|
||
buildCommand = ''
|
||
# Map over the outputs in the package being replaced to make sure the proxy is
|
||
# a fully functional replacement. This is like `symlinkJoin` except for
|
||
# outputs and the contents of `nix-support`, which will be customized.
|
||
function replacePropagatedInputs() {
|
||
local sourcePath=$1
|
||
local targetPath=$2
|
||
|
||
mkdir -p "$targetPath"
|
||
|
||
local sourceFile
|
||
for sourceFile in "$sourcePath"/*; do
|
||
local fileName=$(basename "$sourceFile")
|
||
local targetFile="$targetPath/$fileName"
|
||
|
||
if [ -d "$sourceFile" ]; then
|
||
replacePropagatedInputs "$sourceFile" "$targetPath/$fileName"
|
||
# Check to see if any of the files in the folder were replaced.
|
||
# Otherwise, replace the folder with a symlink if none were changed.
|
||
if [ "$(find -maxdepth 1 "$targetPath/$fileName" -not -type l)" = "" ]; then
|
||
rm "$targetPath/$fileName"/*
|
||
ln -s "$sourceFile" "$targetPath/$fileName"
|
||
fi
|
||
else
|
||
cp "$sourceFile" "$targetFile"
|
||
local original
|
||
for original in "''${!replacements[@]}"; do
|
||
substituteInPlace "$targetFile" \
|
||
--replace-quiet "$original" "''${replacements[$original]}"
|
||
done
|
||
if cmp -s "$sourceFile" "$targetFile"; then
|
||
rm "$targetFile"
|
||
ln -s "$sourceFile" "$targetFile"
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
local outputName
|
||
for outputName in "''${!outputs[@]}"; do
|
||
local outPath=''${outputs[$outputName]}
|
||
mkdir -p "$outPath"
|
||
|
||
local sourcePath
|
||
for sourcePath in "''${sourceOutputs[$outputName]}"/*; do
|
||
sourceName=$(basename "$sourcePath")
|
||
# `nix-support` is special-cased because any propagated inputs need their
|
||
# SDK frameworks replaced with those from the requested SDK.
|
||
if [ "$sourceName" == "nix-support" ]; then
|
||
replacePropagatedInputs "$sourcePath" "$outPath/nix-support"
|
||
else
|
||
ln -s "$sourcePath" "$outPath/$sourceName"
|
||
fi
|
||
done
|
||
done
|
||
'';
|
||
};
|
||
|
||
# Gets all propagated inputs in a package. This does not recurse.
|
||
getPropagatedInputs =
|
||
pkg:
|
||
lib.optionals (lib.isDerivation pkg) (
|
||
lib.concatMap (input: pkg.${input} or [ ]) [
|
||
"depsBuildBuildPropagated"
|
||
"propagatedNativeBuildInputs"
|
||
"depsBuildTargetPropagated"
|
||
"depsHostHostPropagated"
|
||
"propagatedBuildInputs"
|
||
"depsTargetTargetPropagated"
|
||
]
|
||
);
|
||
|
||
# Looks up the replacement for `pkg` in the `newPackages` mapping. If `pkg` is a
|
||
# compiler (meaning it has a `libc` attribute), the compiler will be overriden.
|
||
getReplacement =
|
||
newPackages: pkg:
|
||
let
|
||
pkgOrCC =
|
||
if pkg.libc or null != null then
|
||
# Heuristic to determine whether package is a compiler or bintools.
|
||
if pkg.wrapperName == "CC_WRAPPER" then
|
||
mkCC (getReplacement newPackages pkg.libc) pkg
|
||
else
|
||
mkBintools (getReplacement newPackages pkg.libc) pkg
|
||
else
|
||
pkg;
|
||
in
|
||
if lib.isDerivation pkg then
|
||
newPackages.${builtins.unsafeDiscardStringContext pkg} or pkgOrCC
|
||
else
|
||
pkg;
|
||
|
||
# Replaces all packages propagated by `pkgs` using the `newPackages` mapping.
|
||
# It is assumed that all possible overrides have already been incorporated into
|
||
# the mapping. If any propagated packages are replaced, a proxy package will be
|
||
# created with references to the old packages replaced in `nix-support`.
|
||
replacePropagatedPackages =
|
||
newPackages: pkg:
|
||
let
|
||
propagatedInputs = getPropagatedInputs pkg;
|
||
env = {
|
||
inherit (pkg) outputs;
|
||
|
||
replacements = lib.pipe propagatedInputs [
|
||
(lib.filter (pkg: pkg != null))
|
||
(map (dep: {
|
||
name = builtins.unsafeDiscardStringContext dep;
|
||
value = getReplacement newPackages dep;
|
||
}))
|
||
(lib.filter (mapping: mapping.name != mapping.value))
|
||
lib.listToAttrs
|
||
];
|
||
|
||
sourceOutputs = lib.genAttrs pkg.outputs (output: lib.getOutput output pkg);
|
||
};
|
||
in
|
||
# Only remap the package’s propagated inputs if there are any and if any of them
|
||
# had packages remapped (with frameworks or proxy packages).
|
||
if propagatedInputs != [ ] && env.replacements != { } then mkProxyPackage pkg.name env else pkg;
|
||
|
||
# Gets all propagated dependencies in a package in reverse order sorted topologically.
|
||
# This takes advantage of the fact that items produced by `operator` are pushed to
|
||
# the end of the working set, ensuring that dependencies always appear after their
|
||
# parent in the list with leaf nodes at the end.
|
||
topologicallyOrderedPropagatedDependencies =
|
||
pkgs:
|
||
let
|
||
mapPackageDeps = lib.flip lib.pipe [
|
||
(lib.filter (pkg: pkg != null))
|
||
(map (pkg: {
|
||
key = builtins.unsafeDiscardStringContext pkg;
|
||
package = pkg;
|
||
deps = getPropagatedInputs pkg;
|
||
}))
|
||
];
|
||
in
|
||
lib.genericClosure {
|
||
startSet = mapPackageDeps pkgs;
|
||
operator = { deps, ... }: mapPackageDeps deps;
|
||
};
|
||
|
||
# Returns a package mapping based on remapping all propagated packages.
|
||
getPackageMapping =
|
||
baseMapping: input:
|
||
let
|
||
dependencies = topologicallyOrderedPropagatedDependencies input;
|
||
in
|
||
lib.foldr (
|
||
pkg: newPackages:
|
||
let
|
||
replacement = replacePropagatedPackages newPackages pkg.package;
|
||
outPath = pkg.key;
|
||
in
|
||
if pkg.key == null || newPackages ? ${outPath} then
|
||
newPackages
|
||
else
|
||
newPackages // { ${outPath} = replacement; }
|
||
) baseMapping dependencies;
|
||
|
||
overrideSDK =
|
||
stdenv: sdkVersion:
|
||
let
|
||
newVersion = {
|
||
inherit (stdenv.hostPlatform) darwinMinVersion darwinSdkVersion;
|
||
} // (if lib.isAttrs sdkVersion then sdkVersion else { darwinSdkVersion = sdkVersion; });
|
||
|
||
inherit (newVersion) darwinMinVersion darwinSdkVersion;
|
||
|
||
# Used to get an SDK version corresponding to the requested `darwinSdkVersion`.
|
||
# TODO: Treat `darwinSdkVersion` as a constraint rather than as an exact version.
|
||
resolveSDK = pkgs: pkgs.darwin."apple_sdk_${lib.replaceStrings [ "." ] [ "_" ] darwinSdkVersion}";
|
||
|
||
# `newSdkPackages` is constructed based on the assumption that SDK packages only
|
||
# propagate versioned packages from that SDK -- that they neither propagate
|
||
# unversioned SDK packages nor propagate non-SDK packages (such as curl).
|
||
#
|
||
# Note: `builtins.unsafeDiscardStringContext` is used to allow the path from the
|
||
# original package output to be mapped to the replacement. This is safe because
|
||
# the value is not persisted anywhere and necessary because store paths are not
|
||
# allowed as attrset names otherwise.
|
||
baseSdkMapping = lib.pipe args [
|
||
(lib.flip removeAttrs [
|
||
"lib"
|
||
"stdenvNoCC"
|
||
"extendMkDerivationArgs"
|
||
])
|
||
(lib.filterAttrs (_: lib.hasAttr "darwin"))
|
||
lib.attrValues
|
||
(lib.concatMap (
|
||
pkgs:
|
||
let
|
||
newSDK = resolveSDK pkgs;
|
||
|
||
frameworks = mkMapping "frameworks" pkgs newSDK;
|
||
libs = mkMapping "libs" pkgs newSDK;
|
||
overrides = mkOverrides pkgs newSDK newVersion;
|
||
in
|
||
frameworks ++ libs ++ overrides
|
||
))
|
||
lib.listToAttrs
|
||
];
|
||
|
||
# Remaps all inputs given to the requested SDK version. The result is an attrset
|
||
# that can be passed to `extendMkDerivationArgs`.
|
||
mapInputsToSDK =
|
||
inputs: args:
|
||
lib.pipe inputs [
|
||
(lib.filter (input: args ? ${input}))
|
||
(lib.flip lib.genAttrs (
|
||
inputName:
|
||
let
|
||
input = args.${inputName};
|
||
newPackages = getPackageMapping baseSdkMapping input;
|
||
in
|
||
map (getReplacement newPackages) input
|
||
))
|
||
];
|
||
in
|
||
stdenv.override (
|
||
old:
|
||
{
|
||
buildPlatform = mkPlatform newVersion old.buildPlatform;
|
||
hostPlatform = mkPlatform newVersion old.hostPlatform;
|
||
targetPlatform = mkPlatform newVersion old.targetPlatform;
|
||
}
|
||
# Only perform replacements if the SDK version has changed. Changing only the
|
||
# deployment target does not require replacing the libc or SDK dependencies.
|
||
// lib.optionalAttrs (old.hostPlatform.darwinSdkVersion != darwinSdkVersion) {
|
||
allowedRequisites = null;
|
||
|
||
mkDerivationFromStdenv = extendMkDerivationArgs old (mapInputsToSDK [
|
||
"depsBuildBuild"
|
||
"nativeBuildInputs"
|
||
"depsBuildTarget"
|
||
"depsHostHost"
|
||
"buildInputs"
|
||
"depsTargetTarget"
|
||
"depsBuildBuildPropagated"
|
||
"propagatedNativeBuildInputs"
|
||
"depsBuildTargetPropagated"
|
||
"depsHostHostPropagated"
|
||
"propagatedBuildInputs"
|
||
"depsTargetTargetPropagated"
|
||
]);
|
||
|
||
cc = getReplacement baseSdkMapping old.cc;
|
||
|
||
extraBuildInputs = map (getReplacement baseSdkMapping) stdenv.extraBuildInputs;
|
||
extraNativeBuildInputs = map (getReplacement baseSdkMapping) stdenv.extraNativeBuildInputs;
|
||
}
|
||
);
|
||
in
|
||
overrideSDK
|