Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nix: static builds and release workflow #1133

Merged
merged 8 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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