diff --git a/.github/ci/build-docker.sh b/.github/ci/build-docker.sh new file mode 100755 index 0000000..7d80666 --- /dev/null +++ b/.github/ci/build-docker.sh @@ -0,0 +1,102 @@ +#!/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")" + +# 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" + 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 + +PROJECT="$WORK/project" +if [ -z "$TREE" ]; then + 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 +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 ..." +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/.github/ci/build-ghc-cabal.sh b/.github/ci/build-ghc-cabal.sh new file mode 100755 index 0000000..7895557 --- /dev/null +++ b/.github/ci/build-ghc-cabal.sh @@ -0,0 +1,152 @@ +#!/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 --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. +# +# 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) +# -------------------------------------------------------------------------- + +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))" ;; + *) 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 + 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 +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" + 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 + +note "building (cabal build all)..." +"${cabal[@]}" build all + +# -------------------------------------------------------------------------- +# Blueprint generation + provenance of the linked crypto libraries +# -------------------------------------------------------------------------- + +note "generating the example blueprint..." +"${cabal[@]}" run -v0 exe:gen-auction-validator-blueprint -- "$WORK/blueprint.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 +# 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")" + 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: +$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/.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/bump-plutus-version.sh b/.github/ci/bump-plutus-version.sh new file mode 100755 index 0000000..e54e463 --- /dev/null +++ b/.github/ci/bump-plutus-version.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# +# Bump the plutus version across the template — the logic behind the +# bump-plutus-version workflow, runnable (and testable) locally: +# +# .github/ci/bump-plutus-version.sh 1.66.0.0 +# +# Inside template/ it updates: +# * flake.lock — refreshes the CHaP and hackage inputs +# * cabal.project — index-states derived from the refreshed pins +# * plinth-template.cabal — plutus-* bounds set to ^>=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/.github/ci/lint.sh b/.github/ci/lint.sh new file mode 100755 index 0000000..2fa5e85 --- /dev/null +++ b/.github/ci/lint.sh @@ -0,0 +1,50 @@ +#!/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 + 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/bump-plutus-version.sh + .github/ci/run-all-local.sh + .github/ci/test-blueprint-parity.sh +) + +for f in "${SCRIPTS[@]}"; do + 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" ;; + 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/.github/ci/run-all-local.sh b/.github/ci/run-all-local.sh new file mode 100755 index 0000000..fa0e896 --- /dev/null +++ b/.github/ci/run-all-local.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# +# Run the full test suite locally — the same scripts the GitHub workflows +# run, without the Actions layer. +# +# .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. + +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +if ! cd "$ROOT"; then + exit 1 +fi + +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" .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" + 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" .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" .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" .github/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/.github/ci/test-blueprint-parity.sh b/.github/ci/test-blueprint-parity.sh new file mode 100755 index 0000000..09b2ac5 --- /dev/null +++ b/.github/ci/test-blueprint-parity.sh @@ -0,0 +1,390 @@ +#!/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 (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; 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: +# .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. +# PARITY_ALLOW_GHC_DIVERGENCE=1 Downgrade the cross-GHC comparison to a +# warning (see compare_outputs). + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +# 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" +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 + if [ "$2" != "$3" ]; then + fail "$1: expected '$2', got '$3'" + fi + 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 (crypto libs downloaded by get-crypto-libs.sh) +# -------------------------------------------------------------------------- + +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 get-crypto-libs.sh)" + + # 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" + + 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 + 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. 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 + # 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 "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" \ + "$(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 (crypto libs must come from the nix shell) +# -------------------------------------------------------------------------- + +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)" + + if ! command -v nix >/dev/null 2>&1; then + fail "nix not found" + fi + + # 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" +} + +inner_nix() { + # Runs INSIDE the nix shell. + local scenario="$1" ghc_version="$2" + local builddir="$TESTDIR/$scenario" + local out="$BLUEPRINTS/$scenario.json" + + # 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 + 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)" + + # 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" + 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" \ + "$(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" + 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 + # 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, 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//./}" +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/.github/ci/test-install.sh b/.github/ci/test-install.sh new file mode 100755 index 0000000..92c82b5 --- /dev/null +++ b/.github/ci/test-install.sh @@ -0,0 +1,468 @@ +#!/usr/bin/env bash +# +# 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) +# * 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` +# +# 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 ==" +SRC="$WORK/fixture-repo" +git init -q "$SRC" +git ls-files --cached --others --exclude-standard | while IFS= read -r f; do + 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" +# 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)" + +# -------------------------------------------------------------------------- +# 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 + if ! p="$(command -v "$t" 2>/dev/null)"; then + continue + fi + 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() { + 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() { + 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" + +# -------------------------------------------------------------------------- +# 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 + 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 +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" + if [ ! -e "$WORK/out-nonix" ]; then + pass "nothing created" + else + fail "created despite failed checks" + fi +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" + if [ ! -e "$WORK/out-oldghc" ]; then + pass "nothing created" + else + fail "created despite failed checks" + fi +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) ==" +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 + # 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 + if [ -f "$pc" ]; then + found="$pc" + fi + 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/.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-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/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..0f3b1ae --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,116 @@ +# 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. +# +# There is no native-Windows job on purpose: plutus-tx-plugin declares +# `buildable: False` on Windows (see its os-support stanza), so no Plinth +# project can build there. Windows users go through WSL2 (which the Linux +# jobs cover); install.sh enforces this. +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + 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 + +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-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/.gitignore b/.gitignore index f5e9200..9ecc676 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ dist-newstyle -.pre-commit-config.yaml -validator.uplc -cabal.project.freeze -result \ No newline at end of file +cabal.project.local +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 49f8608..7ab249e 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,81 @@ -# 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 + get-crypto-libs.sh | + +Each project comes with a README covering just that setup. You can also skip +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 +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) +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 + +- [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. +- [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: ``` - gh repo create my-project --private --template IntersectMBO/plinth-template + .github/ci/run-all-local.sh # everything + PLINTH_SKIP_HEAVY=1 .github/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.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` | + | `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. + There is deliberately no native-Windows job: `plutus-tx-plugin` declares + `buildable: False` on Windows, so Plinth projects only work there through + WSL2 (covered by the Linux jobs). diff --git a/get-crypto-libs.sh b/get-crypto-libs.sh new file mode 100755 index 0000000..cc9b5f6 --- /dev/null +++ b/get-crypto-libs.sh @@ -0,0 +1,470 @@ +#!/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" +# +# 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() { if [ "$QUIET" != 1 ]; then echo "$@"; fi; } +die() { echo "get-crypto-libs: ERROR: $*" >&2; exit 1; } + +# 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 heals on the +# 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" </dev/null || true + rm -rf "$LINK" + fi + rm -f "$LINK" + ln -s "$PREFIX" "$LINK" +} + +# -------------------------------------------------------------------------- +# 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 + 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 + +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 + 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 ..." + 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")" + if [ "$actual" != "$expected" ]; then + die "sha256 mismatch for $asset: expected $expected, got $actual" + fi + say "Verified $asset (sha256 OK)" +} + +if [ -d "$PREFIX" ]; then + chmod -R u+w "$PREFIX" +fi +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")" +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)" + +# -------------------------------------------------------------------------- +# 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. + 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 + "$BSDTAR" -xf "$tmp/Payload" -C "$PREFIX" + 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) +# -------------------------------------------------------------------------- + +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" +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 + 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 + 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; + # libblst.dylib is shipped unversioned already). + ( + cd "$PREFIX/lib" + 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) + ( + 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/" + if [ -d "$PREFIX/include" ]; then + cp -R "$PREFIX/include/." "$SYSTEM_PREFIX/include/" + fi +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 + 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")" + if [ "$p" != "$FINAL_PREFIX" ]; then + die "$lib resolves to unexpected prefix: $p" + fi + 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, unless" + say "$SYSTEM_PREFIX/lib is already in the loader's search path, that the" + say "executables you build can load them at run time):" + say "" + say " export PKG_CONFIG_PATH=\"$SYSTEM_PREFIX/lib/pkgconfig\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}\"" + say " export LD_LIBRARY_PATH=\"$SYSTEM_PREFIX/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}\" # Linux" + say " cabal build all" + exit 0 +fi + +touch "$STAMP" +link_into_project +write_env_file + +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/install.sh b/install.sh new file mode 100755 index 0000000..fc3e8f7 --- /dev/null +++ b/install.sh @@ -0,0 +1,862 @@ +#!/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 (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): +# +# 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) +# --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() { + 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. +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() { + if [ "$ASSUME_YES" = 1 ]; then + return 0 + fi + # Test hook: pretend no terminal is available. + if [ -n "${PLINTH_INSTALL_NO_TTY:-}" ]; then + return 0 + fi + if [ -n "${PLINTH_INSTALL_TTY:-}" ]; then + 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 + 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="" + if ! read -r ans <&3; then + ans="" + fi + if [ -z "$ans" ]; then + ans="$2" + fi + 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 + if [ "$INTERACTIVE" != 1 ]; then + die "invalid default answer '$ans'" + fi + 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() { + 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." + 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" ;; + *) + 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) + 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 + 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 + 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() { + 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))" ;; + *) 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 + + 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)" + 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))" + + 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))" + + 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() { + 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 ;; + 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() { + 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)" + 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() { + 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)" + 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() { + 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)" + 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() { + 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)" + 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: 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 + 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 + 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/get-crypto-libs.sh --prefix $prefix" + fi + SYSTEM_CRYPTO_PREFIX="$prefix" + ;; + skip) + say "" + 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 " — and make sure pkg-config can find them (PKG_CONFIG_PATH)." + ;; + esac +} + +# --------------------------------------------------------------------------- +# Cloning +# --------------------------------------------------------------------------- + +# 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%/}" + _tmp="$1.download.$$" + mkdir -p "$_tmp" + if curl -fsSL --proto '=https' --tlsv1.2 \ + "https://codeload.github.com/$slug/tar.gz/HEAD" \ + | tar -xzf - --strip-components=1 -C "$_tmp"; then + mv "$_tmp" "$1" + else + rm -rf "$_tmp" + return 1 + fi +} + +# 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 + if [ ! -d "$FROM_DIR" ]; then + die "--from: '$FROM_DIR' is not a directory" + fi + SRC_DIR="$(cd "$FROM_DIR" && pwd)" + 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" + 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 --single-branch "$REPO" "$SRC_DIR"; then + return 0 + fi + warn "git clone failed; trying a tarball download instead" + fi + 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. +# 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 + ( + 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 (relative to template/) belongs in an +# ENV project. +include_file() { + case "$2" in + # one of these becomes the project README instead (see create_project) + readmes/*) return 1 ;; + esac + case "$1" in + nix|demeter) + case "$2" in .devcontainer/*) return 1 ;; esac ;; + docker) + 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 +} + +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" + copied=0 + while IFS= read -r f; do + # only template/ content goes into projects; everything else in the + # repository (installer, CI, meta files) never does + case "$f" in + 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/$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 + 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" + + # 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 + 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" + 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" + 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}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 + 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 " --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="" + 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=}" ;; + --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}" + 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 + if [ -z "$ENV_CHOICE" ]; then + ENV_CHOICE="$(detected_default_env)" + fi + select_docker_mode + if [ "$ENV_CHOICE" = docker ] && [ -z "$DOCKER_MODE" ]; then + DOCKER_MODE=devcontainer + fi + + explain_crypto_libs + select_crypto_mode + if [ "$ENV_CHOICE" = cabal ] && [ -z "$CRYPTO_MODE" ]; then + CRYPTO_MODE=local + fi + + 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 + + if [ -z "$TARGET_DIR" ]; then + TARGET_DIR="$(ask "Project directory" "my-plinth-project")" + fi + TARGET_DIR="$(expand_tilde "$TARGET_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 + create_project "$ENV_CHOICE" "$TARGET_DIR" + install_crypto_libs + next_steps +} + +main "$@" 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 100% rename from cabal.project rename to template/cabal.project 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 73% rename from nix/outputs.nix rename to template/nix/outputs.nix index 914a23d..5ff6ecc 100644 --- a/nix/outputs.nix +++ b/template/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/pkgs.nix b/template/nix/pkgs.nix similarity index 100% rename from nix/pkgs.nix rename to template/nix/pkgs.nix diff --git a/nix/project.nix b/template/nix/project.nix similarity index 69% rename from nix/project.nix rename to template/nix/project.nix index c8fb03d..cd7be6c 100644 --- a/nix/project.nix +++ b/template/nix/project.nix @@ -2,18 +2,19 @@ let cabalProject = pkgs.haskell-nix.cabalProject' ( - + { config, pkgs, ... }: { name = "my-project"; - compiler-nix-name = lib.mkDefault "ghc966"; + compiler-nix-name = lib.mkDefault "ghc967"; src = lib.cleanSource ../.; 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/template/nix/shell.nix similarity index 68% rename from nix/shell.nix rename to template/nix/shell.nix index 26fb4a6..c3102b8 100644 --- a/nix/shell.nix +++ b/template/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/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/template/readmes/demeter.md b/template/readmes/demeter.md new file mode 100644 index 0000000..1dc3de4 --- /dev/null +++ b/template/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/template/readmes/docker.md b/template/readmes/docker.md new file mode 100644 index 0000000..99b7f18 --- /dev/null +++ b/template/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/template/readmes/ghc-cabal.md b/template/readmes/ghc-cabal.md new file mode 100644 index 0000000..1912e46 --- /dev/null +++ b/template/readmes/ghc-cabal.md @@ -0,0 +1,78 @@ +# 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`. Source that file in every shell you +build from (or add its exports to your shell profile): it puts the +libraries on pkg-config's search path (`PKG_CONFIG_PATH`) so cabal can find +them at build time, and on the dynamic loader's path (`LD_LIBRARY_PATH`) so +the executables you build can load them at run time — on Linux the latter +is required, otherwise running them fails with +*"libblst.so: cannot open shared object file"*. + +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): +> 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 + +Read [Example: An Auction Smart Contract](https://plutus.cardano.intersectmbo.org/docs/category/example-an-auction-smart-contract) +to get started. diff --git a/template/readmes/nix.md b/template/readmes/nix.md new file mode 100644 index 0000000..d943555 --- /dev/null +++ b/template/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/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