diff --git a/nix/kontrol-node/build-systems-overlay.nix b/nix/kontrol-node/build-systems-overlay.nix index d27c1151..7567b17f 100644 --- a/nix/kontrol-node/build-systems-overlay.nix +++ b/nix/kontrol-node/build-systems-overlay.nix @@ -10,8 +10,11 @@ let # `setuptools-scm[toml]` in pyproject.toml would be written as # `foo.setuptools-scm = [ "toml" ]` in Nix buildSystemOverrides = { - # add dependencies here, e.g.: - # pyperclip.setuptools = [ ]; + # git dependencies are built from source, but uv.lock does not record their build systems + kevm-pyk.hatchling = [ ]; + kontrol.hatchling = [ ]; + # sdist-only on PyPI + typing.setuptools = [ ]; }; in mapAttrs ( diff --git a/package/release.sh b/package/release.sh new file mode 100755 index 00000000..7be3fdd2 --- /dev/null +++ b/package/release.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# +# Interactive, local replacement for the former `release.yml` / `update.yml` GitHub workflows. +# +# Everything is built from the local checkout. As with any flake, Nix ignores untracked files. +# +# Steps (each one asks for confirmation before running): +# 1. Create GitHub release `v` targeting HEAD. +# 2. Build `kontrol-node` and push its full build closure to the `k-framework` Cachix cache. +# 3. Publish and pin `kontrol-node` in the `k-framework-binary` Cachix cache (what `kup install` uses). +# +# Steps 1 and 3 reference HEAD on GitHub, so they are skipped unless the working tree is clean and +# HEAD is pushed. Step 2 also runs on uncommitted changes. +# +# Credentials are read from the environment, or prompted for (input hidden) when missing: +# GH_TOKEN GitHub token for `gh` (only needed if `gh auth status` fails) +# CACHIX_SOURCE_TOKEN Cachix auth token for `k-framework` +# CACHIX_BINARY_TOKEN Cachix auth token for `k-framework-binary` +# +# Do NOT run this script with `bash -x`: tracing would print the credentials. + +set -euo pipefail + +REPO="runtimeverification/kontrol-node" +PACKAGE="${PACKAGE:-kontrol-node}" +KEEP_DAYS="${KEEP_DAYS:-180}" +SOURCE_CACHE="k-framework" +BINARY_CACHE="k-framework-binary" + +usage() { + cat <&2 ; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2 ; } +fatal() { echo -e "\033[1;31m[FATAL]\033[0m $*" >&2 ; exit 1 ; } + +ASSUME_YES=false + +confirm() { + local prompt="$1" answer + if ${ASSUME_YES}; then return 0; fi + read -rp "${prompt} [Y/n] " answer &2 + [[ -n "${value}" ]] || fatal "${var} must not be empty." + printf -v "${var}" '%s' "${value}" +} + +nix_() { nix --extra-experimental-features 'nix-command flakes' "$@" ; } + +# Puts tool $1 on PATH, building flake $2 if it is not installed. +ensure_tool() { + local tool="$1" flake="$2" out + command -v "${tool}" &>/dev/null && return + notif "${tool} not found, building ${flake} ..." + out="$(nix_ build "${flake}" --no-link --print-out-paths | head -n1)" + export PATH="${out}/bin:${PATH}" +} + +# --- Steps ------------------------------------------------------------------------------------ + +step_github_release() { + if ! gh auth status &>/dev/null; then + require_secret GH_TOKEN "GitHub token" + fi + local existing + if existing="$(GH_TOKEN="${GH_TOKEN:-}" gh release view "${TAG}" --repo "${REPO}" --json tagName --jq .tagName 2>/dev/null)"; then + warn "Release ${existing} already exists at HEAD, skipping." + return + fi + GH_TOKEN="${GH_TOKEN:-}" gh release create "${TAG}" --repo "${REPO}" --target "${REV}" --title "${TAG}" --notes '' + notif "Created release ${TAG}." +} + +step_source_cache() { + require_secret CACHIX_SOURCE_TOKEN "Cachix token for ${SOURCE_CACHE}" + ensure_tool cachix nixpkgs#cachix + notif "Building ${FLAKE_REF} ..." + nix_ build "${FLAKE_REF}" --no-link --print-build-logs + local drv + drv="$(nix_ path-info --derivation "${FLAKE_REF}")" + notif "Pushing build closure of ${drv} to ${SOURCE_CACHE} ..." + nix-store --query --requisites --include-outputs "${drv}" \ + | CACHIX_AUTH_TOKEN="${CACHIX_SOURCE_TOKEN}" cachix push "${SOURCE_CACHE}" +} + +step_binary_cache() { + require_secret CACHIX_BINARY_TOKEN "Cachix token for ${BINARY_CACHE}" + ensure_tool cachix nixpkgs#cachix + ensure_tool kup github:runtimeverification/kup + # kup builds the local directory and pins the result under `github:/#`. + notif "Publishing ${FLAKE_REF} to ${BINARY_CACHE} (keep ${KEEP_DAYS} days) ..." + CACHIX_AUTH_TOKEN="${CACHIX_BINARY_TOKEN}" kup publish --keep-days "${KEEP_DAYS}" "${BINARY_CACHE}" "${FLAKE_REF}" +} + +# --- Main ------------------------------------------------------------------------------------- + +while [[ $# -gt 0 ]]; do + case "$1" in + --yes|-y) ASSUME_YES=true ; shift ;; + -h|--help) usage ; exit 0 ;; + *) usage ; fatal "Unknown argument: $1" ;; + esac +done + +for tool in git gh nix nix-store; do + command -v "${tool}" &>/dev/null || fatal "Required tool not found: ${tool}" +done + +cd "$(git rev-parse --show-toplevel)" + +notif "Fetching origin ..." +git fetch --quiet origin + +REV="$(git rev-parse HEAD)" +VERSION="$(tr -d '[:space:]' < package/version)" +TAG="v${VERSION}" +FLAKE_REF="${PWD}#${PACKAGE}" +SYSTEM="$(nix_ eval --impure --raw --expr builtins.currentSystem)" + +DIRTY=false +STATE="clean" +if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then + DIRTY=true + STATE="uncommitted changes (included in the build)" +fi +PUSHED=true +if [[ -z "$(git branch --remotes --contains "${REV}")" ]]; then + PUSHED=false + STATE="${STATE}, HEAD not pushed" +fi + +# Consumers resolve a version through its tag, so publishing any commit other than the tagged one +# under this version would be inconsistent. An annotated tag's commit is its peeled `^{}` entry. +TAG_REV="$(git ls-remote --tags origin \ + | awk -v ref="refs/tags/${TAG}" '$2 == ref {c = $1} $2 == ref "^{}" {p = $1} END {print (p ? p : c)}')" +if [[ -z "${TAG_REV}" ]]; then + TAG_STATE="new" +elif [[ "${TAG_REV}" == "${REV}" ]]; then + TAG_STATE="exists at HEAD" +else + fatal "Tag ${TAG} already points at ${TAG_REV}, not HEAD (${REV}). Bump package/version or check out ${TAG}." +fi + +cat >&2 <|| +STEPS=( + "step_github_release|true|Create GitHub release ${TAG}" + "step_source_cache|false|Push build closure to the ${SOURCE_CACHE} cache" + "step_binary_cache|true|Publish ${PACKAGE} to the ${BINARY_CACHE} cache (kup)" +) + +for entry in "${STEPS[@]}"; do + IFS='|' read -r fn needs_published title <<< "${entry}" + echo >&2 + if ${needs_published} && { ${DIRTY} || ! ${PUSHED}; }; then + warn "Skipped: ${title} (needs a clean working tree with HEAD pushed to GitHub)" + elif confirm "${title}?"; then + notif "${title}" + "${fn}" + else + warn "Skipped: ${title}" + fi +done + +echo >&2 +notif "Done." diff --git a/package/version b/package/version index a2d633db..fbde3d5a 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.61 +0.1.62 diff --git a/pyproject.toml b/pyproject.toml index 7f5f3534..f7331280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "kontrol-node" -version = "0.1.60" +version = "0.1.62" description = "A local testnet node powered by KEVM" readme = "README.md" requires-python = ">=3.10,<4" diff --git a/src/kontrol_node/__init__.py b/src/kontrol_node/__init__.py index ce29fa19..d61fd559 100644 --- a/src/kontrol_node/__init__.py +++ b/src/kontrol_node/__init__.py @@ -5,4 +5,4 @@ if TYPE_CHECKING: from typing import Final -VERSION: Final = '0.1.60' +VERSION: Final = '0.1.62' diff --git a/uv.lock b/uv.lock index e9d56dc0..e3abc8ca 100644 --- a/uv.lock +++ b/uv.lock @@ -817,7 +817,7 @@ dependencies = [ [[package]] name = "kontrol-node" -version = "0.1.60" +version = "0.1.62" source = { editable = "." } dependencies = [ { name = "kontrol" },