Shane Xu's Home

Life is too short for so much sorrow.

Moving from Homebrew to Nix Package Manager

前言

2012年我入手了人生第一台MacBook Pro,也正式开启了macOS(以前是 os x)之旅。在很长一段时间里我并不习惯macOS的应用安装方式。应用下载完后,打开dmg,然后拖放到 /Applications 目录下。以及macOS根本就不存在包管理器这种东西,我甚至在MacBook上直接安装了Archlinux。出于某些什么的原因,我又重新返回到了macOS,然后在用了一两个月的MacPorts之后转投了Homebrew。然后在最近重拾了haskell的学习进程之后,又开始折腾起了nix。

安装nix

因为 macOS catalin 的关系,安装 nix 的过程有些许曲折,可以参考我之前的文章 Learning Haskell with nix and Emacs

卸载Homebrew安装的包

列出已经安装的包

  brew list

看看都安装了些什么软件包,如果有必要就在 nix 里面重新安装一遍。

  brew uninstall `brew list`

安装nix包

  nix-env -i coreutils

问题

coreutils包中的命令覆盖了macOS本身的命令

coreutils包中的命令有好几个与macOS命令冲突,并且其行为也不一样,例如 df 命令

coreutils的df

   $ df -h
   Filesystem      Size  Used Avail Use% Mounted on
   /dev/disk1s1    932G   11G  695G   2% /
   /dev/disk1s5    932G  4.1G  695G   1% /private/var/vm
   /dev/disk1s6    932G   17G  695G   3% /nix

macOS 的 df

   $ df -h
   Filesystem      Size   Used  Avail Capacity iused      ifree %iused  Mounted on
   /dev/disk1s1   932Gi   10Gi  694Gi     2%  484150 9767494010    0%   /
   devfs          205Ki  205Ki    0Bi   100%     712          0  100%   /dev
   /dev/disk1s2   932Gi  206Gi  694Gi    23% 2334824 9765643336    0%   /System/Volumes/Data
   /dev/disk1s5   932Gi  4.0Gi  694Gi     1%       4 9767978156    0%   /private/var/vm
   map auto_home    0Bi    0Bi    0Bi   100%       0          0  100%   /System/Volumes/Data/home
   /dev/disk1s6   932Gi   16Gi  694Gi     3%  436832 9767541328    0%   /nix

