diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 4f15517e2..000000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Nix - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - - macos-latest - steps: - - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v23 - with: - nix_path: nixpkgs=channel:nixos-unstable - - run: nix-build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..fad150b10 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.md b/README.md index 48eed773c..7a747eb13 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/flake.nix b/flake.nix index 0a740d00b..4206fa77a 100644 --- a/flake.nix +++ b/flake.nix @@ -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 { @@ -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"; @@ -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 {