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" <