diff --git a/.github/actions/setup-haskell/action.yaml b/.github/actions/setup-haskell/action.yaml new file mode 100644 index 0000000000..117d528b16 --- /dev/null +++ b/.github/actions/setup-haskell/action.yaml @@ -0,0 +1,68 @@ +name: Setup Haskell +description: | + This action sets up a Haskell environment for use in actions by + adding the GHC and Cabal binaries to the PATH. It also caches + the GHC and Cabal installations to speed up subsequent runs. + +inputs: + ghc-version: + description: | + The version of GHC to install. + required: false + default: "8.10.7" + + cabal-version: + description: | + The version of Cabal to install. + required: false + default: "latest" + + cabal-project-dir: + description: | + The working directory for the action. + required: false + default: "waspc" + +runs: + using: composite + + steps: + - uses: haskell-actions/setup@v2 + id: setup-haskell + with: + ghc-version: ${{ inputs.ghc-version }} + cabal-version: ${{ inputs.cabal-version }} + + - name: Verify Haskell setup + shell: bash + run: | + ghc --version + cabal --version + + # Based on the official recipe for Cabal caching: + # https://github.com/actions/cache/blob/v4.2.3/examples.md#haskell---cabal + - name: Cache Cabal dependencies + uses: actions/cache@v4 + with: + # There are two extra directories that are commonly cache, that we've + # decided not to cache: + # + # - `./dist-newstyle`: Our internal code builds quite fast, and changes + # often enough that a cache would need to be invalidated on every run, and + # make our caching story much more complex. + # + # - `~/.cabal/packages`: This is a local cache of the package index. While + # it could be useful, we build in heterogeneous environments and it is + # not always in the same paths. From testing, in packages with a frozen + # `index-state` like ours, the build will just download a single ~4kb file. + # So it is not worth the complexity of caching. + # + # We do cache the Cabal store, which is where the actual built packages + # are stored, as it is the most expensive part of the build, and easily + # reusable. + path: | + ${{ steps.setup-haskell.outputs.cabal-store }} + key: | + cabal-${{ inputs.cabal-project-dir }}-${{ runner.os }}-${{ runner.arch }}-${{ inputs.ghc-version }}-${{ hashFiles('${{ inputs.cabal-project-dir }}/*.cabal', '${{ inputs.cabal-project-dir }}/*.project', '${{ inputs.cabal-project-dir }}/*.project.freeze') }} + restore-keys: | + cabal-${{ inputs.cabal-project-dir }}-${{ runner.os }}-${{ runner.arch }}-${{ inputs.ghc-version }}- diff --git a/.github/actions/setup-haskell/action.yml b/.github/actions/setup-haskell/action.yml deleted file mode 100644 index f443c110bf..0000000000 --- a/.github/actions/setup-haskell/action.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Setup Haskell -description: | - This action sets up a Haskell environment for use in actions by - adding the GHC and Cabal binaries to the PATH. It also caches - the GHC and Cabal installations to speed up subsequent runs. - -inputs: - ghc-version: - description: | - The version of GHC to install. - required: false - default: "8.10.7" - -runs: - using: composite - - steps: - - uses: haskell-actions/setup@v2 - id: setup-haskell - with: - ghc-version: ${{ inputs.ghc-version }} - - # Based on the official recipe for Cabal caching: - # https://github.com/actions/cache/blob/5a3ec84eff668545956fd18022155c47e93e2684/examples.md#haskell---cabal - - name: Cache Cabal dependencies - uses: actions/cache@v4 - with: - path: | - ${{ steps.setup-haskell.outputs.cabal-store }} - waspc/dist-newstyle - # Caches are immutable, so we use the official recipe for simulating an updatable cache: - # https://github.com/actions/cache/blob/5a3ec84eff668545956fd18022155c47e93e2684/tips-and-workarounds.md#update-a-cache - key: ${{ runner.os }}-cabal-${{ inputs.ghc-version }}-${{ hashFiles('waspc/*.cabal', 'waspc/*.project', 'waspc/*.project.freeze') }}-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-cabal-${{ inputs.ghc-version }}-${{ hashFiles('waspc/*.cabal', 'waspc/*.project', 'waspc/*.project.freeze') }}- - ${{ runner.os }}-cabal-${{ inputs.ghc-version }}- diff --git a/.github/workflows/check-formatting.yaml b/.github/workflows/check-formatting.yaml index 140b900e20..835defb72e 100644 --- a/.github/workflows/check-formatting.yaml +++ b/.github/workflows/check-formatting.yaml @@ -37,8 +37,6 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-haskell - with: - cabal-project: dev-tool.project - working-directory: waspc run: ./run ormolu:check diff --git a/.github/workflows/waspc-build.yaml b/.github/workflows/waspc-build.yaml new file mode 100644 index 0000000000..31b3c173d4 --- /dev/null +++ b/.github/workflows/waspc-build.yaml @@ -0,0 +1,174 @@ +name: Build wasp-cli binaries for multiple platforms + +# We never trigger this workflow directly. +# We can call it manually (workflow_dispatch) or from other workflows (workflow_call). +on: + workflow_dispatch: + inputs: + ghc-version: + description: "GHC version to use" + default: "8.10.7" + required: false + node-version: + description: "Node.js version to use" + default: "22" + required: false + workflow_call: + inputs: + ghc-version: + description: "GHC version to use" + default: "8.10.7" + type: string + required: false + node-version: + description: "Node.js version to use" + default: "22" + type: string + required: false + +jobs: + build: + strategy: + fail-fast: false + + matrix: + # == Why such a big, heterogeneous list? == + # This is a bit of mish-mash of different platforms and architectures, + # so we need to build some of these directly in the runners, and some in + # containers. Each environment is a different OS and needs different + # dependencies. + # + # When possible, we build inside containers so we are not affected when + # GitHub updates the runners or deprecates old ones. When we build inside + # containers, the runner is only used to host the container, and all the + # steps are run inside the container and not the host (like a Dockerfile). + env: + - name: linux-x86_64 + runner: ubuntu-latest + # We use an old Ubuntu version so we can link to a low `glibc` version. + # `glibc` is backwards-compatible but not forwards-compatible, so it + # is a good idea to use the oldest version we reasonably can. Otherwise, + # the wasp binary would possibly not work on the system using an older + # glibc than what it was built with (e.g. an older Ubuntu version). + container: ubuntu:20.04 + static: false + install-deps: | + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + # GHCup dependencies (https://www.haskell.org/ghcup/install/#version-2004-2010) + apt-get install -y build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 + # Cabal dependencies + apt-get install -y zlib1g-dev + + # TODO: Add a Linux ARM64 build once we update the GHC version (#1446) + # GHC 8.10.7 does not support ARM64 on Linux yet + + - name: linux-x86_64-static + runner: ubuntu-latest + # actions/setup-node does not work in alpine. + # https://github.com/actions/setup-node/issues/1293 + # To work around this, we use the alpine variant of the official node + # image, which already has a working Node.js version installed. + container: node:${{ inputs.node-version }}-alpine + skip-node-install: true + static: true + install-deps: | + apk update + # GHCup dependencies (https://www.haskell.org/ghcup/install/#linux-alpine) + apk add binutils-gold curl gcc g++ gmp-dev libc-dev libffi-dev make musl-dev ncurses-dev perl pkgconfig tar xz + # `./run` script dependencies + apk add bash + # Cabal dependencies + apk add zlib-dev zlib-static + + - name: darwin-x86_64 + runner: macos-13 # Latest image still based on Intel architecture that can be used for free + + # macOS's syscalls are private and change between versions, so we + # can't statically link the binary. However, on macOS programs link + # to `libSystem`, which is quite stable between releases, so we're + # fine depending on it. + static: false + + - name: darwin-aarch64 + runner: macos-latest # Latest macOS images are already Apple Silicon-based + static: false # Check the comment above for why we can't statically link on macOS + install-deps: | + # We need to install llvm@13 for building on Apple Silicon (prebuilt libraries + # are only available for x86_64). The llvm@13 formula is not available in + # Homebrew by default, but we can edit it and comment out the `disable!` line. + curl -fsSL https://raw.githubusercontent.com/Homebrew/homebrew-core/74572f47ce6a2463c19d7fa164ab9fb8c91bbe61/Formula/l/llvm%4013.rb > /tmp/llvm@13.rb + sed -i '' 's/disable!/# disable!/' /tmp/llvm@13.rb + brew install --formula /tmp/llvm@13.rb + brew link --force llvm@13 + + runs-on: ${{ matrix.env.runner }} + container: ${{ matrix.env.container }} + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: ${{ matrix.env.install-deps }} + + - uses: ./.github/actions/setup-haskell + with: + ghc-version: ${{ inputs.ghc-version }} + + - uses: actions/setup-node@v4 + if: ${{ !matrix.env.skip-node-install }} + with: + node-version: ${{ inputs.node-version }} + + - name: Build and package + working-directory: waspc + env: + LC_ALL: C.UTF-8 # In some Docker containers the LOCALE is not UTF-8 by default + run: | + ./run build:all${{ matrix.env.static && ':static' || '' }} + mkdir -p artifacts + ./tools/make_binary_package.sh "artifacts/wasp-${{ matrix.env.name }}.tar.gz" + + - uses: actions/upload-artifact@v4 + with: + path: ./waspc/artifacts/* + name: wasp-${{ matrix.env.name }} + if-no-files-found: error + + build-universal: + needs: build + runs-on: macos-latest + steps: + - name: Download macOS binaries + uses: actions/download-artifact@v4 + with: + pattern: wasp-darwin-* + + - name: Unpack, create universal binary and pack + run: | + set -ex # Fail on error and print each command + + input_arch=( + darwin-x86_64 + darwin-aarch64 + ) + + # Extract each architecture + for arch in "${input_arch[@]}"; do + mkdir "arch-$arch" + tar -xzf "wasp-${arch}.tar.gz" -C "arch-$arch" + done + + mkdir universal + # Create the universal binary + lipo -create arch-*/wasp-bin -output universal/wasp-bin + # Copy the data folder too + cp -R "arch-${input_arch[0]}/data" universal/ + + # Pack back up + tar -czf wasp-darwin-universal.tar.gz -C universal . + + - uses: actions/upload-artifact@v4 + with: + name: wasp-darwin-universal + path: ./wasp-darwin-universal.tar.gz diff --git a/waspc/run b/waspc/run index 6b6f59f887..0793099f06 100755 --- a/waspc/run +++ b/waspc/run @@ -21,6 +21,7 @@ WASP_PACKAGES_COMPILE="${SCRIPT_DIR}/tools/install_packages_to_data_dir.sh" BUILD_HS_CMD="cabal build all" BUILD_HS_FULL_CMD="cabal build all --enable-tests --enable-benchmarks" BUILD_ALL_CMD="$WASP_PACKAGES_COMPILE && $BUILD_HS_CMD" +BUILD_ALL_STATIC_CMD="$WASP_PACKAGES_COMPILE && $BUILD_HS_CMD --enable-executable-static" INSTALL_CMD="$WASP_PACKAGES_COMPILE && cabal install --overwrite-policy=always" @@ -56,8 +57,8 @@ ORMOLU_CHECK_CMD="$ORMOLU_BASE_CMD --mode check "'$'"(git ls-files '*.hs' '*.hs- ORMOLU_FORMAT_CMD="$ORMOLU_BASE_CMD --mode inplace "'$'"(git ls-files '*.hs' '*.hs-boot')" echo_and_eval() { - echo -e $"${LIGHT_CYAN}Running:${DEFAULT_COLOR}" $1 "\n" - eval $1 + echo -e $"${LIGHT_CYAN}Running:${DEFAULT_COLOR}" "$1" "\n" + eval "$1" } echo_bold() { echo -e $"${BOLD}${1}${RESET}"; } @@ -78,6 +79,8 @@ print_usage() { "Builds the Haskell project." print_usage_cmd "build:all" \ "Builds the Haskell project + all sub-projects (i.e. TS packages)." + print_usage_cmd "build:all:static" \ + "Builds the Haskell project statically + all sub-projects (i.e. TS packages). Only useful for release builds. Needs to be run on a musl-based Linux distribution (e.g. Alpine)." print_usage_cmd "build:packages" \ "Builds the TypeScript projects under packages/." echo "" @@ -139,6 +142,9 @@ case $COMMAND in build:all) echo_and_eval "$BUILD_ALL_CMD" ;; + build:all:static) + echo_and_eval "$BUILD_ALL_STATIC_CMD" + ;; build:packages) echo_and_eval "$WASP_PACKAGES_COMPILE" ;;