Skip to content

Commit

Permalink
nix: static builds and release workflow (#1133)
Browse files Browse the repository at this point in the history
* flake.nix: static build support

Adds a "redistributable" flavor of Echidna that is fully static on
Linux, and mostly static on macOS.

* ci: add Nix & release workflow

Replaces previous Nix workflow

* ci: release: add job timeouts

* ci: release: configure Cachix

* README: update echidna-bundle references to echidna-redistributable

* Fix TERMINFO path for Nix release builds on Linux

ncurses in Nix is built with a TERMINFO path that references `/nix`.
This causes the binaries fail when ran on non-nix systems, unless
TERMINFO=/usr/share/terminfo is exported. This patches the binaries
to use a more sensible default TERMINFO path.

See also commit f76a7f4

* flake.nix: remove redundant stripping
  • Loading branch information
elopez authored Nov 28, 2023
1 parent f52b35a commit 494eb92
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 29 deletions.
24 changes: 0 additions & 24 deletions .github/workflows/nix.yml

This file was deleted.

111 changes: 111 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: "Nix and release"
on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master

jobs:
nixBuild:
name: Build ${{ matrix.name }} binary
timeout-minutes: ${{ matrix.timeout || 30 }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
strategy:
matrix:
include:
- os: ubuntu-latest
name: Linux (x86_64)
tuple: x86_64-linux
timeout: 180
- os: macos-latest
name: macOS (x86_64)
tuple: x86_64-macos
- os: macos-latest-xlarge
name: macOS (aarch64)
tuple: aarch64-macos
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v6

- name: Configure Cachix
uses: cachix/cachix-action@v12
with:
name: trailofbits
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

- name: Configure Nix cache
if: runner.arch == 'X64'
# Unfortunately the action does not work on ARM runners
uses: DeterminateSystems/magic-nix-cache-action@v2
with:
upstream-cache: https://trailofbits.cachix.org

- name: Obtain version number
id: version
run: |
if [[ "$GIT_REF" =~ ^refs/tags/v.* ]]; then
echo "version=$(echo "$GIT_REF" | sed 's#^refs/tags/v##')" >> "$GITHUB_OUTPUT"
else
echo "version=HEAD-$(echo "$GIT_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT"
fi
env:
GIT_REF: ${{ github.ref }}
GIT_SHA: ${{ github.sha }}

- name: Build dynamic echidna
run: |
nix build .#echidna
- name: Build redistributable echidna
run: |
nix build .#echidna-redistributable --out-link redistributable
tar -czf "echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz" -C ./redistributable/bin/ echidna
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: echidna-redistributable
path: echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz

release:
name: Create release
timeout-minutes: 10
needs: [nixBuild]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download binaries
uses: actions/download-artifact@v3
with:
name: echidna-redistributable

- name: Sign binaries
uses: sigstore/gh-action-sigstore-python@v2.1.0
with:
inputs: ./echidna-*.tar.gz

- name: Create GitHub release and upload binaries
uses: softprops/action-gh-release@v0.1.15
with:
draft: true
name: "Echidna ${{ needs.nixBuild.outputs.version }}"
files: |
./echidna-*.tar.gz
./echidna-*.tar.gz.sigstore
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,11 @@ $ nix run github:crytic/echidna/v2.1.1 # specific ref (tag/branch/commit)
```

To build a standalone release for non-Nix macOS systems, the following will
bundle Echidna and all linked dylibs:
build Echidna in a mostly static binary. This can also be used on Linux systems
to produce a fully static binary.

```sh
$ nix build .#echidna-bundle
$ nix build .#echidna-redistributable
```

Nix will automatically install all the dependencies required for development
Expand Down
79 changes: 76 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
outputs = { self, nixpkgs, flake-utils, nix-bundle-exe, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
systemPkgs = nixpkgs.legacyPackages.${system};
# prefer musl on Linux, static glibc + threading does not work properly
# TODO: maybe only override it for echidna-redistributable?
pkgs = if systemPkgs.stdenv.hostPlatform.isLinux then systemPkgs.pkgsMusl else systemPkgs;
# this is not perfect for development as it hardcodes solc to 0.5.7, test suite runs fine though
# would be great to integrate solc-select to be more flexible, improve this in future
solc = pkgs.stdenv.mkDerivation {
Expand All @@ -38,6 +41,12 @@
'';
};

secp256k1-static = pkgs.secp256k1.overrideAttrs (attrs: {
configureFlags = attrs.configureFlags ++ [ "--enable-static" ];
});

ncurses-static = pkgs.ncurses.override { enableStatic = true; };

hevm = pkgs.haskell.lib.dontCheck (
pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
owner = "elopez";
Expand All @@ -55,12 +64,76 @@
(haskell.lib.compose.addTestToolDepends [ haskellPackages.hpack slither-analyzer solc ])
(haskell.lib.compose.disableCabalFlag "static")
]);

echidna-static = with pkgs; lib.pipe
echidna
[
(haskell.lib.compose.appendConfigureFlags
([
"--extra-lib-dirs=${stripDylib (gmp.override { withStatic = true; })}/lib"
"--extra-lib-dirs=${stripDylib secp256k1-static}/lib"
"--extra-lib-dirs=${stripDylib (libff.override { enableStatic = true; })}/lib"
"--extra-lib-dirs=${zlib.static}/lib"
"--extra-lib-dirs=${stripDylib (libffi.overrideAttrs (_: { dontDisableStatic = true; }))}/lib"
"--extra-lib-dirs=${stripDylib (ncurses-static)}/lib"
] ++ (if stdenv.hostPlatform.isDarwin then [
"--extra-lib-dirs=${stripDylib (libiconv.override { enableStatic = true; })}/lib"
] else [])))
(haskell.lib.compose.enableCabalFlag "static")
];

# "static" binary for distribution
# on linux this is actually a real fully static binary
# on macos this has everything except libcxx and libsystem
# statically linked. we can be confident that these two will always
# be provided in a well known location by macos itself.
echidnaRedistributable = let
grep = "${pkgs.gnugrep}/bin/grep";
perl = "${pkgs.perl}/bin/perl";
otool = "${pkgs.darwin.binutils.bintools}/bin/otool";
install_name_tool = "${pkgs.darwin.binutils.bintools}/bin/install_name_tool";
codesign_allocate = "${pkgs.darwin.binutils.bintools}/bin/codesign_allocate";
codesign = "${pkgs.darwin.sigtool}/bin/codesign";
in if pkgs.stdenv.isLinux
then pkgs.runCommand "echidna-stripNixRefs" {} ''
mkdir -p $out/bin
cp ${pkgs.haskell.lib.dontCheck echidna-static}/bin/echidna $out/bin/
# fix TERMINFO path in ncurses
${perl} -i -pe 's#(${ncurses-static}/share/terminfo)#"/usr/share/terminfo" . "\x0" x (length($1) - 19)#e' $out/bin/echidna
chmod 555 $out/bin/echidna
'' else pkgs.runCommand "echidna-stripNixRefs" {} ''
mkdir -p $out/bin
cp ${pkgs.haskell.lib.dontCheck echidna-static}/bin/echidna $out/bin/
# get the list of dynamic libs from otool and tidy the output
libs=$(${otool} -L $out/bin/echidna | tail -n +2 | sed 's/^[[:space:]]*//' | cut -d' ' -f1)
# get the path for libcxx
cxx=$(echo "$libs" | ${grep} '^/nix/store/.*-libcxx')
# rewrite /nix/... library paths to point to /usr/lib
chmod 777 $out/bin/echidna
${install_name_tool} -change "$cxx" /usr/lib/libc++.1.dylib $out/bin/echidna
# fix TERMINFO path in ncurses
${perl} -i -pe 's#(${ncurses-static}/share/terminfo)#"/usr/share/terminfo" . "\x0" x (length($1) - 19)#e' $out/bin/echidna
# re-sign binary
CODESIGN_ALLOCATE=${codesign_allocate} ${codesign} -f -s - $out/bin/echidna
chmod 555 $out/bin/echidna
'';

# if we pass a library folder to ghc via --extra-lib-dirs that contains
# only .a files, then ghc will link that library statically instead of
# dynamically (even if --enable-executable-static is not passed to cabal).
# we use this trick to force static linking of some libraries on macos.
stripDylib = drv : pkgs.runCommand "${drv.name}-strip-dylibs" {} ''
mkdir -p $out
mkdir -p $out/lib
cp -r ${drv}/* $out/
rm -rf $out/**/*.dylib
'';

in rec {
packages.echidna = echidna;
packages.default = echidna;

packages.echidna-bundle =
pkgs.callPackage nix-bundle-exe {} (pkgs.haskell.lib.dontCheck echidna);
packages.echidna-redistributable = echidnaRedistributable;

devShell = with pkgs;
haskellPackages.shellFor {
Expand Down

0 comments on commit 494eb92

Please sign in to comment.