From 5a3dd26223fad8b975eda439b8d405ecdc70aa7f Mon Sep 17 00:00:00 2001 From: zeme-wana <15709674+zeme-wana@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:33:33 +0200 Subject: [PATCH 1/5] Install Script --- .github/workflows/build-devcontainer.yml | 27 - .github/workflows/build-docker.yml | 29 + .github/workflows/build-ghc-cabal.yml | 56 ++ .github/workflows/build-nix.yml | 41 ++ .github/workflows/ci.yml | 37 ++ .gitignore | 2 + README.md | 162 ++--- cabal.project | 14 + dev/ci/build-docker.sh | 79 +++ dev/ci/build-ghc-cabal.sh | 138 ++++ dev/ci/build-nix.sh | 96 +++ dev/ci/lint.sh | 47 ++ dev/ci/run-all-local.sh | 76 +++ dev/ci/test-blueprint-parity.sh | 384 +++++++++++ dev/ci/test-install.sh | 454 +++++++++++++ dev/readmes/demeter.md | 51 ++ dev/readmes/docker.md | 59 ++ dev/readmes/ghc-cabal.md | 80 +++ dev/readmes/nix.md | 42 ++ install.sh | 798 +++++++++++++++++++++++ nix/outputs.nix | 12 +- nix/project.nix | 20 +- nix/shell.nix | 24 +- scripts/get-crypto-libs.sh | 492 ++++++++++++++ scripts/pkg-config | 55 ++ 25 files changed, 3138 insertions(+), 137 deletions(-) delete mode 100644 .github/workflows/build-devcontainer.yml create mode 100644 .github/workflows/build-docker.yml create mode 100644 .github/workflows/build-ghc-cabal.yml create mode 100644 .github/workflows/build-nix.yml create mode 100644 .github/workflows/ci.yml create mode 100755 dev/ci/build-docker.sh create mode 100755 dev/ci/build-ghc-cabal.sh create mode 100755 dev/ci/build-nix.sh create mode 100755 dev/ci/lint.sh create mode 100755 dev/ci/run-all-local.sh create mode 100755 dev/ci/test-blueprint-parity.sh create mode 100755 dev/ci/test-install.sh create mode 100644 dev/readmes/demeter.md create mode 100644 dev/readmes/docker.md create mode 100644 dev/readmes/ghc-cabal.md create mode 100644 dev/readmes/nix.md create mode 100755 install.sh create mode 100755 scripts/get-crypto-libs.sh create mode 100755 scripts/pkg-config diff --git a/.github/workflows/build-devcontainer.yml b/.github/workflows/build-devcontainer.yml deleted file mode 100644 index d10fc8b..0000000 --- a/.github/workflows/build-devcontainer.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Build DevContainer - -on: - workflow_dispatch: - pull_request: - -permissions: - contents: write - pull-requests: write - -jobs: - build-devcontainer: - name: Build DevContainer - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Build Project With Docker - run: | - # Note: the -i flag in "bash -ic" is necessary and causes bash to - # "source ~/.bashrc" which correctly populates the env. - docker run \ - -v ./.:/workspaces/plinth-template \ - -w /workspaces/plinth-template \ - -i ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog \ - bash -ic "cabal update && cabal build all && echo ':q' | cabal repl lib:plinth-validators" diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 0000000..d5f3e01 --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,29 @@ +# Full build of the docker template branch inside the devx devcontainer +# image — the same image devcontainers, Codespaces and standalone docker use. +# Locally: dev/ci/build-docker.sh (skips when docker is unavailable). +name: Build docker project + +on: + workflow_dispatch: + schedule: + - cron: "0 6 * * 1" + pull_request: + paths: + - "cabal.project" + - "plinth-template.cabal" + - "src/**" + - "app/**" + - ".devcontainer/**" + - "install.sh" + - "dev/ci/build-docker.sh" + - ".github/workflows/build-docker.yml" + +jobs: + build: + name: devx devcontainer (ghc96) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: dev/ci/build-docker.sh + env: + PLINTH_REQUIRE_DOCKER: "1" diff --git a/.github/workflows/build-ghc-cabal.yml b/.github/workflows/build-ghc-cabal.yml new file mode 100644 index 0000000..1327a0b --- /dev/null +++ b/.github/workflows/build-ghc-cabal.yml @@ -0,0 +1,56 @@ +# Full build of the ghc-cabal template branch with a real toolchain, on both +# supported GHC series and both OSes. Heavy (compiles plutus and friends), so +# it runs on demand, on a weekly schedule, and when the relevant inputs +# change. Locally: dev/ci/build-ghc-cabal.sh +name: Build ghc-cabal project + +on: + workflow_dispatch: + schedule: + - cron: "0 4 * * 1" + pull_request: + paths: + - "cabal.project" + - "plinth-template.cabal" + - "src/**" + - "app/**" + - "scripts/get-crypto-libs.sh" + - "scripts/pkg-config" + - "install.sh" + - "dev/ci/build-ghc-cabal.sh" + - ".github/workflows/build-ghc-cabal.yml" + +jobs: + build: + name: ghc ${{ matrix.ghc }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + ghc: ["9.6.7", "9.12.2"] + steps: + - uses: actions/checkout@v4 + + - uses: haskell-actions/setup@v2 + id: setup + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: latest + + - name: Ensure pkg-config (macOS) + if: runner.os == 'macOS' + run: command -v pkg-config || brew install pkgconf + + - name: Cache cabal store + uses: actions/cache@v4 + with: + path: | + ${{ steps.setup.outputs.cabal-store }} + ~/.cabal/packages + ~/.cache/cabal/packages + key: crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project') }} + restore-keys: | + crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}- + + - run: dev/ci/build-ghc-cabal.sh diff --git a/.github/workflows/build-nix.yml b/.github/workflows/build-nix.yml new file mode 100644 index 0000000..2f4c538 --- /dev/null +++ b/.github/workflows/build-nix.yml @@ -0,0 +1,41 @@ +# Full build of the nix template branch (which is also the demeter branch's +# content) inside its own `nix develop` shell, for both GHC series. Locally: +# dev/ci/build-nix.sh [--shell ghc96|ghc912] +name: Build nix project + +on: + workflow_dispatch: + schedule: + - cron: "0 5 * * 1" + pull_request: + paths: + - "cabal.project" + - "plinth-template.cabal" + - "src/**" + - "app/**" + - "nix/**" + - "flake.nix" + - "flake.lock" + - "install.sh" + - "dev/ci/build-nix.sh" + - ".github/workflows/build-nix.yml" + +jobs: + build: + name: nix develop .#${{ matrix.shell }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shell: [ghc96, ghc912] + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@v16 + with: + extra-conf: | + extra-substituters = https://cache.iog.io + extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= + accept-flake-config = true + + - run: dev/ci/build-nix.sh --shell ${{ matrix.shell }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab89ce0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +# Fast checks on every change: shell lint and end-to-end tests of install.sh +# (with a local git fixture — no network cloning), including exact manifests +# of the projects it creates for every environment. Everything runs through +# scripts in dev/ci/, which can be run locally exactly the same way +# (see dev/ci/run-all-local.sh). +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + lint: + name: Lint shell scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: dev/ci/lint.sh + + test-install: + name: install.sh (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + # Runners have no nix; install it on the Linux leg so test-install.sh + # also exercises install.sh's `--env nix` happy path. + - uses: DeterminateSystems/nix-installer-action@v16 + if: runner.os == 'Linux' + + - run: dev/ci/test-install.sh diff --git a/.gitignore b/.gitignore index f5e9200..c9add98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ dist-newstyle .pre-commit-config.yaml validator.uplc +blueprint.json cabal.project.freeze +cabal.project.local result \ No newline at end of file diff --git a/README.md b/README.md index 49f8608..66d331b 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,76 @@ -# Plinth Template Repository - -A template repository for your Plinth smart contract project. - -Plinth currently supports GHC `v9.6.x`. Cabal `v3.8+` is recommended. - -### 1. Create the repository - -- From the command line: +# Plinth Template + +Start a new [Plinth](https://plutus.cardano.intersectmbo.org/docs/) smart +contract project with one command: + +``` +curl -fsSL https://raw.githubusercontent.com/IntersectMBO/plinth-template/main/install.sh | sh +``` + +The installer asks which development environment you want, checks that the +required tools are installed, explains what is needed (in particular the +Cardano crypto C libraries), and creates a fresh project folder — named after +your project (default: `my-plinth-project`) — containing just the files that +environment needs: + +| Environment | What you need locally | Project contains | +| ------------ | ------------------------------------------- | ----------------------------- | +| Nix | nix (crypto libs provided by the shell) | sources + nix files | +| Docker | docker or a browser (Codespaces) | sources + .devcontainer | +| Demeter | just a browser (hosted; nix inside) | sources + nix files | +| GHC + Cabal | ghcup with GHC 9.6/9.12, cabal, pkg-config | sources + crypto-libs scripts | + +Each project comes with a README covering just that setup. You can also skip +the installer entirely and clone this repository directly — it carries the +union of all environments, and every setup works from it as-is. + +Whatever you pick, the first thing to run inside the project (and its +environment) is `cabal build all`, which compiles the example auction +validator. + +## About the crypto C libraries + +Plinth projects depend — via `plutus-core` and `cardano-crypto-class` — on +three C libraries: `libsodium` (VRF-patched), `libsecp256k1` and `libblst`. +The Nix shell, the Docker image and Demeter workspaces provide them (via +nix). GHC+Cabal projects instead download IOG's prebuilt, checksum- and +commit-pinned binaries from +[iohk-nix releases](https://github.com/input-output-hk/iohk-nix/releases) +on first build into a per-user cache (`~/.cache/plinth-crypto-libs`), linked +into the project at `dist-newstyle/crypto-libs/` — nothing is installed +system-wide; see [scripts/get-crypto-libs.sh](scripts/get-crypto-libs.sh) +(`--prefix` installs them system-wide instead) and the GHC+Cabal README +([dev/readmes/ghc-cabal.md](dev/readmes/ghc-cabal.md)). + +## Repository layout — for maintainers + +This single branch carries the union of every environment's files plus the +machinery around it; `install.sh` selects the relevant subset when creating a +project (see `include_file` and the marked blocks in `cabal.project` and +`nix/project.nix` it transforms): + +- [install.sh](install.sh) — the installer served over curl. `--from DIR` + installs from a local checkout (offline/CI); `--yes --env ...` runs it + non-interactively. +- [scripts/](scripts) — the crypto-libs bootstrap that ships with GHC+Cabal + projects ([get-crypto-libs.sh](scripts/get-crypto-libs.sh) and the + [pkg-config shim](scripts/pkg-config)). +- [dev/readmes/](dev/readmes) — the per-environment READMEs the installer + places into new projects. +- [dev/ci/](dev/ci) — the test suite. Every GitHub workflow is a thin + wrapper around one of these scripts, so everything can be run locally: ``` - gh repo create my-project --private --template IntersectMBO/plinth-template + dev/ci/run-all-local.sh # everything + PLINTH_SKIP_HEAVY=1 dev/ci/run-all-local.sh # fast checks only ``` -- Or from the [GitHub web page](https://github.com/IntersectMBO/plinth-template), click the top-right green button: - - `Use this template -> Create new repository` - -- Or just fork/clone `plinth-template` (but note that this is a template repository) - - More information on GitHub template repositories can be found [here](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). - -### 2. Setup your development environment - -
- With Nix (recommended) - - 1. Follow [these instructions](https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md) to install and configure nix, even if you already have it installed. - - 2. Then enter the shell using `nix develop`. - - > NOTE: - > The nix files inside this template follow the [`iogx` template](https://github.com/input-output-hk/iogx), but you can delete and replace them with your own. In that case, you might want to include the [`devx` flake](https://github.com/input-output-hk/devx/issues) in your flake inputs as a starting point to supply all the necessary dependencies, making sure to use one of the `-iog` flavors. - - > NOTE (for Windows users):
- > Make sure to have [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2) and the [WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) VSCode extension (if using VSCode) installed before the Nix setup. -
- -
- With Docker / Devcontainer / Codespaces - - - **Docker + Codespaces:** From the [GitHub web page](https://github.com/IntersectMBO/plinth-template), click the top-right green button: - - `Use this template -> Open in a codespace` - - - **Docker + Devcontainer:** - 1. Make sure to have [VSCode](https://code.visualstudio.com/) installed with the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension. - 2. Open this project in VSCode and let it create a local codespace for you (See Dev Containers instructions, if needed). - - - **Stand-alone Docker:** Change the `/path/to/my-project` accordingly and run: - - ``` - docker run \ - -v /path/to/my-project:/workspaces/my-project \ - -it ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog - ``` - - > NOTE: - > You can modify your [`devcontainer.json`](./.devcontainer/devcontainer.json) file to customize the container (more info [here](https://github.com/input-output-hk/devx?tab=readme-ov-file#vscode-devcontainer--github-codespace-support)). - - > NOTE: - > When using this approach, you can ignore/delete/replace the Nix files entirely. - - > NOTE (for Windows users):
- > It is recommended to install and run Docker on your native OS. If you want to run Docker Desktop inside a VM, read through [these notes](https://docs.docker.com/desktop/setup/vm-vdi/). -
- -
- With Demeter - - 1. Create an account in [Demeter](https://demeter.run/). - - 2. Follow [their instructions](https://docs.demeter.run/guides/getting-started) to setup a remote development environment. - - > IMPORTANT: - > Demeter uses its own infrastructure and packages. If something is not working correctly, please contact them before creating an issue. - - > NOTE: - > When using this approach, you can ignore/delete/replace the Nix files entirely. -
- -
- With manually-installed dependencies (not recommended) -
- - Follow the instructions for [cardano-node](https://developers.cardano.org/docs/get-started/cardano-node/installing-cardano-node/) for a custom setup. - - > NOTE: - > When using this approach, you can ignore/delete/replace the Nix files entirely. -
- -### 3. Run the example application - -Run `cabal update` first, then read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) to get started. + | Script | Checks | Workflow | + | -------------------------- | --------------------------------------------------- | ---------------------- | + | `lint.sh` | shellcheck + syntax over all shell scripts | `ci.yml` | + | `test-install.sh` | install.sh end-to-end: exact per-env manifests, | `ci.yml` | + | | transforms, failure modes, real crypto download | | + | `build-ghc-cabal.sh` | full build of an installed GHC+Cabal project | `build-ghc-cabal.yml` | + | `build-nix.sh` | full build of an installed Nix project (= Demeter) | `build-nix.yml` | + | `build-docker.sh` | full build inside the devx devcontainer image | `build-docker.yml` | + | `test-blueprint-parity.sh` | blueprint byte-parity between ghcup and nix | (manual) | diff --git a/cabal.project b/cabal.project index e15e012..260eadd 100644 --- a/cabal.project +++ b/cabal.project @@ -17,3 +17,17 @@ index-state: packages: ./. + +-- BEGIN ghc-cabal only +-- (install.sh removes this whole block for Nix/Docker/Demeter projects) +-- Route pkg-config through the repo shim so that a plain `cabal build` +-- works without nix and without system-wide crypto C libraries: on first +-- use the shim downloads IOG's prebuilt libsodium/libsecp256k1/libblst +-- (sha256- and commit-pinned, cached per user) and answers pkg-config +-- queries from them. Inside nix shells, or when +-- PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 is set, the shim delegates to the real +-- pkg-config. NOTE: the relative path resolves against the directory cabal +-- is invoked from, so run cabal from the repository root. +program-locations + pkg-config-location: ./scripts/pkg-config +-- END ghc-cabal only diff --git a/dev/ci/build-docker.sh b/dev/ci/build-docker.sh new file mode 100755 index 0000000..6e38498 --- /dev/null +++ b/dev/ci/build-docker.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Build a Docker project end-to-end: create it with install.sh --env docker +# (or take a pre-created one as $1) and build it inside the devx devcontainer +# image — the same image devcontainers, Codespaces and the standalone +# `docker run` flow use. +# +# The image is x86_64-linux; on other architectures docker's emulation is +# used automatically (slow but correct). +# +# When docker is unavailable the script SKIPS (exit 0) with a loud notice, +# unless PLINTH_REQUIRE_DOCKER=1 (set in CI) makes that a failure. +# +# Environment: +# PLINTH_REQUIRE_DOCKER=1 Fail instead of skipping when docker is missing. +# PLINTH_CI_KEEP=1 Keep the scratch directory (printed). + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +IMAGE="ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog" +TREE="${1:-}" + +fail() { echo "build-docker: FAIL: $*" >&2; exit 1; } +note() { echo "build-docker: $*"; } + +if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then + if [ "${PLINTH_REQUIRE_DOCKER:-0}" = 1 ]; then + fail "docker is not available (PLINTH_REQUIRE_DOCKER=1)" + fi + echo "build-docker: SKIPPED — docker is not installed or its daemon is not running." >&2 + echo "build-docker: start docker and re-run, or rely on CI for this branch." >&2 + exit 0 +fi + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-docker.XXXXXX")" +cleanup() { + if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then + echo "build-docker: scratch dir kept: $WORK" + else + rm -rf "$WORK" + fi +} +trap cleanup EXIT + +PROJECT="$WORK/project" +if [ -z "$TREE" ]; then + sh "$ROOT/install.sh" --yes --env docker --docker-mode standalone --from "$ROOT" \ + --dir "$PROJECT" >"$WORK/install.log" 2>&1 \ + || { cat "$WORK/install.log" >&2; fail "install.sh --env docker failed"; } +else + mkdir -p "$PROJECT" + cp -R "$TREE/." "$PROJECT/" +fi +[ -f "$PROJECT/.devcontainer/devcontainer.json" ] || fail "$PROJECT is not a docker project" + +# `bash -ic` is required: the devx image loads the nix toolchain environment +# from ~/.bashrc, which only interactive shells source. +note "building inside $IMAGE ..." +docker run --rm \ + -v "$PROJECT:/workspaces/plinth-template" \ + -w /workspaces/plinth-template \ + -i "$IMAGE" \ + bash -ic ' + set -euo pipefail + echo "build-docker(container): ghc $(ghc --numeric-version) at $(command -v ghc)" + pkg-config --exists libsodium libsecp256k1 libblst \ + || { echo "build-docker(container): FAIL: crypto libs not provided by the image" >&2; exit 1; } + echo "build-docker(container): crypto libs provided by the image (via nix)" + cabal update + cabal build all + cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json + [ -s blueprint.json ] || { echo "build-docker(container): FAIL: empty blueprint" >&2; exit 1; } + echo "build-docker(container): blueprint OK" + ' || fail "container build failed" + +echo "build-docker: SUCCESS" diff --git a/dev/ci/build-ghc-cabal.sh b/dev/ci/build-ghc-cabal.sh new file mode 100755 index 0000000..c21e625 --- /dev/null +++ b/dev/ci/build-ghc-cabal.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# Build a GHC+Cabal project end-to-end with the host's ghc+cabal: create the +# project with install.sh --env cabal (or take a pre-created one as $1), run +# `cabal build all` there — proving the pkg-config shim bootstraps the crypto +# libraries with no manual step — then generate the example blueprint and +# assert the produced executable links the crypto libraries from the plinth +# cache. +# +# Requirements: ghc 9.6.x or 9.12.x, cabal >= 3.8, pkg-config, network. +# +# Environment: +# CABAL_STORE_DIR Use a dedicated cabal store (passed as --store-dir); +# speeds up repeated local runs enormously. +# PLINTH_CI_KEEP=1 Keep the scratch directory (printed) for inspection. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +TREE="${1:-}" + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-cabal.XXXXXX")" +cleanup() { + if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then + echo "build-ghc-cabal: scratch dir kept: $WORK" + else + rm -rf "$WORK" + fi +} +trap cleanup EXIT + +fail() { echo "build-ghc-cabal: FAIL: $*" >&2; exit 1; } +note() { echo "build-ghc-cabal: $*"; } + +# -------------------------------------------------------------------------- +# Toolchain assertions (same requirements install.sh enforces) +# -------------------------------------------------------------------------- + +command -v ghc >/dev/null 2>&1 || fail "ghc not on PATH" +command -v cabal >/dev/null 2>&1 || fail "cabal not on PATH" +command -v pkg-config >/dev/null 2>&1 || fail "pkg-config not on PATH" +GHC_VERSION="$(ghc --numeric-version)" +case "$GHC_VERSION" in + 9.6.*|9.12.*) note "ghc $GHC_VERSION ($(command -v ghc))" ;; + *) fail "unsupported ghc $GHC_VERSION (need 9.6.x or 9.12.x)" ;; +esac +note "cabal $(cabal --numeric-version) ($(command -v cabal))" + +# -------------------------------------------------------------------------- +# Fresh copy of the branch tree +# -------------------------------------------------------------------------- + +PROJECT="$WORK/project" +if [ -z "$TREE" ]; then + sh "$ROOT/install.sh" --yes --env cabal --from "$ROOT" \ + --dir "$PROJECT" --crypto-libs skip >"$WORK/install.log" 2>&1 \ + || { cat "$WORK/install.log" >&2; fail "install.sh --env cabal failed"; } +else + mkdir -p "$PROJECT" + cp -R "$TREE/." "$PROJECT/" +fi +[ -f "$PROJECT/scripts/get-crypto-libs.sh" ] || fail "$PROJECT is not a ghc-cabal project" +cd "$PROJECT" + +cabal=(cabal) +if [ -n "${CABAL_STORE_DIR:-}" ]; then + mkdir -p "$CABAL_STORE_DIR" + cabal+=(--store-dir="$CABAL_STORE_DIR") + note "using cabal store: $CABAL_STORE_DIR" +fi + +if [ -n "${CI:-}" ]; then + note "CI: running cabal update" + "${cabal[@]}" update +fi + +# -------------------------------------------------------------------------- +# Build. The very first cold build on a fresh clone stops once, after the +# shim has written cabal.project.local (documented in the branch README); +# a plain re-run completes. +# -------------------------------------------------------------------------- + +note "building (cabal build all)..." +if ! "${cabal[@]}" build all; then + [ -f cabal.project.local ] \ + || fail "build failed without writing cabal.project.local (not the documented cold-start stop)" + note "documented cold-start stop; re-running cabal build all" + "${cabal[@]}" build all +fi + +# (regex, not the exact $PROJECT prefix: macOS mktemp paths appear both as +# /var/... and /private/var/... depending on how they were resolved) +[ -f cabal.project.local ] || fail "cabal.project.local was not created by the shim" +grep -q "pkg-config-location: .*/scripts/pkg-config" cabal.project.local \ + || fail "cabal.project.local does not point at the pkg-config shim" + +# -------------------------------------------------------------------------- +# Blueprint generation + provenance of the linked crypto libraries +# -------------------------------------------------------------------------- + +note "generating the example blueprint..." +"${cabal[@]}" run -v0 exe:gen-auction-validator-blueprint -- "$WORK/blueprint.json" +python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$WORK/blueprint.json" \ + || fail "blueprint output is not valid JSON" +note "blueprint OK: $(wc -c < "$WORK/blueprint.json" | tr -d ' ') bytes" + +# The libraries' real location is the per-user cache; PLINTH_CRYPTO_LIBS_HOME +# overrides it (get-crypto-libs.sh defaults to .../plinth-crypto-libs). +CRYPTO_MARKER="${PLINTH_CRYPTO_LIBS_HOME:-plinth-crypto-libs}" + +bin="$("${cabal[@]}" list-bin exe:gen-auction-validator-blueprint)" +case "$(uname -s)" in + Darwin) + links="$(otool -L "$bin")" + echo "$links" | grep -qF "$CRYPTO_MARKER" \ + || fail "executable does not link crypto libs from the plinth cache ($CRYPTO_MARKER): +$links" + for bad in /nix/store /opt/homebrew "/usr/local/lib"; do + if echo "$links" | grep -qE "(libsodium|libsecp256k1|libblst).*$bad|$bad.*(libsodium|libsecp256k1|libblst)"; then + fail "executable links crypto libs from $bad: +$links" + fi + done + note "otool: crypto libs come from the plinth cache only" + ;; + Linux) + links="$(ldd "$bin" 2>/dev/null || true)" + if echo "$links" | grep -E 'libsodium|libsecp256k1|libblst' | grep -vqF "$CRYPTO_MARKER"; then + fail "executable resolves crypto libs outside the plinth cache ($CRYPTO_MARKER): +$links" + fi + note "ldd: crypto libs come from the plinth cache only (or are absent/static)" + ;; +esac + +echo "build-ghc-cabal: SUCCESS (ghc $GHC_VERSION)" diff --git a/dev/ci/build-nix.sh b/dev/ci/build-nix.sh new file mode 100755 index 0000000..5800226 --- /dev/null +++ b/dev/ci/build-nix.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# +# Build a Nix project end-to-end: create it with install.sh --env nix (or +# take a pre-created one as $1), enter its `nix develop` shell and run +# `cabal build all` plus the example blueprint. +# +# dev/ci/build-nix.sh [TREE] [--shell ghc96|ghc912|default] +# +# Demeter projects have identical nix content, so this also covers demeter. +# +# Requirements: nix with flakes. Configure IOG's binary caches (see the +# nix-setup-guide) or the first run will build GHC from source. +# +# Environment: +# PLINTH_CI_KEEP=1 Keep the scratch directory (printed) for inspection. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +TREE="" +SHELL_NAME="default" +while [ $# -gt 0 ]; do + case "$1" in + --shell) shift; SHELL_NAME="${1:?--shell needs an argument}" ;; + --shell=*) SHELL_NAME="${1#--shell=}" ;; + *) TREE="$1" ;; + esac + shift +done + +fail() { echo "build-nix: FAIL: $*" >&2; exit 1; } +note() { echo "build-nix: $*"; } + +command -v nix >/dev/null 2>&1 || fail "nix not on PATH" + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-nix.XXXXXX")" +# nix's path: fetcher rejects paths with symlinked ancestors (macOS /var); +# use the physical path. +WORK="$(cd "$WORK" && pwd -P)" +cleanup() { + if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then + echo "build-nix: scratch dir kept: $WORK" + else + rm -rf "$WORK" + fi +} +trap cleanup EXIT + +PROJECT="$WORK/project" +if [ -z "$TREE" ]; then + sh "$ROOT/install.sh" --yes --env nix --from "$ROOT" --dir "$PROJECT" \ + >"$WORK/install.log" 2>&1 \ + || { cat "$WORK/install.log" >&2; fail "install.sh --env nix failed"; } +else + mkdir -p "$PROJECT" + cp -R "$TREE/." "$PROJECT/" +fi +[ -f "$PROJECT/flake.nix" ] || fail "$PROJECT is not a nix project" + +# cd matters: `nix develop --command` runs in the calling directory, and the +# in-shell cabal must build the branch copy, not whatever cwd we came from. +cd "$PROJECT" + +note "entering nix develop path:$PROJECT#$SHELL_NAME (first run may take a while)..." +# shellcheck disable=SC2016 # the inner script must expand inside the shell +nix develop "path:$PROJECT#$SHELL_NAME" --accept-flake-config --command bash -c ' + set -euo pipefail + note() { echo "build-nix(shell): $*"; } + + case "$(command -v ghc)" in + /nix/store/*) note "ghc $(ghc --numeric-version) from /nix/store" ;; + *) echo "build-nix(shell): FAIL: ghc not from /nix/store: $(command -v ghc)" >&2; exit 1 ;; + esac + case "$(command -v cabal)" in + /nix/store/*) note "cabal $(cabal --numeric-version) from /nix/store" ;; + *) echo "build-nix(shell): FAIL: cabal not from /nix/store: $(command -v cabal)" >&2; exit 1 ;; + esac + pkg-config --exists libsodium libsecp256k1 libblst \ + || { echo "build-nix(shell): FAIL: crypto libs not visible to pkg-config" >&2; exit 1; } + note "crypto libs provided by the shell: sodium $(pkg-config --modversion libsodium), secp256k1 $(pkg-config --modversion libsecp256k1), blst $(pkg-config --modversion libblst)" + + if [ -n "${CI:-}" ]; then + note "CI: running cabal update" + cabal update + fi + + note "building (cabal build all)..." + cabal build all + cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json + [ -s blueprint.json ] || { echo "build-nix(shell): FAIL: empty blueprint" >&2; exit 1; } + note "blueprint OK: $(wc -c < blueprint.json | tr -d " ") bytes" +' || fail "nix shell build failed" + +echo "build-nix: SUCCESS (shell: $SHELL_NAME)" diff --git a/dev/ci/lint.sh b/dev/ci/lint.sh new file mode 100755 index 0000000..8c225fc --- /dev/null +++ b/dev/ci/lint.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# Lint all shell scripts in the repository: syntax check plus shellcheck. +# Runs locally (falls back to `nix run nixpkgs#shellcheck` when shellcheck is +# not installed) and in CI (ubuntu runners ship shellcheck). + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +SCRIPTS=( + install.sh + scripts/pkg-config + scripts/get-crypto-libs.sh + dev/ci/lint.sh + dev/ci/test-install.sh + dev/ci/build-ghc-cabal.sh + dev/ci/build-nix.sh + dev/ci/build-docker.sh + dev/ci/run-all-local.sh + dev/ci/test-blueprint-parity.sh +) + +for f in "${SCRIPTS[@]}"; do + [ -f "$f" ] || { echo "lint: missing $f" >&2; exit 1; } + case "$(head -1 "$f")" in + '#!/bin/sh'*) sh -n "$f" ;; + *) bash -n "$f" ;; + esac + echo "lint: syntax ok: $f" +done + +if command -v shellcheck >/dev/null 2>&1; then + SHELLCHECK=(shellcheck) +elif command -v nix >/dev/null 2>&1; then + echo "lint: shellcheck not installed; using 'nix run nixpkgs#shellcheck'" + SHELLCHECK=(nix run nixpkgs#shellcheck --) +else + echo "lint: ERROR: shellcheck not found (and no nix to run it with)" >&2 + exit 1 +fi + +# SC1091: sourced files aren't followed; SC2016: single-quoted $ is often +# intentional in messages that tell the user what to run. +"${SHELLCHECK[@]}" --exclude=SC1091 "${SCRIPTS[@]}" +echo "lint: shellcheck ok (${#SCRIPTS[@]} files)" diff --git a/dev/ci/run-all-local.sh b/dev/ci/run-all-local.sh new file mode 100755 index 0000000..a8b3a95 --- /dev/null +++ b/dev/ci/run-all-local.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Run the full test suite locally — the same scripts the GitHub workflows +# run, without the Actions layer. +# +# dev/ci/run-all-local.sh # everything +# PLINTH_SKIP_HEAVY=1 dev/ci/run-all-local.sh # fast checks only +# +# The heavy steps compile the project; to speed up repeated ghc-cabal runs, +# point CABAL_STORE_DIR at a persistent directory. + +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" || exit 1 + +PASS=() +FAIL=() +SKIP=() + +run_step() { + name="$1"; shift + echo "" + echo "===================================================================" + echo "=== $name" + echo "===================================================================" + if "$@"; then + PASS+=("$name") + else + FAIL+=("$name") + fi +} + +skip_step() { + SKIP+=("$1 — $2") + echo "" + echo "=== $1: SKIPPED ($2)" +} + +run_step "lint" dev/ci/lint.sh +run_step "test-install" dev/ci/test-install.sh + +if [ "${PLINTH_SKIP_HEAVY:-0}" = 1 ]; then + skip_step "build-ghc-cabal" "PLINTH_SKIP_HEAVY=1" + skip_step "build-nix" "PLINTH_SKIP_HEAVY=1" + skip_step "build-docker" "PLINTH_SKIP_HEAVY=1" +else + if command -v ghc >/dev/null 2>&1 && command -v cabal >/dev/null 2>&1; then + run_step "build-ghc-cabal" dev/ci/build-ghc-cabal.sh + else + skip_step "build-ghc-cabal" "no ghc/cabal on PATH" + fi + if command -v nix >/dev/null 2>&1; then + run_step "build-nix" dev/ci/build-nix.sh + else + skip_step "build-nix" "no nix on PATH" + fi + # build-docker.sh skips by itself when docker is unavailable + run_step "build-docker" dev/ci/build-docker.sh +fi + +echo "" +echo "===================================================================" +echo "Summary" +echo "===================================================================" +for s in ${PASS+"${PASS[@]}"}; do echo " PASS $s"; done +for s in ${SKIP+"${SKIP[@]}"}; do echo " SKIP $s"; done +for s in ${FAIL+"${FAIL[@]}"}; do echo " FAIL $s"; done + +if [ "${#FAIL[@]}" -gt 0 ]; then + echo "" + echo "run-all-local: ${#FAIL[@]} step(s) failed" >&2 + exit 1 +fi +echo "" +echo "run-all-local: all steps passed" diff --git a/dev/ci/test-blueprint-parity.sh b/dev/ci/test-blueprint-parity.sh new file mode 100755 index 0000000..c97ded5 --- /dev/null +++ b/dev/ci/test-blueprint-parity.sh @@ -0,0 +1,384 @@ +#!/usr/bin/env bash +# +# Test that `cabal run gen-auction-validator-blueprint` produces byte-identical +# output: +# +# * across compilers: GHC 9.6.7 and GHC 9.12.2 +# * across environments: ghcup toolchain vs `nix develop` shells +# +# Four scenarios are built and compared: +# +# 1. ghcup cabal + ghcup ghc-9.6.7 + prebuilt IOG crypto libs (flag ON) +# 2. ghcup cabal + ghcup ghc-9.12.2 + prebuilt IOG crypto libs (flag ON) +# 3. nix develop .#ghc96 (ghc 9.6.7, crypto libs from nix, flag OFF) +# 4. nix develop .#ghc912 (ghc 9.12.2, crypto libs from nix, flag OFF) +# +# The script asserts aggressively that every toolchain component and the +# crypto C libraries come from the expected source in each scenario: +# +# * ghcup scenarios: ghc/cabal resolve to $HOME/.ghcup/bin, and cabal reaches +# the crypto libraries exclusively through the scripts/pkg-config shim +# declared in cabal.project (which downloads them on first use — no manual +# bootstrap step is performed by this test — and restricts the real +# pkg-config to them with PKG_CONFIG_LIBDIR); the produced executable links +# crypto dylibs from dist-newstyle/crypto-libs/ and nowhere else. +# * nix scenarios: ghc/cabal resolve to /nix/store, the prebuilt-libs flag +# is OFF (PLINTH_USE_SYSTEM_CRYPTO_LIBS=1, and we assert the script +# no-ops), and the produced executable links crypto dylibs from /nix/store +# and in particular NOT from dist-newstyle/crypto-libs/. +# +# Usage: +# dev/ci/test-blueprint-parity.sh # run all four scenarios +# dev/ci/test-blueprint-parity.sh ghcup-ghc967 # run a single scenario +# (scenarios: ghcup-ghc967 ghcup-ghc9122 nix-ghc96 nix-ghc912 compare) +# +# Environment: +# PARITY_FRESH=1 Wipe the downloaded crypto libs, the test +# cabal store and the test builddirs first, so +# the ghcup scenarios prove the whole +# cold-start flow (solver bootstrap included). +# PARITY_ALLOW_GHC_DIVERGENCE=1 Downgrade the cross-GHC comparison to a +# warning (see compare_outputs). + +# shellcheck disable=SC2015 # A && B || C used deliberately in assertions +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +# The pkg-config-location in cabal.project is relative to the invocation dir. +cd "$ROOT" +TESTDIR="$ROOT/dist-newstyle/test" +STORE_DIR="$TESTDIR/store" +BLUEPRINTS="$TESTDIR/blueprints" +GHCUP_BIN="$HOME/.ghcup/bin" + +GHC96_VERSION="9.6.7" +GHC912_VERSION="9.12.2" + +mkdir -p "$BLUEPRINTS" + +# -------------------------------------------------------------------------- +# Assertion helpers +# -------------------------------------------------------------------------- + +fail() { + echo "ASSERTION FAILED: $*" >&2 + exit 1 +} + +note() { echo " [ok] $*"; } +banner() { echo; echo "=== $* ==="; } + +assert_eq() { + # assert_eq + [ "$2" = "$3" ] || fail "$1: expected '$2', got '$3'" + note "$1 = $2" +} + +assert_prefix() { + # assert_prefix + case "$3" in + "$2"*) note "$1 = $3 (under $2)" ;; + *) fail "$1: expected something under '$2', got '$3'" ;; + esac +} + +assert_contains() { + # assert_contains + case "$3" in + *"$2"*) note "$1 contains '$2'" ;; + *) fail "$1: does not contain '$2'. Full text: +$3" ;; + esac +} + +assert_not_contains() { + # assert_not_contains + case "$3" in + *"$2"*) fail "$1: unexpectedly contains '$2'. Full text: +$3" ;; + *) note "$1 does not contain '$2'" ;; + esac +} + +linked_libs() { + # Print the dynamic libraries an executable links against. + case "$(uname -s)" in + Darwin) /usr/bin/otool -L "$1" | tail -n +2 | awk '{print $1}' ;; + Linux) ldd "$1" | awk '{print $3}' ;; + esac +} + +plan_field() { + # plan_field + python3 - "$1/cache/plan.json" "$2" <<'EOF' +import json, sys +p = json.load(open(sys.argv[1])) +print(eval(sys.argv[2])) +EOF +} + +# -------------------------------------------------------------------------- +# ghcup scenarios (prebuilt crypto libs flag ON — the default) +# -------------------------------------------------------------------------- + +run_ghcup_scenario() { + # run_ghcup_scenario + local ghc_version="$1" + local scenario="ghcup-ghc${ghc_version//./}" + local builddir="$TESTDIR/$scenario" + local out="$BLUEPRINTS/$scenario.json" + + banner "Scenario $scenario: ghcup cabal + ghcup ghc-$ghc_version + prebuilt IOG crypto libs (via scripts/pkg-config shim)" + + # Hermetic PATH: ghcup, the OS, and the directory of the real pkg-config + # (the shim's backend). The crypto libs can still only be reached through + # the scripts/pkg-config shim that cabal.project points cabal at: the shim + # restricts the real pkg-config to the downloaded libraries with + # PKG_CONFIG_LIBDIR, masking every system search directory. + command -v pkg-config >/dev/null 2>&1 \ + || fail "pkg-config not found; install it (e.g. brew install pkgconf / apt install pkg-config)" + local pkgconfig_dir + pkgconfig_dir="$(dirname "$(command -v pkg-config)")" + local path="$GHCUP_BIN:$pkgconfig_dir:/usr/bin:/bin:/usr/sbin:/sbin" + + local platform + case "$(uname -s)-$(uname -m)" in + Darwin-arm64) platform="arm64-macos" ;; + Darwin-x86_64) platform="x86_64-macos" ;; + Linux-*) platform="debian" ;; + *) fail "unsupported platform for the ghcup scenario" ;; + esac + local prefix="$ROOT/dist-newstyle/crypto-libs/$platform" + local pcdir="$prefix/lib/pkgconfig" + + # 1. Assert the toolchain comes from ghcup. + local ghc_path cabal_path + ghc_path="$(env -i PATH="$path" sh -c "command -v ghc-$ghc_version")" \ + || fail "ghc-$ghc_version not found in $GHCUP_BIN (install with: ghcup install ghc $ghc_version)" + cabal_path="$(env -i PATH="$path" sh -c 'command -v cabal')" || fail "cabal not found in $GHCUP_BIN" + assert_prefix "$scenario: ghc" "$GHCUP_BIN/" "$ghc_path" + assert_prefix "$scenario: cabal" "$GHCUP_BIN/" "$cabal_path" + assert_eq "$scenario: ghc version" "$ghc_version" \ + "$(env -i PATH="$path" sh -c "ghc-$ghc_version --numeric-version")" + + # 2. Build and run — NO manual bootstrap: cabal's solver invokes the shim, + # and the shim downloads and installs the crypto libs on first use. A + # dedicated builddir is used because cabal caches install plans and would + # not re-solve after environment changes; a dedicated store keeps + # artifacts built against other library sources out of reach. + local cabal=(env -i HOME="$HOME" PATH="$path" + cabal --store-dir="$STORE_DIR") + local flags=(-w "ghc-$ghc_version" --builddir="$builddir") + + # On a cold start (no cabal.project.local yet) the first build bootstraps + # the libs and writes cabal.project.local mid-run, too late for its own + # configure phase — it stops once, and the re-run picks the file up. Any + # other failure must fail twice and therefore still fails the test. + if ! "${cabal[@]}" build "${flags[@]}" exe:gen-auction-validator-blueprint; then + [ -f "$ROOT/cabal.project.local" ] \ + || fail "$scenario: cabal build failed without writing cabal.project.local" + note "cold-start build stopped once as documented; re-running with cabal.project.local in place" + "${cabal[@]}" build "${flags[@]}" exe:gen-auction-validator-blueprint + fi + rm -f "$out" + "${cabal[@]}" run -v0 "${flags[@]}" exe:gen-auction-validator-blueprint -- "$out" + [ -s "$out" ] || fail "$scenario: blueprint file was not produced" + + # 3. The build itself must have bootstrapped the libs via the shim, and the + # shim must resolve them exclusively from the local install. + [ -f "$pcdir/libsodium.pc" ] && [ -f "$pcdir/libsecp256k1.pc" ] && [ -f "$pcdir/libblst.pc" ] \ + || fail "$scenario: cabal did not bootstrap the crypto libs via the shim" + [ -L "$prefix" ] \ + || fail "$scenario: $prefix should be a symlink into the per-user cache" + # The real install lives in the per-user cache; the project path is a + # symlink to it, and all recorded paths (pc prefix, install names) use the + # resolved cache location. + local real_prefix + real_prefix="$(cd "$prefix" && pwd -P)" + note "cabal bootstrapped the crypto libs: $prefix -> $real_prefix" + grep -qF "pkg-config-location: $ROOT/scripts/pkg-config" "$ROOT/cabal.project.local" \ + || fail "$scenario: cabal.project.local does not carry the shim stanza" + note "cabal.project.local carries the absolute shim location for package configure" + assert_eq "$scenario: shim libsodium prefix" "$real_prefix" \ + "$(env -i PATH="$path" "$ROOT/scripts/pkg-config" --variable=prefix libsodium)" + env -i PATH="$path" "$ROOT/scripts/pkg-config" --exists libblst libsecp256k1 \ + || fail "$scenario: shim cannot resolve libblst/libsecp256k1" + note "shim resolves libsodium/libsecp256k1/libblst exclusively from the local install" + + # 4. Assert the plan used the expected compiler. + assert_eq "$scenario: plan compiler-id" "ghc-$ghc_version" \ + "$(plan_field "$builddir" "p['compiler-id']")" + + # 5. Assert the built executable links the crypto libs from the per-user + # cache (through which the shim serves them) and nowhere else. + local bin libs + bin="$("${cabal[@]}" list-bin "${flags[@]}" exe:gen-auction-validator-blueprint)" + libs="$(linked_libs "$bin")" + assert_contains "$scenario: linked libsodium" "$real_prefix/lib/libsodium" "$libs" + assert_contains "$scenario: linked libsecp256k1" "$real_prefix/lib/libsecp256k1" "$libs" + assert_not_contains "$scenario: linked libs" "/nix/store" "$libs" + assert_not_contains "$scenario: linked libs" "/opt/homebrew" "$libs" + assert_not_contains "$scenario: linked libs" "/usr/local" "$libs" + + echo "Scenario $scenario OK -> $out" +} + +# -------------------------------------------------------------------------- +# nix scenarios (prebuilt crypto libs flag OFF — libs must come from nix) +# -------------------------------------------------------------------------- + +run_nix_scenario() { + # run_nix_scenario + local shell="$1" ghc_version="$2" + local scenario="nix-$shell" + + banner "Scenario $scenario: nix develop .#$shell (ghc $ghc_version, crypto libs from nix)" + + command -v nix >/dev/null 2>&1 || fail "nix not found" + + # PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 turns the prebuilt-libs feature OFF; + # inside the nix shell the libs are provided by the iohk-nix overlays. + PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 \ + nix develop "$ROOT#$shell" --command bash "$ROOT/dev/ci/test-blueprint-parity.sh" \ + --inner-nix "$scenario" "$ghc_version" + + echo "Scenario $scenario OK -> $BLUEPRINTS/$scenario.json" +} + +inner_nix() { + # Runs INSIDE the nix shell. + local scenario="$1" ghc_version="$2" + local builddir="$TESTDIR/$scenario" + local out="$BLUEPRINTS/$scenario.json" + + # 1. Assert the flag is OFF and the get-crypto-libs script is a no-op. + assert_eq "$scenario: PLINTH_USE_SYSTEM_CRYPTO_LIBS" "1" "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-}" + local msg + msg="$("$ROOT/scripts/get-crypto-libs.sh")" + assert_contains "$scenario: get-crypto-libs.sh" "skipping download" "$msg" + [ -z "${PKG_CONFIG_LIBDIR:-}" ] || fail "$scenario: PKG_CONFIG_LIBDIR leaked into the nix shell" + + # 2. Assert the toolchain comes from the nix store. + local ghc_path cabal_path + ghc_path="$(command -v ghc)" || fail "no ghc in the nix shell" + cabal_path="$(command -v cabal)" || fail "no cabal in the nix shell" + assert_prefix "$scenario: ghc" "/nix/store/" "$ghc_path" + assert_prefix "$scenario: cabal" "/nix/store/" "$cabal_path" + assert_eq "$scenario: ghc version" "$ghc_version" "$(ghc --numeric-version)" + + # 3. Build and run. + rm -f "$out" + cabal build --builddir="$builddir" exe:gen-auction-validator-blueprint + cabal run -v0 --builddir="$builddir" exe:gen-auction-validator-blueprint -- "$out" + [ -s "$out" ] || fail "$scenario: blueprint file was not produced" + + # 4. Assert the plan used the expected compiler. + assert_eq "$scenario: plan compiler-id" "ghc-$ghc_version" \ + "$(plan_field "$builddir" "p['compiler-id']")" + + # 5. Assert the executable links crypto libs from the nix store only. + # (Depending on the iohk-nix overlay they may be linked statically, in + # which case they don't show up at all — what must NEVER show up is a + # crypto lib from outside the nix store.) + local bin libs crypto_libs + bin="$(cabal list-bin --builddir="$builddir" exe:gen-auction-validator-blueprint)" + libs="$(linked_libs "$bin")" + crypto_libs="$(echo "$libs" | grep -iE 'sodium|secp256k1|blst' || true)" + if [ -n "$crypto_libs" ]; then + while IFS= read -r lib; do + assert_prefix "$scenario: linked crypto lib" "/nix/store/" "$lib" + done <<< "$crypto_libs" + else + note "crypto libs are statically linked (not in the dynamic link table)" + fi + assert_not_contains "$scenario: linked libs" "dist-newstyle/crypto-libs" "$libs" + assert_not_contains "$scenario: linked libs" "plinth-crypto-libs" "$libs" + assert_not_contains "$scenario: linked libs" "/opt/homebrew" "$libs" + assert_not_contains "$scenario: linked libs" "/usr/local" "$libs" +} + +# -------------------------------------------------------------------------- +# Final comparison +# -------------------------------------------------------------------------- + +compare_pair() { + # compare_pair + local a="$BLUEPRINTS/$2.json" b="$BLUEPRINTS/$3.json" + if cmp -s "$a" "$b"; then + note "$1: $2 == $3 (byte-identical)" + else + fail "$1: $2 and $3 produced different blueprints ($a vs $b)" + fi +} + +compare_outputs() { + banner "Comparing blueprints" + local f + local all="ghcup-ghc${GHC96_VERSION//./} ghcup-ghc${GHC912_VERSION//./} nix-ghc96 nix-ghc912" + for name in $all; do + f="$BLUEPRINTS/$name.json" + [ -s "$f" ] || fail "missing blueprint for scenario $name ($f). Run that scenario first." + echo " $(shasum -a 256 "$f" 2>/dev/null || sha256sum "$f")" + done + echo + # Environment parity: the same compiler must produce the same output + # whether it comes from ghcup (with the downloaded crypto libs) or from + # nix (with the nix-provided crypto libs). + compare_pair "environment parity (ghc $GHC96_VERSION)" "ghcup-ghc${GHC96_VERSION//./}" "nix-ghc96" + compare_pair "environment parity (ghc $GHC912_VERSION)" "ghcup-ghc${GHC912_VERSION//./}" "nix-ghc912" + # Compiler parity: different GHC versions should produce the same output. + # + # KNOWN LIMITATION: plutus-tx-plugin compiles GHC Core, and GHC 9.6 and + # 9.12 produce different Core for the same source, so the compiledCode + # (and therefore the validator hash) differs across compilers even though + # everything else in the blueprint is identical. This is upstream + # plutus-tx-plugin behavior, independent of ghcup/nix or the crypto libs. + # Set PARITY_ALLOW_GHC_DIVERGENCE=1 to downgrade this to a warning. + if [ "${PARITY_ALLOW_GHC_DIVERGENCE:-0}" = 1 ]; then + if cmp -s "$BLUEPRINTS/ghcup-ghc${GHC96_VERSION//./}.json" "$BLUEPRINTS/ghcup-ghc${GHC912_VERSION//./}.json"; then + note "compiler parity: ghc $GHC96_VERSION == ghc $GHC912_VERSION (byte-identical)" + else + echo " [warn] compiler parity: ghc $GHC96_VERSION and ghc $GHC912_VERSION produce different compiledCode (known plutus-tx-plugin behavior)" + fi + else + compare_pair "compiler parity (ghcup)" "ghcup-ghc${GHC96_VERSION//./}" "ghcup-ghc${GHC912_VERSION//./}" + compare_pair "compiler parity (nix)" "nix-ghc96" "nix-ghc912" + fi + echo + echo "SUCCESS: blueprints are byte-identical across environments (ghcup with downloaded crypto libs vs nix)." +} + +# -------------------------------------------------------------------------- +# Main +# -------------------------------------------------------------------------- + +if [ "${1:-}" = "--inner-nix" ]; then + shift + inner_nix "$@" + exit 0 +fi + +if [ "${PARITY_FRESH:-0}" = 1 ]; then + banner "PARITY_FRESH=1: wiping the crypto libs cache, cabal.project.local, test store and test builddirs (fresh-clone simulation)" + CRYPTO_CACHE="${PLINTH_CRYPTO_LIBS_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/plinth-crypto-libs}" + chmod -R u+w "$ROOT/dist-newstyle/crypto-libs" "$CRYPTO_CACHE" 2>/dev/null || true + rm -rf "$ROOT/dist-newstyle/crypto-libs" "$CRYPTO_CACHE" "$STORE_DIR" \ + "$TESTDIR/ghcup-ghc${GHC96_VERSION//./}" "$TESTDIR/ghcup-ghc${GHC912_VERSION//./}" + rm -f "$ROOT/cabal.project.local" +fi + +case "${1:-all}" in + all) + run_ghcup_scenario "$GHC96_VERSION" + run_ghcup_scenario "$GHC912_VERSION" + run_nix_scenario ghc96 "$GHC96_VERSION" + run_nix_scenario ghc912 "$GHC912_VERSION" + compare_outputs + ;; + ghcup-ghc967) run_ghcup_scenario "$GHC96_VERSION" ;; + ghcup-ghc9122) run_ghcup_scenario "$GHC912_VERSION" ;; + nix-ghc96) run_nix_scenario ghc96 "$GHC96_VERSION" ;; + nix-ghc912) run_nix_scenario ghc912 "$GHC912_VERSION" ;; + compare) compare_outputs ;; + *) echo "unknown scenario: $1" >&2; exit 2 ;; +esac diff --git a/dev/ci/test-install.sh b/dev/ci/test-install.sh new file mode 100755 index 0000000..068cce2 --- /dev/null +++ b/dev/ci/test-install.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2015 # pass/fail assertion helpers never fail themselves +# +# End-to-end test of install.sh. The repository is a single branch carrying +# the union of every environment's files; the installer selects the relevant +# ones into a fresh project directory. This test builds a local git fixture +# of the union tree (no network cloning) and checks, for every environment: +# +# * the produced project contains EXACTLY the expected files (manifest) +# * the environment-specific edits were applied (pkg-config stanza only in +# GHC+Cabal projects, simplified nix/project.nix, no leftover markers) +# * the project has the right README and a fresh git history +# * the final instructions tell the user to run `cabal build all` +# +# plus the --from (local directory) mode, the default project name, scripted +# interactive runs (answers via PLINTH_INSTALL_TTY), the failure modes +# (missing nix, unsupported GHC, old cabal, missing pkg-config, no terminal, +# existing target), and the real crypto-libs download (skipped when +# PLINTH_TEST_OFFLINE=1). +# +# The toolchain checks are exercised hermetically with stub ghc/cabal +# executables, so this test does not require a Haskell toolchain. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-install-test.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +FAILURES=0 +fail() { echo "FAIL: $*" >&2; FAILURES=$((FAILURES + 1)); } +pass() { echo " ok: $*"; } + +# -------------------------------------------------------------------------- +# Fixture 1: a local git repository with the union tree on a single branch +# -------------------------------------------------------------------------- + +echo "== building local template repository (single union branch) ==" +SRC="$WORK/template" +git init -q "$SRC" +git ls-files --cached --others --exclude-standard | while IFS= read -r f; do + [ -f "$f" ] || continue + case "$f" in */*) mkdir -p "$SRC/${f%/*}" ;; esac + cp -p "$f" "$SRC/$f" +done +git -C "$SRC" add -A +git -C "$SRC" -c user.name=ci -c user.email=ci@example.invalid \ + commit -qm "plinth-template union" +# gitignored junk that must never reach a project created with --from +mkdir -p "$SRC/dist-newstyle" +echo junk > "$SRC/dist-newstyle/junk" +echo junk > "$SRC/cabal.project.local" +pass "fixture repo built from the working tree (with gitignored junk seeded)" + +# -------------------------------------------------------------------------- +# Fixture 2: stub toolchains and a minimal PATH +# +# The failure tests must not depend on what the host has installed, so they +# run with PATH = + , where the farm contains symlinks to just +# the external tools install.sh legitimately needs. +# -------------------------------------------------------------------------- + +FARM="$WORK/farm" +mkdir -p "$FARM" +# sh must be in the farm: `env PATH=... sh install.sh` resolves sh via the +# new PATH. +for t in sh bash uname grep sed tr dirname basename mktemp git curl tar rm mkdir \ + cat find chmod cp mv ln awk; do + p="$(command -v "$t" 2>/dev/null)" || continue + ln -s "$p" "$FARM/$t" +done + +make_stub() { # make_stub DIR NAME VERSION + mkdir -p "$1" + cat > "$1/$2" <&2; exit 1 ;; +esac +EOF + chmod +x "$1/$2" +} + +GOOD="$WORK/stubs-good" +make_stub "$GOOD" ghc 9.6.7 +make_stub "$GOOD" cabal 3.12.1.0 +make_stub "$GOOD" pkg-config 2.1.0 "2.1.0" + +OLDGHC="$WORK/stubs-oldghc" +make_stub "$OLDGHC" ghc 9.4.8 +make_stub "$OLDGHC" cabal 3.12.1.0 +make_stub "$OLDGHC" pkg-config 2.1.0 "2.1.0" + +OLDCABAL="$WORK/stubs-oldcabal" +make_stub "$OLDCABAL" ghc 9.6.7 +make_stub "$OLDCABAL" cabal 3.6.2.0 +make_stub "$OLDCABAL" pkg-config 2.1.0 "2.1.0" + +NOPKGCONF="$WORK/stubs-nopkgconf" +make_stub "$NOPKGCONF" ghc 9.12.2 +make_stub "$NOPKGCONF" cabal 3.12.1.0 + +# ghc+cabal stubs only, no pkg-config stub: for tests that need the REAL +# pkg-config from the host (the crypto-libs download sanity check). +TCONLY="$WORK/stubs-toolchain-only" +make_stub "$TCONLY" ghc 9.6.7 +make_stub "$TCONLY" cabal 3.12.1.0 + +# -------------------------------------------------------------------------- +# Helpers +# -------------------------------------------------------------------------- + +# run_install LOGFILE [env-overrides...] -- [install.sh flags...] +run_install() { + log="$1"; shift + envs=() + while [ "$1" != "--" ]; do envs+=("$1"); shift; done + shift + env "${envs[@]}" sh "$ROOT/install.sh" --repo "file://$SRC" "$@" \ + >"$log" 2>&1 /dev/null 2>&1; then + pass "fresh git history (no commits)" + else + fail "$1 should have an initialized repo with zero commits" + fi +} + +expect_in_log() { + if grep -qF "$2" "$1"; then pass "log mentions '$2'"; else fail "log lacks '$2' ($1)"; fi +} + +contains() { grep -qF "$2" "$1" && pass "${1#"$WORK"/} contains '$2'" || fail "${1#"$WORK"/} does not contain '$2'"; } +lacks() { if grep -qF "$2" "$1" 2>/dev/null; then fail "${1#"$WORK"/} contains forbidden '$2'"; else pass "${1#"$WORK"/} lacks '$2'"; fi; } + +# expect_manifest DIR (expected file list on stdin; .git is ignored) +expect_manifest() { + expected="$(sort)" + actual="$(cd "$1" && find . -type f ! -path './.git/*' | sed 's|^\./||' | sort)" + if [ "$expected" = "$actual" ]; then + pass "manifest matches ($(printf '%s\n' "$actual" | grep -c .) files)" + else + fail "manifest mismatch in $1 (< expected, > actual):" + diff <(printf '%s\n' "$expected") <(printf '%s\n' "$actual") | sed 's/^/ /' >&2 || true + fi +} + +COMMON=".gitignore +.hlint.yaml +.stylish-haskell.yaml +LICENSE.md +NOTICE.md +README.md +app/GenAuctionValidatorBlueprint.hs +app/GenMintingPolicyBlueprint.hs +cabal.project +plinth-template.cabal +src/AuctionMintingPolicy.hs +src/AuctionValidator.hs" + +NIX_FILES="flake.lock +flake.nix +nix/outputs.nix +nix/pkgs.nix +nix/project.nix +nix/shell.nix +nix/utils.nix" + +# no_leftover_markers DIR: no installer marker may survive in any project +no_leftover_markers() { + if grep -rqF -e "BEGIN ghc-cabal only" -e "END ghc-cabal only" \ + -e "BEGIN union source only" -e "END union source only" \ + --exclude-dir=.git "$1" 2>/dev/null; then + fail "installer markers leaked into $1" + else + pass "no leftover installer markers" + fi +} + +# -------------------------------------------------------------------------- +# Non-interactive happy paths, one per environment +# -------------------------------------------------------------------------- + +echo "" +echo "== --env cabal (stub toolchain) ==" +if run_install "$WORK/log-cabal" PATH="$GOOD:$FARM" -- \ + --yes --env cabal --dir "$WORK/out-cabal" --crypto-libs skip; then + expect_manifest "$WORK/out-cabal" </dev/null 2>&1; then + NIXDIR="$(dirname "$(command -v nix)")" + if run_install "$WORK/log-nix" PATH="$GOOD:$NIXDIR:$FARM" -- \ + --yes --env nix --dir "$WORK/out-nix"; then + expect_manifest "$WORK/out-nix" <"$WORK/log-from" 2>&1 "$WORK/log-fromwt" 2>&1 "$WORK/answers" +if ( cd "$WORK" && run_install "$WORK/log-interactive" \ + PATH="$GOOD:$FARM" PLINTH_INSTALL_TTY="$WORK/answers" -- ); then + [ -f "$WORK/my-scripted-project/scripts/get-crypto-libs.sh" ] \ + && pass "interactive run created the ghc-cabal project" \ + || fail "interactive run did not create the expected project" + expect_in_log "$WORK/log-interactive" "cabal build all" +else + fail "interactive run exited non-zero"; sed 's/^/ /' "$WORK/log-interactive" | tail -30 +fi + +# -------------------------------------------------------------------------- +# Failure modes +# -------------------------------------------------------------------------- + +echo "" +echo "== failure: --env nix without nix on PATH ==" +if run_install "$WORK/log-nonix" PATH="$GOOD:$FARM" -- \ + --yes --env nix --dir "$WORK/out-nonix"; then + fail "--env nix succeeded although nix is not on PATH" +else + pass "exits non-zero" + expect_in_log "$WORK/log-nonix" "nix is not installed" + [ ! -e "$WORK/out-nonix" ] && pass "nothing created" || fail "created despite failed checks" +fi + +echo "" +echo "== failure: unsupported GHC version (9.4.8) ==" +if run_install "$WORK/log-oldghc" PATH="$OLDGHC:$FARM" -- \ + --yes --env cabal --dir "$WORK/out-oldghc" --crypto-libs skip; then + fail "--env cabal succeeded with GHC 9.4.8" +else + pass "exits non-zero" + expect_in_log "$WORK/log-oldghc" "unsupported GHC version 9.4.8" + [ ! -e "$WORK/out-oldghc" ] && pass "nothing created" || fail "created despite failed checks" +fi + +echo "" +echo "== failure: cabal too old (3.6.2.0) ==" +if run_install "$WORK/log-oldcabal" PATH="$OLDCABAL:$FARM" -- \ + --yes --env cabal --dir "$WORK/out-oldcabal" --crypto-libs skip; then + fail "--env cabal succeeded with cabal 3.6.2.0" +else + pass "exits non-zero" + expect_in_log "$WORK/log-oldcabal" "too old" +fi + +echo "" +echo "== failure: pkg-config missing ==" +if run_install "$WORK/log-nopc" PATH="$NOPKGCONF:$FARM" -- \ + --yes --env cabal --dir "$WORK/out-nopc" --crypto-libs skip; then + fail "--env cabal succeeded without pkg-config" +else + pass "exits non-zero" + expect_in_log "$WORK/log-nopc" "pkg-config not found" +fi + +echo "" +echo "== failure: no terminal, no --env, no --yes ==" +if run_install "$WORK/log-notty" PATH="$GOOD:$FARM" PLINTH_INSTALL_NO_TTY=1 -- \ + --dir "$WORK/out-notty"; then + fail "succeeded with no terminal and no flags" +else + pass "exits non-zero" + expect_in_log "$WORK/log-notty" "no terminal available" +fi + +echo "" +echo "== failure: target directory already exists ==" +mkdir -p "$WORK/out-exists" +if run_install "$WORK/log-exists" PATH="$GOOD:$FARM" -- \ + --yes --env cabal --dir "$WORK/out-exists" --crypto-libs skip; then + fail "succeeded although the target directory exists" +else + pass "exits non-zero" + expect_in_log "$WORK/log-exists" "already exists" +fi + +# -------------------------------------------------------------------------- +# Real crypto-libs download (network) — the one non-hermetic test +# -------------------------------------------------------------------------- + +echo "" +if [ "${PLINTH_TEST_OFFLINE:-0}" = 1 ]; then + echo "== crypto-libs local download: skipped (PLINTH_TEST_OFFLINE=1) ==" +else + echo "== --crypto-libs local: real download (hermetic cache) ==" + # Real pkg-config required for the installer's sanity check of the + # downloaded libraries, so keep the host PATH and prepend ghc/cabal stubs + # only (no pkg-config stub). The per-user cache is redirected into the + # work dir so the test leaves no trace outside it. + if run_install "$WORK/log-crypto" PATH="$TCONLY:$PATH" \ + PLINTH_CRYPTO_LIBS_HOME="$WORK/crypto-cache" -- \ + --yes --env cabal --dir "$WORK/out-crypto" --crypto-libs local; then + found="" + for pc in "$WORK/out-crypto"/dist-newstyle/crypto-libs/*/lib/pkgconfig/libsodium.pc; do + [ -f "$pc" ] && found="$pc" + done + if [ -n "$found" ]; then + pass "libsodium.pc reachable in the project: ${found#"$WORK"/}" + else + fail "no libsodium.pc under out-crypto/dist-newstyle/crypto-libs" + sed 's/^/ /' "$WORK/log-crypto" | tail -30 + fi + if find "$WORK/out-crypto/dist-newstyle/crypto-libs" -maxdepth 1 -type l | grep -q .; then + pass "dist-newstyle/crypto-libs/ is a symlink into the cache" + else + fail "expected a symlink under dist-newstyle/crypto-libs" + fi + if find "$WORK/crypto-cache" -name libsodium.pc | grep -q .; then + pass "real files live in the (redirected) per-user cache" + else + fail "no libsodium.pc in the redirected cache $WORK/crypto-cache" + fi + else + fail "--crypto-libs local exited non-zero" + sed 's/^/ /' "$WORK/log-crypto" | tail -30 + fi +fi + +# -------------------------------------------------------------------------- + +echo "" +if [ "$FAILURES" -gt 0 ]; then + echo "test-install: $FAILURES failure(s)" >&2 + exit 1 +fi +echo "test-install: all tests passed" diff --git a/dev/readmes/demeter.md b/dev/readmes/demeter.md new file mode 100644 index 0000000..1dc3de4 --- /dev/null +++ b/dev/readmes/demeter.md @@ -0,0 +1,51 @@ +# Plinth Template (Demeter edition) + +A template for your Plinth smart contract project, set up for +**[Demeter](https://demeter.run)**, a hosted Cardano development platform. + +Demeter workspaces are cloud VSCode environments that come with **nix** +preinstalled. This project's own nix shell provides the entire toolchain, +including the three Cardano crypto C libraries it needs — `libsodium` +(VRF-patched), `libsecp256k1` and `libblst` — so there is nothing to +install on your machine. + +## 1. Set up a workspace + +1. Push this project to a GitHub repository. +2. Create an account at [demeter.run](https://demeter.run), create a + Project, and add a **Workspace** resource pointing at your repository + (pick the Haskell/Plutus stack). See + [their documentation](https://docs.demeter.run) for details. You can + also link directly to a workspace for your repository with: + + ``` + https://demeter.run/code?repository=&template=plutus + ``` + +3. Press "Open VS Code". + +> IMPORTANT: +> Demeter uses its own infrastructure and packages. If something is not +> working correctly, please contact them before creating an issue. + +## 2. Build + +In the workspace's terminal, enter this project's nix shell and build: + +``` +nix develop --accept-flake-config +cabal build all +``` + +The first `nix develop` downloads the toolchain from IOG's binary cache +(`cache.iog.io`) and can take a while; afterwards it is instant. GHC 9.6 is +the default; `nix develop .#ghc912` gives you GHC 9.12. + +> NOTE: +> Workspace files outside your home directory are lost when the workspace +> restarts; keep your work inside the cloned repository and push often. + +## 3. Run the example application + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/dev/readmes/docker.md b/dev/readmes/docker.md new file mode 100644 index 0000000..99b7f18 --- /dev/null +++ b/dev/readmes/docker.md @@ -0,0 +1,59 @@ +# Plinth Template (Docker edition) + +A template for your Plinth smart contract project, set up for **Docker** +(VSCode devcontainers, GitHub Codespaces, or a standalone container). + +The container image (`ghcr.io/input-output-hk/devx-devcontainer`) ships the +full toolchain, including the three Cardano crypto C libraries the project +needs — `libsodium` (VRF-patched), `libsecp256k1` and `libblst` — all +provided via nix inside the image; there is nothing to install on your +system besides Docker itself. + +## 1. Choose how to run it + +- **Devcontainer:** + 1. Make sure to have [VSCode](https://code.visualstudio.com/) installed with + the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) + extension. + 2. Open this project in VSCode and accept "Reopen in Container". + +- **Codespaces:** push this project to GitHub, then: + `Code -> Codespaces -> Create codespace`. + +- **Standalone Docker:** from the project directory, run: + + ``` + docker run \ + -v "$PWD:/workspaces/my-project" \ + -w /workspaces/my-project \ + -it ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog + ``` + +> NOTE: +> You can modify your [`devcontainer.json`](./.devcontainer/devcontainer.json) +> file to customize the container (more info +> [here](https://github.com/input-output-hk/devx?tab=readme-ov-file#vscode-devcontainer--github-codespace-support)). + +> NOTE (for Windows users): +> It is recommended to install and run Docker on your native OS. If you want +> to run Docker Desktop inside a VM, read through +> [these notes](https://docs.docker.com/desktop/setup/vm-vdi/). + +> NOTE: +> The devcontainer image is currently published for **x86_64-linux with +> GHC 9.6 only**. It runs on Apple Silicon through Docker's emulation +> (slower). For GHC 9.12 or native ARM, use the Nix setup instead. + +## 2. Build + +In the container's terminal: + +``` +cabal update +cabal build all +``` + +## 3. Run the example application + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/dev/readmes/ghc-cabal.md b/dev/readmes/ghc-cabal.md new file mode 100644 index 0000000..9d7a0b6 --- /dev/null +++ b/dev/readmes/ghc-cabal.md @@ -0,0 +1,80 @@ +# Plinth Template (GHC + Cabal edition) + +A template for your Plinth smart contract project, built with your own +**GHC and Cabal** — no nix, no docker, and no system-wide C libraries. + +## 1. Prerequisites + +- [ghcup](https://www.haskell.org/ghcup/) with **GHC 9.6.x or 9.12.x** + (e.g. `ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7`) +- **Cabal 3.8+** (`ghcup install cabal latest`) +- a **pkg-config** executable + (macOS: `brew install pkgconf`, Debian/Ubuntu: `apt install pkg-config`) + +## 2. Build + +From the project root: + +``` +cabal update +cabal build all +``` + +## The crypto C libraries + +`plutus-core` depends (via `cardano-crypto-class`) on three C libraries — +`libsodium` (VRF-patched), `libsecp256k1` and `libblst` — which cabal locates +with pkg-config. This project routes pkg-config through `scripts/pkg-config` +(see the `program-locations` stanza in `cabal.project`): on first use the +shim downloads the prebuilt libraries published by IOG at +[iohk-nix releases](https://github.com/input-output-hk/iohk-nix/releases) +into a per-user cache (`~/.cache/plinth-crypto-libs`, override with +`PLINTH_CRYPTO_LIBS_HOME`), links them into the project at +`dist-newstyle/crypto-libs/`, and answers cabal's pkg-config queries from +there. Nothing is installed system-wide; the cache is shared by all your +Plinth projects, and it is the cache path (not the project path) that gets +baked into the packages cabal compiles, so the cabal store stays valid if +the project is moved or deleted. Every downloaded artifact is verified +against sha256 digests and an iohk-nix commit hash pinned inside +`scripts/get-crypto-libs.sh`. Only `curl`, `tar` and `shasum`/`sha256sum` +are needed, all of which ship with macOS and Linux. + +This behavior is ON by default. To opt out and use crypto libraries +installed system-wide instead, set `PLINTH_USE_SYSTEM_CRYPTO_LIBS=1`: the +shim then delegates to the real pkg-config on your PATH and nothing is +downloaded. To install IOG's prebuilt libraries system-wide in the first +place, run `./scripts/get-crypto-libs.sh --prefix /usr/local` (or any other +prefix; see `--help`). + +When cross compiling, set `PLINTH_CRYPTO_LIBS_PLATFORM` to the target +platform (`arm64-macos`, `x86_64-macos` or `debian`) and the shim serves +the target's libraries regardless of the build host. + +> NOTE: +> Run cabal from the project root: the `pkg-config-location` path in +> `cabal.project` is resolved relative to the invocation directory. + +> NOTE (first build only): +> The very first build on a fresh clone downloads the libraries and writes +> their absolute location into `cabal.project.local` (gitignored) — too late +> for that same run's configure phase, so it stops once with +> *"Cannot find the program 'pkg-config'"*. Simply re-run `cabal build`; +> everything already built is reused, and the interruption never happens +> again (not even after `cabal clean`). To skip it entirely, run +> `./scripts/get-crypto-libs.sh` once before the first build. + +> NOTE: +> Only a symlink lives under `dist-newstyle/`, so `cabal clean` costs +> nothing: the next build re-links the cached libraries instantly (no +> re-download). Run `./scripts/get-crypto-libs.sh` yourself at any time. + +> NOTE (for Windows users): +> Prebuilt MSYS2 packages (`msys2.*.pkg.tar.zstd`) are published in the same +> release. Install them with `pacman -U` inside GHC's MSYS2 environment +> (`ghcup run mingw-pacman -- -U `), or use WSL2 and follow the Linux +> instructions. + +## 3. Run the example application + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/dev/readmes/nix.md b/dev/readmes/nix.md new file mode 100644 index 0000000..d943555 --- /dev/null +++ b/dev/readmes/nix.md @@ -0,0 +1,42 @@ +# Plinth Template (Nix edition) + +A template for your Plinth smart contract project, set up for **Nix**. + +Plinth currently supports GHC `9.6.x` and `9.12.x`. + +## 1. Set up Nix + +Follow [these instructions](https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md) +to install and configure nix, **even if you already have it installed** — the +configuration step enables IOG's binary caches, without which the first build +compiles GHC from source and takes hours. + +> NOTE (for Windows users): +> Make sure to have [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2) +> and the [WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) +> VSCode extension (if using VSCode) installed before the Nix setup. + +## 2. Enter the shell and build + +``` +nix develop +cabal build all +``` + +GHC 9.6 is the default. For GHC 9.12 use `nix develop .#ghc912`. + +The three Cardano crypto C libraries the project needs — `libsodium` +(VRF-patched), `libsecp256k1` and `libblst` — are provided automatically by +the nix shell; there is nothing to install on your system. + +> NOTE: +> The nix files inside this template follow the [`iogx` template](https://github.com/input-output-hk/iogx), +> but you can delete and replace them with your own. In that case, you might +> want to include the [`devx` flake](https://github.com/input-output-hk/devx) +> in your flake inputs as a starting point to supply all the necessary +> dependencies, making sure to use one of the `-iog` flavors. + +## 3. Run the example application + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9f2e546 --- /dev/null +++ b/install.sh @@ -0,0 +1,798 @@ +#!/bin/sh +# +# plinth-template installer +# +# curl -fsSL https://raw.githubusercontent.com/IntersectMBO/plinth-template/main/install.sh | sh +# +# Interactively sets up a new Plinth smart contract project from +# https://github.com/IntersectMBO/plinth-template. It asks which development +# environment you want (Nix, Docker, Demeter, or plain GHC+Cabal), verifies +# the required tools are installed, then creates a fresh project directory +# containing just the files that environment needs (the repository itself +# carries the union of all environments) and — for the GHC+Cabal +# environment — optionally installs the Cardano crypto C libraries. +# +# Non-interactive use (all prompts have flags; --yes accepts defaults): +# +# sh install.sh --env cabal --dir my-project --crypto-libs local --yes +# +# Flags: +# --env ENV nix | docker | demeter | cabal +# --docker-mode MODE codespaces | devcontainer | standalone (with --env docker) +# --crypto-libs MODE local | system | skip (with --env cabal) +# --prefix DIR prefix for --crypto-libs system (default /usr/local) +# --dir NAME project directory to create (default: my-plinth-project) +# --repo URL template repository (default: official plinth-template) +# --branch NAME fetch NAME instead of the repository's default branch +# --from DIR take the template from a local directory instead of +# fetching it (offline installs, CI) +# --yes, -y don't ask; use defaults for unanswered questions +# --help, -h this text +# +# Environment variables: +# PLINTH_TEMPLATE_REPO same as --repo +# NO_COLOR disable colored output +# +# POSIX sh; no bashisms. The entire logic lives in functions and the last +# line is `main "$@"`, so a partially downloaded script executes nothing. + +set -eu + +REPO_DEFAULT="https://github.com/IntersectMBO/plinth-template" + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- + +setup_colors() { + if [ -t 2 ] && [ "${TERM:-dumb}" != dumb ] && [ -z "${NO_COLOR:-}" ]; then + BOLD="$(printf '\033[1m')" + DIM="$(printf '\033[2m')" + RED="$(printf '\033[31m')" + GREEN="$(printf '\033[32m')" + YELLOW="$(printf '\033[33m')" + CYAN="$(printf '\033[36m')" + RESET="$(printf '\033[0m')" + else + BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET="" + fi +} + +say() { printf '%s\n' "$*" >&2; } +info() { printf '%s\n' "${CYAN}==>${RESET} ${BOLD}$*${RESET}" >&2; } +ok() { printf '%s\n' "${GREEN} ok${RESET} $*" >&2; } +warn() { printf '%s\n' "${YELLOW}warning:${RESET} $*" >&2; } +die() { printf '%s\n' "${RED}error:${RESET} $*" >&2; exit 1; } + +have() { command -v "$1" >/dev/null 2>&1; } + +# On a fresh macOS, /usr/bin/git is the Xcode CLT stub: it exists but every +# invocation pops the "install developer tools" dialog and fails. Only treat +# git as available when it actually runs. +git_works() { have git && git --version >/dev/null 2>&1; } + +# The shell does not expand ~ in `read` answers or in flag values that were +# quoted; do it ourselves for everything used as a path. +expand_tilde() { + # shellcheck disable=SC2088 # matching a LITERAL ~ the shell didn't expand + case "$1" in + "~") printf '%s' "$HOME" ;; + "~/"*) printf '%s/%s' "$HOME" "${1#"~"/}" ;; + *) printf '%s' "$1" ;; + esac +} + +# expand_tilde + absolutize, for paths echoed back as export lines. +normalize_path() { + _p="$(expand_tilde "$1")" + case "$_p" in + /*) printf '%s' "$_p" ;; + *) printf '%s/%s' "$PWD" "$_p" ;; + esac +} + +# --------------------------------------------------------------------------- +# Interaction. When the script is piped into sh (`curl ... | sh`) stdin is the +# script itself, so questions are read from /dev/tty instead. With --yes, or +# when no terminal is available at all, defaults are used. Answers are read +# from fd 3. PLINTH_INSTALL_TTY overrides the answer source (used by the CI +# tests to feed scripted answers). +# --------------------------------------------------------------------------- + +INTERACTIVE=0 + +setup_input() { + [ "$ASSUME_YES" = 1 ] && return 0 + # Test hook: pretend no terminal is available. + [ -n "${PLINTH_INSTALL_NO_TTY:-}" ] && return 0 + if [ -n "${PLINTH_INSTALL_TTY:-}" ]; then + [ -r "$PLINTH_INSTALL_TTY" ] || die "cannot read from PLINTH_INSTALL_TTY=$PLINTH_INSTALL_TTY" + exec 3< "$PLINTH_INSTALL_TTY" + INTERACTIVE=1 + elif [ -t 0 ]; then + exec 3<&0 + INTERACTIVE=1 + elif (exec < /dev/tty) 2>/dev/null; then + exec 3< /dev/tty + INTERACTIVE=1 + fi +} + +# ask PROMPT DEFAULT -> stdout: the answer (DEFAULT when non-interactive, +# empty input, or EOF). +ask() { + if [ "$INTERACTIVE" = 0 ]; then + printf '%s' "$2" + return 0 + fi + printf '%s [%s]: ' "${BOLD}$1${RESET}" "$2" >&2 + ans="" + read -r ans <&3 || ans="" + [ -n "$ans" ] || ans="$2" + printf '%s' "$ans" +} + +# confirm PROMPT DEFAULT(y|n) -> exit status +confirm() { + ans="$(ask "$1 (y/n)" "$2")" + case "$ans" in + y|Y|yes|YES) return 0 ;; + *) return 1 ;; + esac +} + +# choose DEFAULT_NUMBER N -> stdout: chosen number (1..N). The caller prints +# the menu beforehand. +choose() { + while :; do + ans="$(ask "Enter a number (1-$2)" "$1")" + case "$ans" in + *[!0-9]*|'') ;; + *) if [ "$ans" -ge 1 ] && [ "$ans" -le "$2" ]; then printf '%s' "$ans"; return 0; fi ;; + esac + [ "$INTERACTIVE" = 1 ] || die "invalid default answer '$ans'" + warn "please answer with a number between 1 and $2" + done +} + +# --------------------------------------------------------------------------- +# Platform detection +# --------------------------------------------------------------------------- + +IS_WSL=0 + +detect_platform() { + case "$(uname -s)" in + Darwin) ;; + Linux) + if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=1; fi + ;; + MINGW*|MSYS*|CYGWIN*) + die "native Windows is not supported by this installer. +Install WSL2 (https://learn.microsoft.com/windows/wsl/install) and run the +installer again from your WSL shell." + ;; + *) + warn "unrecognized platform '$(uname -s)'; continuing as if it were Linux" + ;; + esac +} + +# --------------------------------------------------------------------------- +# Per-environment tool checks +# --------------------------------------------------------------------------- + +check_nix() { + have nix || die "nix is not installed. +Follow https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md +to install AND configure it (the configuration step sets up IOG's binary +caches — without them the first build compiles GHC from source and takes +hours), then run this installer again." + feats="$(nix config show experimental-features 2>/dev/null \ + || nix show-config 2>/dev/null | sed -n 's/^experimental-features = //p')" || feats="" + case " $feats " in + *" flakes "*) ok "nix $(nix --version 2>/dev/null | sed 's/^nix (Nix) //') with flakes enabled" ;; + *) + warn "could not confirm that nix flakes are enabled. If 'nix develop' +fails, add 'experimental-features = nix-command flakes' to your nix.conf +(see https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md)." + ;; + esac + warn "make sure IOG's binary caches are configured (cache.iog.io); the +nix-setup-guide linked above explains how. Without them the first +'nix develop' builds GHC from source." +} + +check_docker() { + # $1 = docker mode + case "$1" in + codespaces) + say "Codespaces run in GitHub's cloud; nothing to check locally." + ;; + devcontainer|standalone) + have docker || die "docker is not installed (https://docs.docker.com/get-docker/). +On Windows, install Docker on the native OS, not inside a VM +(https://docs.docker.com/desktop/setup/vm-vdi/)." + if docker info >/dev/null 2>&1; then + ok "docker daemon is running" + else + warn "docker is installed but the daemon does not respond; start Docker before building." + fi + if [ "$1" = devcontainer ]; then + say "You will also need VSCode with the 'Dev Containers' extension." + fi + ;; + esac +} + +check_demeter() { + say "Demeter (https://demeter.run) is a hosted platform; nothing to check locally." +} + +# ver_ge A B: true when major.minor of A >= major.minor of B +ver_ge() { + a_major=${1%%.*}; a_rest=${1#*.}; a_minor=${a_rest%%.*} + b_major=${2%%.*}; b_rest=${2#*.}; b_minor=${b_rest%%.*} + case "$a_major$a_minor" in *[!0-9]*) return 1 ;; esac + [ "$a_major" -gt "$b_major" ] || { [ "$a_major" -eq "$b_major" ] && [ "$a_minor" -ge "$b_minor" ]; } +} + +check_cabal_env() { + have ghc || die "ghc not found on PATH. Plinth supports GHC 9.6.x and 9.12.x. +Install one with ghcup (https://www.haskell.org/ghcup/): + ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7" + ghc_version="$(ghc --numeric-version)" + case "$ghc_version" in + 9.6.*|9.12.*) ok "ghc $ghc_version ($(command -v ghc))" ;; + *) die "unsupported GHC version $ghc_version: Plinth supports 9.6.x and 9.12.x. +Switch with ghcup, e.g.: ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7" ;; + esac + + have cabal || die "cabal not found on PATH. Install it with ghcup: + ghcup install cabal latest && ghcup set cabal latest" + cabal_version="$(cabal --numeric-version)" + ver_ge "$cabal_version" 3.8 \ + || die "cabal $cabal_version is too old: 3.8 or newer is required (3.12+ recommended). +Upgrade with: ghcup install cabal latest && ghcup set cabal latest" + ok "cabal $cabal_version ($(command -v cabal))" + + have pkg-config || die "pkg-config not found on PATH; the project uses it to +locate the crypto C libraries. Install it with: + macOS: brew install pkgconf + Debian/Ubuntu: sudo apt install pkg-config" + ok "pkg-config $(pkg-config --version) ($(command -v pkg-config))" + + have curl || die "curl is required (to download the crypto C libraries)" +} + +# Quiet probe used only to pick a sensible default menu entry. +cabal_env_looks_ready() { + have ghc && have cabal && have pkg-config || return 1 + case "$(ghc --numeric-version 2>/dev/null)" in + 9.6.*|9.12.*) ;; + *) return 1 ;; + esac + ver_ge "$(cabal --numeric-version 2>/dev/null)" 3.8 +} + +# --------------------------------------------------------------------------- +# Environment selection +# --------------------------------------------------------------------------- + +detected_default_env() { + if have nix; then echo nix + elif cabal_env_looks_ready; then echo cabal + elif have docker; then echo docker + else echo nix + fi +} + +mark() { # mark CMD: "(detected)" suffix for menu lines + if have "$1"; then printf '%s' " ${GREEN}(detected)${RESET}"; fi +} + +select_env() { + [ -n "$ENV_CHOICE" ] && return 0 + if [ "$INTERACTIVE" = 0 ]; then + ENV_CHOICE="$(detected_default_env)" + info "No answers available (--yes / no terminal): using environment '$ENV_CHOICE' (override with --env)" + return 0 + fi + default_env="$(detected_default_env)" + say "" + info "Which development environment do you want to use?" + say "" + say " 1) Nix ${DIM}nix develop shell with the full toolchain (recommended)${RESET}$(mark nix)" + say " 2) Docker ${DIM}devx container: Codespaces, VSCode devcontainer or standalone${RESET}$(mark docker)" + say " 3) Demeter ${DIM}hosted cloud workspace at https://demeter.run${RESET}" + say " 4) GHC+Cabal ${DIM}your own ghc/cabal from ghcup, no nix, no docker${RESET}$(mark ghc)" + say "" + case "$default_env" in + nix) default_n=1 ;; + docker) default_n=2 ;; + demeter) default_n=3 ;; + cabal) default_n=4 ;; + esac + n="$(choose "$default_n" 4)" + case "$n" in + 1) ENV_CHOICE=nix ;; + 2) ENV_CHOICE=docker ;; + 3) ENV_CHOICE=demeter ;; + 4) ENV_CHOICE=cabal ;; + esac +} + +select_docker_mode() { + [ "$ENV_CHOICE" = docker ] || return 0 + [ -n "$DOCKER_MODE" ] && return 0 + if [ "$INTERACTIVE" = 0 ]; then + DOCKER_MODE=devcontainer + info "Using default docker mode 'devcontainer' (override with --docker-mode)" + return 0 + fi + say "" + info "How do you want to run the Docker environment?" + say "" + say " 1) Devcontainer ${DIM}open the project in VSCode's Dev Containers${RESET}" + say " 2) Codespaces ${DIM}run it on GitHub's cloud, in the browser${RESET}" + say " 3) Standalone ${DIM}plain 'docker run' with the project mounted${RESET}" + say "" + n="$(choose 1 3)" + case "$n" in + 1) DOCKER_MODE=devcontainer ;; + 2) DOCKER_MODE=codespaces ;; + 3) DOCKER_MODE=standalone ;; + esac +} + +# --------------------------------------------------------------------------- +# Crypto C libraries +# --------------------------------------------------------------------------- + +explain_crypto_libs() { + say "" + info "About the crypto C libraries" + say "" + say " Plinth projects depend (via plutus-core and cardano-crypto-class) on" + say " three C libraries: ${BOLD}libsodium${RESET} (VRF-patched), ${BOLD}libsecp256k1${RESET} and ${BOLD}libblst${RESET}." + case "$ENV_CHOICE" in + nix) + say " Your Nix shell provides all three automatically — nothing to install." + ;; + docker) + say " The devx container image provides all three via nix — nothing to install." + ;; + demeter) + say " Demeter workspaces come with nix preinstalled, and the project's own" + say " nix shell provides all three libraries — nothing to install locally." + ;; + cabal) + say " Without nix, they must be available on your machine. This template can" + say " download the prebuilt binaries IOG publishes at" + say " https://github.com/input-output-hk/iohk-nix/releases (sha256- and" + say " commit-pinned) into a per-user cache linked into the project, or" + say " system-wide, or you can install them manually." + ;; + esac +} + +select_crypto_mode() { + [ "$ENV_CHOICE" = cabal ] || return 0 + [ -n "$CRYPTO_MODE" ] && return 0 + if [ "$INTERACTIVE" = 0 ]; then + CRYPTO_MODE=local + info "Using default crypto-libs mode 'local' (override with --crypto-libs)" + return 0 + fi + say "" + info "How do you want to install the crypto C libraries?" + say "" + say " 1) Managed ${DIM}downloaded once into ~/.cache/plinth-crypto-libs, linked into" + say " /dist-newstyle — nothing system-wide (recommended)${RESET}" + say " 2) System-wide ${DIM}into a prefix such as /usr/local (may need sudo)${RESET}" + say " 3) Skip ${DIM}install them yourself later${RESET}" + say "" + n="$(choose 1 3)" + case "$n" in + 1) CRYPTO_MODE=local ;; + 2) CRYPTO_MODE=system ;; + 3) CRYPTO_MODE=skip ;; + esac +} + +install_crypto_libs() { + [ "$ENV_CHOICE" = cabal ] || return 0 + case "$CRYPTO_MODE" in + local) + say "" + info "Installing the crypto C libraries (per-user cache, linked into the project)" + (cd "$TARGET_DIR" && ./scripts/get-crypto-libs.sh) \ + || die "crypto library installation failed; you can retry later with + cd $TARGET_DIR && ./scripts/get-crypto-libs.sh" + say "" + say " ${YELLOW}Note:${RESET} the project only holds a link (dist-newstyle/crypto-libs) to" + say " the per-user cache (~/.cache/plinth-crypto-libs, shared by all your" + say " Plinth projects), so 'cabal clean' costs nothing: the next build" + say " re-links instantly, and nothing is ever re-downloaded." + ;; + system) + prefix="$(normalize_path "$(ask "Install prefix" "$CRYPTO_PREFIX")")" + say "" + info "Installing the crypto C libraries into $prefix" + if mkdir -p "$prefix/lib" "$prefix/include" 2>/dev/null && [ -w "$prefix/lib" ]; then + "$TARGET_DIR/scripts/get-crypto-libs.sh" --prefix "$prefix" \ + || die "crypto library installation failed" + elif [ "$INTERACTIVE" = 1 ] && confirm "$prefix is not writable; use sudo?" n; then + sudo "$TARGET_DIR/scripts/get-crypto-libs.sh" --prefix "$prefix" \ + || die "crypto library installation failed" + else + die "$prefix is not writable. Rerun the installation yourself with: + sudo $TARGET_DIR/scripts/get-crypto-libs.sh --prefix $prefix" + fi + SYSTEM_CRYPTO_PREFIX="$prefix" + ;; + skip) + say "" + say " Skipping. Install libsodium (VRF-patched), libsecp256k1 and libblst" + say " yourself — see the 'installing with cabal' section of" + say " https://developers.cardano.org/docs/get-started/cardano-node/installing-cardano-node/" + say " — or run ./scripts/get-crypto-libs.sh inside the project later." + say "" + say " If you install them system-wide, build with" + say " PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 (and PKG_CONFIG_PATH pointing at" + say " them) so the project uses your libraries instead of downloading" + say " its own." + ;; + esac +} + +# --------------------------------------------------------------------------- +# Cloning +# --------------------------------------------------------------------------- + +# fetch_tarball REF DIR: download a github.com tarball into DIR (REF is a +# branch name, or HEAD for the default branch). Extracts into a sibling temp +# dir first so a failure never leaves a half-created DIR behind. +fetch_tarball() { + case "$REPO" in + https://github.com/*) ;; + *) return 1 ;; + esac + slug="${REPO#https://github.com/}"; slug="${slug%.git}"; slug="${slug%/}" + case "$1" in + HEAD) _ref="HEAD" ;; + *) _ref="refs/heads/$1" ;; + esac + _tmp="$2.download.$$" + mkdir -p "$_tmp" + if curl -fsSL --proto '=https' --tlsv1.2 \ + "https://codeload.github.com/$slug/tar.gz/$_ref" \ + | tar -xzf - --strip-components=1 -C "$_tmp"; then + mv "$_tmp" "$2" + else + rm -rf "$_tmp" + return 1 + fi +} + +# The repository is a single branch carrying the union of every +# environment's files. fetch_source obtains a copy (git clone, tarball, or a +# local directory via --from) and create_project copies just the files the +# chosen environment needs into the new project directory. + +SRC_DIR="" +SRC_CLEANUP="" + +fetch_source() { + if [ -n "$FROM_DIR" ]; then + [ -d "$FROM_DIR" ] || die "--from: '$FROM_DIR' is not a directory" + SRC_DIR="$(cd "$FROM_DIR" && pwd)" + [ -f "$SRC_DIR/plinth-template.cabal" ] \ + || die "--from: '$FROM_DIR' does not look like a plinth-template checkout" + return 0 + fi + say "" + info "Fetching $REPO${BRANCH_OVERRIDE:+ (branch $BRANCH_OVERRIDE)}" + SRC_DIR="$(mktemp -d "${TMPDIR:-/tmp}/plinth-template-src.XXXXXX")" + SRC_CLEANUP="$SRC_DIR" + rm -rf "$SRC_DIR" + if git_works; then + if git clone --quiet --depth 1 ${BRANCH_OVERRIDE:+--branch "$BRANCH_OVERRIDE"} \ + --single-branch "$REPO" "$SRC_DIR"; then + return 0 + fi + warn "git clone failed; trying a tarball download instead" + fi + fetch_tarball "${BRANCH_OVERRIDE:-HEAD}" "$SRC_DIR" \ + || die "could not fetch $REPO +(the tarball fallback only works for github.com repositories)" +} + +# list_source_files: relative paths of the template files, one per line. +# Inside a git checkout (--from on a working tree) this respects .gitignore, +# so build artifacts never leak into the new project. `.git` may be a FILE +# (worktrees), hence -e, and the find fallback must skip it by name. +list_source_files() { + if [ -e "$SRC_DIR/.git" ] && git_works \ + && git -C "$SRC_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C "$SRC_DIR" ls-files --cached --others --exclude-standard + else + (cd "$SRC_DIR" && find . -type f ! -name .git ! -path './.git/*' | sed 's|^\./||') + fi +} + +# include_file ENV PATH: 0 when PATH belongs in an ENV project. +include_file() { + case "$2" in + # never copy git metadata (a copied worktree .git POINTER FILE would make + # `git init` reinitialize the source repository!) + .git|.git/*) return 1 ;; + # replaced by the environment-specific README from dev/readmes/ + README.md) return 1 ;; + # repo machinery, never part of a project + .github/*|install.sh|dev/*) return 1 ;; + SECURITY.md|CODEOWNERS.md|CONTRIBUTING.md|CODE_OF_CONDUCT.md) return 1 ;; + CHANGELOG.md|DESCRIPTION.md) return 1 ;; + esac + case "$1" in + nix|demeter) + case "$2" in .devcontainer/*|scripts/*) return 1 ;; esac ;; + docker) + case "$2" in nix/*|flake.nix|flake.lock|scripts/*) return 1 ;; esac ;; + cabal) + case "$2" in nix/*|flake.nix|flake.lock|.devcontainer/*) return 1 ;; esac ;; + esac + return 0 +} + +# transform_block FILE BEGIN-MARKER END-MARKER [REPLACEMENT-LINE] +# Removes the marked block (markers included), optionally putting a single +# replacement line in its place. Fails loudly when the marker is missing — +# a silently kept block would produce a broken project. +transform_block() { + grep -qF -e "$2" "$1" || die "template drift: marker '$2' not found in $1" + awk -v b="$2" -v e="$3" -v r="${4:-}" ' + $0 == b { skip = 1; if (r != "") print r; next } + $0 == e { skip = 0; next } + !skip { print } + ' "$1" > "$1.tmp" && mv "$1.tmp" "$1" +} + +create_project() { + # $1 = env, $2 = target dir + say "" + info "Creating $2 ($1 project)" + + files_list="$(mktemp "${TMPDIR:-/tmp}/plinth-files.XXXXXX")" + list_source_files > "$files_list" + mkdir -p "$2" + while IFS= read -r f; do + [ -f "$SRC_DIR/$f" ] || continue + include_file "$1" "$f" || continue + case "$f" in + */*) mkdir -p "$2/${f%/*}" ;; + esac + cp -p "$SRC_DIR/$f" "$2/$f" + done < "$files_list" + rm -f "$files_list" + + # Environment-specific README + case "$1" in + cabal) _readme="ghc-cabal" ;; + *) _readme="$1" ;; + esac + [ -f "$SRC_DIR/dev/readmes/$_readme.md" ] \ + || die "template drift: dev/readmes/$_readme.md missing from the template" + cp "$SRC_DIR/dev/readmes/$_readme.md" "$2/README.md" + + # Environment-specific bits inside shared files + case "$1" in + nix|demeter|docker) + transform_block "$2/cabal.project" \ + "-- BEGIN ghc-cabal only" "-- END ghc-cabal only" + ;; + cabal) + # the block stays; only the installer-facing marker lines go + grep -vF -e "-- BEGIN ghc-cabal only" -e "-- END ghc-cabal only" \ + -e "-- (install.sh removes this whole block for Nix/Docker/Demeter projects)" \ + "$2/cabal.project" > "$2/cabal.project.tmp" \ + && mv "$2/cabal.project.tmp" "$2/cabal.project" + ;; + esac + case "$1" in + nix|demeter) + transform_block "$2/nix/project.nix" \ + " # BEGIN union source only" " # END union source only" \ + " src = lib.cleanSource ../.;" + ;; + esac + + # Fresh history: this is a template, not a fork. + if git_works; then + (cd "$2" && { git init -q -b main 2>/dev/null || git init -q; }) || warn "git init failed" + ok "project created in $2 (fresh git history — make your first commit when ready)" + else + ok "project created in $2" + fi +} + +# --------------------------------------------------------------------------- +# Next steps +# --------------------------------------------------------------------------- + +next_steps() { + say "" + info "All set! Next steps" + say "" + case "$ENV_CHOICE" in + nix) + say " cd $TARGET_DIR" + say " nix develop ${DIM}# first run downloads the toolchain from IOG's cache${RESET}" + say " cabal build all ${DIM}# builds the example auction validator${RESET}" + say "" + say " ${DIM}GHC 9.6 is the default; 'nix develop .#ghc912' gives you GHC 9.12.${RESET}" + ;; + docker) + case "$DOCKER_MODE" in + devcontainer) + say " 1. Open $TARGET_DIR in VSCode (with the Dev Containers extension)." + say " 2. Accept 'Reopen in Container' when prompted." + say " 3. In the container's terminal, run: cabal build all" + ;; + codespaces) + say " 1. Push $TARGET_DIR to a GitHub repository." + say " 2. On GitHub: Code -> Codespaces -> Create codespace." + say " 3. In the codespace's terminal, run: cabal build all" + ;; + standalone) + say " cd $TARGET_DIR" + say " docker run -v \"\$PWD:/workspaces/my-project\" -w /workspaces/my-project \\" + say " -it ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog" + say " # then, inside the container:" + say " cabal build all" + ;; + esac + ;; + demeter) + say " 1. Push $TARGET_DIR to a GitHub repository." + say " 2. Create an account at https://demeter.run and follow" + say " https://docs.demeter.run to open a workspace from your repository." + say " 3. In the workspace's terminal, run:" + say " nix develop --accept-flake-config ${DIM}# first run downloads the toolchain${RESET}" + say " cabal build all" + ;; + cabal) + say " cd $TARGET_DIR" + say " cabal update" + if [ "$CRYPTO_MODE" = system ]; then + say " export PKG_CONFIG_PATH=\"$SYSTEM_CRYPTO_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" + say " export PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 ${DIM}# add both to your shell profile${RESET}" + fi + say " cabal build all" + if [ "$CRYPTO_MODE" = local ] || [ "$CRYPTO_MODE" = skip ]; then + say "" + say " ${DIM}Run cabal from the project root. If the very first build stops once" + say " with \"Cannot find the program 'pkg-config'\", just re-run it — that" + say " happens only on the first cold build (see README).${RESET}" + fi + ;; + esac + say "" + say " ${BOLD}cabal build all${RESET} compiles the example auction validator; then follow" + say " https://plutus.cardano.intersectmbo.org/docs/ to make it your own." + say "" +} + +# --------------------------------------------------------------------------- + +usage() { + say "plinth-template installer — set up a new Plinth smart contract project." + say "" + say "Usage: install.sh [flags] (interactive when a terminal is available)" + say "" + say " --env ENV nix | docker | demeter | cabal" + say " --docker-mode MODE codespaces | devcontainer | standalone (with --env docker)" + say " --crypto-libs MODE local | system | skip (with --env cabal)" + say " --prefix DIR prefix for --crypto-libs system (default /usr/local)" + say " --dir NAME project directory to create (default: my-plinth-project)" + say " --repo URL template repository (or set PLINTH_TEMPLATE_REPO)" + say " --branch NAME fetch NAME instead of the repository's default branch" + say " --from DIR take the template from a local directory (offline, CI)" + say " --yes, -y don't ask; use defaults for unanswered questions" + say " --help, -h this text" +} + +main() { + setup_colors + REPO="${PLINTH_TEMPLATE_REPO:-$REPO_DEFAULT}" + ENV_CHOICE="" + DOCKER_MODE="" + CRYPTO_MODE="" + CRYPTO_PREFIX="/usr/local" + SYSTEM_CRYPTO_PREFIX="" + TARGET_DIR="" + BRANCH_OVERRIDE="" + FROM_DIR="" + ASSUME_YES=0 + + while [ $# -gt 0 ]; do + case "$1" in + --env) shift; ENV_CHOICE="${1:?--env needs an argument}" ;; + --env=*) ENV_CHOICE="${1#--env=}" ;; + --docker-mode) shift; DOCKER_MODE="${1:?--docker-mode needs an argument}" ;; + --docker-mode=*) DOCKER_MODE="${1#--docker-mode=}" ;; + --crypto-libs) shift; CRYPTO_MODE="${1:?--crypto-libs needs an argument}" ;; + --crypto-libs=*) CRYPTO_MODE="${1#--crypto-libs=}" ;; + --prefix) shift; CRYPTO_PREFIX="${1:?--prefix needs an argument}" ;; + --prefix=*) CRYPTO_PREFIX="${1#--prefix=}" ;; + --dir) shift; TARGET_DIR="${1:?--dir needs an argument}" ;; + --dir=*) TARGET_DIR="${1#--dir=}" ;; + --repo) shift; REPO="${1:?--repo needs an argument}" ;; + --repo=*) REPO="${1#--repo=}" ;; + --branch) shift; BRANCH_OVERRIDE="${1:?--branch needs an argument}" ;; + --branch=*) BRANCH_OVERRIDE="${1#--branch=}" ;; + --from) shift; FROM_DIR="${1:?--from needs an argument}" ;; + --from=*) FROM_DIR="${1#--from=}" ;; + -y|--yes) ASSUME_YES=1 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown flag: $1 (see --help)" ;; + esac + shift + done + + case "$ENV_CHOICE" in + ''|nix|docker|demeter|cabal) ;; + ghc-cabal|ghc+cabal) ENV_CHOICE=cabal ;; + *) die "invalid --env '$ENV_CHOICE' (valid: nix, docker, demeter, cabal)" ;; + esac + case "$DOCKER_MODE" in + ''|codespaces|devcontainer|standalone) ;; + *) die "invalid --docker-mode '$DOCKER_MODE' (valid: codespaces, devcontainer, standalone)" ;; + esac + case "$CRYPTO_MODE" in + ''|local|system|skip) ;; + *) die "invalid --crypto-libs '$CRYPTO_MODE' (valid: local, system, skip)" ;; + esac + + setup_input + detect_platform + + say "" + say "${BOLD}plinth-template${RESET} — set up a new Plinth smart contract project" + say "${DIM}Plinth is Cardano's Haskell-based smart contract language (GHC 9.6/9.12).${RESET}" + [ "$IS_WSL" = 1 ] && say "${DIM}(WSL detected — following the Linux path.)${RESET}" + + if [ "$INTERACTIVE" = 0 ] && [ "$ASSUME_YES" = 0 ] && [ -z "$ENV_CHOICE" ]; then + die "no terminal available for questions: pass --env (and other flags), or --yes for defaults" + fi + + select_env + [ -z "$ENV_CHOICE" ] && ENV_CHOICE="$(detected_default_env)" + select_docker_mode + [ "$ENV_CHOICE" = docker ] && [ -z "$DOCKER_MODE" ] && DOCKER_MODE=devcontainer + + explain_crypto_libs + select_crypto_mode + [ "$ENV_CHOICE" = cabal ] && [ -z "$CRYPTO_MODE" ] && CRYPTO_MODE=local + + say "" + info "Checking prerequisites for the '$ENV_CHOICE' environment" + case "$ENV_CHOICE" in + nix) check_nix ;; + docker) check_docker "$DOCKER_MODE" ;; + demeter) check_demeter ;; + cabal) check_cabal_env ;; + esac + + [ -n "$TARGET_DIR" ] || TARGET_DIR="$(ask "Project directory" "my-plinth-project")" + TARGET_DIR="$(expand_tilde "$TARGET_DIR")" + [ -e "$TARGET_DIR" ] && die "target directory '$TARGET_DIR' already exists; pick another name (--dir)" + + trap 'if [ -n "$SRC_CLEANUP" ]; then rm -rf "$SRC_CLEANUP"; fi' EXIT + fetch_source + create_project "$ENV_CHOICE" "$TARGET_DIR" + install_crypto_libs + next_steps +} + +main "$@" diff --git a/nix/outputs.nix b/nix/outputs.nix index 914a23d..5ff6ecc 100644 --- a/nix/outputs.nix +++ b/nix/outputs.nix @@ -11,15 +11,17 @@ let mkShell = ghc: import ./shell.nix { inherit inputs pkgs lib project utils ghc; }; - devShells.default = mkShell "ghc966"; + devShells.default = mkShell "ghc96"; + devShells.ghc96 = mkShell "ghc96"; + devShells.ghc912 = mkShell "ghc912"; projectFlake = project.flake {}; - defaultHydraJobs = { - ghc966 = projectFlake.hydraJobs.ghc966; - inherit packages; + defaultHydraJobs = { + ghc96 = projectFlake.hydraJobs.ghc96; + inherit packages; inherit devShells; - required = utils.makeHydraRequiredJob hydraJobs; + required = utils.makeHydraRequiredJob hydraJobs; }; hydraJobsPerSystem = { diff --git a/nix/project.nix b/nix/project.nix index c8fb03d..cb74418 100644 --- a/nix/project.nix +++ b/nix/project.nix @@ -8,12 +8,26 @@ let { name = "my-project"; - compiler-nix-name = lib.mkDefault "ghc966"; + compiler-nix-name = lib.mkDefault "ghc967"; + + # BEGIN union source only + # (install.sh replaces this whole block with a plain + # `src = lib.cleanSource ../.;` for Nix/Demeter projects, which carry + # neither the pkg-config stanza in cabal.project nor a shim to strip) + src = lib.cleanSourceWith { + src = lib.cleanSource ../.; + filter = path: type: baseNameOf path != "cabal.project.local"; + }; - src = lib.cleanSource ../.; + cabalProject = builtins.replaceStrings + [ "program-locations\n pkg-config-location: ./scripts/pkg-config" ] + [ "" ] + (builtins.readFile ../cabal.project); + # END union source only flake.variants = { - ghc966 = {}; # Alias for the default variant + ghc96 = {}; # Alias for the default variant + ghc912 = { compiler-nix-name = "ghc9122"; }; }; inputMap = { "https://chap.intersectmbo.org/" = inputs.CHaP; }; diff --git a/nix/shell.nix b/nix/shell.nix index 26fb4a6..c3102b8 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -2,21 +2,21 @@ let - allTools = { - "ghc966".cabal = project.projectVariants.ghc966.tool "cabal" "latest"; - "ghc966".cabal-fmt = project.projectVariants.ghc966.tool "cabal-fmt" "latest"; - "ghc966".haskell-language-server = project.projectVariants.ghc966.tool "haskell-language-server" "latest"; - "ghc966".stylish-haskell = project.projectVariants.ghc966.tool "stylish-haskell" "latest"; - "ghc966".fourmolu = project.projectVariants.ghc966.tool "fourmolu" "latest"; - "ghc966".hlint = project.projectVariants.ghc966.tool "hlint" "latest"; - }; + variant = project.projectVariants.${ghc}; - tools = allTools.${ghc}; + tools = { + cabal = variant.tool "cabal" "latest"; + haskell-language-server = variant.tool "haskell-language-server" "latest"; + cabal-fmt = project.tool "cabal-fmt" "latest"; + stylish-haskell = project.tool "stylish-haskell" "latest"; + fourmolu = project.tool "fourmolu" "latest"; + hlint = project.tool "hlint" "latest"; + }; preCommitCheck = inputs.pre-commit-hooks.lib.${pkgs.system}.run { src = lib.cleanSources ../.; - + hooks = { shellcheck = { enable = false; @@ -76,8 +76,8 @@ let pkgs.which ]; - shell = project.shellFor { - name = "plinth-${project.args.compiler-nix-name}"; + shell = variant.shellFor { + name = "plinth-${variant.args.compiler-nix-name}"; buildInputs = lib.concatLists [ commonPkgs diff --git a/scripts/get-crypto-libs.sh b/scripts/get-crypto-libs.sh new file mode 100755 index 0000000..d28331b --- /dev/null +++ b/scripts/get-crypto-libs.sh @@ -0,0 +1,492 @@ +#!/usr/bin/env bash +# +# Download the prebuilt Cardano crypto C libraries (libsodium, libsecp256k1, +# libblst) published by IOG at https://github.com/input-output-hk/iohk-nix/releases +# so that cardano-crypto-class (a transitive dependency of plutus-core) can +# satisfy its pkgconfig-depends without nix and without installing anything +# system-wide. The files are stored once per user in +# ~/.cache/plinth-crypto-libs/ (override with PLINTH_CRYPTO_LIBS_HOME) and +# linked into the project at dist-newstyle/crypto-libs/: the cache +# path is what cabal bakes into compiled packages, so the user-wide cabal +# store stays valid across projects and survives `cabal clean` — which only +# removes the link; the next run relinks instantly, nothing is re-downloaded. +# +# After running this script, point pkg-config at the local install, e.g.: +# +# source dist-newstyle/crypto-libs/env.sh +# cabal build all +# +# or, fully hermetic (ONLY the downloaded libs are visible to pkg-config): +# +# export PKG_CONFIG_LIBDIR="$(pwd)/dist-newstyle/crypto-libs//lib/pkgconfig" +# +# The feature is ON by default. To opt out (i.e. you have the crypto libs +# installed system-wide and want cabal to use those), set +# +# PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 +# +# in which case this script is a no-op. +# +# Every downloaded artifact is verified against a sha256 pinned in this file, +# and the release's COMMIT_SHA asset is required to match the pinned iohk-nix +# commit below. +# +# Required tools: curl, tar (the system one), and shasum or sha256sum. +# All of these ship by default on macOS and on typical Linux distributions. +# On Windows, use the msys2.* release assets with pacman inside GHC's MSYS2 +# environment (see README). + +set -euo pipefail + +# -------------------------------------------------------------------------- +# Pinned release +# -------------------------------------------------------------------------- + +RELEASE_TAG="v3.1" +# The iohk-nix commit the release was built from. The release publishes this +# in its COMMIT_SHA asset, which we download and check. +IOHK_NIX_COMMIT="bfdd1c3c12829d26a0e9a44f474d2adba45bf6c0" +COMMIT_SHA_ASSET_SHA256="a82bb754e8566a8c0e7814a429200d064420bed3de3a6a6a1458366a1402e2f6" + +BASE_URL="https://github.com/input-output-hk/iohk-nix/releases/download/${RELEASE_TAG}" + +# Per-platform assets and their pinned sha256 digests (from the GitHub release). +# Format: " " +ASSETS_arm64_macos=" +arm64-macos.libsodium.pkg b122d53cdc65ac1bf7f0c68ec093bbc49ffd9ab4a6472f45d043691fe1461c9d +arm64-macos.libsecp256k1.pkg 9cb674391baac56d45b7da6c4509f85037601d51eeae56db6b196f866a1db1dd +arm64-macos.libblst.pkg 71b0146d6b0310f2b6a7cb554444e98b0c9a5d6b9aeac025aeb351238593c45e +" +ASSETS_x86_64_macos=" +x86_64-macos.libsodium.pkg e7a54367f652314ddaeb929a49dd59ae64061196f9df109841c21bb5d4c8eddd +x86_64-macos.libsecp256k1.pkg 2b3587189149d7a84caa7d1c90459f9d4ee155f95d83f7245b868cd948946138 +x86_64-macos.libblst.pkg 118dd9078e894b98192643b5405463e85bb8fef50db84581c453e4ad2bce4491 +" +ASSETS_debian=" +debian.libsodium.deb 48d36a4a2c683325b12c801a55b2efc0104e31695e99850bc9bfa7e40513d935 +debian.libsecp256k1.deb 4ff5b3d834478da36a5f4ec90bf592ac8b2445afed55d326f05b73aba5bae317 +debian.libblst.deb ad6bbf94d98d7ded1947beea21e8accb0407086729f1b33b898980a9b684e797 +" + +# -------------------------------------------------------------------------- + +usage() { + cat <&2; usage >&2; exit 2 ;; + esac + shift +done + +say() { [ "$QUIET" = 1 ] || echo "$@"; } +die() { echo "get-crypto-libs: ERROR: $*" >&2; exit 1; } + +if [ "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-0}" = 1 ] && [ -z "$SYSTEM_PREFIX" ]; then + say "PLINTH_USE_SYSTEM_CRYPTO_LIBS=1: skipping download, expecting system-wide crypto libs." + exit 0 +fi + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# --------------------------------------------------------------------------- +# Project-side helpers (local mode). They run both after a fresh install and +# on the fast already-installed path, so a wiped dist-newstyle or a deleted +# cabal.project.local heals on the next run — without re-downloading. +# --------------------------------------------------------------------------- + +write_env_file() { + mkdir -p "$(dirname "$ENV_FILE")" + cat > "$ENV_FILE" </dev/null || true + rm -rf "$LINK" + fi + rm -f "$LINK" + ln -s "$PREFIX" "$LINK" +} + +# The program-locations stanza in cabal.project (relative path) covers +# cabal's dependency SOLVER, but the configure step of non-local packages +# (cardano-crypto-class) runs with a different working directory and needs an +# absolute path, which cannot be committed. It is therefore kept in the +# machine-local, gitignored cabal.project.local, inside a marked block that +# this script owns and refreshes. +maintain_cabal_project_local() { + LOCAL_FILE="$REPO_ROOT/cabal.project.local" + MARK_BEGIN="-- BEGIN plinth-template crypto-libs (generated by scripts/get-crypto-libs.sh)" + MARK_END="-- END plinth-template crypto-libs" + SHIM="$REPO_ROOT/scripts/pkg-config" + + if ! grep -qF "pkg-config-location: $SHIM" "$LOCAL_FILE" 2>/dev/null; then + tmp="$LOCAL_FILE.tmp.$$" + if [ -f "$LOCAL_FILE" ]; then + awk -v begin="$MARK_BEGIN" -v end="$MARK_END" ' + $0 == begin { skip = 1; next } + $0 == end { skip = 0; next } + !skip { print } + ' "$LOCAL_FILE" > "$tmp" + else + : > "$tmp" + fi + { + echo "$MARK_BEGIN" + echo "package *" + echo " pkg-config-location: $SHIM" + echo "$MARK_END" + } >> "$tmp" + mv "$tmp" "$LOCAL_FILE" + say "Updated $LOCAL_FILE (pkg-config shim location for package configure)." + say "NOTE: a cabal command that was already running when this file was" + say "created cannot pick it up; if that build stops with 'Cannot find the" + say "program pkg-config', simply re-run it (this happens at most once)." + fi +} + +# -------------------------------------------------------------------------- +# Platform selection: explicit (--platform / PLINTH_CRYPTO_LIBS_PLATFORM, +# e.g. when cross compiling) or detected from the host. +# -------------------------------------------------------------------------- + +if [ -z "$PLATFORM" ]; then + case "$(uname -s)-$(uname -m)" in + Darwin-arm64) PLATFORM="arm64-macos" ;; + Darwin-x86_64) PLATFORM="x86_64-macos" ;; + Linux-*) PLATFORM="debian" ;; + MINGW*|MSYS*|CYGWIN*) + die "on Windows, install the msys2.* assets from ${BASE_URL} with pacman inside GHC's MSYS2 environment (see README)" ;; + *) die "cannot detect platform from '$(uname -s)-$(uname -m)'; pass --platform" ;; + esac +fi + +case "$PLATFORM" in + arm64-macos) ASSETS="$ASSETS_arm64_macos" ;; + x86_64-macos) ASSETS="$ASSETS_x86_64_macos" ;; + debian) ASSETS="$ASSETS_debian" ;; + *) die "unsupported platform '$PLATFORM' (valid: arm64-macos, x86_64-macos, debian)" ;; +esac + +# bsdtar understands the xar container of macOS .pkg files, the gzipped-cpio +# Payload inside it, and the ar container of .deb files. On macOS /usr/bin/tar +# is always bsdtar (a GNU tar earlier in \$PATH, e.g. from nix, would not +# work); elsewhere fall back to a bsdtar in PATH. +if [ -x /usr/bin/tar ] && /usr/bin/tar --version 2>/dev/null | grep -q bsdtar; then + BSDTAR=/usr/bin/tar +elif command -v bsdtar >/dev/null 2>&1; then + BSDTAR="$(command -v bsdtar)" +else + BSDTAR="" +fi + +# Two install modes: +# - local (default): extract straight into dist-newstyle/crypto-libs/, +# which is entirely owned by this script (safe to wipe and rebuild). +# - system (--prefix DIR): extract into a throwaway staging directory, fix the +# files up for DIR, and only then copy them over. DIR is never wiped; only +# the three libraries' own lib/ and include/ entries are (over)written. +if [ -n "$SYSTEM_PREFIX" ]; then + # A quoted "~" is not expanded by the caller's shell; do it here, then + # absolutize (relative prefixes would silently depend on the cwd). + # shellcheck disable=SC2088 # matching a LITERAL ~ the shell didn't expand + case "$SYSTEM_PREFIX" in + "~") SYSTEM_PREFIX="$HOME" ;; + "~/"*) SYSTEM_PREFIX="$HOME/${SYSTEM_PREFIX#"~"/}" ;; + esac + case "$SYSTEM_PREFIX" in + /*) : ;; + *) SYSTEM_PREFIX="$(pwd)/$SYSTEM_PREFIX" ;; + esac + if ! mkdir -p "$SYSTEM_PREFIX/lib" "$SYSTEM_PREFIX/include" 2>/dev/null \ + || [ ! -w "$SYSTEM_PREFIX/lib" ]; then + die "cannot write to $SYSTEM_PREFIX; rerun with sudo (or pick a writable --prefix)" + fi + STAGE="$(mktemp -d "${TMPDIR:-/tmp}/plinth-crypto-libs.XXXXXX")" + trap 'chmod -R u+w "$STAGE" 2>/dev/null || true; rm -rf "$STAGE"' EXIT + PREFIX="$STAGE/root" + DOWNLOADS="$STAGE/downloads" + FINAL_PREFIX="$SYSTEM_PREFIX" +else + # Local (default) mode: the real files live in a stable per-user cache and + # dist-newstyle/crypto-libs/ is a symlink to them. cabal bakes + # the libraries' absolute paths (dylib install names / runpaths) into the + # packages it compiles into the user-wide store; a path under one + # project's dist-newstyle would break every other project's builds as soon + # as this project moved or disappeared. The cache path is stable, keyed by + # release tag, shared by all Plinth projects, and nothing else on the + # system is touched. + CACHE_HOME="${PLINTH_CRYPTO_LIBS_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/plinth-crypto-libs}" + PREFIX="$CACHE_HOME/$RELEASE_TAG/$PLATFORM" + DOWNLOADS="$CACHE_HOME/downloads" + LINK_DIR="$REPO_ROOT/dist-newstyle/crypto-libs" + LINK="$LINK_DIR/$PLATFORM" + ENV_FILE="$LINK_DIR/env.sh" + STAMP="$PREFIX/.installed-$RELEASE_TAG-$IOHK_NIX_COMMIT" + FINAL_PREFIX="$PREFIX" + + if [ -f "$STAMP" ] && [ "$FORCE" != 1 ]; then + link_into_project + write_env_file + maintain_cabal_project_local + say "Crypto libs already installed in $PREFIX" + say "(release $RELEASE_TAG, iohk-nix commit $IOHK_NIX_COMMIT; linked from $LINK)." + say "Use --force to reinstall. To use them: source $ENV_FILE" + exit 0 + fi +fi + +command -v curl >/dev/null 2>&1 || die "curl is required" + +sha256_of() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + die "need shasum or sha256sum to verify downloads" + fi +} + +download() { + # download + local asset="$1" expected="$2" out="$DOWNLOADS/$1" actual + if [ ! -f "$out" ] || [ "$(sha256_of "$out")" != "$expected" ]; then + say "Downloading $asset ..." + curl --fail --silent --show-error --location --retry 3 \ + --output "$out.tmp" "$BASE_URL/$asset" \ + || die "failed to download $BASE_URL/$asset" + mv "$out.tmp" "$out" + fi + actual="$(sha256_of "$out")" + [ "$actual" = "$expected" ] \ + || die "sha256 mismatch for $asset: expected $expected, got $actual" + say "Verified $asset (sha256 OK)" +} + +[ -d "$PREFIX" ] && chmod -R u+w "$PREFIX" +rm -rf "$PREFIX" +mkdir -p "$PREFIX" "$DOWNLOADS" + +# -------------------------------------------------------------------------- +# Check the release's COMMIT_SHA against the pinned iohk-nix commit +# -------------------------------------------------------------------------- + +download "COMMIT_SHA" "$COMMIT_SHA_ASSET_SHA256" +RELEASE_COMMIT="$(cat "$DOWNLOADS/COMMIT_SHA")" +[ "$RELEASE_COMMIT" = "$IOHK_NIX_COMMIT" ] \ + || die "release COMMIT_SHA ($RELEASE_COMMIT) does not match pinned iohk-nix commit ($IOHK_NIX_COMMIT)" +say "Release $RELEASE_TAG was built from pinned iohk-nix commit $IOHK_NIX_COMMIT (verified)" + +# -------------------------------------------------------------------------- +# Download, verify and extract each library +# -------------------------------------------------------------------------- + +extract_into_prefix() { + # extract_into_prefix + local file="$1" + case "$file" in + *.pkg) + # macOS flat installer package: a xar archive whose Payload member is a + # gzipped cpio with the files rooted at the install prefix. + [ -n "$BSDTAR" ] || die "extracting $file requires bsdtar (libarchive)" + local tmp + tmp="$(mktemp -d "$DOWNLOADS/expand.XXXXXX")" + "$BSDTAR" -xf "$file" -C "$tmp" Payload + (cd "$PREFIX" && "$BSDTAR" -xf "$tmp/Payload") + rm -rf "$tmp" + ;; + *.deb) + # Debian package: payload in data.tar.gz under ./usr/local/opt/cardano/. + local tmp + tmp="$(mktemp -d "$DOWNLOADS/expand.XXXXXX")" + if command -v dpkg-deb >/dev/null 2>&1; then + dpkg-deb -x "$file" "$tmp" + elif [ -n "$BSDTAR" ]; then + "$BSDTAR" -xOf "$file" data.tar.gz | tar -xzf - -C "$tmp" + elif command -v ar >/dev/null 2>&1; then + ar p "$file" data.tar.gz | tar -xzf - -C "$tmp" + else + die "need dpkg-deb, bsdtar or ar to extract $file" + fi + cp -a "$tmp/usr/local/opt/cardano/." "$PREFIX/" + chmod -R u+w "$tmp" + rm -rf "$tmp" + ;; + *) + die "don't know how to extract $file" + ;; + esac +} + +for entry in $(echo "$ASSETS" | awk 'NF {print $1 "@" $2}'); do + asset="${entry%@*}" + sha="${entry#*@}" + download "$asset" "$sha" + extract_into_prefix "$DOWNLOADS/$asset" + # The payloads were built in the nix store and carry read-only permission + # bits; make everything user-writable so subsequent extractions and + # reinstalls into the same prefix work. + chmod -R u+w "$PREFIX" +done + +# -------------------------------------------------------------------------- +# Fix up the local install: +# - rewrite the .pc files' prefix to the local install dir +# - create the unversioned .dylib/.so symlinks the linker needs +# - (macOS) rewrite dylib install names to their real local path, so that +# executables linked against them work without DYLD_LIBRARY_PATH +# - drop libtool .la files (they carry stale paths and are not needed) +# -------------------------------------------------------------------------- + +[ -d "$PREFIX/lib/pkgconfig" ] || die "extraction failed: $PREFIX/lib/pkgconfig missing" + +# (not sed: an install path containing '&', '|' or '\' would corrupt the +# replacement. prefix= must stay first — later variables reference it.) +for pc in "$PREFIX"/lib/pkgconfig/*.pc; do + { echo "prefix=$FINAL_PREFIX"; grep -v '^prefix=' "$pc"; } > "$pc.tmp" \ + && mv "$pc.tmp" "$pc" +done + +rm -f "$PREFIX"/lib/*.la + +case "$PLATFORM" in + *macos) + if command -v install_name_tool >/dev/null 2>&1; then + for dylib in "$PREFIX"/lib/*.dylib; do + [ -L "$dylib" ] && continue + install_name_tool -id "$FINAL_PREFIX/lib/$(basename "$dylib")" "$dylib" 2>/dev/null + done + elif [ "$(uname -s)" = Darwin ]; then + die "install_name_tool not found (install the Xcode command line tools: xcode-select --install)" + else + say "NOTE: install_name_tool unavailable on this host; dylib install names keep their original (nix store) paths." + fi + # Unversioned symlinks (the .pkg payloads only ship versioned dylibs). + ( + cd "$PREFIX/lib" + [ -e libsodium.dylib ] || ln -s libsodium.*.dylib libsodium.dylib + [ -e libsecp256k1.dylib ] || ln -s libsecp256k1.*.dylib libsecp256k1.dylib + [ -e libblst.dylib ] || true # shipped unversioned already + ) + ;; + debian) + ( + cd "$PREFIX/lib" + for base in libsodium libsecp256k1 libblst; do + if [ ! -e "$base.so" ]; then + for versioned in "$base".so.*; do + if [ -e "$versioned" ]; then ln -s "$versioned" "$base.so"; break; fi + done + fi + done + ) + ;; +esac + +# -------------------------------------------------------------------------- +# System mode: merge-copy the staged, fixed-up files into the final prefix. +# Only lib/ and include/ contents coming from the three payloads are written; +# nothing already in the prefix is removed. +# -------------------------------------------------------------------------- + +if [ -n "$SYSTEM_PREFIX" ]; then + chmod -R u+w "$PREFIX" + cp -R "$PREFIX/lib/." "$SYSTEM_PREFIX/lib/" + [ -d "$PREFIX/include" ] && cp -R "$PREFIX/include/." "$SYSTEM_PREFIX/include/" +fi + +# -------------------------------------------------------------------------- +# Sanity-check with pkg-config if available, write env file and stamp +# -------------------------------------------------------------------------- + +if command -v pkg-config >/dev/null 2>&1; then + for lib in libsodium libsecp256k1 libblst; do + v="$(PKG_CONFIG_LIBDIR="$FINAL_PREFIX/lib/pkgconfig" pkg-config --modversion "$lib")" \ + || die "pkg-config cannot resolve $lib from $FINAL_PREFIX/lib/pkgconfig" + p="$(PKG_CONFIG_LIBDIR="$FINAL_PREFIX/lib/pkgconfig" pkg-config --variable=prefix "$lib")" + [ "$p" = "$FINAL_PREFIX" ] || die "$lib resolves to unexpected prefix: $p" + say "$lib $v -> $FINAL_PREFIX (pkg-config OK)" + done +else + say "NOTE: pkg-config not found. cabal needs a pkg-config executable to use these libs." +fi + +if [ -n "$SYSTEM_PREFIX" ]; then + say "" + say "Installed libsodium, libsecp256k1 and libblst into: $SYSTEM_PREFIX" + say "" + say "To build against them, make sure pkg-config can see them and tell the" + say "project to use system libraries:" + say "" + say " export PKG_CONFIG_PATH=\"$SYSTEM_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" + say " export PLINTH_USE_SYSTEM_CRYPTO_LIBS=1" + say " cabal build all" + exit 0 +fi + +touch "$STAMP" +link_into_project +write_env_file +maintain_cabal_project_local + +say "" +say "Installed libsodium, libsecp256k1 and libblst into the per-user cache:" +say " $PREFIX" +say "and linked them into the project at:" +say " $LINK" +say "" +say "To build with them:" +say " source $ENV_FILE" +say " cabal build all" diff --git a/scripts/pkg-config b/scripts/pkg-config new file mode 100755 index 0000000..ae8fddc --- /dev/null +++ b/scripts/pkg-config @@ -0,0 +1,55 @@ +#!/bin/sh +# +# pkg-config wrapper for plinth-template. cabal is pointed at it by +# cabal.project (solver) and cabal.project.local (package configure, written +# by get-crypto-libs.sh). It runs scripts/get-crypto-libs.sh — which downloads +# IOG's prebuilt libsodium/libsecp256k1/libblst into dist-newstyle/crypto-libs/ +# on first use and is a fast no-op afterwards — and then answers the query +# with the real pkg-config, restricted to the downloaded libraries. +# +# Inside nix (IN_NIX_SHELL / NIX_BUILD_TOP) or when +# PLINTH_USE_SYSTEM_CRYPTO_LIBS=1, it delegates to the real pkg-config +# untouched and downloads nothing. When cross compiling, set +# PLINTH_CRYPTO_LIBS_PLATFORM to the target platform (arm64-macos, +# x86_64-macos or debian). + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +root=$(CDPATH='' cd -- "$script_dir/.." && pwd) + +real=$( + IFS=: + for d in $PATH; do + [ -x "$d/pkg-config" ] && [ "$d/pkg-config" != "$script_dir/pkg-config" ] \ + && { printf '%s\n' "$d/pkg-config"; break; } + done + # keep the subshell's status 0: under set -e a failed final test would + # otherwise kill the shim before the diagnostic below can print + : +) +if [ -z "$real" ]; then + echo "scripts/pkg-config: no real pkg-config found on PATH (install it, e.g. brew install pkgconf / apt install pkg-config)" >&2 + exit 1 +fi + +if [ "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-0}" = 1 ] \ + || [ -n "${IN_NIX_SHELL:-}" ] || [ -n "${NIX_BUILD_TOP:-}" ]; then + exec "$real" "$@" +fi + +platform="${PLINTH_CRYPTO_LIBS_PLATFORM:-}" +if [ -z "$platform" ]; then + case "$(uname -s)-$(uname -m)" in + Darwin-arm64) platform=arm64-macos ;; + Darwin-x86_64) platform=x86_64-macos ;; + Linux-*) platform=debian ;; + *) echo "scripts/pkg-config: cannot detect platform; set PLINTH_CRYPTO_LIBS_PLATFORM" >&2; exit 1 ;; + esac +fi + +# stdout must carry only query results; the installer's output goes to stderr. +"$script_dir/get-crypto-libs.sh" --quiet --platform "$platform" 1>&2 + +PKG_CONFIG_LIBDIR="$root/dist-newstyle/crypto-libs/$platform/lib/pkgconfig" \ + exec "$real" "$@" From ded049ddea9d489a36dba01ececf1ca5c72de9f3 Mon Sep 17 00:00:00 2001 From: zeme Date: Tue, 28 Jul 2026 14:20:10 +0200 Subject: [PATCH 2/5] Restructure repository around a template/ directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganize the repository so that everything a generated project is made of lives in one place, and simplify how projects are created: * template/ now carries the union of every environment's project files (sources, cabal files, nix files, .devcontainer, configs, licenses, per-environment readmes). install.sh copies a verbatim per-environment subset of it — no marked blocks, no file transforms. * Remove the pkg-config shim (scripts/pkg-config) and its whole feature: the program-locations stanza in cabal.project, the generated cabal.project.local, and the cold-start build interruption. GHC+Cabal projects now run ./get-crypto-libs.sh (done by the installer) and source dist-newstyle/crypto-libs/env.sh instead. * Move get-crypto-libs.sh to the repository root, next to install.sh; GHC+Cabal projects carry it at their project root. * Move the CI scripts from dev/ci/ to .github/ci/ and merge all build workflows into a single ci.yaml running every job on every pull request (no path filters): lint, install.sh end-to-end tests (ubuntu/macos), GHC+Cabal builds (ubuntu/macos × GHC 9.6.7/9.12.2), native Windows builds (MSYS2 crypto libs, GHC 9.6.7/9.12.2), nix builds (ghc96/ghc912) and the devx docker build. * Extract the bump-plutus-version workflow logic into .github/ci/bump-plutus-version.sh so it can be run and tested locally (portable: python3 instead of GNU date/tar). * Remove .github/copilot-instructions.md and the obsolete --branch flag from install.sh. * Shell style: replace all statement-level `[ x ] && cmd` / `[ x ] || cmd` shortcut control flow with explicit if/else. Co-Authored-By: Claude Fable 5 --- {dev => .github}/ci/build-docker.sh | 51 ++- {dev => .github}/ci/build-ghc-cabal.sh | 82 +++-- .github/ci/build-nix.sh | 109 ++++++ .github/ci/build-windows.sh | 128 +++++++ .github/ci/bump-plutus-version.sh | 103 ++++++ {dev => .github}/ci/lint.sh | 24 +- {dev => .github}/ci/run-all-local.sh | 21 +- {dev => .github}/ci/test-blueprint-parity.sh | 184 +++++----- {dev => .github}/ci/test-install.sh | 112 +++--- .github/copilot-instructions.md | 267 -------------- .github/workflows/build-docker.yml | 29 -- .github/workflows/build-ghc-cabal.yml | 56 --- .github/workflows/build-nix.yml | 41 --- .github/workflows/bump-plutus-version.yml | 48 +-- .github/workflows/ci.yaml | 142 +++++++ .github/workflows/ci.yml | 37 -- .gitignore | 7 +- CODEOWNERS.md | 2 +- DESCRIPTION.md | 2 +- README.md | 75 ++-- dev/ci/build-nix.sh | 96 ----- dev/readmes/ghc-cabal.md | 80 ---- .../get-crypto-libs.sh => get-crypto-libs.sh | 143 +++----- install.sh | 346 +++++++++++------- nix/project.nix | 43 --- scripts/pkg-config | 55 --- .../.devcontainer}/devcontainer.json | 0 template/.gitignore | 7 + .hlint.yaml => template/.hlint.yaml | 0 .../.stylish-haskell.yaml | 0 template/LICENSE.md | 201 ++++++++++ template/NOTICE.md | 11 + .../app}/GenAuctionValidatorBlueprint.hs | 0 .../app}/GenMintingPolicyBlueprint.hs | 0 cabal.project => template/cabal.project | 14 - flake.lock => template/flake.lock | 0 flake.nix => template/flake.nix | 0 {nix => template/nix}/outputs.nix | 0 {nix => template/nix}/pkgs.nix | 0 template/nix/project.nix | 30 ++ {nix => template/nix}/shell.nix | 0 {nix => template/nix}/utils.nix | 0 .../plinth-template.cabal | 0 {dev => template}/readmes/demeter.md | 0 {dev => template}/readmes/docker.md | 0 template/readmes/ghc-cabal.md | 76 ++++ {dev => template}/readmes/nix.md | 0 {src => template/src}/AuctionMintingPolicy.hs | 0 {src => template/src}/AuctionValidator.hs | 0 49 files changed, 1379 insertions(+), 1243 deletions(-) rename {dev => .github}/ci/build-docker.sh (59%) rename {dev => .github}/ci/build-ghc-cabal.sh (62%) create mode 100755 .github/ci/build-nix.sh create mode 100755 .github/ci/build-windows.sh create mode 100755 .github/ci/bump-plutus-version.sh rename {dev => .github}/ci/lint.sh (74%) rename {dev => .github}/ci/run-all-local.sh (76%) rename {dev => .github}/ci/test-blueprint-parity.sh (65%) rename {dev => .github}/ci/test-install.sh (83%) delete mode 100644 .github/copilot-instructions.md delete mode 100644 .github/workflows/build-docker.yml delete mode 100644 .github/workflows/build-ghc-cabal.yml delete mode 100644 .github/workflows/build-nix.yml create mode 100644 .github/workflows/ci.yaml delete mode 100644 .github/workflows/ci.yml delete mode 100755 dev/ci/build-nix.sh delete mode 100644 dev/readmes/ghc-cabal.md rename scripts/get-crypto-libs.sh => get-crypto-libs.sh (78%) delete mode 100644 nix/project.nix delete mode 100755 scripts/pkg-config rename {.devcontainer => template/.devcontainer}/devcontainer.json (100%) create mode 100644 template/.gitignore rename .hlint.yaml => template/.hlint.yaml (100%) rename .stylish-haskell.yaml => template/.stylish-haskell.yaml (100%) create mode 100644 template/LICENSE.md create mode 100644 template/NOTICE.md rename {app => template/app}/GenAuctionValidatorBlueprint.hs (100%) rename {app => template/app}/GenMintingPolicyBlueprint.hs (100%) rename cabal.project => template/cabal.project (51%) rename flake.lock => template/flake.lock (100%) rename flake.nix => template/flake.nix (100%) rename {nix => template/nix}/outputs.nix (100%) rename {nix => template/nix}/pkgs.nix (100%) create mode 100644 template/nix/project.nix rename {nix => template/nix}/shell.nix (100%) rename {nix => template/nix}/utils.nix (100%) rename plinth-template.cabal => template/plinth-template.cabal (100%) rename {dev => template}/readmes/demeter.md (100%) rename {dev => template}/readmes/docker.md (100%) create mode 100644 template/readmes/ghc-cabal.md rename {dev => template}/readmes/nix.md (100%) rename {src => template/src}/AuctionMintingPolicy.hs (100%) rename {src => template/src}/AuctionValidator.hs (100%) diff --git a/dev/ci/build-docker.sh b/.github/ci/build-docker.sh similarity index 59% rename from dev/ci/build-docker.sh rename to .github/ci/build-docker.sh index 6e38498..fee174b 100755 --- a/dev/ci/build-docker.sh +++ b/.github/ci/build-docker.sh @@ -47,33 +47,44 @@ trap cleanup EXIT PROJECT="$WORK/project" if [ -z "$TREE" ]; then - sh "$ROOT/install.sh" --yes --env docker --docker-mode standalone --from "$ROOT" \ - --dir "$PROJECT" >"$WORK/install.log" 2>&1 \ - || { cat "$WORK/install.log" >&2; fail "install.sh --env docker failed"; } + if ! sh "$ROOT/install.sh" --yes --env docker --docker-mode standalone --from "$ROOT" \ + --dir "$PROJECT" >"$WORK/install.log" 2>&1; then + cat "$WORK/install.log" >&2 + fail "install.sh --env docker failed" + fi else mkdir -p "$PROJECT" cp -R "$TREE/." "$PROJECT/" fi -[ -f "$PROJECT/.devcontainer/devcontainer.json" ] || fail "$PROJECT is not a docker project" +if [ ! -f "$PROJECT/.devcontainer/devcontainer.json" ]; then + fail "$PROJECT is not a docker project" +fi # `bash -ic` is required: the devx image loads the nix toolchain environment # from ~/.bashrc, which only interactive shells source. note "building inside $IMAGE ..." -docker run --rm \ - -v "$PROJECT:/workspaces/plinth-template" \ - -w /workspaces/plinth-template \ - -i "$IMAGE" \ - bash -ic ' - set -euo pipefail - echo "build-docker(container): ghc $(ghc --numeric-version) at $(command -v ghc)" - pkg-config --exists libsodium libsecp256k1 libblst \ - || { echo "build-docker(container): FAIL: crypto libs not provided by the image" >&2; exit 1; } - echo "build-docker(container): crypto libs provided by the image (via nix)" - cabal update - cabal build all - cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json - [ -s blueprint.json ] || { echo "build-docker(container): FAIL: empty blueprint" >&2; exit 1; } - echo "build-docker(container): blueprint OK" - ' || fail "container build failed" +if ! docker run --rm \ + -v "$PROJECT:/workspaces/plinth-template" \ + -w /workspaces/plinth-template \ + -i "$IMAGE" \ + bash -ic ' + set -euo pipefail + echo "build-docker(container): ghc $(ghc --numeric-version) at $(command -v ghc)" + if ! pkg-config --exists libsodium libsecp256k1 libblst; then + echo "build-docker(container): FAIL: crypto libs not provided by the image" >&2 + exit 1 + fi + echo "build-docker(container): crypto libs provided by the image (via nix)" + cabal update + cabal build all + cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json + if [ ! -s blueprint.json ]; then + echo "build-docker(container): FAIL: empty blueprint" >&2 + exit 1 + fi + echo "build-docker(container): blueprint OK" + '; then + fail "container build failed" +fi echo "build-docker: SUCCESS" diff --git a/dev/ci/build-ghc-cabal.sh b/.github/ci/build-ghc-cabal.sh similarity index 62% rename from dev/ci/build-ghc-cabal.sh rename to .github/ci/build-ghc-cabal.sh index c21e625..7895557 100755 --- a/dev/ci/build-ghc-cabal.sh +++ b/.github/ci/build-ghc-cabal.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash # # Build a GHC+Cabal project end-to-end with the host's ghc+cabal: create the -# project with install.sh --env cabal (or take a pre-created one as $1), run -# `cabal build all` there — proving the pkg-config shim bootstraps the crypto -# libraries with no manual step — then generate the example blueprint and -# assert the produced executable links the crypto libraries from the plinth -# cache. +# project with install.sh --env cabal --crypto-libs local (or take a +# pre-created one as $1), which downloads the crypto C libraries into the +# per-user cache, then run `cabal build all` with the generated env.sh +# sourced (it points PKG_CONFIG_PATH at the libraries), generate the example +# blueprint and assert the produced executable links the crypto libraries +# from the plinth cache. # # Requirements: ghc 9.6.x or 9.12.x, cabal >= 3.8, pkg-config, network. # @@ -38,9 +39,15 @@ note() { echo "build-ghc-cabal: $*"; } # Toolchain assertions (same requirements install.sh enforces) # -------------------------------------------------------------------------- -command -v ghc >/dev/null 2>&1 || fail "ghc not on PATH" -command -v cabal >/dev/null 2>&1 || fail "cabal not on PATH" -command -v pkg-config >/dev/null 2>&1 || fail "pkg-config not on PATH" +if ! command -v ghc >/dev/null 2>&1; then + fail "ghc not on PATH" +fi +if ! command -v cabal >/dev/null 2>&1; then + fail "cabal not on PATH" +fi +if ! command -v pkg-config >/dev/null 2>&1; then + fail "pkg-config not on PATH" +fi GHC_VERSION="$(ghc --numeric-version)" case "$GHC_VERSION" in 9.6.*|9.12.*) note "ghc $GHC_VERSION ($(command -v ghc))" ;; @@ -54,16 +61,38 @@ note "cabal $(cabal --numeric-version) ($(command -v cabal))" PROJECT="$WORK/project" if [ -z "$TREE" ]; then - sh "$ROOT/install.sh" --yes --env cabal --from "$ROOT" \ - --dir "$PROJECT" --crypto-libs skip >"$WORK/install.log" 2>&1 \ - || { cat "$WORK/install.log" >&2; fail "install.sh --env cabal failed"; } + if ! sh "$ROOT/install.sh" --yes --env cabal --from "$ROOT" \ + --dir "$PROJECT" --crypto-libs local >"$WORK/install.log" 2>&1; then + cat "$WORK/install.log" >&2 + fail "install.sh --env cabal failed" + fi else mkdir -p "$PROJECT" cp -R "$TREE/." "$PROJECT/" fi -[ -f "$PROJECT/scripts/get-crypto-libs.sh" ] || fail "$PROJECT is not a ghc-cabal project" +if [ ! -f "$PROJECT/get-crypto-libs.sh" ]; then + fail "$PROJECT is not a ghc-cabal project" +fi cd "$PROJECT" +# install.sh (--crypto-libs local) already ran this; re-running is an +# instant no-op re-link. For a pre-created tree ($1) it does the install. +if ! ./get-crypto-libs.sh; then + fail "get-crypto-libs.sh failed" +fi + +# Source the generated env.sh so pkg-config resolves the libraries (this is +# exactly what the README tells users to do). +if [ ! -f dist-newstyle/crypto-libs/env.sh ]; then + fail "dist-newstyle/crypto-libs/env.sh was not created by get-crypto-libs.sh" +fi +# shellcheck source=/dev/null +. dist-newstyle/crypto-libs/env.sh +if ! pkg-config --exists libsodium libsecp256k1 libblst; then + fail "crypto libs not visible to pkg-config after sourcing env.sh" +fi +note "crypto libs: sodium $(pkg-config --modversion libsodium), secp256k1 $(pkg-config --modversion libsecp256k1), blst $(pkg-config --modversion libblst)" + cabal=(cabal) if [ -n "${CABAL_STORE_DIR:-}" ]; then mkdir -p "$CABAL_STORE_DIR" @@ -76,25 +105,8 @@ if [ -n "${CI:-}" ]; then "${cabal[@]}" update fi -# -------------------------------------------------------------------------- -# Build. The very first cold build on a fresh clone stops once, after the -# shim has written cabal.project.local (documented in the branch README); -# a plain re-run completes. -# -------------------------------------------------------------------------- - note "building (cabal build all)..." -if ! "${cabal[@]}" build all; then - [ -f cabal.project.local ] \ - || fail "build failed without writing cabal.project.local (not the documented cold-start stop)" - note "documented cold-start stop; re-running cabal build all" - "${cabal[@]}" build all -fi - -# (regex, not the exact $PROJECT prefix: macOS mktemp paths appear both as -# /var/... and /private/var/... depending on how they were resolved) -[ -f cabal.project.local ] || fail "cabal.project.local was not created by the shim" -grep -q "pkg-config-location: .*/scripts/pkg-config" cabal.project.local \ - || fail "cabal.project.local does not point at the pkg-config shim" +"${cabal[@]}" build all # -------------------------------------------------------------------------- # Blueprint generation + provenance of the linked crypto libraries @@ -102,8 +114,9 @@ grep -q "pkg-config-location: .*/scripts/pkg-config" cabal.project.local \ note "generating the example blueprint..." "${cabal[@]}" run -v0 exe:gen-auction-validator-blueprint -- "$WORK/blueprint.json" -python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$WORK/blueprint.json" \ - || fail "blueprint output is not valid JSON" +if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$WORK/blueprint.json"; then + fail "blueprint output is not valid JSON" +fi note "blueprint OK: $(wc -c < "$WORK/blueprint.json" | tr -d ' ') bytes" # The libraries' real location is the per-user cache; PLINTH_CRYPTO_LIBS_HOME @@ -114,9 +127,10 @@ bin="$("${cabal[@]}" list-bin exe:gen-auction-validator-blueprint)" case "$(uname -s)" in Darwin) links="$(otool -L "$bin")" - echo "$links" | grep -qF "$CRYPTO_MARKER" \ - || fail "executable does not link crypto libs from the plinth cache ($CRYPTO_MARKER): + if ! echo "$links" | grep -qF "$CRYPTO_MARKER"; then + fail "executable does not link crypto libs from the plinth cache ($CRYPTO_MARKER): $links" + fi for bad in /nix/store /opt/homebrew "/usr/local/lib"; do if echo "$links" | grep -qE "(libsodium|libsecp256k1|libblst).*$bad|$bad.*(libsodium|libsecp256k1|libblst)"; then fail "executable links crypto libs from $bad: diff --git a/.github/ci/build-nix.sh b/.github/ci/build-nix.sh new file mode 100755 index 0000000..5ea41bc --- /dev/null +++ b/.github/ci/build-nix.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# +# Build a Nix project end-to-end: create it with install.sh --env nix (or +# take a pre-created one as $1), enter its `nix develop` shell and run +# `cabal build all` plus the example blueprint. +# +# .github/ci/build-nix.sh [TREE] [--shell ghc96|ghc912|default] +# +# Demeter projects have identical nix content, so this also covers demeter. +# +# Requirements: nix with flakes. Configure IOG's binary caches (see the +# nix-setup-guide) or the first run will build GHC from source. +# +# Environment: +# PLINTH_CI_KEEP=1 Keep the scratch directory (printed) for inspection. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +TREE="" +SHELL_NAME="default" +while [ $# -gt 0 ]; do + case "$1" in + --shell) shift; SHELL_NAME="${1:?--shell needs an argument}" ;; + --shell=*) SHELL_NAME="${1#--shell=}" ;; + *) TREE="$1" ;; + esac + shift +done + +fail() { echo "build-nix: FAIL: $*" >&2; exit 1; } +note() { echo "build-nix: $*"; } + +if ! command -v nix >/dev/null 2>&1; then + fail "nix not on PATH" +fi + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-nix.XXXXXX")" +# nix's path: fetcher rejects paths with symlinked ancestors (macOS /var); +# use the physical path. +WORK="$(cd "$WORK" && pwd -P)" +cleanup() { + if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then + echo "build-nix: scratch dir kept: $WORK" + else + rm -rf "$WORK" + fi +} +trap cleanup EXIT + +PROJECT="$WORK/project" +if [ -z "$TREE" ]; then + if ! sh "$ROOT/install.sh" --yes --env nix --from "$ROOT" --dir "$PROJECT" \ + >"$WORK/install.log" 2>&1; then + cat "$WORK/install.log" >&2 + fail "install.sh --env nix failed" + fi +else + mkdir -p "$PROJECT" + cp -R "$TREE/." "$PROJECT/" +fi +if [ ! -f "$PROJECT/flake.nix" ]; then + fail "$PROJECT is not a nix project" +fi + +# cd matters: `nix develop --command` runs in the calling directory, and the +# in-shell cabal must build the branch copy, not whatever cwd we came from. +cd "$PROJECT" + +note "entering nix develop path:$PROJECT#$SHELL_NAME (first run may take a while)..." +# shellcheck disable=SC2016 # the inner script must expand inside the shell +if ! nix develop "path:$PROJECT#$SHELL_NAME" --accept-flake-config --command bash -c ' + set -euo pipefail + note() { echo "build-nix(shell): $*"; } + + case "$(command -v ghc)" in + /nix/store/*) note "ghc $(ghc --numeric-version) from /nix/store" ;; + *) echo "build-nix(shell): FAIL: ghc not from /nix/store: $(command -v ghc)" >&2; exit 1 ;; + esac + case "$(command -v cabal)" in + /nix/store/*) note "cabal $(cabal --numeric-version) from /nix/store" ;; + *) echo "build-nix(shell): FAIL: cabal not from /nix/store: $(command -v cabal)" >&2; exit 1 ;; + esac + if ! pkg-config --exists libsodium libsecp256k1 libblst; then + echo "build-nix(shell): FAIL: crypto libs not visible to pkg-config" >&2 + exit 1 + fi + note "crypto libs provided by the shell: sodium $(pkg-config --modversion libsodium), secp256k1 $(pkg-config --modversion libsecp256k1), blst $(pkg-config --modversion libblst)" + + if [ -n "${CI:-}" ]; then + note "CI: running cabal update" + cabal update + fi + + note "building (cabal build all)..." + cabal build all + cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json + if [ ! -s blueprint.json ]; then + echo "build-nix(shell): FAIL: empty blueprint" >&2 + exit 1 + fi + note "blueprint OK: $(wc -c < blueprint.json | tr -d " ") bytes" + '; then + fail "nix shell build failed" +fi + +echo "build-nix: SUCCESS (shell: $SHELL_NAME)" diff --git a/.github/ci/build-windows.sh b/.github/ci/build-windows.sh new file mode 100755 index 0000000..d022212 --- /dev/null +++ b/.github/ci/build-windows.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# Build the template on native Windows: GHC + Cabal with the Cardano crypto +# C libraries installed as MSYS2 (MINGW64) pacman packages from IOG's pinned +# iohk-nix releases — the same releases get-crypto-libs.sh downloads from on +# macOS and Linux. +# +# CI runs this inside the runner's MSYS2 bash (see ci.yaml); locally, run it +# from an "MSYS2 MINGW64" shell on a Windows machine with ghc 9.6.x/9.12.x +# and cabal (>= 3.8) on PATH: +# +# bash .github/ci/build-windows.sh +# +# The libraries are installed into /mingw64/opt/cardano (that is, +# C:\msys64\mingw64\opt\cardano) — pacman-managed, confined to the MSYS2 +# prefix (a throwaway on CI runners). +# +# NOTE: install.sh does not support native Windows (it points users to +# WSL2); this script instead proves the template itself builds with a native +# Windows toolchain, which is what a user gets by copying template/ by hand +# and installing the msys2.* packages as described in the GHC+Cabal README. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +fail() { echo "build-windows: FAIL: $*" >&2; exit 1; } +note() { echo "build-windows: $*"; } + +case "$(uname -s)" in + MINGW64_NT*|MSYS_NT*) ;; + *) fail "this script must run inside an MSYS2 (MINGW64) environment on Windows" ;; +esac +if ! command -v pacman >/dev/null 2>&1; then + fail "pacman not found (not an MSYS2 environment?)" +fi +if ! command -v ghc >/dev/null 2>&1; then + fail "ghc not on PATH" +fi +if ! command -v cabal >/dev/null 2>&1; then + fail "cabal not on PATH" +fi +GHC_VERSION="$(ghc --numeric-version)" +case "$GHC_VERSION" in + 9.6.*|9.12.*) note "ghc $GHC_VERSION ($(command -v ghc))" ;; + *) fail "unsupported ghc $GHC_VERSION (need 9.6.x or 9.12.x)" ;; +esac +note "cabal $(cabal --numeric-version) ($(command -v cabal))" + +# -------------------------------------------------------------------------- +# Crypto C libraries, as sha256-pinned MSYS2 pacman packages from iohk-nix +# releases. +# +# libblst deliberately comes from the older v2.2 release: the newer build +# trips GHC's runtime linker on Windows ("duplicate definition for symbol +# __blst_platform_cap") — see input-output-hk/actions/base, which carries +# the same workaround. +# -------------------------------------------------------------------------- + +RELEASES="https://github.com/input-output-hk/iohk-nix/releases/download" +ASSETS=" +v3.1/msys2.libsodium.pkg.tar.zstd c9ed5b531309369f92d67e6f7c1e003d3c0c96d777f66c9d8a8c1aeca6d8ef4d +v3.1/msys2.libsecp256k1.pkg.tar.zstd b1cf83dce1a38241491209ac0ff75c92b06a05012eecb81d713373bc0d861a40 +v2.2/msys2.libblst.pkg.tar.zstd f03037ff3384fed4af70ecaabe890ddbd7ff766e871f76d0e9a48e42d818282c +" + +# cabal locates the libraries with pkg-config; the mingw-w64 pkg-config +# build is the one variant that copes with Windows-style PKG_CONFIG_PATH +# values (see input-output-hk/actions/base for the sad full story). +note "installing mingw-w64-x86_64-pkg-config (pacman)..." +pacman -S --noconfirm --needed mingw-w64-x86_64-pkg-config + +DOWNLOADS="$(mktemp -d)" +trap 'rm -rf "$DOWNLOADS"' EXIT +while read -r asset sha; do + if [ -z "$asset" ]; then + continue + fi + file="$DOWNLOADS/${asset##*/}" + note "downloading $asset ..." + curl -fsSL --retry 3 -o "$file" "$RELEASES/$asset" + if ! echo "$sha $file" | sha256sum -c - >/dev/null; then + fail "sha256 mismatch for $asset" + fi + note "verified $asset (sha256 OK)" +done <=VERSION +# +# Requirements: nix (with flakes), curl, python3. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +fail() { echo "bump-plutus-version: ERROR: $*" >&2; exit 1; } +note() { echo "bump-plutus-version: $*"; } + +PLUTUS_VERSION="${1:-}" +if [ -z "$PLUTUS_VERSION" ]; then + fail "usage: bump-plutus-version.sh PLUTUS-VERSION (e.g. 1.66.0.0)" +fi +case "$PLUTUS_VERSION" in + *[!0-9.]*|*..*|.*|*.) fail "'$PLUTUS_VERSION' does not look like a plutus release version" ;; +esac + +if ! command -v nix >/dev/null 2>&1; then + fail "nix is required" +fi +if ! command -v curl >/dev/null 2>&1; then + fail "curl is required" +fi +if ! command -v python3 >/dev/null 2>&1; then + fail "python3 is required" +fi + +cd "$ROOT/template" + +note "updating flake inputs CHaP and hackage..." +nix flake update CHaP hackage --accept-flake-config + +# Derive the hackage index-state from the flake-pinned hackage commit, +# subtracting 2 hours as safety margin (commit timestamp > latest index +# entry). +HACKAGE_DATE="$(python3 -c ' +import json, datetime +epoch = json.load(open("flake.lock"))["nodes"]["hackage"]["locked"]["lastModified"] - 7200 +print(datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc) + .strftime("%Y-%m-%dT%H:%M:%SZ")) +')" + +# For CHaP, read the index that ships inside the flake-pinned repository: +# the mtimes of the entries in 01-index.tar.gz ARE the index-states, so the +# newest one is the exact latest index-state of the pinned CHaP — +# independent of CHaP's git commit timestamp, which can be arbitrarily +# newer (e.g. ghost automation re-runs with 0 file changes). +CHAP_REV="$(python3 -c ' +import json +print(json.load(open("flake.lock"))["nodes"]["CHaP"]["locked"]["rev"]) +')" +CHAP_DATE="$(curl -fsSL \ + "https://raw.githubusercontent.com/IntersectMBO/cardano-haskell-packages/${CHAP_REV}/01-index.tar.gz" \ + | python3 -c ' +import sys, tarfile, datetime +tf = tarfile.open(fileobj=sys.stdin.buffer, mode="r:gz") +print(datetime.datetime.fromtimestamp(max(m.mtime for m in tf), + datetime.timezone.utc) + .strftime("%Y-%m-%dT%H:%M:%SZ")) +')" +if [ -z "$CHAP_DATE" ]; then + fail "failed to determine the CHaP index-state" +fi + +note "hackage index-state: $HACKAGE_DATE" +note "CHaP index-state: $CHAP_DATE" + +# sed -i is not portable (GNU vs BSD); write to a temp file and move. +sed_i() { + sed "$1" "$2" > "$2.tmp" + mv "$2.tmp" "$2" +} + +sed_i "s/\(hackage.haskell.org \).*$/\1$HACKAGE_DATE/" cabal.project +sed_i "s/\(cardano-haskell-packages \).*$/\1$CHAP_DATE/" cabal.project +if ! grep -qF "hackage.haskell.org $HACKAGE_DATE" cabal.project; then + fail "failed to update the hackage index-state in cabal.project" +fi +if ! grep -qF "cardano-haskell-packages $CHAP_DATE" cabal.project; then + fail "failed to update the CHaP index-state in cabal.project" +fi + +for pkg in plutus-core plutus-ledger-api plutus-tx plutus-tx-plugin; do + sed_i "s/\($pkg \).*$/\1\^>=$PLUTUS_VERSION/" plinth-template.cabal + if ! grep -qE "$pkg +\^>=$PLUTUS_VERSION" plinth-template.cabal; then + fail "failed to update the $pkg bound in plinth-template.cabal" + fi +done + +note "done — changed files:" +git -C "$ROOT" --no-pager diff --stat -- template/ diff --git a/dev/ci/lint.sh b/.github/ci/lint.sh similarity index 74% rename from dev/ci/lint.sh rename to .github/ci/lint.sh index 8c225fc..9606d68 100755 --- a/dev/ci/lint.sh +++ b/.github/ci/lint.sh @@ -11,19 +11,23 @@ cd "$ROOT" SCRIPTS=( install.sh - scripts/pkg-config - scripts/get-crypto-libs.sh - dev/ci/lint.sh - dev/ci/test-install.sh - dev/ci/build-ghc-cabal.sh - dev/ci/build-nix.sh - dev/ci/build-docker.sh - dev/ci/run-all-local.sh - dev/ci/test-blueprint-parity.sh + get-crypto-libs.sh + .github/ci/lint.sh + .github/ci/test-install.sh + .github/ci/build-ghc-cabal.sh + .github/ci/build-nix.sh + .github/ci/build-docker.sh + .github/ci/build-windows.sh + .github/ci/bump-plutus-version.sh + .github/ci/run-all-local.sh + .github/ci/test-blueprint-parity.sh ) for f in "${SCRIPTS[@]}"; do - [ -f "$f" ] || { echo "lint: missing $f" >&2; exit 1; } + if [ ! -f "$f" ]; then + echo "lint: missing $f" >&2 + exit 1 + fi case "$(head -1 "$f")" in '#!/bin/sh'*) sh -n "$f" ;; *) bash -n "$f" ;; diff --git a/dev/ci/run-all-local.sh b/.github/ci/run-all-local.sh similarity index 76% rename from dev/ci/run-all-local.sh rename to .github/ci/run-all-local.sh index a8b3a95..eda090c 100755 --- a/dev/ci/run-all-local.sh +++ b/.github/ci/run-all-local.sh @@ -3,16 +3,21 @@ # Run the full test suite locally — the same scripts the GitHub workflows # run, without the Actions layer. # -# dev/ci/run-all-local.sh # everything -# PLINTH_SKIP_HEAVY=1 dev/ci/run-all-local.sh # fast checks only +# .github/ci/run-all-local.sh # everything +# PLINTH_SKIP_HEAVY=1 .github/ci/run-all-local.sh # fast checks only # # The heavy steps compile the project; to speed up repeated ghc-cabal runs, # point CABAL_STORE_DIR at a persistent directory. +# +# (build-windows.sh is not run here: it only works on a Windows machine +# inside an MSYS2 MINGW64 shell.) set -uo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -cd "$ROOT" || exit 1 +if ! cd "$ROOT"; then + exit 1 +fi PASS=() FAIL=() @@ -37,8 +42,8 @@ skip_step() { echo "=== $1: SKIPPED ($2)" } -run_step "lint" dev/ci/lint.sh -run_step "test-install" dev/ci/test-install.sh +run_step "lint" .github/ci/lint.sh +run_step "test-install" .github/ci/test-install.sh if [ "${PLINTH_SKIP_HEAVY:-0}" = 1 ]; then skip_step "build-ghc-cabal" "PLINTH_SKIP_HEAVY=1" @@ -46,17 +51,17 @@ if [ "${PLINTH_SKIP_HEAVY:-0}" = 1 ]; then skip_step "build-docker" "PLINTH_SKIP_HEAVY=1" else if command -v ghc >/dev/null 2>&1 && command -v cabal >/dev/null 2>&1; then - run_step "build-ghc-cabal" dev/ci/build-ghc-cabal.sh + run_step "build-ghc-cabal" .github/ci/build-ghc-cabal.sh else skip_step "build-ghc-cabal" "no ghc/cabal on PATH" fi if command -v nix >/dev/null 2>&1; then - run_step "build-nix" dev/ci/build-nix.sh + run_step "build-nix" .github/ci/build-nix.sh else skip_step "build-nix" "no nix on PATH" fi # build-docker.sh skips by itself when docker is unavailable - run_step "build-docker" dev/ci/build-docker.sh + run_step "build-docker" .github/ci/build-docker.sh fi echo "" diff --git a/dev/ci/test-blueprint-parity.sh b/.github/ci/test-blueprint-parity.sh similarity index 65% rename from dev/ci/test-blueprint-parity.sh rename to .github/ci/test-blueprint-parity.sh index c97ded5..09b2ac5 100755 --- a/dev/ci/test-blueprint-parity.sh +++ b/.github/ci/test-blueprint-parity.sh @@ -8,44 +8,44 @@ # # Four scenarios are built and compared: # -# 1. ghcup cabal + ghcup ghc-9.6.7 + prebuilt IOG crypto libs (flag ON) -# 2. ghcup cabal + ghcup ghc-9.12.2 + prebuilt IOG crypto libs (flag ON) -# 3. nix develop .#ghc96 (ghc 9.6.7, crypto libs from nix, flag OFF) -# 4. nix develop .#ghc912 (ghc 9.12.2, crypto libs from nix, flag OFF) +# 1. ghcup cabal + ghcup ghc-9.6.7 + prebuilt IOG crypto libs (downloaded) +# 2. ghcup cabal + ghcup ghc-9.12.2 + prebuilt IOG crypto libs (downloaded) +# 3. nix develop .#ghc96 (ghc 9.6.7, crypto libs from nix) +# 4. nix develop .#ghc912 (ghc 9.12.2, crypto libs from nix) # # The script asserts aggressively that every toolchain component and the # crypto C libraries come from the expected source in each scenario: # -# * ghcup scenarios: ghc/cabal resolve to $HOME/.ghcup/bin, and cabal reaches -# the crypto libraries exclusively through the scripts/pkg-config shim -# declared in cabal.project (which downloads them on first use — no manual -# bootstrap step is performed by this test — and restricts the real -# pkg-config to them with PKG_CONFIG_LIBDIR); the produced executable links -# crypto dylibs from dist-newstyle/crypto-libs/ and nowhere else. -# * nix scenarios: ghc/cabal resolve to /nix/store, the prebuilt-libs flag -# is OFF (PLINTH_USE_SYSTEM_CRYPTO_LIBS=1, and we assert the script -# no-ops), and the produced executable links crypto dylibs from /nix/store -# and in particular NOT from dist-newstyle/crypto-libs/. +# * ghcup scenarios: ghc/cabal resolve to $HOME/.ghcup/bin; the crypto +# libraries are installed by ./get-crypto-libs.sh (per-user cache, linked +# into dist-newstyle/crypto-libs/) and the build sees them EXCLUSIVELY — +# PKG_CONFIG_LIBDIR masks every system pkg-config directory; the produced +# executable links crypto dylibs from the plinth cache and nowhere else. +# * nix scenarios: ghc/cabal resolve to /nix/store, the crypto libraries +# come from the nix shell, and the produced executable links crypto +# dylibs from /nix/store and in particular NOT from the plinth cache. # # Usage: -# dev/ci/test-blueprint-parity.sh # run all four scenarios -# dev/ci/test-blueprint-parity.sh ghcup-ghc967 # run a single scenario +# .github/ci/test-blueprint-parity.sh # run all four scenarios +# .github/ci/test-blueprint-parity.sh ghcup-ghc967 # run a single scenario # (scenarios: ghcup-ghc967 ghcup-ghc9122 nix-ghc96 nix-ghc912 compare) # # Environment: # PARITY_FRESH=1 Wipe the downloaded crypto libs, the test # cabal store and the test builddirs first, so # the ghcup scenarios prove the whole -# cold-start flow (solver bootstrap included). +# cold-start flow. # PARITY_ALLOW_GHC_DIVERGENCE=1 Downgrade the cross-GHC comparison to a # warning (see compare_outputs). -# shellcheck disable=SC2015 # A && B || C used deliberately in assertions set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -# The pkg-config-location in cabal.project is relative to the invocation dir. -cd "$ROOT" +# The buildable project (cabal.project et al.) lives in template/; +# get-crypto-libs.sh lives at the repository root and links the libraries +# into $ROOT/dist-newstyle/crypto-libs/. +TEMPLATE="$ROOT/template" +cd "$TEMPLATE" TESTDIR="$ROOT/dist-newstyle/test" STORE_DIR="$TESTDIR/store" BLUEPRINTS="$TESTDIR/blueprints" @@ -70,7 +70,9 @@ banner() { echo; echo "=== $* ==="; } assert_eq() { # assert_eq - [ "$2" = "$3" ] || fail "$1: expected '$2', got '$3'" + if [ "$2" != "$3" ]; then + fail "$1: expected '$2', got '$3'" + fi note "$1 = $2" } @@ -118,7 +120,7 @@ EOF } # -------------------------------------------------------------------------- -# ghcup scenarios (prebuilt crypto libs flag ON — the default) +# ghcup scenarios (crypto libs downloaded by get-crypto-libs.sh) # -------------------------------------------------------------------------- run_ghcup_scenario() { @@ -128,15 +130,14 @@ run_ghcup_scenario() { local builddir="$TESTDIR/$scenario" local out="$BLUEPRINTS/$scenario.json" - banner "Scenario $scenario: ghcup cabal + ghcup ghc-$ghc_version + prebuilt IOG crypto libs (via scripts/pkg-config shim)" + banner "Scenario $scenario: ghcup cabal + ghcup ghc-$ghc_version + prebuilt IOG crypto libs (via get-crypto-libs.sh)" - # Hermetic PATH: ghcup, the OS, and the directory of the real pkg-config - # (the shim's backend). The crypto libs can still only be reached through - # the scripts/pkg-config shim that cabal.project points cabal at: the shim - # restricts the real pkg-config to the downloaded libraries with - # PKG_CONFIG_LIBDIR, masking every system search directory. - command -v pkg-config >/dev/null 2>&1 \ - || fail "pkg-config not found; install it (e.g. brew install pkgconf / apt install pkg-config)" + # Hermetic PATH: ghcup, the OS, and the directory of the real pkg-config. + # The build reaches the crypto libs exclusively through PKG_CONFIG_LIBDIR, + # which masks every system pkg-config search directory. + if ! command -v pkg-config >/dev/null 2>&1; then + fail "pkg-config not found; install it (e.g. brew install pkgconf / apt install pkg-config)" + fi local pkgconfig_dir pkgconfig_dir="$(dirname "$(command -v pkg-config)")" local path="$GHCUP_BIN:$pkgconfig_dir:/usr/bin:/bin:/usr/sbin:/sbin" @@ -153,57 +154,55 @@ run_ghcup_scenario() { # 1. Assert the toolchain comes from ghcup. local ghc_path cabal_path - ghc_path="$(env -i PATH="$path" sh -c "command -v ghc-$ghc_version")" \ - || fail "ghc-$ghc_version not found in $GHCUP_BIN (install with: ghcup install ghc $ghc_version)" - cabal_path="$(env -i PATH="$path" sh -c 'command -v cabal')" || fail "cabal not found in $GHCUP_BIN" + if ! ghc_path="$(env -i PATH="$path" sh -c "command -v ghc-$ghc_version")"; then + fail "ghc-$ghc_version not found in $GHCUP_BIN (install with: ghcup install ghc $ghc_version)" + fi + if ! cabal_path="$(env -i PATH="$path" sh -c 'command -v cabal')"; then + fail "cabal not found in $GHCUP_BIN" + fi assert_prefix "$scenario: ghc" "$GHCUP_BIN/" "$ghc_path" assert_prefix "$scenario: cabal" "$GHCUP_BIN/" "$cabal_path" assert_eq "$scenario: ghc version" "$ghc_version" \ "$(env -i PATH="$path" sh -c "ghc-$ghc_version --numeric-version")" - # 2. Build and run — NO manual bootstrap: cabal's solver invokes the shim, - # and the shim downloads and installs the crypto libs on first use. A - # dedicated builddir is used because cabal caches install plans and would - # not re-solve after environment changes; a dedicated store keeps - # artifacts built against other library sources out of reach. - local cabal=(env -i HOME="$HOME" PATH="$path" - cabal --store-dir="$STORE_DIR") - local flags=(-w "ghc-$ghc_version" --builddir="$builddir") - - # On a cold start (no cabal.project.local yet) the first build bootstraps - # the libs and writes cabal.project.local mid-run, too late for its own - # configure phase — it stops once, and the re-run picks the file up. Any - # other failure must fail twice and therefore still fails the test. - if ! "${cabal[@]}" build "${flags[@]}" exe:gen-auction-validator-blueprint; then - [ -f "$ROOT/cabal.project.local" ] \ - || fail "$scenario: cabal build failed without writing cabal.project.local" - note "cold-start build stopped once as documented; re-running with cabal.project.local in place" - "${cabal[@]}" build "${flags[@]}" exe:gen-auction-validator-blueprint + # 2. Install the crypto libraries (per-user cache, linked into the repo's + # dist-newstyle/crypto-libs/) — a fast no-op when already cached. + if ! "$ROOT/get-crypto-libs.sh" --quiet --platform "$platform"; then + fail "$scenario: get-crypto-libs.sh failed" + fi + if [ ! -f "$pcdir/libsodium.pc" ] || [ ! -f "$pcdir/libsecp256k1.pc" ] || [ ! -f "$pcdir/libblst.pc" ]; then + fail "$scenario: get-crypto-libs.sh did not install the crypto libs" + fi + if [ ! -L "$prefix" ]; then + fail "$scenario: $prefix should be a symlink into the per-user cache" fi - rm -f "$out" - "${cabal[@]}" run -v0 "${flags[@]}" exe:gen-auction-validator-blueprint -- "$out" - [ -s "$out" ] || fail "$scenario: blueprint file was not produced" - - # 3. The build itself must have bootstrapped the libs via the shim, and the - # shim must resolve them exclusively from the local install. - [ -f "$pcdir/libsodium.pc" ] && [ -f "$pcdir/libsecp256k1.pc" ] && [ -f "$pcdir/libblst.pc" ] \ - || fail "$scenario: cabal did not bootstrap the crypto libs via the shim" - [ -L "$prefix" ] \ - || fail "$scenario: $prefix should be a symlink into the per-user cache" # The real install lives in the per-user cache; the project path is a # symlink to it, and all recorded paths (pc prefix, install names) use the # resolved cache location. local real_prefix real_prefix="$(cd "$prefix" && pwd -P)" - note "cabal bootstrapped the crypto libs: $prefix -> $real_prefix" - grep -qF "pkg-config-location: $ROOT/scripts/pkg-config" "$ROOT/cabal.project.local" \ - || fail "$scenario: cabal.project.local does not carry the shim stanza" - note "cabal.project.local carries the absolute shim location for package configure" - assert_eq "$scenario: shim libsodium prefix" "$real_prefix" \ - "$(env -i PATH="$path" "$ROOT/scripts/pkg-config" --variable=prefix libsodium)" - env -i PATH="$path" "$ROOT/scripts/pkg-config" --exists libblst libsecp256k1 \ - || fail "$scenario: shim cannot resolve libblst/libsecp256k1" - note "shim resolves libsodium/libsecp256k1/libblst exclusively from the local install" + note "crypto libs installed: $prefix -> $real_prefix" + assert_eq "$scenario: libsodium prefix" "$real_prefix" \ + "$(env -i PATH="$path" PKG_CONFIG_LIBDIR="$pcdir" pkg-config --variable=prefix libsodium)" + if ! env -i PATH="$path" PKG_CONFIG_LIBDIR="$pcdir" pkg-config --exists libblst libsecp256k1; then + fail "$scenario: pkg-config cannot resolve libblst/libsecp256k1 from $pcdir" + fi + note "pkg-config resolves libsodium/libsecp256k1/libblst exclusively from the local install" + + # 3. Build and run. A dedicated builddir is used because cabal caches + # install plans and would not re-solve after environment changes; a + # dedicated store keeps artifacts built against other library sources + # out of reach. + local cabal=(env -i HOME="$HOME" PATH="$path" PKG_CONFIG_LIBDIR="$pcdir" + cabal --store-dir="$STORE_DIR") + local flags=(-w "ghc-$ghc_version" --builddir="$builddir") + + "${cabal[@]}" build "${flags[@]}" exe:gen-auction-validator-blueprint + rm -f "$out" + "${cabal[@]}" run -v0 "${flags[@]}" exe:gen-auction-validator-blueprint -- "$out" + if [ ! -s "$out" ]; then + fail "$scenario: blueprint file was not produced" + fi # 4. Assert the plan used the expected compiler. assert_eq "$scenario: plan compiler-id" "ghc-$ghc_version" \ @@ -224,7 +223,7 @@ run_ghcup_scenario() { } # -------------------------------------------------------------------------- -# nix scenarios (prebuilt crypto libs flag OFF — libs must come from nix) +# nix scenarios (crypto libs must come from the nix shell) # -------------------------------------------------------------------------- run_nix_scenario() { @@ -234,13 +233,15 @@ run_nix_scenario() { banner "Scenario $scenario: nix develop .#$shell (ghc $ghc_version, crypto libs from nix)" - command -v nix >/dev/null 2>&1 || fail "nix not found" + if ! command -v nix >/dev/null 2>&1; then + fail "nix not found" + fi - # PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 turns the prebuilt-libs feature OFF; - # inside the nix shell the libs are provided by the iohk-nix overlays. - PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 \ - nix develop "$ROOT#$shell" --command bash "$ROOT/dev/ci/test-blueprint-parity.sh" \ - --inner-nix "$scenario" "$ghc_version" + # Inside the nix shell the libs are provided by the iohk-nix overlays; + # nothing is downloaded and nothing of get-crypto-libs.sh's output is used. + nix develop "path:$TEMPLATE#$shell" --command bash \ + "$ROOT/.github/ci/test-blueprint-parity.sh" \ + --inner-nix "$scenario" "$ghc_version" echo "Scenario $scenario OK -> $BLUEPRINTS/$scenario.json" } @@ -251,17 +252,19 @@ inner_nix() { local builddir="$TESTDIR/$scenario" local out="$BLUEPRINTS/$scenario.json" - # 1. Assert the flag is OFF and the get-crypto-libs script is a no-op. - assert_eq "$scenario: PLINTH_USE_SYSTEM_CRYPTO_LIBS" "1" "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-}" - local msg - msg="$("$ROOT/scripts/get-crypto-libs.sh")" - assert_contains "$scenario: get-crypto-libs.sh" "skipping download" "$msg" - [ -z "${PKG_CONFIG_LIBDIR:-}" ] || fail "$scenario: PKG_CONFIG_LIBDIR leaked into the nix shell" + # 1. The downloaded libs must not be reachable through the environment. + if [ -n "${PKG_CONFIG_LIBDIR:-}" ]; then + fail "$scenario: PKG_CONFIG_LIBDIR leaked into the nix shell" + fi # 2. Assert the toolchain comes from the nix store. local ghc_path cabal_path - ghc_path="$(command -v ghc)" || fail "no ghc in the nix shell" - cabal_path="$(command -v cabal)" || fail "no cabal in the nix shell" + if ! ghc_path="$(command -v ghc)"; then + fail "no ghc in the nix shell" + fi + if ! cabal_path="$(command -v cabal)"; then + fail "no cabal in the nix shell" + fi assert_prefix "$scenario: ghc" "/nix/store/" "$ghc_path" assert_prefix "$scenario: cabal" "/nix/store/" "$cabal_path" assert_eq "$scenario: ghc version" "$ghc_version" "$(ghc --numeric-version)" @@ -270,7 +273,9 @@ inner_nix() { rm -f "$out" cabal build --builddir="$builddir" exe:gen-auction-validator-blueprint cabal run -v0 --builddir="$builddir" exe:gen-auction-validator-blueprint -- "$out" - [ -s "$out" ] || fail "$scenario: blueprint file was not produced" + if [ ! -s "$out" ]; then + fail "$scenario: blueprint file was not produced" + fi # 4. Assert the plan used the expected compiler. assert_eq "$scenario: plan compiler-id" "ghc-$ghc_version" \ @@ -317,7 +322,9 @@ compare_outputs() { local all="ghcup-ghc${GHC96_VERSION//./} ghcup-ghc${GHC912_VERSION//./} nix-ghc96 nix-ghc912" for name in $all; do f="$BLUEPRINTS/$name.json" - [ -s "$f" ] || fail "missing blueprint for scenario $name ($f). Run that scenario first." + if [ ! -s "$f" ]; then + fail "missing blueprint for scenario $name ($f). Run that scenario first." + fi echo " $(shasum -a 256 "$f" 2>/dev/null || sha256sum "$f")" done echo @@ -359,12 +366,11 @@ if [ "${1:-}" = "--inner-nix" ]; then fi if [ "${PARITY_FRESH:-0}" = 1 ]; then - banner "PARITY_FRESH=1: wiping the crypto libs cache, cabal.project.local, test store and test builddirs (fresh-clone simulation)" + banner "PARITY_FRESH=1: wiping the crypto libs cache, test store and test builddirs (fresh-clone simulation)" CRYPTO_CACHE="${PLINTH_CRYPTO_LIBS_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/plinth-crypto-libs}" chmod -R u+w "$ROOT/dist-newstyle/crypto-libs" "$CRYPTO_CACHE" 2>/dev/null || true rm -rf "$ROOT/dist-newstyle/crypto-libs" "$CRYPTO_CACHE" "$STORE_DIR" \ "$TESTDIR/ghcup-ghc${GHC96_VERSION//./}" "$TESTDIR/ghcup-ghc${GHC912_VERSION//./}" - rm -f "$ROOT/cabal.project.local" fi case "${1:-all}" in diff --git a/dev/ci/test-install.sh b/.github/ci/test-install.sh similarity index 83% rename from dev/ci/test-install.sh rename to .github/ci/test-install.sh index 068cce2..92c82b5 100755 --- a/dev/ci/test-install.sh +++ b/.github/ci/test-install.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# shellcheck disable=SC2015 # pass/fail assertion helpers never fail themselves # -# End-to-end test of install.sh. The repository is a single branch carrying -# the union of every environment's files; the installer selects the relevant -# ones into a fresh project directory. This test builds a local git fixture -# of the union tree (no network cloning) and checks, for every environment: +# End-to-end test of install.sh. The repository's template/ directory +# carries the union of every environment's project files; the installer +# selects the relevant ones into a fresh project directory. This test builds +# a local git fixture of the repository (no network cloning) and checks, for +# every environment: # # * the produced project contains EXACTLY the expected files (manifest) -# * the environment-specific edits were applied (pkg-config stanza only in -# GHC+Cabal projects, simplified nix/project.nix, no leftover markers) +# * only the right files were selected (crypto-libs script only in +# GHC+Cabal projects, nix files only in Nix/Demeter projects, ...) # * the project has the right README and a fresh git history # * the final instructions tell the user to run `cabal build all` # @@ -37,21 +37,24 @@ pass() { echo " ok: $*"; } # Fixture 1: a local git repository with the union tree on a single branch # -------------------------------------------------------------------------- -echo "== building local template repository (single union branch) ==" -SRC="$WORK/template" +echo "== building local template repository ==" +SRC="$WORK/fixture-repo" git init -q "$SRC" git ls-files --cached --others --exclude-standard | while IFS= read -r f; do - [ -f "$f" ] || continue + if [ ! -f "$f" ]; then + continue + fi case "$f" in */*) mkdir -p "$SRC/${f%/*}" ;; esac cp -p "$f" "$SRC/$f" done git -C "$SRC" add -A git -C "$SRC" -c user.name=ci -c user.email=ci@example.invalid \ - commit -qm "plinth-template union" -# gitignored junk that must never reach a project created with --from -mkdir -p "$SRC/dist-newstyle" -echo junk > "$SRC/dist-newstyle/junk" -echo junk > "$SRC/cabal.project.local" + commit -qm "plinth-template" +# gitignored junk (a local build inside template/) that must never reach a +# project created with --from +mkdir -p "$SRC/template/dist-newstyle" +echo junk > "$SRC/template/dist-newstyle/junk" +echo junk > "$SRC/template/cabal.project.local" pass "fixture repo built from the working tree (with gitignored junk seeded)" # -------------------------------------------------------------------------- @@ -68,7 +71,9 @@ mkdir -p "$FARM" # new PATH. for t in sh bash uname grep sed tr dirname basename mktemp git curl tar rm mkdir \ cat find chmod cp mv ln awk; do - p="$(command -v "$t" 2>/dev/null)" || continue + if ! p="$(command -v "$t" 2>/dev/null)"; then + continue + fi ln -s "$p" "$FARM/$t" done @@ -136,8 +141,13 @@ expect_in_log() { if grep -qF "$2" "$1"; then pass "log mentions '$2'"; else fail "log lacks '$2' ($1)"; fi } -contains() { grep -qF "$2" "$1" && pass "${1#"$WORK"/} contains '$2'" || fail "${1#"$WORK"/} does not contain '$2'"; } -lacks() { if grep -qF "$2" "$1" 2>/dev/null; then fail "${1#"$WORK"/} contains forbidden '$2'"; else pass "${1#"$WORK"/} lacks '$2'"; fi; } +contains() { + if grep -qF "$2" "$1"; then + pass "${1#"$WORK"/} contains '$2'" + else + fail "${1#"$WORK"/} does not contain '$2'" + fi +} # expect_manifest DIR (expected file list on stdin; .git is ignored) expect_manifest() { @@ -172,17 +182,6 @@ nix/project.nix nix/shell.nix nix/utils.nix" -# no_leftover_markers DIR: no installer marker may survive in any project -no_leftover_markers() { - if grep -rqF -e "BEGIN ghc-cabal only" -e "END ghc-cabal only" \ - -e "BEGIN union source only" -e "END union source only" \ - --exclude-dir=.git "$1" 2>/dev/null; then - fail "installer markers leaked into $1" - else - pass "no leftover installer markers" - fi -} - # -------------------------------------------------------------------------- # Non-interactive happy paths, one per environment # -------------------------------------------------------------------------- @@ -193,13 +192,14 @@ if run_install "$WORK/log-cabal" PATH="$GOOD:$FARM" -- \ --yes --env cabal --dir "$WORK/out-cabal" --crypto-libs skip; then expect_manifest "$WORK/out-cabal" </dev/null 2>&1; then $COMMON $NIX_FILES EOF - lacks "$WORK/out-nix/cabal.project" "program-locations" contains "$WORK/out-nix/nix/project.nix" "src = lib.cleanSource ../.;" contains "$WORK/out-nix/README.md" "Nix edition" - no_leftover_markers "$WORK/out-nix" expect_fresh_git "$WORK/out-nix" expect_in_log "$WORK/log-nix" "cabal build all" expect_in_log "$WORK/log-nix" "nix develop" @@ -283,7 +275,13 @@ if ( cd "$WORK/fromtest" && \ else fail "--from did not create my-plinth-project" fi - lacks "$WORK/fromtest/my-plinth-project/cabal.project" "program-locations" + if [ -e "$WORK/fromtest/my-plinth-project/install.sh" ] \ + || [ -e "$WORK/fromtest/my-plinth-project/template" ] \ + || [ -e "$WORK/fromtest/my-plinth-project/.github" ]; then + fail "repository machinery leaked into the project" + else + pass "no repository machinery (install.sh, template/, .github/) in the project" + fi # --from a working checkout must respect .gitignore (the fixture is seeded # with gitignored junk) and never copy git metadata if [ -e "$WORK/fromtest/my-plinth-project/dist-newstyle" ] \ @@ -327,9 +325,11 @@ echo "== interactive (scripted): choose GHC+Cabal, skip crypto libs ==" printf '4\n3\nmy-scripted-project\n' > "$WORK/answers" if ( cd "$WORK" && run_install "$WORK/log-interactive" \ PATH="$GOOD:$FARM" PLINTH_INSTALL_TTY="$WORK/answers" -- ); then - [ -f "$WORK/my-scripted-project/scripts/get-crypto-libs.sh" ] \ - && pass "interactive run created the ghc-cabal project" \ - || fail "interactive run did not create the expected project" + if [ -f "$WORK/my-scripted-project/get-crypto-libs.sh" ]; then + pass "interactive run created the ghc-cabal project" + else + fail "interactive run did not create the expected project" + fi expect_in_log "$WORK/log-interactive" "cabal build all" else fail "interactive run exited non-zero"; sed 's/^/ /' "$WORK/log-interactive" | tail -30 @@ -347,7 +347,11 @@ if run_install "$WORK/log-nonix" PATH="$GOOD:$FARM" -- \ else pass "exits non-zero" expect_in_log "$WORK/log-nonix" "nix is not installed" - [ ! -e "$WORK/out-nonix" ] && pass "nothing created" || fail "created despite failed checks" + if [ ! -e "$WORK/out-nonix" ]; then + pass "nothing created" + else + fail "created despite failed checks" + fi fi echo "" @@ -358,7 +362,11 @@ if run_install "$WORK/log-oldghc" PATH="$OLDGHC:$FARM" -- \ else pass "exits non-zero" expect_in_log "$WORK/log-oldghc" "unsupported GHC version 9.4.8" - [ ! -e "$WORK/out-oldghc" ] && pass "nothing created" || fail "created despite failed checks" + if [ ! -e "$WORK/out-oldghc" ]; then + pass "nothing created" + else + fail "created despite failed checks" + fi fi echo "" @@ -409,6 +417,10 @@ fi echo "" if [ "${PLINTH_TEST_OFFLINE:-0}" = 1 ]; then echo "== crypto-libs local download: skipped (PLINTH_TEST_OFFLINE=1) ==" +elif ! command -v pkg-config >/dev/null 2>&1; then + # (CI runners always have pkg-config; this only skips on bare dev machines) + echo "== crypto-libs local download: SKIPPED — no pkg-config on this host ==" + echo " install one (brew install pkgconf / apt install pkg-config) to run it" else echo "== --crypto-libs local: real download (hermetic cache) ==" # Real pkg-config required for the installer's sanity check of the @@ -420,7 +432,9 @@ else --yes --env cabal --dir "$WORK/out-crypto" --crypto-libs local; then found="" for pc in "$WORK/out-crypto"/dist-newstyle/crypto-libs/*/lib/pkgconfig/libsodium.pc; do - [ -f "$pc" ] && found="$pc" + if [ -f "$pc" ]; then + found="$pc" + fi done if [ -n "$found" ]; then pass "libsodium.pc reachable in the project: ${found#"$WORK"/}" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index cc81196..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,267 +0,0 @@ -# Plinth Template - Plutus Smart Contract Development - -**ALWAYS follow these instructions first and only fallback to search or additional context gathering if the information here is incomplete or found to be in error.** - -This is a template repository for Plutus smart contract development using Haskell. It provides example auction smart contracts and tools for generating Plutus script blueprints. - -## Working Effectively - -### Prerequisites and Setup -Use one of these development environments (listed in order of preference): - -#### Option 1: Nix (Recommended) -- Install and configure Nix following [these instructions](https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md) -- Enter the development shell: `nix develop` -- **CRITICAL**: Initial Nix setup can take 30-60 minutes for downloading and building dependencies. NEVER CANCEL. Set timeout to 90+ minutes. - -#### Option 2: Docker/Devcontainer -- Use the provided devcontainer: `ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog` -- For VSCode: Install Dev Containers extension and open project in devcontainer -- For standalone Docker: - ```bash - docker run \ - -v /path/to/your-project:/workspaces/plinth-template \ - -w /workspaces/plinth-template \ - -it ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog - ``` - -#### Option 3: Manual Setup (Not Recommended) -- Requires GHC 9.6.x and Cabal 3.8+ -- Follow [cardano-node installation instructions](https://developers.cardano.org/docs/get-started/cardano-node/installing-cardano-node/) - -### Essential Build Commands - -#### Bootstrap and Build -1. **Update package index** (requires network access to CHaP repository): - ```bash - cabal update - ``` - - **TIMING**: Usually takes 1-3 minutes - - **NOTE**: Requires access to `chap.intersectmbo.org`. If network access is limited, this step may fail with DNS resolution errors. - -2. **Build all components**: - ```bash - cabal build all - ``` - - **CRITICAL**: Build time ranges from 15-45 minutes depending on cache state. NEVER CANCEL. Set timeout to 60+ minutes. - - **NEVER CANCEL**: First builds take significantly longer as dependencies are compiled. - -3. **Build specific executables**: - ```bash - cabal build gen-auction-validator-blueprint - cabal build gen-minting-policy-blueprint - ``` - -#### Docker-based Build (Alternative) -If local environment has issues, use the Docker approach from CI: -```bash -docker run \ - -v ./.:/workspaces/plinth-template \ - -w /workspaces/plinth-template \ - -i ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog \ - bash -ic "cabal update && cabal build all" -``` -- **CRITICAL**: Total time including Docker image pull: 10-50 minutes. NEVER CANCEL. Set timeout to 75+ minutes. - -## Testing and Validation - -### No Unit Tests Available -- This template repository does not include automated test suites -- Validation is done through successful compilation and blueprint generation - -### Manual Validation Scenarios -After making any changes, ALWAYS perform these validation steps: - -#### 1. Compilation Validation -```bash -cabal build all -``` -Ensure all components compile without errors. - -#### 2. Blueprint Generation Validation -```bash -# Generate auction validator blueprint -cabal run gen-auction-validator-blueprint -- auction-validator.json - -# Generate minting policy blueprint -cabal run gen-minting-policy-blueprint -- minting-policy.json - -# Verify generated files -ls -la auction-validator.json minting-policy.json -head -20 auction-validator.json # Should show JSON with validator metadata -``` -- **Expected Output**: JSON blueprint files should be created successfully in the current directory -- **File Size**: Each blueprint file should be several KB (typically 2-10 KB) -- **Content Validation**: Files should contain JSON with fields like "contractId", "preamble", "validators" -- **Example Content**: Blueprint should include "auction-validator" contract ID and Plutus script metadata - -#### 3. Smart Contract Code Validation -- Review changes to `src/AuctionValidator.hs` and `src/AuctionMintingPolicy.hs` -- Ensure Plutus compiler pragmas are preserved (essential for on-chain compilation): - ```haskell - {-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-} - {-# OPTIONS_GHC -fno-unbox-strict-fields #-} - ``` -- Verify that any changes maintain the validator logic integrity -- Check that Template Haskell compilation markers remain intact: - ```haskell - $$(PlutusTx.compile [||auctionUntypedValidator||]) - ``` - -#### 4. Docker Environment Validation -```bash -# Test Docker environment has correct GHC version -docker run \ - -v ./.:/workspaces/plinth-template \ - -w /workspaces/plinth-template \ - -i ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog \ - bash -ic "ghc --version" -``` -- **Expected Output**: "The Glorious Glasgow Haskell Compilation System, version 9.6.6" - -## Linting and Code Quality - -### Available Linting Tools -The project includes configuration for: -- **HLint**: Haskell linter (`.hlint.yaml` - currently empty, uses defaults) -- **stylish-haskell**: Code formatter (`.stylish-haskell.yaml`) -- **fourmolu**: Alternative formatter (via Nix environment) - -### Pre-commit Validation -```bash -# Format code (available in Nix environment) -stylish-haskell --config .stylish-haskell.yaml -i src/*.hs app/*.hs - -# Lint code (available in Nix environment) -hlint --hint .hlint.yaml src/ app/ - -# Alternative: Use fourmolu for formatting (Nix environment) -fourmolu --mode inplace src/ app/ -``` - -**IMPORTANT**: These tools are primarily available in the Nix environment. In Docker/manual setups, they may not be available. - -## Common Issues and Workarounds - -## Common Issues and Workarounds - -### Network Connectivity Issues -- **Problem**: `cabal update` fails with "Could not resolve host: chap.intersectmbo.org" -- **Root Cause**: CHaP (Cardano Haskell Packages) repository access required for Plutus dependencies -- **Primary Solution**: Use Docker-based build which may have better network access -- **Alternative**: Use Nix environment which may handle repository access more reliably -- **Document If Persistent**: If consistent failures occur, note in comments that CHaP access is required for builds - -### GHC Version Compatibility -- **Required**: GHC 9.6.x specifically (not 9.10.x or 9.12.x) -- **Problem**: System may have different GHC version installed -- **Solution**: Use Nix (`nix develop`) or Docker environments which provide correct GHC 9.6.6 -- **Verification**: Run `ghc --version` in your environment - should show 9.6.x - -### Build Cache Issues -- **Problem**: Builds taking excessively long (>60 minutes) -- **Solution**: Remove build cache and rebuild: `rm -rf dist-newstyle && cabal build all` -- **Prevention**: Use consistent build environment (Nix or Docker) to maintain cache - -### Permission Denied Errors -- **Problem**: `cabal update` fails with "permission denied" on cache directories -- **Common Causes**: Running in restricted environments or mixed user permissions -- **Solution**: Use Docker environment which isolates filesystem permissions - -### Docker Image Pull Issues -- **Problem**: Docker image pull hangs or fails -- **Solution**: Use explicit image pull: `docker pull ghcr.io/input-output-hk/devx-devcontainer:x86_64-linux.ghc96-iog` -- **Timing**: Initial image pull is ~2-5 GB, takes 5-15 minutes on good connections - -## Common Tasks and Quick Reference - -### Repo Exploration (Reference these outputs instead of running commands repeatedly) - -#### Repository Root Structure -``` -ls -la -total 144 -drwxr-xr-x 8 runner docker 4096 . -drwxr-xr-x 3 runner docker 4096 .. -drwxr-xr-x 2 runner docker 4096 .devcontainer -drwxr-xr-x 7 runner docker 4096 .git -drwxr-xr-x 4 runner docker 4096 .github --rw-r--r-- 1 runner docker 80 .gitignore --rw-r--r-- 1 runner docker 0 .hlint.yaml --rw-r--r-- 1 runner docker 17675 .stylish-haskell.yaml --rw-r--r-- 1 runner docker 69 CHANGELOG.md --rw-r--r-- 1 runner docker 45 CODEOWNERS.md --rw-r--r-- 1 runner docker 5522 CODE_OF_CONDUCT.md --rw-r--r-- 1 runner docker 162 CONTRIBUTING.md --rw-r--r-- 1 runner docker 170 DESCRIPTION.md --rw-r--r-- 1 runner docker 11356 LICENSE.md --rw-r--r-- 1 runner docker 560 NOTICE.md --rw-r--r-- 1 runner docker 4361 README.md --rw-r--r-- 1 runner docker 875 SECURITY.md -drwxr-xr-x 2 runner docker 4096 app --rw-r--r-- 1 runner docker 959 cabal.project --rw-r--r-- 1 runner docker 23035 flake.lock --rw-r--r-- 1 runner docker 1193 flake.nix -drwxr-xr-x 2 runner docker 4096 nix --rw-r--r-- 1 runner docker 1196 plinth-template.cabal -drwxr-xr-x 2 runner docker 4096 src -``` - -#### Source Code Files -``` -ls -la src/ app/ -app/: -GenAuctionValidatorBlueprint.hs # Executable: Generates auction validator blueprint -GenMintingPolicyBlueprint.hs # Executable: Generates minting policy blueprint - -src/: -AuctionMintingPolicy.hs # Smart contract: Token minting policy -AuctionValidator.hs # Smart contract: Main auction validator logic -``` - -#### Build Configuration Summary -- **Project Name**: plinth-template -- **GHC Version**: 9.6.x (enforced by Nix/Docker environments) -- **Cabal Version**: 3.8+ recommended -- **Dependencies**: Plutus libraries (plutus-core, plutus-ledger-api, plutus-tx) -- **Repository Access**: Requires CHaP (chap.intersectmbo.org) for Plutus dependencies - -## Repository Structure and Key Files - -### Important Directories -``` -├── src/ # Haskell source files -│ ├── AuctionValidator.hs # Main auction smart contract -│ └── AuctionMintingPolicy.hs # Token minting policy -├── app/ # Executable applications -│ ├── GenAuctionValidatorBlueprint.hs # Blueprint generator -│ └── GenMintingPolicyBlueprint.hs # Minting policy blueprint -├── nix/ # Nix configuration -├── .devcontainer/ # Docker devcontainer setup -└── .github/workflows/ # CI pipeline definitions -``` - -### Key Configuration Files -- `plinth-template.cabal`: Project dependencies and build configuration -- `cabal.project`: Cabal project settings and package repositories -- `flake.nix`: Nix development environment definition -- `.devcontainer/devcontainer.json`: Docker development environment - -### After Making Changes -1. **ALWAYS** build and validate using the steps above -2. **ALWAYS** test blueprint generation for affected smart contracts -3. **NEVER** commit without ensuring compilation succeeds -4. Document any new dependencies or build requirements in these instructions - -## Timing Expectations Summary - -| Operation | Expected Time | Timeout Setting | Critical Notes | -|-----------|---------------|-----------------|----------------| -| `nix develop` (first time) | 30-60 minutes | 90+ minutes | NEVER CANCEL - Downloads entire toolchain | -| `cabal update` | 1-3 minutes | 10 minutes | Requires CHaP network access | -| `cabal build all` (first time) | 15-45 minutes | 60+ minutes | NEVER CANCEL - Compiles all dependencies | -| `cabal build all` (cached) | 2-10 minutes | 15 minutes | Much faster with existing cache | -| Docker image pull | 5-15 minutes | 30 minutes | One-time download | -| Blueprint generation | 30 seconds - 2 minutes | 5 minutes | Fast once dependencies built | - -**CRITICAL REMINDER**: Plutus/Cardano builds are notoriously slow. Patience is essential. NEVER CANCEL long-running build operations. \ No newline at end of file diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index d5f3e01..0000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Full build of the docker template branch inside the devx devcontainer -# image — the same image devcontainers, Codespaces and standalone docker use. -# Locally: dev/ci/build-docker.sh (skips when docker is unavailable). -name: Build docker project - -on: - workflow_dispatch: - schedule: - - cron: "0 6 * * 1" - pull_request: - paths: - - "cabal.project" - - "plinth-template.cabal" - - "src/**" - - "app/**" - - ".devcontainer/**" - - "install.sh" - - "dev/ci/build-docker.sh" - - ".github/workflows/build-docker.yml" - -jobs: - build: - name: devx devcontainer (ghc96) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: dev/ci/build-docker.sh - env: - PLINTH_REQUIRE_DOCKER: "1" diff --git a/.github/workflows/build-ghc-cabal.yml b/.github/workflows/build-ghc-cabal.yml deleted file mode 100644 index 1327a0b..0000000 --- a/.github/workflows/build-ghc-cabal.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Full build of the ghc-cabal template branch with a real toolchain, on both -# supported GHC series and both OSes. Heavy (compiles plutus and friends), so -# it runs on demand, on a weekly schedule, and when the relevant inputs -# change. Locally: dev/ci/build-ghc-cabal.sh -name: Build ghc-cabal project - -on: - workflow_dispatch: - schedule: - - cron: "0 4 * * 1" - pull_request: - paths: - - "cabal.project" - - "plinth-template.cabal" - - "src/**" - - "app/**" - - "scripts/get-crypto-libs.sh" - - "scripts/pkg-config" - - "install.sh" - - "dev/ci/build-ghc-cabal.sh" - - ".github/workflows/build-ghc-cabal.yml" - -jobs: - build: - name: ghc ${{ matrix.ghc }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - ghc: ["9.6.7", "9.12.2"] - steps: - - uses: actions/checkout@v4 - - - uses: haskell-actions/setup@v2 - id: setup - with: - ghc-version: ${{ matrix.ghc }} - cabal-version: latest - - - name: Ensure pkg-config (macOS) - if: runner.os == 'macOS' - run: command -v pkg-config || brew install pkgconf - - - name: Cache cabal store - uses: actions/cache@v4 - with: - path: | - ${{ steps.setup.outputs.cabal-store }} - ~/.cabal/packages - ~/.cache/cabal/packages - key: crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project') }} - restore-keys: | - crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}- - - - run: dev/ci/build-ghc-cabal.sh diff --git a/.github/workflows/build-nix.yml b/.github/workflows/build-nix.yml deleted file mode 100644 index 2f4c538..0000000 --- a/.github/workflows/build-nix.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Full build of the nix template branch (which is also the demeter branch's -# content) inside its own `nix develop` shell, for both GHC series. Locally: -# dev/ci/build-nix.sh [--shell ghc96|ghc912] -name: Build nix project - -on: - workflow_dispatch: - schedule: - - cron: "0 5 * * 1" - pull_request: - paths: - - "cabal.project" - - "plinth-template.cabal" - - "src/**" - - "app/**" - - "nix/**" - - "flake.nix" - - "flake.lock" - - "install.sh" - - "dev/ci/build-nix.sh" - - ".github/workflows/build-nix.yml" - -jobs: - build: - name: nix develop .#${{ matrix.shell }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shell: [ghc96, ghc912] - steps: - - uses: actions/checkout@v4 - - - uses: DeterminateSystems/nix-installer-action@v16 - with: - extra-conf: | - extra-substituters = https://cache.iog.io - extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= - accept-flake-config = true - - - run: dev/ci/build-nix.sh --shell ${{ matrix.shell }} diff --git a/.github/workflows/bump-plutus-version.yml b/.github/workflows/bump-plutus-version.yml index 48f4eac..8b1c1bd 100644 --- a/.github/workflows/bump-plutus-version.yml +++ b/.github/workflows/bump-plutus-version.yml @@ -22,46 +22,10 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - name: Update flake.lock - run: | - nix flake update CHaP hackage --accept-flake-config - - - name: Change Plutus Versions - run: | - set -o pipefail - - PLUTUS_VERSION=${{ github.event.inputs.version }} - - # Derive hackage index-state from the flake-pinned hackage commit, - # subtracting 2 hours as safety margin (commit timestamp > latest index entry). - HACKAGE_EPOCH=$(jq '.nodes.hackage.locked.lastModified' flake.lock) - HACKAGE_EPOCH=$((HACKAGE_EPOCH - 7200)) - HACKAGE_DATE=$(date -u -d "@$HACKAGE_EPOCH" +"%Y-%m-%dT%H:%M:%SZ") - - # For CHaP, read the index that ships inside the flake-pinned repository: - # the mtimes of the entries in 01-index.tar.gz ARE the index-states, so - # the newest one is the exact latest index-state of the pinned CHaP — - # independent of CHaP's git commit timestamp, which can be arbitrarily - # newer (e.g. ghost automation re-runs with 0 file changes). - CHAP_REV=$(jq -r '.nodes.CHaP.locked.rev' flake.lock) - CHAP_DATE=$(curl -fsSL \ - "https://raw.githubusercontent.com/IntersectMBO/cardano-haskell-packages/${CHAP_REV}/01-index.tar.gz" \ - | tar -tvz --full-time -f - \ - | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}' \ - | sort | tail -1 | sed 's/ /T/;s/$/Z/') - - if [ -z "$CHAP_DATE" ]; then - echo "Failed to determine CHaP index-state" >&2 - exit 1 - fi - - sed -i "s/\(hackage.haskell.org \).*$/\1$HACKAGE_DATE/" cabal.project - sed -i "s/\(cardano-haskell-packages \).*$/\1$CHAP_DATE/" cabal.project - - sed -i "s/\(plutus-core \).*$/\1\^>=$PLUTUS_VERSION/" "plinth-template.cabal" - sed -i "s/\(plutus-ledger-api \).*$/\1\^>=$PLUTUS_VERSION/" "plinth-template.cabal" - sed -i "s/\(plutus-tx \).*$/\1\^>=$PLUTUS_VERSION/" "plinth-template.cabal" - sed -i "s/\(plutus-tx-plugin \).*$/\1\^>=$PLUTUS_VERSION/" "plinth-template.cabal" + - name: Bump Plutus Version + env: + PLUTUS_VERSION: ${{ github.event.inputs.version }} + run: .github/ci/bump-plutus-version.sh "$PLUTUS_VERSION" - name: Create Pull Request id: cpr @@ -70,7 +34,7 @@ jobs: branch: "bump-plutus-${{ github.event.inputs.version }}" title: Bump plutus Version to ${{ github.event.inputs.version }} commit-message: Bump plutus Version to ${{ github.event.inputs.version }} - delete-branch: true + delete-branch: true token: ${{ secrets.GITHUB_TOKEN }} - name: Enable Pull Request Auto-Merge @@ -80,5 +44,3 @@ jobs: pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} merge-method: squash token: ${{ secrets.GITHUB_TOKEN }} - - diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..f5d7894 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,142 @@ +# The single CI workflow: everything is rebuilt and retested on every pull +# request (no path filters — the repository is small and the jobs run in +# parallel), on pushes to main, on demand, and once a week to catch upstream +# drift (toolchains, the devx image, the binary caches). +# +# Every job is a thin wrapper around a script in .github/ci/, so the same +# checks run locally: see .github/ci/run-all-local.sh (build-windows.sh is +# the exception — it needs a Windows machine with MSYS2). +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + schedule: + - cron: "0 4 * * 1" + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint shell scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: .github/ci/lint.sh + + test-install: + name: install.sh (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + # Runners have no nix; install it on the Linux leg so test-install.sh + # also exercises install.sh's `--env nix` happy path. + - uses: DeterminateSystems/nix-installer-action@v16 + if: runner.os == 'Linux' + + - run: .github/ci/test-install.sh + + build-cabal: + name: ghc-cabal project (ghc ${{ matrix.ghc }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + ghc: ["9.6.7", "9.12.2"] + steps: + - uses: actions/checkout@v4 + + - uses: haskell-actions/setup@v2 + id: setup + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: latest + + - name: Ensure pkg-config (macOS) + if: runner.os == 'macOS' + run: command -v pkg-config || brew install pkgconf + + - name: Cache cabal store + uses: actions/cache@v4 + with: + path: | + ${{ steps.setup.outputs.cabal-store }} + ~/.cabal/packages + ~/.cache/cabal/packages + key: crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('template/cabal.project') }} + restore-keys: | + crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}- + + - run: .github/ci/build-ghc-cabal.sh + + build-windows: + name: template build (windows, ghc ${{ matrix.ghc }}) + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + ghc: ["9.6.7", "9.12.2"] + steps: + - uses: actions/checkout@v4 + + - uses: haskell-actions/setup@v2 + id: setup + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: latest + + - name: Cache cabal store + uses: actions/cache@v4 + with: + path: ${{ steps.setup.outputs.cabal-store }} + key: crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('template/cabal.project') }} + restore-keys: | + crypto-cabal-${{ runner.os }}-${{ matrix.ghc }}- + + # install.sh does not support native Windows; this builds template/ + # directly with the msys2 crypto libraries (see the script's header). + - name: Build inside MSYS2 (MINGW64) + shell: C:/msys64/usr/bin/bash.exe -e '{0}' + env: + MSYSTEM: MINGW64 + MSYS2_PATH_TYPE: inherit + CHERE_INVOKING: 1 + run: bash .github/ci/build-windows.sh + + build-nix: + name: nix project (nix develop .#${{ matrix.shell }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shell: [ghc96, ghc912] + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@v16 + with: + extra-conf: | + extra-substituters = https://cache.iog.io + extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= + accept-flake-config = true + + - run: .github/ci/build-nix.sh --shell ${{ matrix.shell }} + + build-docker: + name: docker project (devx devcontainer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: .github/ci/build-docker.sh + env: + PLINTH_REQUIRE_DOCKER: "1" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ab89ce0..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Fast checks on every change: shell lint and end-to-end tests of install.sh -# (with a local git fixture — no network cloning), including exact manifests -# of the projects it creates for every environment. Everything runs through -# scripts in dev/ci/, which can be run locally exactly the same way -# (see dev/ci/run-all-local.sh). -name: CI - -on: - push: - branches: [main] - pull_request: - workflow_dispatch: - -jobs: - lint: - name: Lint shell scripts - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: dev/ci/lint.sh - - test-install: - name: install.sh (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - uses: actions/checkout@v4 - - # Runners have no nix; install it on the Linux leg so test-install.sh - # also exercises install.sh's `--env nix` happy path. - - uses: DeterminateSystems/nix-installer-action@v16 - if: runner.os == 'Linux' - - - run: dev/ci/test-install.sh diff --git a/.gitignore b/.gitignore index c9add98..9ecc676 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ dist-newstyle -.pre-commit-config.yaml -validator.uplc -blueprint.json -cabal.project.freeze cabal.project.local -result \ No newline at end of file +blueprint.json +result diff --git a/CODEOWNERS.md b/CODEOWNERS.md index 19cc976..e8b23f8 100644 --- a/CODEOWNERS.md +++ b/CODEOWNERS.md @@ -1,3 +1,3 @@ -# Plutus Tx Template Codeowners +# Plinth Template Codeowners * @zeme-wana \ No newline at end of file diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 91a9d51..d95d4c6 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -1,4 +1,4 @@ -# Plinth Template +# Plinth Template Description This is a template repository for kickstarting your Plinth smart contract project. diff --git a/README.md b/README.md index 66d331b..9266130 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,17 @@ Cardano crypto C libraries), and creates a fresh project folder — named after your project (default: `my-plinth-project`) — containing just the files that environment needs: -| Environment | What you need locally | Project contains | -| ------------ | ------------------------------------------- | ----------------------------- | -| Nix | nix (crypto libs provided by the shell) | sources + nix files | -| Docker | docker or a browser (Codespaces) | sources + .devcontainer | -| Demeter | just a browser (hosted; nix inside) | sources + nix files | -| GHC + Cabal | ghcup with GHC 9.6/9.12, cabal, pkg-config | sources + crypto-libs scripts | +| Environment | What you need locally | Project contains | +| ------------ | ------------------------------------------- | ------------------------------ | +| Nix | nix (crypto libs provided by the shell) | sources + nix files | +| Docker | docker or a browser (Codespaces) | sources + .devcontainer | +| Demeter | just a browser (hosted; nix inside) | sources + nix files | +| GHC + Cabal | ghcup with GHC 9.6/9.12, cabal, pkg-config | sources + get-crypto-libs.sh | Each project comes with a README covering just that setup. You can also skip -the installer entirely and clone this repository directly — it carries the -union of all environments, and every setup works from it as-is. +the installer entirely: every project is a subset of the +[template/](template) directory, so copying it (plus +[get-crypto-libs.sh](get-crypto-libs.sh) for the GHC+Cabal setup) works too. Whatever you pick, the first thing to run inside the project (and its environment) is `cabal build all`, which compiles the example auction @@ -36,41 +37,43 @@ The Nix shell, the Docker image and Demeter workspaces provide them (via nix). GHC+Cabal projects instead download IOG's prebuilt, checksum- and commit-pinned binaries from [iohk-nix releases](https://github.com/input-output-hk/iohk-nix/releases) -on first build into a per-user cache (`~/.cache/plinth-crypto-libs`), linked -into the project at `dist-newstyle/crypto-libs/` — nothing is installed -system-wide; see [scripts/get-crypto-libs.sh](scripts/get-crypto-libs.sh) -(`--prefix` installs them system-wide instead) and the GHC+Cabal README -([dev/readmes/ghc-cabal.md](dev/readmes/ghc-cabal.md)). +into a per-user cache (`~/.cache/plinth-crypto-libs`), linked into the +project at `dist-newstyle/crypto-libs/` — nothing is installed system-wide; +see [get-crypto-libs.sh](get-crypto-libs.sh) (`--prefix` installs them +system-wide instead) and the GHC+Cabal README +([template/readmes/ghc-cabal.md](template/readmes/ghc-cabal.md)). ## Repository layout — for maintainers -This single branch carries the union of every environment's files plus the -machinery around it; `install.sh` selects the relevant subset when creating a -project (see `include_file` and the marked blocks in `cabal.project` and -`nix/project.nix` it transforms): - +- [template/](template) — the project files. The union of every + environment's files; `install.sh` selects the relevant subset when + creating a project (see `include_file`). One of + [template/readmes/](template/readmes) becomes the project's `README.md`. - [install.sh](install.sh) — the installer served over curl. `--from DIR` installs from a local checkout (offline/CI); `--yes --env ...` runs it non-interactively. -- [scripts/](scripts) — the crypto-libs bootstrap that ships with GHC+Cabal - projects ([get-crypto-libs.sh](scripts/get-crypto-libs.sh) and the - [pkg-config shim](scripts/pkg-config)). -- [dev/readmes/](dev/readmes) — the per-environment READMEs the installer - places into new projects. -- [dev/ci/](dev/ci) — the test suite. Every GitHub workflow is a thin - wrapper around one of these scripts, so everything can be run locally: +- [get-crypto-libs.sh](get-crypto-libs.sh) — the crypto-libs bootstrap, + copied into GHC+Cabal projects next to their `cabal.project`. +- [.github/ci/](.github/ci) — the test suite. Every GitHub workflow is a + thin wrapper around one of these scripts, so everything can be run + locally: ``` - dev/ci/run-all-local.sh # everything - PLINTH_SKIP_HEAVY=1 dev/ci/run-all-local.sh # fast checks only + .github/ci/run-all-local.sh # everything + PLINTH_SKIP_HEAVY=1 .github/ci/run-all-local.sh # fast checks only ``` - | Script | Checks | Workflow | - | -------------------------- | --------------------------------------------------- | ---------------------- | - | `lint.sh` | shellcheck + syntax over all shell scripts | `ci.yml` | - | `test-install.sh` | install.sh end-to-end: exact per-env manifests, | `ci.yml` | - | | transforms, failure modes, real crypto download | | - | `build-ghc-cabal.sh` | full build of an installed GHC+Cabal project | `build-ghc-cabal.yml` | - | `build-nix.sh` | full build of an installed Nix project (= Demeter) | `build-nix.yml` | - | `build-docker.sh` | full build inside the devx devcontainer image | `build-docker.yml` | - | `test-blueprint-parity.sh` | blueprint byte-parity between ghcup and nix | (manual) | + | Script | Checks | Workflow | + | -------------------------- | -------------------------------------------------- | ------------------------ | + | `lint.sh` | shellcheck + syntax over all shell scripts | `ci.yaml` | + | `test-install.sh` | install.sh end-to-end: exact per-env manifests, | `ci.yaml` | + | | failure modes, real crypto download | | + | `build-ghc-cabal.sh` | full build of an installed GHC+Cabal project | `ci.yaml` | + | `build-nix.sh` | full build of an installed Nix project (= Demeter) | `ci.yaml` | + | `build-docker.sh` | full build inside the devx devcontainer image | `ci.yaml` | + | `build-windows.sh` | template build on native Windows (MSYS2 libs) | `ci.yaml` | + | `bump-plutus-version.sh` | bumps plutus + index-states in template/ | `bump-plutus-version.yml`| + | `test-blueprint-parity.sh` | blueprint byte-parity between ghcup and nix | (manual) | + + [ci.yaml](.github/workflows/ci.yaml) runs all of its jobs in parallel on + every pull request — no path filters, everything is rebuilt and retested. diff --git a/dev/ci/build-nix.sh b/dev/ci/build-nix.sh deleted file mode 100755 index 5800226..0000000 --- a/dev/ci/build-nix.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# -# Build a Nix project end-to-end: create it with install.sh --env nix (or -# take a pre-created one as $1), enter its `nix develop` shell and run -# `cabal build all` plus the example blueprint. -# -# dev/ci/build-nix.sh [TREE] [--shell ghc96|ghc912|default] -# -# Demeter projects have identical nix content, so this also covers demeter. -# -# Requirements: nix with flakes. Configure IOG's binary caches (see the -# nix-setup-guide) or the first run will build GHC from source. -# -# Environment: -# PLINTH_CI_KEEP=1 Keep the scratch directory (printed) for inspection. - -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -cd "$ROOT" - -TREE="" -SHELL_NAME="default" -while [ $# -gt 0 ]; do - case "$1" in - --shell) shift; SHELL_NAME="${1:?--shell needs an argument}" ;; - --shell=*) SHELL_NAME="${1#--shell=}" ;; - *) TREE="$1" ;; - esac - shift -done - -fail() { echo "build-nix: FAIL: $*" >&2; exit 1; } -note() { echo "build-nix: $*"; } - -command -v nix >/dev/null 2>&1 || fail "nix not on PATH" - -WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-nix.XXXXXX")" -# nix's path: fetcher rejects paths with symlinked ancestors (macOS /var); -# use the physical path. -WORK="$(cd "$WORK" && pwd -P)" -cleanup() { - if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then - echo "build-nix: scratch dir kept: $WORK" - else - rm -rf "$WORK" - fi -} -trap cleanup EXIT - -PROJECT="$WORK/project" -if [ -z "$TREE" ]; then - sh "$ROOT/install.sh" --yes --env nix --from "$ROOT" --dir "$PROJECT" \ - >"$WORK/install.log" 2>&1 \ - || { cat "$WORK/install.log" >&2; fail "install.sh --env nix failed"; } -else - mkdir -p "$PROJECT" - cp -R "$TREE/." "$PROJECT/" -fi -[ -f "$PROJECT/flake.nix" ] || fail "$PROJECT is not a nix project" - -# cd matters: `nix develop --command` runs in the calling directory, and the -# in-shell cabal must build the branch copy, not whatever cwd we came from. -cd "$PROJECT" - -note "entering nix develop path:$PROJECT#$SHELL_NAME (first run may take a while)..." -# shellcheck disable=SC2016 # the inner script must expand inside the shell -nix develop "path:$PROJECT#$SHELL_NAME" --accept-flake-config --command bash -c ' - set -euo pipefail - note() { echo "build-nix(shell): $*"; } - - case "$(command -v ghc)" in - /nix/store/*) note "ghc $(ghc --numeric-version) from /nix/store" ;; - *) echo "build-nix(shell): FAIL: ghc not from /nix/store: $(command -v ghc)" >&2; exit 1 ;; - esac - case "$(command -v cabal)" in - /nix/store/*) note "cabal $(cabal --numeric-version) from /nix/store" ;; - *) echo "build-nix(shell): FAIL: cabal not from /nix/store: $(command -v cabal)" >&2; exit 1 ;; - esac - pkg-config --exists libsodium libsecp256k1 libblst \ - || { echo "build-nix(shell): FAIL: crypto libs not visible to pkg-config" >&2; exit 1; } - note "crypto libs provided by the shell: sodium $(pkg-config --modversion libsodium), secp256k1 $(pkg-config --modversion libsecp256k1), blst $(pkg-config --modversion libblst)" - - if [ -n "${CI:-}" ]; then - note "CI: running cabal update" - cabal update - fi - - note "building (cabal build all)..." - cabal build all - cabal run -v0 exe:gen-auction-validator-blueprint -- blueprint.json - [ -s blueprint.json ] || { echo "build-nix(shell): FAIL: empty blueprint" >&2; exit 1; } - note "blueprint OK: $(wc -c < blueprint.json | tr -d " ") bytes" -' || fail "nix shell build failed" - -echo "build-nix: SUCCESS (shell: $SHELL_NAME)" diff --git a/dev/readmes/ghc-cabal.md b/dev/readmes/ghc-cabal.md deleted file mode 100644 index 9d7a0b6..0000000 --- a/dev/readmes/ghc-cabal.md +++ /dev/null @@ -1,80 +0,0 @@ -# Plinth Template (GHC + Cabal edition) - -A template for your Plinth smart contract project, built with your own -**GHC and Cabal** — no nix, no docker, and no system-wide C libraries. - -## 1. Prerequisites - -- [ghcup](https://www.haskell.org/ghcup/) with **GHC 9.6.x or 9.12.x** - (e.g. `ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7`) -- **Cabal 3.8+** (`ghcup install cabal latest`) -- a **pkg-config** executable - (macOS: `brew install pkgconf`, Debian/Ubuntu: `apt install pkg-config`) - -## 2. Build - -From the project root: - -``` -cabal update -cabal build all -``` - -## The crypto C libraries - -`plutus-core` depends (via `cardano-crypto-class`) on three C libraries — -`libsodium` (VRF-patched), `libsecp256k1` and `libblst` — which cabal locates -with pkg-config. This project routes pkg-config through `scripts/pkg-config` -(see the `program-locations` stanza in `cabal.project`): on first use the -shim downloads the prebuilt libraries published by IOG at -[iohk-nix releases](https://github.com/input-output-hk/iohk-nix/releases) -into a per-user cache (`~/.cache/plinth-crypto-libs`, override with -`PLINTH_CRYPTO_LIBS_HOME`), links them into the project at -`dist-newstyle/crypto-libs/`, and answers cabal's pkg-config queries from -there. Nothing is installed system-wide; the cache is shared by all your -Plinth projects, and it is the cache path (not the project path) that gets -baked into the packages cabal compiles, so the cabal store stays valid if -the project is moved or deleted. Every downloaded artifact is verified -against sha256 digests and an iohk-nix commit hash pinned inside -`scripts/get-crypto-libs.sh`. Only `curl`, `tar` and `shasum`/`sha256sum` -are needed, all of which ship with macOS and Linux. - -This behavior is ON by default. To opt out and use crypto libraries -installed system-wide instead, set `PLINTH_USE_SYSTEM_CRYPTO_LIBS=1`: the -shim then delegates to the real pkg-config on your PATH and nothing is -downloaded. To install IOG's prebuilt libraries system-wide in the first -place, run `./scripts/get-crypto-libs.sh --prefix /usr/local` (or any other -prefix; see `--help`). - -When cross compiling, set `PLINTH_CRYPTO_LIBS_PLATFORM` to the target -platform (`arm64-macos`, `x86_64-macos` or `debian`) and the shim serves -the target's libraries regardless of the build host. - -> NOTE: -> Run cabal from the project root: the `pkg-config-location` path in -> `cabal.project` is resolved relative to the invocation directory. - -> NOTE (first build only): -> The very first build on a fresh clone downloads the libraries and writes -> their absolute location into `cabal.project.local` (gitignored) — too late -> for that same run's configure phase, so it stops once with -> *"Cannot find the program 'pkg-config'"*. Simply re-run `cabal build`; -> everything already built is reused, and the interruption never happens -> again (not even after `cabal clean`). To skip it entirely, run -> `./scripts/get-crypto-libs.sh` once before the first build. - -> NOTE: -> Only a symlink lives under `dist-newstyle/`, so `cabal clean` costs -> nothing: the next build re-links the cached libraries instantly (no -> re-download). Run `./scripts/get-crypto-libs.sh` yourself at any time. - -> NOTE (for Windows users): -> Prebuilt MSYS2 packages (`msys2.*.pkg.tar.zstd`) are published in the same -> release. Install them with `pacman -U` inside GHC's MSYS2 environment -> (`ghcup run mingw-pacman -- -U `), or use WSL2 and follow the Linux -> instructions. - -## 3. Run the example application - -Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) -to get started. diff --git a/scripts/get-crypto-libs.sh b/get-crypto-libs.sh similarity index 78% rename from scripts/get-crypto-libs.sh rename to get-crypto-libs.sh index d28331b..998f195 100755 --- a/scripts/get-crypto-libs.sh +++ b/get-crypto-libs.sh @@ -20,13 +20,6 @@ # # export PKG_CONFIG_LIBDIR="$(pwd)/dist-newstyle/crypto-libs//lib/pkgconfig" # -# The feature is ON by default. To opt out (i.e. you have the crypto libs -# installed system-wide and want cabal to use those), set -# -# PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 -# -# in which case this script is a no-op. -# # Every downloaded artifact is verified against a sha256 pinned in this file, # and the release's COMMIT_SHA asset is required to match the pinned iohk-nix # commit below. @@ -91,14 +84,11 @@ Options: entries for the three libraries are written; nothing else in DIR is touched. DIR must be writable (rerun with sudo if needed). In this mode the project files - (cabal.project.local, env.sh) are NOT touched: build - with PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 and make sure - DIR/lib/pkgconfig is on pkg-config's search path. + (dist-newstyle/crypto-libs, env.sh) are NOT touched: + make sure DIR/lib/pkgconfig is on pkg-config's search + path when building. Environment: - PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 Opt out: do nothing, use system libraries. - (Ignored when --prefix is given: that mode - IS the system-wide install.) PLINTH_CRYPTO_LIBS_PLATFORM Same as --platform. PLINTH_CRYPTO_LIBS_HOME Cache location (default: \$XDG_CACHE_HOME/plinth-crypto-libs or @@ -124,26 +114,22 @@ while [ $# -gt 0 ]; do shift done -say() { [ "$QUIET" = 1 ] || echo "$@"; } +say() { if [ "$QUIET" != 1 ]; then echo "$@"; fi; } die() { echo "get-crypto-libs: ERROR: $*" >&2; exit 1; } -if [ "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-0}" = 1 ] && [ -z "$SYSTEM_PREFIX" ]; then - say "PLINTH_USE_SYSTEM_CRYPTO_LIBS=1: skipping download, expecting system-wide crypto libs." - exit 0 -fi - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# The script lives at the project root, next to cabal.project. +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" # --------------------------------------------------------------------------- # Project-side helpers (local mode). They run both after a fresh install and -# on the fast already-installed path, so a wiped dist-newstyle or a deleted -# cabal.project.local heals on the next run — without re-downloading. +# on the fast already-installed path, so a wiped dist-newstyle heals on the +# next run — without re-downloading. # --------------------------------------------------------------------------- write_env_file() { mkdir -p "$(dirname "$ENV_FILE")" cat > "$ENV_FILE" </dev/null; then - tmp="$LOCAL_FILE.tmp.$$" - if [ -f "$LOCAL_FILE" ]; then - awk -v begin="$MARK_BEGIN" -v end="$MARK_END" ' - $0 == begin { skip = 1; next } - $0 == end { skip = 0; next } - !skip { print } - ' "$LOCAL_FILE" > "$tmp" - else - : > "$tmp" - fi - { - echo "$MARK_BEGIN" - echo "package *" - echo " pkg-config-location: $SHIM" - echo "$MARK_END" - } >> "$tmp" - mv "$tmp" "$LOCAL_FILE" - say "Updated $LOCAL_FILE (pkg-config shim location for package configure)." - say "NOTE: a cabal command that was already running when this file was" - say "created cannot pick it up; if that build stops with 'Cannot find the" - say "program pkg-config', simply re-run it (this happens at most once)." - fi -} - # -------------------------------------------------------------------------- # Platform selection: explicit (--platform / PLINTH_CRYPTO_LIBS_PLATFORM, # e.g. when cross compiling) or detected from the host. @@ -279,7 +228,6 @@ else if [ -f "$STAMP" ] && [ "$FORCE" != 1 ]; then link_into_project write_env_file - maintain_cabal_project_local say "Crypto libs already installed in $PREFIX" say "(release $RELEASE_TAG, iohk-nix commit $IOHK_NIX_COMMIT; linked from $LINK)." say "Use --force to reinstall. To use them: source $ENV_FILE" @@ -287,7 +235,9 @@ else fi fi -command -v curl >/dev/null 2>&1 || die "curl is required" +if ! command -v curl >/dev/null 2>&1; then + die "curl is required" +fi sha256_of() { if command -v shasum >/dev/null 2>&1; then @@ -304,18 +254,22 @@ download() { local asset="$1" expected="$2" out="$DOWNLOADS/$1" actual if [ ! -f "$out" ] || [ "$(sha256_of "$out")" != "$expected" ]; then say "Downloading $asset ..." - curl --fail --silent --show-error --location --retry 3 \ - --output "$out.tmp" "$BASE_URL/$asset" \ - || die "failed to download $BASE_URL/$asset" + if ! curl --fail --silent --show-error --location --retry 3 \ + --output "$out.tmp" "$BASE_URL/$asset"; then + die "failed to download $BASE_URL/$asset" + fi mv "$out.tmp" "$out" fi actual="$(sha256_of "$out")" - [ "$actual" = "$expected" ] \ - || die "sha256 mismatch for $asset: expected $expected, got $actual" + if [ "$actual" != "$expected" ]; then + die "sha256 mismatch for $asset: expected $expected, got $actual" + fi say "Verified $asset (sha256 OK)" } -[ -d "$PREFIX" ] && chmod -R u+w "$PREFIX" +if [ -d "$PREFIX" ]; then + chmod -R u+w "$PREFIX" +fi rm -rf "$PREFIX" mkdir -p "$PREFIX" "$DOWNLOADS" @@ -325,8 +279,9 @@ mkdir -p "$PREFIX" "$DOWNLOADS" download "COMMIT_SHA" "$COMMIT_SHA_ASSET_SHA256" RELEASE_COMMIT="$(cat "$DOWNLOADS/COMMIT_SHA")" -[ "$RELEASE_COMMIT" = "$IOHK_NIX_COMMIT" ] \ - || die "release COMMIT_SHA ($RELEASE_COMMIT) does not match pinned iohk-nix commit ($IOHK_NIX_COMMIT)" +if [ "$RELEASE_COMMIT" != "$IOHK_NIX_COMMIT" ]; then + die "release COMMIT_SHA ($RELEASE_COMMIT) does not match pinned iohk-nix commit ($IOHK_NIX_COMMIT)" +fi say "Release $RELEASE_TAG was built from pinned iohk-nix commit $IOHK_NIX_COMMIT (verified)" # -------------------------------------------------------------------------- @@ -340,11 +295,13 @@ extract_into_prefix() { *.pkg) # macOS flat installer package: a xar archive whose Payload member is a # gzipped cpio with the files rooted at the install prefix. - [ -n "$BSDTAR" ] || die "extracting $file requires bsdtar (libarchive)" + if [ -z "$BSDTAR" ]; then + die "extracting $file requires bsdtar (libarchive)" + fi local tmp tmp="$(mktemp -d "$DOWNLOADS/expand.XXXXXX")" "$BSDTAR" -xf "$file" -C "$tmp" Payload - (cd "$PREFIX" && "$BSDTAR" -xf "$tmp/Payload") + "$BSDTAR" -xf "$tmp/Payload" -C "$PREFIX" rm -rf "$tmp" ;; *.deb) @@ -390,13 +347,15 @@ done # - drop libtool .la files (they carry stale paths and are not needed) # -------------------------------------------------------------------------- -[ -d "$PREFIX/lib/pkgconfig" ] || die "extraction failed: $PREFIX/lib/pkgconfig missing" +if [ ! -d "$PREFIX/lib/pkgconfig" ]; then + die "extraction failed: $PREFIX/lib/pkgconfig missing" +fi # (not sed: an install path containing '&', '|' or '\' would corrupt the # replacement. prefix= must stay first — later variables reference it.) for pc in "$PREFIX"/lib/pkgconfig/*.pc; do - { echo "prefix=$FINAL_PREFIX"; grep -v '^prefix=' "$pc"; } > "$pc.tmp" \ - && mv "$pc.tmp" "$pc" + { echo "prefix=$FINAL_PREFIX"; grep -v '^prefix=' "$pc"; } > "$pc.tmp" + mv "$pc.tmp" "$pc" done rm -f "$PREFIX"/lib/*.la @@ -405,7 +364,9 @@ case "$PLATFORM" in *macos) if command -v install_name_tool >/dev/null 2>&1; then for dylib in "$PREFIX"/lib/*.dylib; do - [ -L "$dylib" ] && continue + if [ -L "$dylib" ]; then + continue + fi install_name_tool -id "$FINAL_PREFIX/lib/$(basename "$dylib")" "$dylib" 2>/dev/null done elif [ "$(uname -s)" = Darwin ]; then @@ -413,12 +374,16 @@ case "$PLATFORM" in else say "NOTE: install_name_tool unavailable on this host; dylib install names keep their original (nix store) paths." fi - # Unversioned symlinks (the .pkg payloads only ship versioned dylibs). + # Unversioned symlinks (the .pkg payloads only ship versioned dylibs; + # libblst.dylib is shipped unversioned already). ( cd "$PREFIX/lib" - [ -e libsodium.dylib ] || ln -s libsodium.*.dylib libsodium.dylib - [ -e libsecp256k1.dylib ] || ln -s libsecp256k1.*.dylib libsecp256k1.dylib - [ -e libblst.dylib ] || true # shipped unversioned already + if [ ! -e libsodium.dylib ]; then + ln -s libsodium.*.dylib libsodium.dylib + fi + if [ ! -e libsecp256k1.dylib ]; then + ln -s libsecp256k1.*.dylib libsecp256k1.dylib + fi ) ;; debian) @@ -444,7 +409,9 @@ esac if [ -n "$SYSTEM_PREFIX" ]; then chmod -R u+w "$PREFIX" cp -R "$PREFIX/lib/." "$SYSTEM_PREFIX/lib/" - [ -d "$PREFIX/include" ] && cp -R "$PREFIX/include/." "$SYSTEM_PREFIX/include/" + if [ -d "$PREFIX/include" ]; then + cp -R "$PREFIX/include/." "$SYSTEM_PREFIX/include/" + fi fi # -------------------------------------------------------------------------- @@ -453,10 +420,13 @@ fi if command -v pkg-config >/dev/null 2>&1; then for lib in libsodium libsecp256k1 libblst; do - v="$(PKG_CONFIG_LIBDIR="$FINAL_PREFIX/lib/pkgconfig" pkg-config --modversion "$lib")" \ - || die "pkg-config cannot resolve $lib from $FINAL_PREFIX/lib/pkgconfig" + if ! v="$(PKG_CONFIG_LIBDIR="$FINAL_PREFIX/lib/pkgconfig" pkg-config --modversion "$lib")"; then + die "pkg-config cannot resolve $lib from $FINAL_PREFIX/lib/pkgconfig" + fi p="$(PKG_CONFIG_LIBDIR="$FINAL_PREFIX/lib/pkgconfig" pkg-config --variable=prefix "$lib")" - [ "$p" = "$FINAL_PREFIX" ] || die "$lib resolves to unexpected prefix: $p" + if [ "$p" != "$FINAL_PREFIX" ]; then + die "$lib resolves to unexpected prefix: $p" + fi say "$lib $v -> $FINAL_PREFIX (pkg-config OK)" done else @@ -467,11 +437,9 @@ if [ -n "$SYSTEM_PREFIX" ]; then say "" say "Installed libsodium, libsecp256k1 and libblst into: $SYSTEM_PREFIX" say "" - say "To build against them, make sure pkg-config can see them and tell the" - say "project to use system libraries:" + say "To build against them, make sure pkg-config can see them:" say "" say " export PKG_CONFIG_PATH=\"$SYSTEM_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" - say " export PLINTH_USE_SYSTEM_CRYPTO_LIBS=1" say " cabal build all" exit 0 fi @@ -479,7 +447,6 @@ fi touch "$STAMP" link_into_project write_env_file -maintain_cabal_project_local say "" say "Installed libsodium, libsecp256k1 and libblst into the per-user cache:" diff --git a/install.sh b/install.sh index 9f2e546..fc3e8f7 100755 --- a/install.sh +++ b/install.sh @@ -8,9 +8,9 @@ # https://github.com/IntersectMBO/plinth-template. It asks which development # environment you want (Nix, Docker, Demeter, or plain GHC+Cabal), verifies # the required tools are installed, then creates a fresh project directory -# containing just the files that environment needs (the repository itself -# carries the union of all environments) and — for the GHC+Cabal -# environment — optionally installs the Cardano crypto C libraries. +# containing just the files that environment needs (selected from the +# repository's template/ directory) and — for the GHC+Cabal environment — +# optionally installs the Cardano crypto C libraries. # # Non-interactive use (all prompts have flags; --yes accepts defaults): # @@ -23,7 +23,6 @@ # --prefix DIR prefix for --crypto-libs system (default /usr/local) # --dir NAME project directory to create (default: my-plinth-project) # --repo URL template repository (default: official plinth-template) -# --branch NAME fetch NAME instead of the repository's default branch # --from DIR take the template from a local directory instead of # fetching it (offline installs, CI) # --yes, -y don't ask; use defaults for unanswered questions @@ -69,7 +68,12 @@ have() { command -v "$1" >/dev/null 2>&1; } # On a fresh macOS, /usr/bin/git is the Xcode CLT stub: it exists but every # invocation pops the "install developer tools" dialog and fails. Only treat # git as available when it actually runs. -git_works() { have git && git --version >/dev/null 2>&1; } +git_works() { + if ! have git; then + return 1 + fi + git --version >/dev/null 2>&1 +} # The shell does not expand ~ in `read` answers or in flag values that were # quoted; do it ourselves for everything used as a path. @@ -102,11 +106,17 @@ normalize_path() { INTERACTIVE=0 setup_input() { - [ "$ASSUME_YES" = 1 ] && return 0 + if [ "$ASSUME_YES" = 1 ]; then + return 0 + fi # Test hook: pretend no terminal is available. - [ -n "${PLINTH_INSTALL_NO_TTY:-}" ] && return 0 + if [ -n "${PLINTH_INSTALL_NO_TTY:-}" ]; then + return 0 + fi if [ -n "${PLINTH_INSTALL_TTY:-}" ]; then - [ -r "$PLINTH_INSTALL_TTY" ] || die "cannot read from PLINTH_INSTALL_TTY=$PLINTH_INSTALL_TTY" + if [ ! -r "$PLINTH_INSTALL_TTY" ]; then + die "cannot read from PLINTH_INSTALL_TTY=$PLINTH_INSTALL_TTY" + fi exec 3< "$PLINTH_INSTALL_TTY" INTERACTIVE=1 elif [ -t 0 ]; then @@ -127,8 +137,12 @@ ask() { fi printf '%s [%s]: ' "${BOLD}$1${RESET}" "$2" >&2 ans="" - read -r ans <&3 || ans="" - [ -n "$ans" ] || ans="$2" + if ! read -r ans <&3; then + ans="" + fi + if [ -z "$ans" ]; then + ans="$2" + fi printf '%s' "$ans" } @@ -150,7 +164,9 @@ choose() { *[!0-9]*|'') ;; *) if [ "$ans" -ge 1 ] && [ "$ans" -le "$2" ]; then printf '%s' "$ans"; return 0; fi ;; esac - [ "$INTERACTIVE" = 1 ] || die "invalid default answer '$ans'" + if [ "$INTERACTIVE" != 1 ]; then + die "invalid default answer '$ans'" + fi warn "please answer with a number between 1 and $2" done } @@ -183,13 +199,20 @@ installer again from your WSL shell." # --------------------------------------------------------------------------- check_nix() { - have nix || die "nix is not installed. + if ! have nix; then + die "nix is not installed. Follow https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md to install AND configure it (the configuration step sets up IOG's binary caches — without them the first build compiles GHC from source and takes hours), then run this installer again." - feats="$(nix config show experimental-features 2>/dev/null \ - || nix show-config 2>/dev/null | sed -n 's/^experimental-features = //p')" || feats="" + fi + if ! feats="$( + if ! nix config show experimental-features 2>/dev/null; then + nix show-config 2>/dev/null | sed -n 's/^experimental-features = //p' + fi + )"; then + feats="" + fi case " $feats " in *" flakes "*) ok "nix $(nix --version 2>/dev/null | sed 's/^nix (Nix) //') with flakes enabled" ;; *) @@ -210,9 +233,11 @@ check_docker() { say "Codespaces run in GitHub's cloud; nothing to check locally." ;; devcontainer|standalone) - have docker || die "docker is not installed (https://docs.docker.com/get-docker/). + if ! have docker; then + die "docker is not installed (https://docs.docker.com/get-docker/). On Windows, install Docker on the native OS, not inside a VM (https://docs.docker.com/desktop/setup/vm-vdi/)." + fi if docker info >/dev/null 2>&1; then ok "docker daemon is running" else @@ -234,13 +259,21 @@ ver_ge() { a_major=${1%%.*}; a_rest=${1#*.}; a_minor=${a_rest%%.*} b_major=${2%%.*}; b_rest=${2#*.}; b_minor=${b_rest%%.*} case "$a_major$a_minor" in *[!0-9]*) return 1 ;; esac - [ "$a_major" -gt "$b_major" ] || { [ "$a_major" -eq "$b_major" ] && [ "$a_minor" -ge "$b_minor" ]; } + if [ "$a_major" -gt "$b_major" ]; then + return 0 + fi + if [ "$a_major" -eq "$b_major" ] && [ "$a_minor" -ge "$b_minor" ]; then + return 0 + fi + return 1 } check_cabal_env() { - have ghc || die "ghc not found on PATH. Plinth supports GHC 9.6.x and 9.12.x. + if ! have ghc; then + die "ghc not found on PATH. Plinth supports GHC 9.6.x and 9.12.x. Install one with ghcup (https://www.haskell.org/ghcup/): ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7" + fi ghc_version="$(ghc --numeric-version)" case "$ghc_version" in 9.6.*|9.12.*) ok "ghc $ghc_version ($(command -v ghc))" ;; @@ -248,26 +281,35 @@ Install one with ghcup (https://www.haskell.org/ghcup/): Switch with ghcup, e.g.: ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7" ;; esac - have cabal || die "cabal not found on PATH. Install it with ghcup: + if ! have cabal; then + die "cabal not found on PATH. Install it with ghcup: ghcup install cabal latest && ghcup set cabal latest" + fi cabal_version="$(cabal --numeric-version)" - ver_ge "$cabal_version" 3.8 \ - || die "cabal $cabal_version is too old: 3.8 or newer is required (3.12+ recommended). + if ! ver_ge "$cabal_version" 3.8; then + die "cabal $cabal_version is too old: 3.8 or newer is required (3.12+ recommended). Upgrade with: ghcup install cabal latest && ghcup set cabal latest" + fi ok "cabal $cabal_version ($(command -v cabal))" - have pkg-config || die "pkg-config not found on PATH; the project uses it to + if ! have pkg-config; then + die "pkg-config not found on PATH; the project uses it to locate the crypto C libraries. Install it with: macOS: brew install pkgconf Debian/Ubuntu: sudo apt install pkg-config" + fi ok "pkg-config $(pkg-config --version) ($(command -v pkg-config))" - have curl || die "curl is required (to download the crypto C libraries)" + if ! have curl; then + die "curl is required (to download the crypto C libraries)" + fi } # Quiet probe used only to pick a sensible default menu entry. cabal_env_looks_ready() { - have ghc && have cabal && have pkg-config || return 1 + if ! have ghc || ! have cabal || ! have pkg-config; then + return 1 + fi case "$(ghc --numeric-version 2>/dev/null)" in 9.6.*|9.12.*) ;; *) return 1 ;; @@ -292,7 +334,9 @@ mark() { # mark CMD: "(detected)" suffix for menu lines } select_env() { - [ -n "$ENV_CHOICE" ] && return 0 + if [ -n "$ENV_CHOICE" ]; then + return 0 + fi if [ "$INTERACTIVE" = 0 ]; then ENV_CHOICE="$(detected_default_env)" info "No answers available (--yes / no terminal): using environment '$ENV_CHOICE' (override with --env)" @@ -323,8 +367,12 @@ select_env() { } select_docker_mode() { - [ "$ENV_CHOICE" = docker ] || return 0 - [ -n "$DOCKER_MODE" ] && return 0 + if [ "$ENV_CHOICE" != docker ]; then + return 0 + fi + if [ -n "$DOCKER_MODE" ]; then + return 0 + fi if [ "$INTERACTIVE" = 0 ]; then DOCKER_MODE=devcontainer info "Using default docker mode 'devcontainer' (override with --docker-mode)" @@ -377,8 +425,12 @@ explain_crypto_libs() { } select_crypto_mode() { - [ "$ENV_CHOICE" = cabal ] || return 0 - [ -n "$CRYPTO_MODE" ] && return 0 + if [ "$ENV_CHOICE" != cabal ]; then + return 0 + fi + if [ -n "$CRYPTO_MODE" ]; then + return 0 + fi if [ "$INTERACTIVE" = 0 ]; then CRYPTO_MODE=local info "Using default crypto-libs mode 'local' (override with --crypto-libs)" @@ -401,47 +453,49 @@ select_crypto_mode() { } install_crypto_libs() { - [ "$ENV_CHOICE" = cabal ] || return 0 + if [ "$ENV_CHOICE" != cabal ]; then + return 0 + fi case "$CRYPTO_MODE" in local) say "" info "Installing the crypto C libraries (per-user cache, linked into the project)" - (cd "$TARGET_DIR" && ./scripts/get-crypto-libs.sh) \ - || die "crypto library installation failed; you can retry later with - cd $TARGET_DIR && ./scripts/get-crypto-libs.sh" + if ! "$TARGET_DIR/get-crypto-libs.sh"; then + die "crypto library installation failed; you can retry later with: + cd $TARGET_DIR + ./get-crypto-libs.sh" + fi say "" say " ${YELLOW}Note:${RESET} the project only holds a link (dist-newstyle/crypto-libs) to" say " the per-user cache (~/.cache/plinth-crypto-libs, shared by all your" - say " Plinth projects), so 'cabal clean' costs nothing: the next build" - say " re-links instantly, and nothing is ever re-downloaded." + say " Plinth projects), so 'cabal clean' costs nothing: re-running" + say " ./get-crypto-libs.sh re-links instantly, nothing is re-downloaded." ;; system) prefix="$(normalize_path "$(ask "Install prefix" "$CRYPTO_PREFIX")")" say "" info "Installing the crypto C libraries into $prefix" if mkdir -p "$prefix/lib" "$prefix/include" 2>/dev/null && [ -w "$prefix/lib" ]; then - "$TARGET_DIR/scripts/get-crypto-libs.sh" --prefix "$prefix" \ - || die "crypto library installation failed" + if ! "$TARGET_DIR/get-crypto-libs.sh" --prefix "$prefix"; then + die "crypto library installation failed" + fi elif [ "$INTERACTIVE" = 1 ] && confirm "$prefix is not writable; use sudo?" n; then - sudo "$TARGET_DIR/scripts/get-crypto-libs.sh" --prefix "$prefix" \ - || die "crypto library installation failed" + if ! sudo "$TARGET_DIR/get-crypto-libs.sh" --prefix "$prefix"; then + die "crypto library installation failed" + fi else die "$prefix is not writable. Rerun the installation yourself with: - sudo $TARGET_DIR/scripts/get-crypto-libs.sh --prefix $prefix" + sudo $TARGET_DIR/get-crypto-libs.sh --prefix $prefix" fi SYSTEM_CRYPTO_PREFIX="$prefix" ;; skip) say "" - say " Skipping. Install libsodium (VRF-patched), libsecp256k1 and libblst" - say " yourself — see the 'installing with cabal' section of" + say " Skipping. Run ./get-crypto-libs.sh inside the project later, or" + say " install libsodium (VRF-patched), libsecp256k1 and libblst yourself" + say " — see the 'installing with cabal' section of" say " https://developers.cardano.org/docs/get-started/cardano-node/installing-cardano-node/" - say " — or run ./scripts/get-crypto-libs.sh inside the project later." - say "" - say " If you install them system-wide, build with" - say " PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 (and PKG_CONFIG_PATH pointing at" - say " them) so the project uses your libraries instead of downloading" - say " its own." + say " — and make sure pkg-config can find them (PKG_CONFIG_PATH)." ;; esac } @@ -450,62 +504,62 @@ install_crypto_libs() { # Cloning # --------------------------------------------------------------------------- -# fetch_tarball REF DIR: download a github.com tarball into DIR (REF is a -# branch name, or HEAD for the default branch). Extracts into a sibling temp -# dir first so a failure never leaves a half-created DIR behind. +# fetch_tarball DIR: download a github.com tarball of the repository's +# default branch into DIR. Extracts into a sibling temp dir first so a +# failure never leaves a half-created DIR behind. fetch_tarball() { case "$REPO" in https://github.com/*) ;; *) return 1 ;; esac slug="${REPO#https://github.com/}"; slug="${slug%.git}"; slug="${slug%/}" - case "$1" in - HEAD) _ref="HEAD" ;; - *) _ref="refs/heads/$1" ;; - esac - _tmp="$2.download.$$" + _tmp="$1.download.$$" mkdir -p "$_tmp" if curl -fsSL --proto '=https' --tlsv1.2 \ - "https://codeload.github.com/$slug/tar.gz/$_ref" \ + "https://codeload.github.com/$slug/tar.gz/HEAD" \ | tar -xzf - --strip-components=1 -C "$_tmp"; then - mv "$_tmp" "$2" + mv "$_tmp" "$1" else rm -rf "$_tmp" return 1 fi } -# The repository is a single branch carrying the union of every -# environment's files. fetch_source obtains a copy (git clone, tarball, or a -# local directory via --from) and create_project copies just the files the -# chosen environment needs into the new project directory. +# The repository's template/ directory carries the union of every +# environment's project files. fetch_source obtains a copy of the repository +# (git clone, tarball, or a local directory via --from) and create_project +# copies just the template files the chosen environment needs into the new +# project directory. SRC_DIR="" SRC_CLEANUP="" fetch_source() { if [ -n "$FROM_DIR" ]; then - [ -d "$FROM_DIR" ] || die "--from: '$FROM_DIR' is not a directory" + if [ ! -d "$FROM_DIR" ]; then + die "--from: '$FROM_DIR' is not a directory" + fi SRC_DIR="$(cd "$FROM_DIR" && pwd)" - [ -f "$SRC_DIR/plinth-template.cabal" ] \ - || die "--from: '$FROM_DIR' does not look like a plinth-template checkout" + if [ ! -f "$SRC_DIR/template/plinth-template.cabal" ]; then + die "--from: '$FROM_DIR' does not look like a plinth-template checkout" + fi return 0 fi say "" - info "Fetching $REPO${BRANCH_OVERRIDE:+ (branch $BRANCH_OVERRIDE)}" + info "Fetching $REPO" SRC_DIR="$(mktemp -d "${TMPDIR:-/tmp}/plinth-template-src.XXXXXX")" SRC_CLEANUP="$SRC_DIR" rm -rf "$SRC_DIR" if git_works; then - if git clone --quiet --depth 1 ${BRANCH_OVERRIDE:+--branch "$BRANCH_OVERRIDE"} \ - --single-branch "$REPO" "$SRC_DIR"; then + if git clone --quiet --depth 1 --single-branch "$REPO" "$SRC_DIR"; then return 0 fi warn "git clone failed; trying a tarball download instead" fi - fetch_tarball "${BRANCH_OVERRIDE:-HEAD}" "$SRC_DIR" \ - || die "could not fetch $REPO + if ! fetch_tarball "$SRC_DIR"; then + die "could not fetch $REPO (the tarball fallback only works for github.com repositories)" + fi } # list_source_files: relative paths of the template files, one per line. @@ -517,47 +571,31 @@ list_source_files() { && git -C "$SRC_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$SRC_DIR" ls-files --cached --others --exclude-standard else - (cd "$SRC_DIR" && find . -type f ! -name .git ! -path './.git/*' | sed 's|^\./||') + ( + if ! cd "$SRC_DIR"; then exit 1; fi + find . -type f ! -name .git ! -path './.git/*' | sed 's|^\./||' + ) fi } -# include_file ENV PATH: 0 when PATH belongs in an ENV project. +# include_file ENV PATH: 0 when PATH (relative to template/) belongs in an +# ENV project. include_file() { case "$2" in - # never copy git metadata (a copied worktree .git POINTER FILE would make - # `git init` reinitialize the source repository!) - .git|.git/*) return 1 ;; - # replaced by the environment-specific README from dev/readmes/ - README.md) return 1 ;; - # repo machinery, never part of a project - .github/*|install.sh|dev/*) return 1 ;; - SECURITY.md|CODEOWNERS.md|CONTRIBUTING.md|CODE_OF_CONDUCT.md) return 1 ;; - CHANGELOG.md|DESCRIPTION.md) return 1 ;; + # one of these becomes the project README instead (see create_project) + readmes/*) return 1 ;; esac case "$1" in nix|demeter) - case "$2" in .devcontainer/*|scripts/*) return 1 ;; esac ;; + case "$2" in .devcontainer/*) return 1 ;; esac ;; docker) - case "$2" in nix/*|flake.nix|flake.lock|scripts/*) return 1 ;; esac ;; + case "$2" in nix/*|flake.nix|flake.lock) return 1 ;; esac ;; cabal) case "$2" in nix/*|flake.nix|flake.lock|.devcontainer/*) return 1 ;; esac ;; esac return 0 } -# transform_block FILE BEGIN-MARKER END-MARKER [REPLACEMENT-LINE] -# Removes the marked block (markers included), optionally putting a single -# replacement line in its place. Fails loudly when the marker is missing — -# a silently kept block would produce a broken project. -transform_block() { - grep -qF -e "$2" "$1" || die "template drift: marker '$2' not found in $1" - awk -v b="$2" -v e="$3" -v r="${4:-}" ' - $0 == b { skip = 1; if (r != "") print r; next } - $0 == e { skip = 0; next } - !skip { print } - ' "$1" > "$1.tmp" && mv "$1.tmp" "$1" -} - create_project() { # $1 = env, $2 = target dir say "" @@ -566,50 +604,59 @@ create_project() { files_list="$(mktemp "${TMPDIR:-/tmp}/plinth-files.XXXXXX")" list_source_files > "$files_list" mkdir -p "$2" + copied=0 while IFS= read -r f; do - [ -f "$SRC_DIR/$f" ] || continue - include_file "$1" "$f" || continue + # only template/ content goes into projects; everything else in the + # repository (installer, CI, meta files) never does case "$f" in - */*) mkdir -p "$2/${f%/*}" ;; + template/*) rel="${f#template/}" ;; + *) continue ;; + esac + if [ ! -f "$SRC_DIR/$f" ]; then + continue + fi + if ! include_file "$1" "$rel"; then + continue + fi + case "$rel" in + */*) mkdir -p "$2/${rel%/*}" ;; esac - cp -p "$SRC_DIR/$f" "$2/$f" + cp -p "$SRC_DIR/$f" "$2/$rel" + copied=$((copied + 1)) done < "$files_list" rm -f "$files_list" + if [ "$copied" -eq 0 ]; then + die "template drift: no files found under template/" + fi # Environment-specific README case "$1" in cabal) _readme="ghc-cabal" ;; *) _readme="$1" ;; esac - [ -f "$SRC_DIR/dev/readmes/$_readme.md" ] \ - || die "template drift: dev/readmes/$_readme.md missing from the template" - cp "$SRC_DIR/dev/readmes/$_readme.md" "$2/README.md" + if [ ! -f "$SRC_DIR/template/readmes/$_readme.md" ]; then + die "template drift: template/readmes/$_readme.md missing from the template" + fi + cp "$SRC_DIR/template/readmes/$_readme.md" "$2/README.md" - # Environment-specific bits inside shared files - case "$1" in - nix|demeter|docker) - transform_block "$2/cabal.project" \ - "-- BEGIN ghc-cabal only" "-- END ghc-cabal only" - ;; - cabal) - # the block stays; only the installer-facing marker lines go - grep -vF -e "-- BEGIN ghc-cabal only" -e "-- END ghc-cabal only" \ - -e "-- (install.sh removes this whole block for Nix/Docker/Demeter projects)" \ - "$2/cabal.project" > "$2/cabal.project.tmp" \ - && mv "$2/cabal.project.tmp" "$2/cabal.project" - ;; - esac - case "$1" in - nix|demeter) - transform_block "$2/nix/project.nix" \ - " # BEGIN union source only" " # END union source only" \ - " src = lib.cleanSource ../.;" - ;; - esac + # GHC+Cabal projects also carry the crypto-libs installer (it lives next + # to install.sh in the repository, at the project root once copied). + if [ "$1" = cabal ]; then + if [ ! -f "$SRC_DIR/get-crypto-libs.sh" ]; then + die "template drift: get-crypto-libs.sh missing from the repository" + fi + cp -p "$SRC_DIR/get-crypto-libs.sh" "$2/get-crypto-libs.sh" + chmod +x "$2/get-crypto-libs.sh" + fi # Fresh history: this is a template, not a fork. if git_works; then - (cd "$2" && { git init -q -b main 2>/dev/null || git init -q; }) || warn "git init failed" + if ! ( + if ! cd "$2"; then exit 1; fi + if ! git init -q -b main 2>/dev/null; then git init -q; fi + ); then + warn "git init failed" + fi ok "project created in $2 (fresh git history — make your first commit when ready)" else ok "project created in $2" @@ -664,16 +711,25 @@ next_steps() { cabal) say " cd $TARGET_DIR" say " cabal update" - if [ "$CRYPTO_MODE" = system ]; then - say " export PKG_CONFIG_PATH=\"$SYSTEM_CRYPTO_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" - say " export PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 ${DIM}# add both to your shell profile${RESET}" - fi + case "$CRYPTO_MODE" in + local) + say " source dist-newstyle/crypto-libs/env.sh ${DIM}# points pkg-config at the crypto libs${RESET}" + ;; + system) + say " export PKG_CONFIG_PATH=\"$SYSTEM_CRYPTO_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" + say " ${DIM}# add this to your shell profile${RESET}" + ;; + skip) + say " ./get-crypto-libs.sh" + say " source dist-newstyle/crypto-libs/env.sh" + say " ${DIM}# or point PKG_CONFIG_PATH at your own libraries${RESET}" + ;; + esac say " cabal build all" if [ "$CRYPTO_MODE" = local ] || [ "$CRYPTO_MODE" = skip ]; then say "" - say " ${DIM}Run cabal from the project root. If the very first build stops once" - say " with \"Cannot find the program 'pkg-config'\", just re-run it — that" - say " happens only on the first cold build (see README).${RESET}" + say " ${DIM}After 'cabal clean', re-run ./get-crypto-libs.sh (instant: it only" + say " re-links the per-user cache and rewrites env.sh — see README).${RESET}" fi ;; esac @@ -696,7 +752,6 @@ usage() { say " --prefix DIR prefix for --crypto-libs system (default /usr/local)" say " --dir NAME project directory to create (default: my-plinth-project)" say " --repo URL template repository (or set PLINTH_TEMPLATE_REPO)" - say " --branch NAME fetch NAME instead of the repository's default branch" say " --from DIR take the template from a local directory (offline, CI)" say " --yes, -y don't ask; use defaults for unanswered questions" say " --help, -h this text" @@ -711,7 +766,6 @@ main() { CRYPTO_PREFIX="/usr/local" SYSTEM_CRYPTO_PREFIX="" TARGET_DIR="" - BRANCH_OVERRIDE="" FROM_DIR="" ASSUME_YES=0 @@ -729,8 +783,6 @@ main() { --dir=*) TARGET_DIR="${1#--dir=}" ;; --repo) shift; REPO="${1:?--repo needs an argument}" ;; --repo=*) REPO="${1#--repo=}" ;; - --branch) shift; BRANCH_OVERRIDE="${1:?--branch needs an argument}" ;; - --branch=*) BRANCH_OVERRIDE="${1#--branch=}" ;; --from) shift; FROM_DIR="${1:?--from needs an argument}" ;; --from=*) FROM_DIR="${1#--from=}" ;; -y|--yes) ASSUME_YES=1 ;; @@ -760,20 +812,28 @@ main() { say "" say "${BOLD}plinth-template${RESET} — set up a new Plinth smart contract project" say "${DIM}Plinth is Cardano's Haskell-based smart contract language (GHC 9.6/9.12).${RESET}" - [ "$IS_WSL" = 1 ] && say "${DIM}(WSL detected — following the Linux path.)${RESET}" + if [ "$IS_WSL" = 1 ]; then + say "${DIM}(WSL detected — following the Linux path.)${RESET}" + fi if [ "$INTERACTIVE" = 0 ] && [ "$ASSUME_YES" = 0 ] && [ -z "$ENV_CHOICE" ]; then die "no terminal available for questions: pass --env (and other flags), or --yes for defaults" fi select_env - [ -z "$ENV_CHOICE" ] && ENV_CHOICE="$(detected_default_env)" + if [ -z "$ENV_CHOICE" ]; then + ENV_CHOICE="$(detected_default_env)" + fi select_docker_mode - [ "$ENV_CHOICE" = docker ] && [ -z "$DOCKER_MODE" ] && DOCKER_MODE=devcontainer + if [ "$ENV_CHOICE" = docker ] && [ -z "$DOCKER_MODE" ]; then + DOCKER_MODE=devcontainer + fi explain_crypto_libs select_crypto_mode - [ "$ENV_CHOICE" = cabal ] && [ -z "$CRYPTO_MODE" ] && CRYPTO_MODE=local + if [ "$ENV_CHOICE" = cabal ] && [ -z "$CRYPTO_MODE" ]; then + CRYPTO_MODE=local + fi say "" info "Checking prerequisites for the '$ENV_CHOICE' environment" @@ -784,9 +844,13 @@ main() { cabal) check_cabal_env ;; esac - [ -n "$TARGET_DIR" ] || TARGET_DIR="$(ask "Project directory" "my-plinth-project")" + if [ -z "$TARGET_DIR" ]; then + TARGET_DIR="$(ask "Project directory" "my-plinth-project")" + fi TARGET_DIR="$(expand_tilde "$TARGET_DIR")" - [ -e "$TARGET_DIR" ] && die "target directory '$TARGET_DIR' already exists; pick another name (--dir)" + if [ -e "$TARGET_DIR" ]; then + die "target directory '$TARGET_DIR' already exists; pick another name (--dir)" + fi trap 'if [ -n "$SRC_CLEANUP" ]; then rm -rf "$SRC_CLEANUP"; fi' EXIT fetch_source diff --git a/nix/project.nix b/nix/project.nix deleted file mode 100644 index cb74418..0000000 --- a/nix/project.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ inputs, pkgs, lib }: - -let - cabalProject = pkgs.haskell-nix.cabalProject' ( - - { config, pkgs, ... }: - - { - name = "my-project"; - - compiler-nix-name = lib.mkDefault "ghc967"; - - # BEGIN union source only - # (install.sh replaces this whole block with a plain - # `src = lib.cleanSource ../.;` for Nix/Demeter projects, which carry - # neither the pkg-config stanza in cabal.project nor a shim to strip) - src = lib.cleanSourceWith { - src = lib.cleanSource ../.; - filter = path: type: baseNameOf path != "cabal.project.local"; - }; - - cabalProject = builtins.replaceStrings - [ "program-locations\n pkg-config-location: ./scripts/pkg-config" ] - [ "" ] - (builtins.readFile ../cabal.project); - # END union source only - - flake.variants = { - ghc96 = {}; # Alias for the default variant - ghc912 = { compiler-nix-name = "ghc9122"; }; - }; - - inputMap = { "https://chap.intersectmbo.org/" = inputs.CHaP; }; - - modules = [{ - packages = {}; - }]; - } - ); - -in - -cabalProject diff --git a/scripts/pkg-config b/scripts/pkg-config deleted file mode 100755 index ae8fddc..0000000 --- a/scripts/pkg-config +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/sh -# -# pkg-config wrapper for plinth-template. cabal is pointed at it by -# cabal.project (solver) and cabal.project.local (package configure, written -# by get-crypto-libs.sh). It runs scripts/get-crypto-libs.sh — which downloads -# IOG's prebuilt libsodium/libsecp256k1/libblst into dist-newstyle/crypto-libs/ -# on first use and is a fast no-op afterwards — and then answers the query -# with the real pkg-config, restricted to the downloaded libraries. -# -# Inside nix (IN_NIX_SHELL / NIX_BUILD_TOP) or when -# PLINTH_USE_SYSTEM_CRYPTO_LIBS=1, it delegates to the real pkg-config -# untouched and downloads nothing. When cross compiling, set -# PLINTH_CRYPTO_LIBS_PLATFORM to the target platform (arm64-macos, -# x86_64-macos or debian). - -set -eu - -script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) -root=$(CDPATH='' cd -- "$script_dir/.." && pwd) - -real=$( - IFS=: - for d in $PATH; do - [ -x "$d/pkg-config" ] && [ "$d/pkg-config" != "$script_dir/pkg-config" ] \ - && { printf '%s\n' "$d/pkg-config"; break; } - done - # keep the subshell's status 0: under set -e a failed final test would - # otherwise kill the shim before the diagnostic below can print - : -) -if [ -z "$real" ]; then - echo "scripts/pkg-config: no real pkg-config found on PATH (install it, e.g. brew install pkgconf / apt install pkg-config)" >&2 - exit 1 -fi - -if [ "${PLINTH_USE_SYSTEM_CRYPTO_LIBS:-0}" = 1 ] \ - || [ -n "${IN_NIX_SHELL:-}" ] || [ -n "${NIX_BUILD_TOP:-}" ]; then - exec "$real" "$@" -fi - -platform="${PLINTH_CRYPTO_LIBS_PLATFORM:-}" -if [ -z "$platform" ]; then - case "$(uname -s)-$(uname -m)" in - Darwin-arm64) platform=arm64-macos ;; - Darwin-x86_64) platform=x86_64-macos ;; - Linux-*) platform=debian ;; - *) echo "scripts/pkg-config: cannot detect platform; set PLINTH_CRYPTO_LIBS_PLATFORM" >&2; exit 1 ;; - esac -fi - -# stdout must carry only query results; the installer's output goes to stderr. -"$script_dir/get-crypto-libs.sh" --quiet --platform "$platform" 1>&2 - -PKG_CONFIG_LIBDIR="$root/dist-newstyle/crypto-libs/$platform/lib/pkgconfig" \ - exec "$real" "$@" diff --git a/.devcontainer/devcontainer.json b/template/.devcontainer/devcontainer.json similarity index 100% rename from .devcontainer/devcontainer.json rename to template/.devcontainer/devcontainer.json diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..c9add98 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,7 @@ +dist-newstyle +.pre-commit-config.yaml +validator.uplc +blueprint.json +cabal.project.freeze +cabal.project.local +result \ No newline at end of file diff --git a/.hlint.yaml b/template/.hlint.yaml similarity index 100% rename from .hlint.yaml rename to template/.hlint.yaml diff --git a/.stylish-haskell.yaml b/template/.stylish-haskell.yaml similarity index 100% rename from .stylish-haskell.yaml rename to template/.stylish-haskell.yaml diff --git a/template/LICENSE.md b/template/LICENSE.md new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/template/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/template/NOTICE.md b/template/NOTICE.md new file mode 100644 index 0000000..a0a7486 --- /dev/null +++ b/template/NOTICE.md @@ -0,0 +1,11 @@ +Copyright 2024 Intersect MBO + +Licensed under the Apache License, Version 2.0 (the "License”). +You may not use this file except in compliance with the License. +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.txt + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the specific +language governing permissions and limitations under the License. \ No newline at end of file diff --git a/app/GenAuctionValidatorBlueprint.hs b/template/app/GenAuctionValidatorBlueprint.hs similarity index 100% rename from app/GenAuctionValidatorBlueprint.hs rename to template/app/GenAuctionValidatorBlueprint.hs diff --git a/app/GenMintingPolicyBlueprint.hs b/template/app/GenMintingPolicyBlueprint.hs similarity index 100% rename from app/GenMintingPolicyBlueprint.hs rename to template/app/GenMintingPolicyBlueprint.hs diff --git a/cabal.project b/template/cabal.project similarity index 51% rename from cabal.project rename to template/cabal.project index 260eadd..e15e012 100644 --- a/cabal.project +++ b/template/cabal.project @@ -17,17 +17,3 @@ index-state: packages: ./. - --- BEGIN ghc-cabal only --- (install.sh removes this whole block for Nix/Docker/Demeter projects) --- Route pkg-config through the repo shim so that a plain `cabal build` --- works without nix and without system-wide crypto C libraries: on first --- use the shim downloads IOG's prebuilt libsodium/libsecp256k1/libblst --- (sha256- and commit-pinned, cached per user) and answers pkg-config --- queries from them. Inside nix shells, or when --- PLINTH_USE_SYSTEM_CRYPTO_LIBS=1 is set, the shim delegates to the real --- pkg-config. NOTE: the relative path resolves against the directory cabal --- is invoked from, so run cabal from the repository root. -program-locations - pkg-config-location: ./scripts/pkg-config --- END ghc-cabal only diff --git a/flake.lock b/template/flake.lock similarity index 100% rename from flake.lock rename to template/flake.lock diff --git a/flake.nix b/template/flake.nix similarity index 100% rename from flake.nix rename to template/flake.nix diff --git a/nix/outputs.nix b/template/nix/outputs.nix similarity index 100% rename from nix/outputs.nix rename to template/nix/outputs.nix diff --git a/nix/pkgs.nix b/template/nix/pkgs.nix similarity index 100% rename from nix/pkgs.nix rename to template/nix/pkgs.nix diff --git a/template/nix/project.nix b/template/nix/project.nix new file mode 100644 index 0000000..cd7be6c --- /dev/null +++ b/template/nix/project.nix @@ -0,0 +1,30 @@ +{ inputs, pkgs, lib }: + +let + cabalProject = pkgs.haskell-nix.cabalProject' ( + + { config, pkgs, ... }: + + { + name = "my-project"; + + compiler-nix-name = lib.mkDefault "ghc967"; + + src = lib.cleanSource ../.; + + flake.variants = { + ghc96 = {}; # Alias for the default variant + ghc912 = { compiler-nix-name = "ghc9122"; }; + }; + + inputMap = { "https://chap.intersectmbo.org/" = inputs.CHaP; }; + + modules = [{ + packages = {}; + }]; + } + ); + +in + +cabalProject diff --git a/nix/shell.nix b/template/nix/shell.nix similarity index 100% rename from nix/shell.nix rename to template/nix/shell.nix diff --git a/nix/utils.nix b/template/nix/utils.nix similarity index 100% rename from nix/utils.nix rename to template/nix/utils.nix diff --git a/plinth-template.cabal b/template/plinth-template.cabal similarity index 100% rename from plinth-template.cabal rename to template/plinth-template.cabal diff --git a/dev/readmes/demeter.md b/template/readmes/demeter.md similarity index 100% rename from dev/readmes/demeter.md rename to template/readmes/demeter.md diff --git a/dev/readmes/docker.md b/template/readmes/docker.md similarity index 100% rename from dev/readmes/docker.md rename to template/readmes/docker.md diff --git a/template/readmes/ghc-cabal.md b/template/readmes/ghc-cabal.md new file mode 100644 index 0000000..b01052c --- /dev/null +++ b/template/readmes/ghc-cabal.md @@ -0,0 +1,76 @@ +# Plinth Template (GHC + Cabal edition) + +A template for your Plinth smart contract project, built with your own +**GHC and Cabal** — no nix, no docker, and no system-wide C libraries. + +## 1. Prerequisites + +- [ghcup](https://www.haskell.org/ghcup/) with **GHC 9.6.x or 9.12.x** + (e.g. `ghcup install ghc 9.6.7 && ghcup set ghc 9.6.7`) +- **Cabal 3.8+** (`ghcup install cabal latest`) +- a **pkg-config** executable + (macOS: `brew install pkgconf`, Debian/Ubuntu: `apt install pkg-config`) + +## 2. Build + +From the project root: + +``` +./get-crypto-libs.sh # first time only; see below +source dist-newstyle/crypto-libs/env.sh # points pkg-config at the libraries +cabal update +cabal build all +``` + +(If you created this project with `install.sh`, the installer already ran +`./get-crypto-libs.sh` for you.) + +## The crypto C libraries + +`plutus-core` depends (via `cardano-crypto-class`) on three C libraries — +`libsodium` (VRF-patched), `libsecp256k1` and `libblst` — which cabal locates +with pkg-config. `./get-crypto-libs.sh` downloads the prebuilt libraries +published by IOG at +[iohk-nix releases](https://github.com/input-output-hk/iohk-nix/releases) +into a per-user cache (`~/.cache/plinth-crypto-libs`, override with +`PLINTH_CRYPTO_LIBS_HOME`), links them into the project at +`dist-newstyle/crypto-libs/`, and writes +`dist-newstyle/crypto-libs/env.sh`, which puts them on pkg-config's search +path (`PKG_CONFIG_PATH`) — source it in every shell you build from (or add +the export to your shell profile). + +Nothing is installed system-wide; the cache is shared by all your Plinth +projects, and it is the cache path (not the project path) that gets baked +into the packages cabal compiles, so the cabal store stays valid if the +project is moved or deleted. Every downloaded artifact is verified against +sha256 digests and an iohk-nix commit hash pinned inside +`get-crypto-libs.sh`. Only `curl`, `tar` and `shasum`/`sha256sum` are +needed, all of which ship with macOS and Linux. + +To install IOG's prebuilt libraries system-wide instead, run +`./get-crypto-libs.sh --prefix /usr/local` (or any other prefix; see +`--help`) and put `/lib/pkgconfig` on `PKG_CONFIG_PATH`. If you +already have the three libraries installed some other way, skip the script +entirely and make sure pkg-config can find them. + +When cross compiling, set `PLINTH_CRYPTO_LIBS_PLATFORM` to the target +platform (`arm64-macos`, `x86_64-macos` or `debian`) and the script installs +the target's libraries regardless of the build host. + +> NOTE: +> Only a symlink (and `env.sh`) lives under `dist-newstyle/`, so +> `cabal clean` costs nothing: re-running `./get-crypto-libs.sh` restores +> both instantly from the cache (no re-download). + +> NOTE (for Windows users): +> Prebuilt MSYS2 packages (`msys2.*.pkg.tar.zstd`) are published in the same +> release. Install them with `pacman -U` inside an MSYS2 MINGW64 environment +> and point `PKG_CONFIG_PATH` at them — this is exactly what the template's +> Windows CI does, see `.github/ci/build-windows.sh` in the +> [template repository](https://github.com/IntersectMBO/plinth-template). +> Alternatively, use WSL2 and follow the Linux instructions. + +## 3. Run the example application + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/dev/readmes/nix.md b/template/readmes/nix.md similarity index 100% rename from dev/readmes/nix.md rename to template/readmes/nix.md diff --git a/src/AuctionMintingPolicy.hs b/template/src/AuctionMintingPolicy.hs similarity index 100% rename from src/AuctionMintingPolicy.hs rename to template/src/AuctionMintingPolicy.hs diff --git a/src/AuctionValidator.hs b/template/src/AuctionValidator.hs similarity index 100% rename from src/AuctionValidator.hs rename to template/src/AuctionValidator.hs From 242ecf7226b6d60276745f2bc083a0e09b023fb0 Mon Sep 17 00:00:00 2001 From: zeme Date: Tue, 28 Jul 2026 14:31:40 +0200 Subject: [PATCH 3/5] Fix Windows CI: put MSYS2's /usr/bin on PATH, don't resolve bash via PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bash.exe invoked directly runs no login profile, so MSYSTEM is never processed and /usr/bin (pacman, cygpath, sha256sum) is not on PATH — and the bare `bash` in the run line resolved to Git Bash (which has uname but no pacman), producing "pacman not found". Invoke the script with /usr/bin/bash (absolute) and have it prepend /usr/bin itself; drop the MSYS2_PATH_TYPE env, which only login shells read. Co-Authored-By: Claude Fable 5 --- .github/ci/build-windows.sh | 8 +++++++- .github/workflows/ci.yaml | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/ci/build-windows.sh b/.github/ci/build-windows.sh index d022212..fe001da 100755 --- a/.github/ci/build-windows.sh +++ b/.github/ci/build-windows.sh @@ -27,12 +27,18 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)" fail() { echo "build-windows: FAIL: $*" >&2; exit 1; } note() { echo "build-windows: $*"; } +# When bash.exe is invoked directly (as CI does) no login profile runs, so +# MSYSTEM is never processed and MSYS2's own /usr/bin — pacman, cygpath, +# sha256sum — is not on PATH; only the inherited Windows PATH is. Prepend it +# explicitly. (In a regular "MSYS2 MINGW64" shell this is a no-op.) +export PATH="/usr/bin:$PATH" + case "$(uname -s)" in MINGW64_NT*|MSYS_NT*) ;; *) fail "this script must run inside an MSYS2 (MINGW64) environment on Windows" ;; esac if ! command -v pacman >/dev/null 2>&1; then - fail "pacman not found (not an MSYS2 environment?)" + fail "pacman not found — run this from an MSYS2 shell (C:\\msys64), not Git Bash" fi if ! command -v ghc >/dev/null 2>&1; then fail "ghc not on PATH" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f5d7894..d1561db 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -105,13 +105,15 @@ jobs: # install.sh does not support native Windows; this builds template/ # directly with the msys2 crypto libraries (see the script's header). + # The script must run under MSYS2's own bash (absolute path — a bare + # `bash` would resolve to Git Bash, which has no pacman); ghc/cabal + # reach it through the inherited Windows PATH. - name: Build inside MSYS2 (MINGW64) shell: C:/msys64/usr/bin/bash.exe -e '{0}' env: MSYSTEM: MINGW64 - MSYS2_PATH_TYPE: inherit CHERE_INVOKING: 1 - run: bash .github/ci/build-windows.sh + run: /usr/bin/bash .github/ci/build-windows.sh build-nix: name: nix project (nix develop .#${{ matrix.shell }}) From 1d322383292202766a223ec9fb46c2415273afd7 Mon Sep 17 00:00:00 2001 From: zeme Date: Tue, 28 Jul 2026 14:44:04 +0200 Subject: [PATCH 4/5] Remove the native-Windows CI builds: plutus-tx-plugin forbids Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows jobs can never work: plutus-tx-plugin.cabal carries an os-support stanza (if (impl(ghcjs) || os(windows)) buildable: False) imported by its library, so cabal rejects the plugin — and with it any Plinth project — on native Windows at solve time. The stanza was introduced by IntersectMBO/plutus#6299, which moved the long-standing "not buildable on Windows" marking out of their haskell.nix cross-compilation config: the plugin is a GHC compiler plugin (it links the ghc package and runs inside the compiler), which upstream only ever supported on Windows as a non-functional cross-compiled stub (plutus-ghc-stub). Windows users go through WSL2 — which install.sh already enforces and the Linux jobs already cover. The crypto-libs pacman machinery itself worked (the job failed at dependency resolution, after the libraries installed and verified), so this removes build-windows.sh, the ci.yaml job, and the msys2 instructions in the GHC+Cabal README in favor of a clear "use WSL2" note. Co-Authored-By: Claude Fable 5 --- .github/ci/build-windows.sh | 134 ---------------------------------- .github/ci/lint.sh | 1 - .github/ci/run-all-local.sh | 3 - .github/workflows/ci.yaml | 44 ++--------- README.md | 4 +- template/readmes/ghc-cabal.md | 10 +-- 6 files changed, 13 insertions(+), 183 deletions(-) delete mode 100755 .github/ci/build-windows.sh diff --git a/.github/ci/build-windows.sh b/.github/ci/build-windows.sh deleted file mode 100755 index fe001da..0000000 --- a/.github/ci/build-windows.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -# -# Build the template on native Windows: GHC + Cabal with the Cardano crypto -# C libraries installed as MSYS2 (MINGW64) pacman packages from IOG's pinned -# iohk-nix releases — the same releases get-crypto-libs.sh downloads from on -# macOS and Linux. -# -# CI runs this inside the runner's MSYS2 bash (see ci.yaml); locally, run it -# from an "MSYS2 MINGW64" shell on a Windows machine with ghc 9.6.x/9.12.x -# and cabal (>= 3.8) on PATH: -# -# bash .github/ci/build-windows.sh -# -# The libraries are installed into /mingw64/opt/cardano (that is, -# C:\msys64\mingw64\opt\cardano) — pacman-managed, confined to the MSYS2 -# prefix (a throwaway on CI runners). -# -# NOTE: install.sh does not support native Windows (it points users to -# WSL2); this script instead proves the template itself builds with a native -# Windows toolchain, which is what a user gets by copying template/ by hand -# and installing the msys2.* packages as described in the GHC+Cabal README. - -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" - -fail() { echo "build-windows: FAIL: $*" >&2; exit 1; } -note() { echo "build-windows: $*"; } - -# When bash.exe is invoked directly (as CI does) no login profile runs, so -# MSYSTEM is never processed and MSYS2's own /usr/bin — pacman, cygpath, -# sha256sum — is not on PATH; only the inherited Windows PATH is. Prepend it -# explicitly. (In a regular "MSYS2 MINGW64" shell this is a no-op.) -export PATH="/usr/bin:$PATH" - -case "$(uname -s)" in - MINGW64_NT*|MSYS_NT*) ;; - *) fail "this script must run inside an MSYS2 (MINGW64) environment on Windows" ;; -esac -if ! command -v pacman >/dev/null 2>&1; then - fail "pacman not found — run this from an MSYS2 shell (C:\\msys64), not Git Bash" -fi -if ! command -v ghc >/dev/null 2>&1; then - fail "ghc not on PATH" -fi -if ! command -v cabal >/dev/null 2>&1; then - fail "cabal not on PATH" -fi -GHC_VERSION="$(ghc --numeric-version)" -case "$GHC_VERSION" in - 9.6.*|9.12.*) note "ghc $GHC_VERSION ($(command -v ghc))" ;; - *) fail "unsupported ghc $GHC_VERSION (need 9.6.x or 9.12.x)" ;; -esac -note "cabal $(cabal --numeric-version) ($(command -v cabal))" - -# -------------------------------------------------------------------------- -# Crypto C libraries, as sha256-pinned MSYS2 pacman packages from iohk-nix -# releases. -# -# libblst deliberately comes from the older v2.2 release: the newer build -# trips GHC's runtime linker on Windows ("duplicate definition for symbol -# __blst_platform_cap") — see input-output-hk/actions/base, which carries -# the same workaround. -# -------------------------------------------------------------------------- - -RELEASES="https://github.com/input-output-hk/iohk-nix/releases/download" -ASSETS=" -v3.1/msys2.libsodium.pkg.tar.zstd c9ed5b531309369f92d67e6f7c1e003d3c0c96d777f66c9d8a8c1aeca6d8ef4d -v3.1/msys2.libsecp256k1.pkg.tar.zstd b1cf83dce1a38241491209ac0ff75c92b06a05012eecb81d713373bc0d861a40 -v2.2/msys2.libblst.pkg.tar.zstd f03037ff3384fed4af70ecaabe890ddbd7ff766e871f76d0e9a48e42d818282c -" - -# cabal locates the libraries with pkg-config; the mingw-w64 pkg-config -# build is the one variant that copes with Windows-style PKG_CONFIG_PATH -# values (see input-output-hk/actions/base for the sad full story). -note "installing mingw-w64-x86_64-pkg-config (pacman)..." -pacman -S --noconfirm --needed mingw-w64-x86_64-pkg-config - -DOWNLOADS="$(mktemp -d)" -trap 'rm -rf "$DOWNLOADS"' EXIT -while read -r asset sha; do - if [ -z "$asset" ]; then - continue - fi - file="$DOWNLOADS/${asset##*/}" - note "downloading $asset ..." - curl -fsSL --retry 3 -o "$file" "$RELEASES/$asset" - if ! echo "$sha $file" | sha256sum -c - >/dev/null; then - fail "sha256 mismatch for $asset" - fi - note "verified $asset (sha256 OK)" -done < both instantly from the cache (no re-download). > NOTE (for Windows users): -> Prebuilt MSYS2 packages (`msys2.*.pkg.tar.zstd`) are published in the same -> release. Install them with `pacman -U` inside an MSYS2 MINGW64 environment -> and point `PKG_CONFIG_PATH` at them — this is exactly what the template's -> Windows CI does, see `.github/ci/build-windows.sh` in the -> [template repository](https://github.com/IntersectMBO/plinth-template). -> Alternatively, use WSL2 and follow the Linux instructions. +> Plinth does not work on native Windows: `plutus-tx-plugin` declares +> `buildable: False` there. Use +> [WSL2](https://learn.microsoft.com/windows/wsl/install) and follow the +> Linux instructions. ## 3. Run the example application From 3fdfe2919299bfd562c0e45980dd3496f57e3948 Mon Sep 17 00:00:00 2001 From: zeme Date: Tue, 28 Jul 2026 15:24:11 +0200 Subject: [PATCH 5/5] Fix Linux runtime library lookup, docker cleanup, and CI token permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CI failures, three distinct causes: * env.sh now also exports LD_LIBRARY_PATH. The .pc files carry no -rpath, so on Linux an executable linked against the downloaded crypto libs records a bare "libblst.so" and ld.so cannot find it in the per-user cache: running the generated blueprint died with "libblst.so: cannot open shared object file". macOS was unaffected because the install names rewritten by get-crypto-libs.sh are absolute. This is a user-facing bug, not just a CI one — the GHC+Cabal README now explains what env.sh sets and why. * build-docker.sh no longer fails after a successful build. The devx container builds as root, leaving root-owned dist-newstyle files on the host, so the EXIT trap's rm failed and turned a green build red ("build-docker: SUCCESS" is in the log, followed by rm: Permission denied). Cleanup now hands the files back through a throwaway container and warns instead of failing if it still cannot remove them. * ci.yaml declares `permissions: contents: read` (CodeQL: workflow does not limit GITHUB_TOKEN). No job in it writes to the repository. Co-Authored-By: Claude Fable 5 --- .github/ci/build-docker.sh | 16 ++++++++++++++-- .github/workflows/ci.yaml | 4 ++++ get-crypto-libs.sh | 13 ++++++++++++- template/readmes/ghc-cabal.md | 10 +++++++--- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/ci/build-docker.sh b/.github/ci/build-docker.sh index fee174b..7d80666 100755 --- a/.github/ci/build-docker.sh +++ b/.github/ci/build-docker.sh @@ -36,11 +36,23 @@ if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then fi WORK="$(mktemp -d "${TMPDIR:-/tmp}/plinth-build-docker.XXXXXX")" + +# The container builds as root, so dist-newstyle/ ends up owned by root on +# the host and a plain rm cannot remove it. Hand the files back through a +# throwaway container before removing them, and never let a cleanup problem +# fail an otherwise successful build. cleanup() { if [ "${PLINTH_CI_KEEP:-0}" = 1 ]; then echo "build-docker: scratch dir kept: $WORK" - else - rm -rf "$WORK" + return 0 + fi + if rm -rf "$WORK" 2>/dev/null; then + return 0 + fi + docker run --rm -v "$WORK:/scratch" "$IMAGE" \ + chown -R "$(id -u):$(id -g)" /scratch >/dev/null 2>&1 || true + if ! rm -rf "$WORK"; then + echo "build-docker: warning: could not remove the scratch dir $WORK" >&2 fi } trap cleanup EXIT diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 78106ed..0f3b1ae 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,10 @@ on: schedule: - cron: "0 4 * * 1" +# Every job only ever reads the repository; nothing here writes to it. +permissions: + contents: read + concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true diff --git a/get-crypto-libs.sh b/get-crypto-libs.sh index 998f195..cc9b5f6 100755 --- a/get-crypto-libs.sh +++ b/get-crypto-libs.sh @@ -126,11 +126,19 @@ REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" # next run — without re-downloading. # --------------------------------------------------------------------------- +# PKG_CONFIG_PATH lets cabal FIND the libraries at build time. +# LD_LIBRARY_PATH lets the dynamic loader find them at RUN time: the .pc +# files carry no -rpath, so on Linux an executable linked against them +# records a bare "libblst.so" and ld.so would not look inside the cache. +# (On macOS this is not needed — the install names rewritten below are +# absolute — and dyld ignores LD_LIBRARY_PATH anyway; exporting it is +# harmless and keeps the file identical across platforms.) write_env_file() { mkdir -p "$(dirname "$ENV_FILE")" cat > "$ENV_FILE" <