于是参考homebrew的coreutils自己改了下 coreutils nix表达式:

  1:    { stdenv, lib, buildPackages
  2:    , autoreconfHook, bison, texinfo, fetchurl, perl, xz, libiconv, gmp ? null
  3:    , aclSupport ? stdenv.isLinux, acl ? null
  4:    , attrSupport ? stdenv.isLinux, attr ? null
  5:    , selinuxSupport? false, libselinux ? null, libsepol ? null
  6:    # No openssl in default version, so openssl-induced rebuilds aren't too big.
  7:    # It makes *sum functions significantly faster.
  8:    , minimal ? true, withOpenssl ? !minimal, openssl ? null
  9:    , withPrefix ? false
 10:    , singleBinary ? "symlinks" # you can also pass "shebangs" or false
 11:    , unprefixNoConflict ? false
 12:    }:
 13: 
 14:    assert aclSupport -> acl != null;
 15:    assert selinuxSupport -> libselinux != null && libsepol != null;
 16: 
 17:    with lib;
 18: 
 19:    stdenv.mkDerivation rec {
 20:      pname = "coreutils";
 21:      version = "8.31";
 22: 
 23:      src = fetchurl {
 24:        url = "mirror://gnu/${pname}/${pname}-${version}.tar.xz";
 25:        sha256 = "1zg9m79x1i2nifj4kb0waf9x3i5h6ydkypkjnbsb9rnwis8rqypz";
 26:      };
 27: 
 28:      patches = optional stdenv.hostPlatform.isCygwin ./coreutils-8.23-4.cygwin.patch
 29:             # Fix failing test with musl. See https://lists.gnu.org/r/coreutils/2019-05/msg00031.html
 30:             # To be removed in coreutils-8.32.
 31:             ++ optional stdenv.hostPlatform.isMusl ./avoid-false-positive-in-date-debug-test.patch
 32:             # Fix compilation in musl-cross environments. To be removed in coreutils-8.32.
 33:             ++ optional stdenv.hostPlatform.isMusl ./coreutils-8.31-musl-cross.patch;
 34: 
 35:      postPatch = ''
 36:        # The test tends to fail on btrfs,f2fs and maybe other unusual filesystems.
 37:        sed '2i echo Skipping dd sparse test && exit 77' -i ./tests/dd/sparse.sh
 38:        sed '2i echo Skipping du threshold test && exit 77' -i ./tests/du/threshold.sh
 39:        sed '2i echo Skipping cp sparse test && exit 77' -i ./tests/cp/sparse.sh
 40:        sed '2i echo Skipping rm deep-2 test && exit 77' -i ./tests/rm/deep-2.sh
 41:        sed '2i echo Skipping du long-from-unreadable test && exit 77' -i ./tests/du/long-from-unreadable.sh
 42: 
 43:        # Some target platforms, especially when building inside a container have
 44:        # issues with the inotify test.
 45:        sed '2i echo Skipping tail inotify dir recreate test && exit 77' -i ./tests/tail-2/inotify-dir-recreate.sh
 46: 
 47:        # sandbox does not allow setgid
 48:        sed '2i echo Skipping chmod setgid test && exit 77' -i ./tests/chmod/setgid.sh
 49:        substituteInPlace ./tests/install/install-C.sh \
 50:          --replace 'mode3=2755' 'mode3=1755'
 51: 
 52:        sed '2i print "Skipping env -S test";  exit 77;' -i ./tests/misc/env-S.pl
 53: 
 54:        # these tests fail in the unprivileged nix sandbox (without nix-daemon) as we break posix assumptions
 55:        for f in ./tests/chgrp/{basic.sh,recurse.sh,default-no-deref.sh,no-x.sh,posix-H.sh}; do
 56:          sed '2i echo Skipping chgrp && exit 77' -i "$f"
 57:        done
 58:        for f in gnulib-tests/{test-chown.c,test-fchownat.c,test-lchown.c}; do
 59:          echo "int main() { return 77; }" > "$f"
 60:        done
 61:      '' + optionalString (stdenv.hostPlatform.libc == "musl") (lib.concatStringsSep "\n" [
 62:        ''
 63:          echo "int main() { return 77; }" > gnulib-tests/test-parse-datetime.c
 64:          echo "int main() { return 77; }" > gnulib-tests/test-getlogin.c
 65:        ''
 66:      ]);
 67: 
 68:      outputs = [ "out" "info" ];
 69: 
 70:      nativeBuildInputs = [ perl xz.bin ]
 71:        ++ optionals stdenv.hostPlatform.isCygwin [ autoreconfHook texinfo ]   # due to patch
 72:        ++ optionals stdenv.hostPlatform.isMusl [ autoreconfHook bison ];   # due to patch
 73:      configureFlags = [ "--with-packager=https://NixOS.org" ]
 74:        ++ optional (singleBinary != false)
 75:          ("--enable-single-binary" + optionalString (isString singleBinary) "=${singleBinary}")
 76:        ++ optional withOpenssl "--with-openssl"
 77:        ++ optional stdenv.hostPlatform.isSunOS "ac_cv_func_inotify_init=no"
 78:        ++ optional withPrefix "--program-prefix=g"
 79:        ++ optionals (stdenv.hostPlatform != stdenv.buildPlatform && stdenv.hostPlatform.libc == "glibc") [
 80:          # TODO(19b98110126fde7cbb1127af7e3fe1568eacad3d): Needed for fstatfs() I
 81:          # don't know why it is not properly detected cross building with glibc.
 82:          "fu_cv_sys_stat_statfs2_bsize=yes"
 83:        ];
 84: 
 85: 
 86:      buildInputs = [ gmp ]
 87:        ++ optional aclSupport acl
 88:        ++ optional attrSupport attr
 89:        ++ optional withOpenssl openssl
 90:        ++ optionals selinuxSupport [ libselinux libsepol ]
 91:           # TODO(@Ericson2314): Investigate whether Darwin could benefit too
 92:        ++ optional (stdenv.hostPlatform != stdenv.buildPlatform && stdenv.hostPlatform.libc != "glibc") libiconv;
 93: 
 94:      # The tests are known broken on Cygwin
 95:      # (http://article.gmane.org/gmane.comp.gnu.core-utils.bugs/19025),
 96:      # Darwin (http://article.gmane.org/gmane.comp.gnu.core-utils.bugs/19351),
 97:      # and {Open,Free}BSD.
 98:      # With non-standard storeDir: https://github.com/NixOS/nix/issues/512
 99:      doCheck = stdenv.hostPlatform == stdenv.buildPlatform
100:        && (stdenv.hostPlatform.libc == "glibc" || stdenv.hostPlatform.isMusl)
101:        && builtins.storeDir == "/nix/store";
102: 
103:      # Prevents attempts of running 'help2man' on cross-built binaries.
104:      PERL = if stdenv.hostPlatform == stdenv.buildPlatform then null else "missing";
105: 
106:      # Saw random failures like ‘help2man: can't get '--help' info from
107:      # man/sha512sum.td/sha512sum’.
108:      enableParallelBuilding = false;
109: 
110:      NIX_LDFLAGS = optionalString selinuxSupport "-lsepol";
111:      FORCE_UNSAFE_CONFIGURE = optionalString stdenv.hostPlatform.isSunOS "1";
112: 
113:      # Works around a bug with 8.26:
114:      # Makefile:3440: *** Recursive variable 'INSTALL' references itself (eventually).  Stop.
115:      preInstall = optionalString (stdenv.hostPlatform != stdenv.buildPlatform) ''
116:        sed -i Makefile -e 's|^INSTALL =.*|INSTALL = ${buildPackages.coreutils}/bin/install -c|'
117:      '';
118: 
119:      postInstall = optionalString (stdenv.hostPlatform != stdenv.buildPlatform && !minimal) ''
120:        rm $out/share/man/man1/*
121:        cp ${buildPackages.coreutils-full}/share/man/man1/* $out/share/man/man1
122:      ''
123:      # du: 8.7 M locale + 0.4 M man pages
124:      + optionalString minimal ''
125:        rm -r "$out/share"
126:      ''
127:      + optionalString (stdenv.isDarwin && withPrefix && unprefixNoConflict) ''
128:        cd $out/bin
129:        ${concatStringsSep "\n" (builtins.map (x: "ln -s g${x} ${x}") (splitString " " "b2sum base32 chcon hostid md5sum nproc numfmt pinky ptx realpath runcon sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf stdbuf tac timeout truncate"))}
130:      '';
131: 
132:      meta = {
133:        homepage = https://www.gnu.org/software/coreutils/;
134:        description = "The basic file, shell and text manipulation utilities of the GNU operating system";
135: 
136:        longDescription = ''
137:          The GNU Core Utilities are the basic file, shell and text
138:          manipulation utilities of the GNU operating system.  These are
139:          the core utilities which are expected to exist on every
140:          operating system.
141:        '';
142: 
143:        license = licenses.gpl3Plus;
144: 
145:        platforms = platforms.unix ++ platforms.windows;
146: 
147:        priority = 10;
148: 
149:        maintainers = [ maintainers.eelco ];
150:      };
151: 
152:    } // optionalAttrs stdenv.hostPlatform.isMusl {
153:      # Work around a bogus warning in conjunction with musl.
154:      NIX_CFLAGS_COMPILE = "-Wno-error";
155:    }

emacs-libvterm 编译问题

emacs-libvterm 前段时间开始使用这个包,可以说这个包是目前为止,我用过的在emacs下表现最好的终端模拟器了。然后在换成nix之后遇到了些问题。

glibtool 命令找不到

emacs-libvterm在macOS下,回去寻找glibtool这个命令,而glibtool这个命令实际上就是libtool命令加了前缀 g 。nix里面对应的包名是libtool。需要修改nix增加前缀。

libtool 静态编译问题

nix下的包默认都是非静态编译,而emacs-libvterm又需要静态编译。这里又要改glibtool的nix表达式。

综上,最后修改完的 nix 文件

 1:     { stdenv, fetchurl, m4, perl, help2man
 2:     , withPrefix ? false
 3:     , static ? false
 4:     }:
 5: 
 6:     stdenv.mkDerivation rec {
 7:       pname = "libtool";
 8:       version = "2.4.6";
 9: 
10:       src = fetchurl {
11:         url = "mirror://gnu/libtool/${pname}-${version}.tar.gz";
12:         sha256 = "1qq61k6lp1fp75xs398yzi6wvbx232l7xbyn3p13cnh27mflvgg3";
13:       };
14: 
15:       outputs = [ "out" "lib" ];
16: 
17:       nativeBuildInputs = [ perl help2man m4 ];
18:       propagatedBuildInputs = [ m4 ];
19:       configureFlags = if withPrefix then [ "--program-prefix=g" ] else [];
20:       dontDisableStatic = static;
21: 
22:       # Don't fixup "#! /bin/sh" in Libtool, otherwise it will use the
23:       # "fixed" path in generated files!
24:       dontPatchShebangs = true;
25: 
26:       # XXX: The GNU ld wrapper does all sorts of nasty things wrt. RPATH, which
27:       # leads to the failure of a number of tests.
28:       doCheck = false;
29:       doInstallCheck = false;
30: 
31:       enableParallelBuilding = true;
32: 
33:       # Don't run the native `strip' when cross-compiling.  This breaks at least
34:       # with `.a' files for MinGW.
35:       dontStrip = stdenv.hostPlatform != stdenv.buildPlatform;
36: 
37:       meta = with stdenv.lib; {
38:         description = "GNU Libtool, a generic library support script";
39:         longDescription = ''
40:           GNU libtool is a generic library support script.  Libtool hides
41:           the complexity of using shared libraries behind a consistent,
42:           portable interface.
43: 
44:           To use libtool, add the new generic library building commands to
45:           your Makefile, Makefile.in, or Makefile.am.  See the
46:           documentation for details.
47:         '';
48:         homepage = https://www.gnu.org/software/libtool/;
49:         license = licenses.gpl2Plus;
50:         maintainers = [ ];
51:         platforms = platforms.unix;
52:       };
53:     }
54: 

jenv 包不存在问题

jenv包是为了解决java环境问题的。而nix本身就是为了解决开发环境问题的“终极”方案。理论上根本就不需要jenv这样的工具。但是所有开发都使用 nix-shell 去定制开发环境,又稍显太重。所以有时候还是需要jenv这样的工具的。于是自己写个 nix表达式。

 1:    { stdenv, fetchurl }:
 2: 
 3:    stdenv.mkDerivation rec {
 4:      version = "0.5.2";
 5:      pname = "jenv";
 6:      src = fetchurl {
 7:        url = "https://github.com/jenv/jenv/archive/${version}.tar.gz";
 8:        sha256 = "4cdce828bfaeb6561733bab641ed2912107a8bc24758a17f2387ee78403afb9a";
 9:      };
10:      buildPhase =
11:        ''
12:        outdir=$out/libexec
13:        mkdir -p $outdir
14:        cp -r * $outdir
15:        '';
16:      installPhase =
17:        ''
18:        mkdir $out/bin
19:        ln -s $outdir/libexec/jenv $out/bin/jenv;
20:        '';
21:      dontFixup = true;
22:      meta = with stdenv.lib; {
23:        description = "jEnv is an updated fork of jenv, a beloved Java environment manager adapted from rbenv.";
24:        longDescription =
25:          ''This is an updated fork of jenv, a beloved Java environment manager adapted from rbenv.
26: 
27:            jenv gives you a few critical affordances for using java on development machines:
28: 
29:              * It lets you switch between java versions. This is useful when developing Android applications, which generally require Java 8 for its tools, versus server applications, which use later versions like Java 11.
30:              * It sets JAVA_HOME inside your shell, in a way that can be set globally, local to the current working directory or per shell.
31: 
32:            However, this project does not:
33: 
34:              * Install java for you. Use your platform appropriate package manager to install java. On macOS, brew is recommended.
35:              * This document will show you how to install jenv, review its most common commands, show example workflows and identify known issues.
36:          '';
37:        homepage = http://www.jenv.be;
38:        license = licenses.mit;
39:        platforms = platforms.all;
40:        maintainers = [ ];
41:      };
42:    }

mit-scheme 安装问题

nix的channel里有mit-scheme包,但是不支持darwin平台。使用mit-scheme-c的包来安装的话,又有编译问题。nix编译darwin包用的stdenv中clang的版本比较低。

   $ nix-shell --pure -p stdenv --command "clang --version"
   clang version 7.1.0 (tags/RELEASE_710/final)
   Target: x86_64-apple-darwin19.2.0
   Thread model: posix
   InstalledDir: /nix/store/jdmg20b8rgvs1s4fxb585lffz07vv52a-clang-7.1.0/bin

解决clang版本低问题,可以直接使用xcode command line tools编译

 1:     { stdenv, requireFile, lib }:
 2: 
 3:     let requireCLTool = version: sha256:
 4:       let
 5:         version' = lib.replaceStrings ["."] ["_"] version;
 6:         dmg = "Command_Line_Tools_for_Xcode_${version'}.dmg";
 7:         app = requireFile rec {
 8:           name = "CommandLineTools";
 9:           url = "https://download.developer.apple.com/Developer_Tools/Command_Line_Tools_for_Xcode_${version'}/Command_Line_Tools_for_Xcode_${version'}.dmg";
10:           hashMode = "recursive";
11:           inherit sha256;
12:           message  = ''
13:             Unfortunately, we cannot download ${name} automatically.
14:             Please go to ${url}
15:             to download it yourself, and add it to the Nix store by running the following commands.
16:             Note: download (~ 5GB), extraction and storing of Xcode will take a while
17: 
18:             nix-store --add-fixed --recursive sha256 /Library/Developer/CommandLineTools
19:           '';
20:         };
21:         meta = with stdenv.lib; {
22:           homepage = https://developer.apple.com/downloads/;
23:           description = "Apple's Command Line Tools for Xcode";
24:           license = licenses.unfree;
25:           platforms = platforms.darwin;
26:         };
27:       in app.overrideAttrs (oldAttrs : oldAttrs // { inherit meta; });
28: 
29:     in lib.makeExtensible (self: {
30:       Command_Line_Tools_for_Xcode_11_2 = requireCLTool "11.2" "76ec9816dc26955c0d3d05cbd39b9500d18842ddd33a448c98fb896f1a917dc5";
31:       Command_Line_Tools_for_Xcode = self."Command_Line_Tools_for_Xcode_${lib.replaceStrings ["."] ["_"] (if stdenv.targetPlatform.useiOSPrebuilt then stdenv.targetPlatform.xcodeVer else "11.2")}";
32:     })

修改 mit-scheme nix表达式,使用xcode command line tools编译

 1:     { pkgs, stdenvNoCC, fetchurl, makeWrapper, gnum4, texinfo, texLive, automake, lib, macosVersion, xcodeVersion }:
 2: 
 3:     let
 4:       version = "9.2";
 5:       xcode = pkgs.darwin."Command_Line_Tools_for_Xcode_${lib.replaceStrings ["."] ["_"] xcodeVersion}";
 6:     in
 7:     stdenvNoCC.mkDerivation {
 8:       name = "mit-scheme-macos${lib.replaceStrings ["."] ["_"] macosVersion}-xcode${lib.replaceStrings ["."] ["_"] xcodeVersion}-${version}";
 9: 
10:       # MIT/GNU Scheme is not bootstrappable, so it's recommended to compile from
11:       # the platform-specific tarballs, which contain pre-built binaries.  It
12:       # leads to more efficient code than when building the tarball that contains
13:       # generated C code instead of those binaries.
14:       src = fetchurl {
15:           url = "mirror://gnu/mit-scheme/stable.pkg/${version}/mit-scheme-c-${version}.tar.gz";
16:           sha256 = "0w5ib5vsidihb4hb6fma3sp596ykr8izagm57axvgd6lqzwicsjg";
17:         };
18: 
19:       buildInputs = [ xcode ];
20: 
21:       configurePhase = "(cd doc && ./configure)";
22: 
23:       buildPhase =
24:         '' export PATH=${xcode}/usr/bin:$PATH
25:            export CPATH=${xcode}/SDKs/MacOSX${macosVersion}.sdk/usr/include
26: 
27:            cd src
28:            for i in 6001/edextra.scm \
29:                     6001/floppy.scm \
30:                     compiler/etc/disload.scm \
31:                     edwin/techinfo.scm \
32:                     edwin/unix.scm \
33:                     swat/c/tk3.2-custom/Makefile \
34:                     swat/c/tk3.2-custom/tcl/Makefile \
35:                     swat/scheme/other/btest.scm \
36:                     microcode/configure
37:            do
38:                sed -i "s~/usr/local~$out~g" $i
39:            done
40:            sed -i 's/run_configure/run_configure --without-x --with-macosx-version=10.15/g' ./etc/make-liarc.sh
41:            ./etc/make-liarc.sh --prefix=$out
42: 
43:            cd ../doc
44: 
45:            # Provide a `texinfo.tex'.
46:            export TEXINPUTS="$(echo ${automake}/share/automake-*)"
47:            echo "\$TEXINPUTS is \`$TEXINPUTS'"
48:            make
49: 
50:            cd ..
51:         '';
52: 
53:       installPhase =
54:         '' make prefix=$out install -C src
55:            make prefix=$out install -C doc
56:         '';
57: 
58:       fixupPhase =
59:         '' wrapProgram $out/bin/mit-scheme-c --set MITSCHEME_LIBRARY_PATH \
60:              $out/lib/mit-scheme-c
61:         '';
62: 
63:       nativeBuildInputs = [ makeWrapper gnum4 texinfo texLive automake ];
64: 
65:       # XXX: The `check' target doesn't exist.
66:       doCheck = false;
67: 
68:       meta = with stdenvNoCC.lib; {
69:         description = "MIT/GNU Scheme, a native code Scheme compiler";
70: 
71:         longDescription =
72:           '' MIT/GNU Scheme is an implementation of the Scheme programming
73:              language, providing an interpreter, compiler, source-code debugger,
74:              integrated Emacs-like editor, and a large runtime library.  MIT/GNU
75:              Scheme is best suited to programming large applications with a rapid
76:              development cycle.
77:           '';
78: 
79:         homepage = https://www.gnu.org/software/mit-scheme/;
80: 
81:         license = licenses.gpl2Plus;
82: 
83:         maintainers = [ ];
84: 
85:         # Build fails on Cygwin and Darwin:
86:         # <http://article.gmane.org/gmane.lisp.scheme.mit-scheme.devel/489>.
87:         platforms = platforms.darwin;
88:       };
89:     }

我的自定义nix-channels

后记

nix是个好工具,对于构建应用的开发环境来说是一个杀手级的工具,但是对于macOS这种系统的日常使用来说还是有些不方便。最后我给自己定下了一些使用策略。

  1. 使用homebrew cask安装桌面应用,比如firefox、chrome等。
  2. 使用homebrew安装日常命令行工具。
  3. 对一些依赖比较复杂的应用,使用nix构造开发环境

参考文档

https://www.softinio.com/post/moving-from-homebrew-to-nix-package-manager/

Comments

comments powered by Disqus