about summary refs log tree commit diff
path: root/pkgs/stdenv/darwin/override-sdk.nix
blob: 6de67537b49903b2dffbfcc1cdeb13b1c51dd4ae (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# 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