From d68a66e53791501065265a4b3b665fcd811c4ee0 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Sun, 3 May 2026 20:23:19 -0400 Subject: [PATCH 1/8] Prepare Theme for Local Build Rendering Render external requires entries as direct links without forcing Zola to resolve them as local pages. This supports targeted --only rendering, where cross-repo proposal dependencies can be rewritten to external URLs instead of local pages. Move the theme demo homepage badges into front matter, a homepage_badges macro, and theme-owned styles so the demo homepage stays markdown-friendly. Declare Zola 0.22.1 as the minimum supported version, enable bottom footnotes, and update markdown highlighting to the class-based config with github-light/dark themes and ABNF/CSL-JSON grammars. --- config.toml | 18 +++++++++++------- content/_index.md | 16 +++++++++------- sass/assets/css/style.scss | 28 ++++++++++++++++++++++++++++ templates/eip.html | 6 ++++++ templates/index.html | 12 ++++++++++++ templates/macros.html | 24 ++++++++++++++++++++++-- theme.toml | 4 ++-- 7 files changed, 90 insertions(+), 18 deletions(-) diff --git a/config.toml b/config.toml index 81e0f5e..a160d5f 100644 --- a/config.toml +++ b/config.toml @@ -22,15 +22,19 @@ generate_feeds = true internal_level = "warn" [markdown] -# Whether to do syntax highlighting -# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola -highlight_code = true +bottom_footnotes = true -highlight_theme = "css" +[markdown.highlighting] +error_on_missing_language = true -highlight_themes_css = [ - { theme = "base16-ocean-dark", filename = "assets/css/syntax-theme-dark.css" }, - { theme = "base16-ocean-light", filename = "assets/css/syntax-theme-light.css" }, +style = "class" + +light_theme = "github-light" +dark_theme = "github-dark" + +extra_grammars = [ + "syntaxes/abnf/ABNF.json", + "syntaxes/csl-json/CSL-JSON.json", ] [extra.github] diff --git a/content/_index.md b/content/_index.md index bbf4869..cdf17d9 100644 --- a/content/_index.md +++ b/content/_index.md @@ -1,13 +1,15 @@ +++ +[extra] +homepage_badges = [ + { href = "https://discord.gg/Nz6rtfJ8Cu", image = "https://dcbadge.limes.pink/api/server/Nz6rtfJ8Cu?style=flat", alt = "Badge for EIP Editor Discord channel" }, + { href = "https://discord.gg/EVTQ9crVgQ", image = "https://dcbadge.limes.pink/api/server/EVTQ9crVgQ?style=flat", alt = "Badge for Ethereum R&D Discord channel" }, + { href = "https://discord.gg/mRzPXmmYEA", image = "https://dcbadge.limes.pink/api/server/mRzPXmmYEA?style=flat", alt = "Badge for Ethereum Wallets Discord channel" }, + { href = "/atom.xml", image = "https://img.shields.io/badge/rss-Everything-red.svg", alt = "RSS feed for everything" }, + { href = "/status/last-call/atom.xml", image = "https://img.shields.io/badge/rss-Last%20Calls-red.svg", alt = "RSS feed for last calls" }, +] +++ -

EIPs - Discord channel for ECH eip-editer - Discord channel for Eth R&D eip-editing - Discord server for discussions about proposals that impact Ethereum wallets - RSS - RSS -

+# EIPs

Ethereum Improvement Proposals (EIPs) describe standards for the Ethereum platform, including core protocol specifications, client APIs, and contract standards. Network upgrades are discussed separately in the Ethereum Project Management repository.

diff --git a/sass/assets/css/style.scss b/sass/assets/css/style.scss index 4bf4052..a56bd86 100644 --- a/sass/assets/css/style.scss +++ b/sass/assets/css/style.scss @@ -33,6 +33,34 @@ $content-width: 1152px; vertical-align: baseline; } +.homepage-badges { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin: -0.25rem 0 1.25rem; +} + +.homepage-badge-link { + display: inline-flex; + align-items: center; + line-height: 1; +} + +.homepage-badge-link:hover { + text-decoration: none; +} + +.homepage-badge-link img { + display: block; + max-width: 100%; + height: 20px; +} + +.homepage-badge-link .badge { + font-size: 0.875rem; +} + .footer-col-wrapper { color: #111; } diff --git a/templates/eip.html b/templates/eip.html index 66e73cb..5469312 100644 --- a/templates/eip.html +++ b/templates/eip.html @@ -230,11 +230,17 @@

Requires {% for required in page.extra.requires -%} + {% if required is starting_with("http") %} + {% set required_segment = required | split(pat="/") | last %} + {% set required_label = required_segment | replace(from="eip-", to="EIP-") | replace(from="erc-", to="ERC-") %} + {{ required_label }} + {% else %} {% set path = required | trim_start_matches(pat="@/") %} {% set page = get_page(path=path) %} {{ macros::eip_number(page=page) }} + {% endif %} {%- if not loop.last %}, {% endif -%} {%- endfor %} diff --git a/templates/index.html b/templates/index.html index 575141f..5cd5ace 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,17 @@ {% extends "base.html" %} +{% import "macros.html" as macros %} {% block content %} +{% if section.extra.homepage_badges %} +{% set badge_markup = macros::homepage_badges(badges=section.extra.homepage_badges) %} +{% set content_with_badges = section.content | replace(from="

", to="" ~ badge_markup) %} +{% if content_with_badges == section.content %} +{{ badge_markup | safe }} {{ section.content | safe }} +{% else %} +{{ content_with_badges | safe }} +{% endif %} +{% else %} +{{ section.content | safe }} +{% endif %} {% endblock content %} diff --git a/templates/macros.html b/templates/macros.html index 1ec4536..4ce27b7 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -1,7 +1,27 @@ {%- macro eip_number(page) -%} {%- if page.extra.category and page.extra.category == "ERC" -%} - ERC- + ERC- {%- else -%} - EIP- + EIP- {%- endif -%}{{- page.extra.number -}} {%- endmacro eip_number -%} + +{%- macro homepage_badges(badges) -%} +{%- if badges -%} +
+ {%- for badge in badges -%} + + {%- if badge.image -%} + {{ badge.alt | default(value=badge.label | default(value='Homepage badge')) }} + {%- else -%} + {{ badge.label }} + {%- endif -%} + + {%- endfor -%} +
+{%- endif -%} +{%- endmacro homepage_badges -%} diff --git a/theme.toml b/theme.toml index d0df4e4..f58d1d0 100644 --- a/theme.toml +++ b/theme.toml @@ -1,9 +1,9 @@ name = "eips-theme" description = "A theme for Ethereum Improvement Proposals" license = "MIT" -homepage = "https://github.com/eips-wg/eips-theme" +homepage = "https://github.com/eips-wg/theme" # The minimum version of Zola required -min_version = "0.17.2" +min_version = "0.22.1" # An optional live demo URL demo = "https://eips-wg.github.io/eips-theme" From 2b1a901e7fdb5254f4c3caf64ede0cd9eb33221c Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Sun, 3 May 2026 20:23:57 -0400 Subject: [PATCH 2/8] Add Theme Development Setup Add POSIX and PowerShell setup scripts for theme contributors that reuse or install build-eips, ensure a supported Zola is available, and bootstrap the surrounding workspace through an active proposal repo. Document the theme local development workflow, including workspace layout, setup flags, serve versus preview, server/base URL settings, targeted rendering, and source/output overrides. Keep this focused on setup and documentation; theme rendering behavior continues to be owned by the earlier theme PR. --- README.md | 146 ++++++++++ scripts/dev-setup | 646 ++++++++++++++++++++++++++++++++++++++++++ scripts/dev-setup.ps1 | 609 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1401 insertions(+) create mode 100644 README.md create mode 100755 scripts/dev-setup create mode 100644 scripts/dev-setup.ps1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f8ee87 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# EIPs Theme + +Zola theme used by the Ethereum Improvement Proposal (EIP) site, including EIPs and ERCs. This repo contains the shared templates, Sass, static assets, syntax definitions, and Zola configuration used when rendering proposal content. + +## Local Development + +### Minimum Requirements + +Local workspace commands require these tools on `PATH`: + +* Git +* `build-eips` +* Zola 0.22.1 + +Git must be installed separately. The setup script locates or installs `build-eips` and Zola, adds locally installed tool directories to `PATH` for the current shell session, and prints guidance for making those `PATH` changes permanent. + +### Bootstrap The Workspace + +Run the setup script once from this repo. + +Linux and macOS: + +```sh +./scripts/dev-setup +``` + +Windows PowerShell: + +```powershell +.\scripts\dev-setup.ps1 +``` + +The setup script initializes the workspace one directory above this repo, runs `build-eips doctor`, and prints the next local commands. + +After setup, the generated workspace guide is available at `../WORKSPACE.md`. Use that file for the full command reference and workspace details. + +The script expects the theme repo to live inside the surrounding multi-repo workspace. After setup, the workspace has this layout: + +```text +EIPs-project/ +├── .build-eips.toml +├── WORKSPACE.md +├── .local-build/ +├── EIPs/ +├── ERCs/ +└── theme/ +``` + +The script anchors setup through `../EIPs` by default and clones `https://github.com/ethereum/EIPs.git` there when that checkout is missing. Set `ACTIVE_REPO_ROOT` to use another proposal repo checkout: + +```sh +ACTIVE_REPO_ROOT=../ERCs ./scripts/dev-setup +``` + +Validate the workspace at any point with: + +```sh +build-eips -C ../EIPs doctor +``` + +### Advanced Setup Options + +Pass optional flags through the script when you want to work on the system tooling or an additional proposal repo: + +```sh +./scripts/dev-setup --platform-dev +./scripts/dev-setup --template +./scripts/dev-setup --template --platform-dev +``` + +`--template` adds the optional `template/` proposal template repo. `--platform-dev` adds local `preprocessor/` and `eipw/` checkouts for build system development. + +After setup, run local site commands against the active proposal repo: + +```sh +build-eips -C ../EIPs check +build-eips -C ../EIPs serve +build-eips -C ../EIPs doctor +``` + +### Serve And Preview + +Local serving keeps two distinct modes: + +* `build-eips serve` for the runtime dev loop +* `build-eips preview` for serving already-built static output (`build-eips build` must be run first) + +`build-eips serve` runs Zola's fast serve mode under the hood. It watches tracked edits in the active proposal repo and incrementally updates the local site for content changes. + +It also watches tracked edits under `theme/` in the workspace. Theme changes can take longer to apply because they affect the whole rendered site. During `serve`, staging a new theme file with `git add` triggers a theme rescan; no extra file edit or restart should be needed. + +`serve --clean` ignores active-repo dirty edits but still watches the local theme. + +`build-eips preview` serves the resolved output directory for the active repo without invoking Zola, preprocessing markdown, or rebuilding anything. If the output directory does not exist yet, it fails and tells you to run `build-eips build` first. + +### Local Server And Base URL + +The `[server]` table in `.build-eips.toml` controls the local bind address for both `serve` and `preview`; the default is `127.0.0.1:1111`. Per-command `--host` and `--port` flags override `.build-eips.toml` for one run. These settings do not change build base URLs. + +The `[site].base_url` value in `.build-eips.toml` is the default local rendered site URL. Starter `.build-eips.toml` files set it to `http://127.0.0.1:1111`. If you change `[server].port`, update `[site].base_url` too when generated links should match the local server. + +Per-command `--base-url` on `build` or `serve` is a one-run override and wins over `.build-eips.toml`, including with staging or parity commands. + +`preview` serves existing output, so build with `--base-url` first when previewed HTML should contain a different local link target. + +### Target Specific Proposals + +Full local `build` and `serve` runs can take time because they process every proposal file. Use targeted rendering when you only need to test a few proposals or theme changes against a small proposal set: + +```bash +build-eips serve --only 555 +build-eips build --only 555 +build-eips build --only 555 678 +``` + +You can also set a default target list in the workspace `.build-eips.toml`: + +```toml +[render] +only = [555, 678] +``` + +CLI `--only` replaces `[render].only` for that run. For edge cases and exact filtering behavior, see `../WORKSPACE.md`. + +### Source And Output Overrides + +Workspace-local sources come from the standard workspace layout. The local theme is `workspace/theme`, and local sibling repos are `workspace/` from the active repo manifest. + +Use `--remote-siblings` when you need to force remote sibling proposal sources for a single command. + +Use global `--build-root ` when you want a separate prepared repo and output directory, for example to compare two builds side by side. The path replaces the default `.local-build/` location for each command where you pass it, so use the same `--build-root` value when serving or previewing builds. + + +Example: + +```bash +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-local build --base-url http://127.0.0.1:1111 +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-staging --staging build --base-url http://127.0.0.1:1112 + +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-local preview --port 1111 +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-staging preview --port 1112 + +# Or using serve +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-local serve --port 1111 +build-eips -C /work/EIPs-project/EIPs --build-root /tmp/eips-staging --staging serve --port 1112 +``` diff --git a/scripts/dev-setup b/scripts/dev-setup new file mode 100755 index 0000000..50bd5ff --- /dev/null +++ b/scripts/dev-setup @@ -0,0 +1,646 @@ +#!/bin/sh + +set -eu + +DEFAULT_ACTIVE_REPO_URL=https://github.com/ethereum/EIPs.git + +say() { + printf '%s\n' "$*" +} + +die() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +shell_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +absolute_path() { + case "$1" in + /*) + printf '%s\n' "$1" + ;; + *) + printf '%s/%s\n' "$INVOCATION_DIR" "$1" + ;; + esac +} + +ensure_active_repo_root() { + if [ "$ACTIVE_REPO_EXPLICIT" = true ]; then + if [ ! -d "$ACTIVE_REPO_ROOT" ]; then + die "configured ACTIVE_REPO_ROOT does not exist: $ACTIVE_REPO_ROOT. Fix ACTIVE_REPO_ROOT or unset it to let this script clone EIPs." + fi + if [ ! -e "$ACTIVE_REPO_ROOT/.git" ]; then + die "configured ACTIVE_REPO_ROOT is not a git checkout: $ACTIVE_REPO_ROOT. Fix ACTIVE_REPO_ROOT or unset it to let this script clone EIPs." + fi + else + if [ ! -e "$ACTIVE_REPO_ROOT" ]; then + command -v git >/dev/null 2>&1 || die "need git to clone default proposal repo from $DEFAULT_ACTIVE_REPO_URL" + + say "No active proposal repo found at $ACTIVE_REPO_ROOT." + say "Cloning default proposal repo from $DEFAULT_ACTIVE_REPO_URL" + say "This may take a few minutes..." + if ! git clone "$DEFAULT_ACTIVE_REPO_URL" "$ACTIVE_REPO_ROOT"; then + die "failed to clone default proposal repo from $DEFAULT_ACTIVE_REPO_URL to $ACTIVE_REPO_ROOT" + fi + say "Cloned default proposal repo to $ACTIVE_REPO_ROOT" + fi + + if [ ! -d "$ACTIVE_REPO_ROOT" ]; then + die "default active proposal repo path exists but is not a directory: $ACTIVE_REPO_ROOT. Remove it or set ACTIVE_REPO_ROOT." + fi + if [ ! -e "$ACTIVE_REPO_ROOT/.git" ]; then + die "default active proposal repo path exists but is not a git checkout: $ACTIVE_REPO_ROOT. Remove it or set ACTIVE_REPO_ROOT." + fi + fi + + ACTIVE_REPO_ROOT=$(CDPATH= cd -- "$ACTIVE_REPO_ROOT" && pwd) +} + +normalize_dir_for_path_compare() { + dir=$1 + [ -n "$dir" ] || return 1 + + while [ "$dir" != "/" ] && [ "${dir%/}" != "$dir" ]; do + dir=${dir%/} + done + + case "$dir" in + /*) + absolute_dir=$dir + ;; + *) + absolute_dir=$(pwd -P)/$dir + ;; + esac + + if [ -d "$absolute_dir" ]; then + (CDPATH= cd -P -- "$absolute_dir" 2>/dev/null && pwd -P) + else + printf '%s\n' "$absolute_dir" + fi +} + +path_contains_dir() { + target=$(normalize_dir_for_path_compare "$1") || return 1 + old_ifs=$IFS + IFS=: + for path_entry in ${PATH:-}; do + [ -n "$path_entry" ] || continue + candidate=$(normalize_dir_for_path_compare "$path_entry") || continue + if [ "$candidate" = "$target" ]; then + IFS=$old_ifs + return 0 + fi + done + IFS=$old_ifs + return 1 +} + +move_dir_to_front_of_script_path() { + dir=$1 + target=$(normalize_dir_for_path_compare "$dir") || return 1 + new_path= + old_ifs=$IFS + IFS=: + for path_entry in ${PATH:-}; do + [ -n "$path_entry" ] || continue + candidate=$(normalize_dir_for_path_compare "$path_entry") || continue + if [ "$candidate" = "$target" ]; then + continue + fi + + if [ -n "$new_path" ]; then + new_path=$new_path:$path_entry + else + new_path=$path_entry + fi + done + IFS=$old_ifs + + if [ -n "$new_path" ]; then + updated_path=$dir:$new_path + else + updated_path=$dir + fi + + if [ "${PATH:-}" != "$updated_path" ]; then + PATH=$updated_path + add_path_note "$dir" + fi +} + +add_path_note() { + dir=$1 + target=$(normalize_dir_for_path_compare "$dir") || return 1 + old_ifs=$IFS + IFS=' +' + for path_note in $PATH_NOTES; do + candidate=$(normalize_dir_for_path_compare "$path_note") || continue + if [ "$candidate" = "$target" ]; then + IFS=$old_ifs + return 0 + fi + done + IFS=$old_ifs + + case " +$PATH_NOTES +" in + *" +$dir +"*) + return 0 + ;; + esac + + if [ -n "$PATH_NOTES" ]; then + PATH_NOTES=$PATH_NOTES' +'$dir + else + PATH_NOTES=$dir + fi +} + +pick_home_install_dir() { + [ -n "${HOME:-}" ] || return 1 + dir=$HOME/.local/bin + mkdir -p "$dir" 2>/dev/null || return 1 + [ -d "$dir" ] || return 1 + [ -w "$dir" ] || return 1 + printf '%s\n' "$dir" +} + +pick_writable_path_dir() { + old_ifs=$IFS + IFS=: + for dir in ${PATH:-}; do + [ -n "$dir" ] || continue + [ -d "$dir" ] || continue + [ -w "$dir" ] || continue + printf '%s\n' "$dir" + IFS=$old_ifs + return 0 + done + IFS=$old_ifs + return 1 +} + +pick_install_dir() { + if dir=$(pick_home_install_dir); then + printf '%s\n' "$dir" + return 0 + fi + + pick_writable_path_dir +} + +download() { + url=$1 + destination=$2 + + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "$destination" + return 0 + fi + + if command -v wget >/dev/null 2>&1; then + wget -qO "$destination" "$url" + return 0 + fi + + die "need curl or wget to download release binaries" +} + +file_sha256() { + path=$1 + + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{ print $1 }' | tr 'A-F' 'a-f' + return 0 + fi + + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$path" | awk '{ print $1 }' | tr 'A-F' 'a-f' + return 0 + fi + + if command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 -r "$path" | awk '{ print $1 }' | tr 'A-F' 'a-f' + return 0 + fi + + die "need sha256sum, shasum, or openssl to verify release checksums" +} + +normalized_sidecar_text() { + sidecar_path=$1 + bom=$(printf '\357\273\277') + cr=$(printf '\r') + first_three=$(dd if="$sidecar_path" bs=3 count=1 2>/dev/null || true) + + if [ "$first_three" = "$bom" ]; then + dd if="$sidecar_path" bs=3 skip=1 2>/dev/null | tr -d "$cr" + else + tr -d "$cr" < "$sidecar_path" + fi +} + +verify_archive_checksum() { + archive_path=$1 + sidecar_path=$2 + archive_name=$3 + + [ -f "$sidecar_path" ] || die "missing checksum sidecar: $sidecar_path" + + checksum_lines=$(normalized_sidecar_text "$sidecar_path" | sed '/^[[:space:]]*$/d') + checksum_line_count=$(printf '%s\n' "$checksum_lines" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ') + [ "$checksum_line_count" = "1" ] || die "checksum sidecar must contain exactly one checksum line" + + set -f + set -- $checksum_lines + set +f + [ "$#" -eq 2 ] || die "checksum sidecar must contain only a hash and archive filename" + + expected_hash=$(printf '%s' "$1" | tr 'A-F' 'a-f') + expected_name=$2 + + [ ${#expected_hash} -eq 64 ] || die "checksum sidecar hash must be 64 hex characters" + case "$expected_hash" in + *[!0123456789abcdef]*) + die "checksum sidecar hash must be hex" + ;; + esac + case "$expected_name" in + */*|*\\*) + die "checksum sidecar filename must be a basename" + ;; + esac + [ "$expected_name" = "$archive_name" ] || die "checksum sidecar filename '$expected_name' does not match '$archive_name'" + + actual_hash=$(file_sha256 "$archive_path") + [ "$actual_hash" = "$expected_hash" ] || die "checksum mismatch for $archive_name" +} + +verify_file_hash() { + archive_path=$1 + expected_hash=$2 + archive_name=$3 + + actual_hash=$(file_sha256 "$archive_path") + [ "$actual_hash" = "$expected_hash" ] || die "checksum mismatch for $archive_name" +} + +cleanup_tmpdir() { + if [ -n "${INSTALL_TMP_TO_CLEAN:-}" ]; then + rm -f "$INSTALL_TMP_TO_CLEAN" + INSTALL_TMP_TO_CLEAN= + fi + if [ -n "${TMPDIR_TO_CLEAN:-}" ]; then + rm -rf "$TMPDIR_TO_CLEAN" + TMPDIR_TO_CLEAN= + fi +} + +cleanup_for_signal() { + cleanup_tmpdir + trap - EXIT INT TERM HUP QUIT + exit 1 +} + +set_cleanup_traps() { + trap cleanup_tmpdir EXIT + trap cleanup_for_signal INT TERM HUP QUIT +} + +clear_cleanup_traps() { + trap - EXIT INT TERM HUP QUIT +} + +ensure_install_dir() { + [ -n "${INSTALL_DIR:-}" ] && return 0 + + INSTALL_DIR=$(pick_install_dir) || die "could not find a writable install directory" + move_dir_to_front_of_script_path "$INSTALL_DIR" +} + +install_build_eips() { + case "$(uname -s)" in + Linux) + archive_name=build-eips-ubuntu.tar.xz + ;; + Darwin) + archive_name=build-eips-macos.tar.xz + ;; + *) + die "unsupported operating system for automatic build-eips installation" + ;; + esac + + command -v tar >/dev/null 2>&1 || die "need tar to unpack the build-eips release archive" + ensure_install_dir + + tmpdir=$(mktemp -d) + TMPDIR_TO_CLEAN=$tmpdir + set_cleanup_traps + + archive_path=$tmpdir/$archive_name + sidecar_path=$tmpdir/$archive_name.sha256 + release_base_url=https://github.com/eips-wg/preprocessor/releases/latest/download + + say "Installing build-eips from $release_base_url/$archive_name" + download "$release_base_url/$archive_name" "$archive_path" + download "$release_base_url/$archive_name.sha256" "$sidecar_path" + verify_archive_checksum "$archive_path" "$sidecar_path" "$archive_name" + + tar -xf "$archive_path" -C "$tmpdir" + [ -f "$tmpdir/build-eips" ] || die "release archive did not contain build-eips" + + install_tmp=$(mktemp "$INSTALL_DIR/.build-eips.XXXXXX") || die "could not create temporary install file in $INSTALL_DIR" + INSTALL_TMP_TO_CLEAN=$install_tmp + cp "$tmpdir/build-eips" "$install_tmp" + chmod +x "$install_tmp" + mv "$install_tmp" "$INSTALL_DIR/build-eips" + INSTALL_TMP_TO_CLEAN= + + BUILD_EIPS=$INSTALL_DIR/build-eips + cleanup_tmpdir + clear_cleanup_traps +} + +unsupported_zola_platform() { + os=$1 + arch=$2 + die "Unsupported platform $os/$arch for automatic Zola install. Install Zola 0.22.1 manually from https://github.com/getzola/zola/releases and ensure it is on PATH." +} + +is_linux_musl() { + if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then + return 0 + fi + + set -- /lib/ld-musl-*.so.1 + [ -e "$1" ] +} + +select_zola_release() { + os=$(uname -s) + arch=$(uname -m) + + case "$os" in + Linux) + if is_linux_musl; then + case "$arch" in + x86_64) + ZOLA_ARCHIVE_NAME=zola-v0.22.1-x86_64-unknown-linux-musl.tar.gz + ZOLA_ARCHIVE_HASH=227df99b664421240a8ba77747067c51259b08159125d5603763b3b173b9a881 + ;; + *) + unsupported_zola_platform "$os" "$arch" + ;; + esac + else + case "$arch" in + x86_64) + ZOLA_ARCHIVE_NAME=zola-v0.22.1-x86_64-unknown-linux-gnu.tar.gz + ZOLA_ARCHIVE_HASH=0ca09aa40376aaa9ddfb512ff9ad963262ef95edb0d0f2d5ec6961b6f5cf22ef + ;; + aarch64|arm64) + ZOLA_ARCHIVE_NAME=zola-v0.22.1-aarch64-unknown-linux-gnu.tar.gz + ZOLA_ARCHIVE_HASH=8af437ec6352f33ccd24d7a1cfcb54a3db95d3ce376dc69525b4ef3fb6b8c1d1 + ;; + *) + unsupported_zola_platform "$os" "$arch" + ;; + esac + fi + ;; + Darwin) + case "$arch" in + x86_64) + ZOLA_ARCHIVE_NAME=zola-v0.22.1-x86_64-apple-darwin.tar.gz + ZOLA_ARCHIVE_HASH=3898709e154ae0593933264a540c869348bdb10d7f1b03a42dfb78d63703b3b5 + ;; + arm64|aarch64) + ZOLA_ARCHIVE_NAME=zola-v0.22.1-aarch64-apple-darwin.tar.gz + ZOLA_ARCHIVE_HASH=46ac45a9e7628dba8593b124ee8794f4f9aa1c6b569918ecd4bbc5d0be190515 + ;; + *) + unsupported_zola_platform "$os" "$arch" + ;; + esac + ;; + *) + unsupported_zola_platform "$os" "$arch" + ;; + esac +} + +parse_zola_version() { + output=$1 + + set -f + set -- $output + set +f + [ "$#" -ge 2 ] || return 1 + + ZOLA_FOUND_VERSION=$2 + version_core=$(printf '%s\n' "$ZOLA_FOUND_VERSION" | sed -n 's/^\([0-9][0-9]*\)\.\([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2 \3/p') + [ -n "$version_core" ] || return 1 + + set -- $version_core + ZOLA_VERSION_MAJOR=$1 + ZOLA_VERSION_MINOR=$2 + ZOLA_VERSION_PATCH=$3 + ZOLA_VERSION_SUFFIX=$(printf '%s\n' "$ZOLA_FOUND_VERSION" | sed -n 's/^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*//p') + + return 0 +} + +zola_version_relation() { + if [ "$ZOLA_VERSION_MAJOR" -lt 0 ]; then + printf '%s\n' below + elif [ "$ZOLA_VERSION_MAJOR" -gt 0 ]; then + printf '%s\n' newer + elif [ "$ZOLA_VERSION_MINOR" -lt 22 ]; then + printf '%s\n' below + elif [ "$ZOLA_VERSION_MINOR" -gt 22 ]; then + printf '%s\n' newer + elif [ "$ZOLA_VERSION_PATCH" -lt 1 ]; then + printf '%s\n' below + elif [ "$ZOLA_VERSION_PATCH" -gt 1 ]; then + printf '%s\n' newer + elif [ -n "$ZOLA_VERSION_SUFFIX" ]; then + printf '%s\n' below + else + printf '%s\n' equal + fi +} + +install_zola() { + command -v tar >/dev/null 2>&1 || die "need tar to unpack the zola release archive" + select_zola_release + ensure_install_dir + + tmpdir=$(mktemp -d) + TMPDIR_TO_CLEAN=$tmpdir + set_cleanup_traps + + archive_path=$tmpdir/$ZOLA_ARCHIVE_NAME + release_base_url=https://github.com/getzola/zola/releases/download/v0.22.1 + + say "Installing zola 0.22.1 from $release_base_url/$ZOLA_ARCHIVE_NAME" + download "$release_base_url/$ZOLA_ARCHIVE_NAME" "$archive_path" + verify_file_hash "$archive_path" "$ZOLA_ARCHIVE_HASH" "$ZOLA_ARCHIVE_NAME" + + tar -xzf "$archive_path" -C "$tmpdir" + [ -f "$tmpdir/zola" ] || die "zola release archive did not contain zola" + + install_tmp=$(mktemp "$INSTALL_DIR/.zola.XXXXXX") || die "could not create temporary zola install file in $INSTALL_DIR" + INSTALL_TMP_TO_CLEAN=$install_tmp + cp "$tmpdir/zola" "$install_tmp" + chmod +x "$install_tmp" + mv "$install_tmp" "$INSTALL_DIR/zola" + INSTALL_TMP_TO_CLEAN= + + ZOLA=$INSTALL_DIR/zola + cleanup_tmpdir + clear_cleanup_traps +} + +ensure_zola() { + if command -v zola >/dev/null 2>&1; then + ZOLA=$(command -v zola) + elif [ -n "${HOME:-}" ] && [ -x "$HOME/.local/bin/zola" ]; then + INSTALL_DIR=$HOME/.local/bin + ZOLA=$INSTALL_DIR/zola + move_dir_to_front_of_script_path "$INSTALL_DIR" + else + install_zola + return 0 + fi + + if [ -n "${ZOLA:-}" ]; then + zola_output=$("$ZOLA" --version 2>/dev/null || true) + if parse_zola_version "$zola_output"; then + relation=$(zola_version_relation) + case "$relation" in + below) + say "Found zola $ZOLA_FOUND_VERSION below supported 0.22.1. Installing zola 0.22.1." + install_zola + ;; + equal) + say "Using existing zola $ZOLA_FOUND_VERSION at $ZOLA" + ;; + newer) + say "Found zola $ZOLA_FOUND_VERSION. build-eips is tested with zola 0.22.1 or newer. Continuing with the installed version." + ;; + esac + else + say "Found zola with unparseable version output. Installing zola 0.22.1." + install_zola + fi + fi +} + +WORKSPACE_FLAGS= + +while [ "$#" -gt 0 ]; do + case "$1" in + --template|--platform-dev) + WORKSPACE_FLAGS="$WORKSPACE_FLAGS $1" + ;; + -h|--help) + say "usage: $0 [--template] [--platform-dev]" + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac + shift +done + +INVOCATION_DIR=$(pwd) +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +THEME_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +WORKSPACE_ROOT=$(CDPATH= cd -- "$THEME_ROOT/.." && pwd) +INSTALL_DIR= +PATH_NOTES= +TMPDIR_TO_CLEAN= +INSTALL_TMP_TO_CLEAN= + +# Theme is not an active proposal repo, so build-eips init is anchored through a +# sibling proposal repo. Default to EIPs and allow ERCs/custom layouts via env. +if [ -n "${ACTIVE_REPO_ROOT:-}" ]; then + ACTIVE_REPO_EXPLICIT=true + ACTIVE_REPO_ROOT=$(absolute_path "$ACTIVE_REPO_ROOT") +else + ACTIVE_REPO_EXPLICIT=false + ACTIVE_REPO_ROOT=$WORKSPACE_ROOT/EIPs +fi + +ensure_active_repo_root + +if command -v build-eips >/dev/null 2>&1; then + BUILD_EIPS=$(command -v build-eips) + say "Using existing build-eips at $BUILD_EIPS" +elif [ -n "${HOME:-}" ] && [ -x "$HOME/.local/bin/build-eips" ]; then + BUILD_EIPS=$HOME/.local/bin/build-eips + INSTALL_DIR=$HOME/.local/bin + move_dir_to_front_of_script_path "$INSTALL_DIR" + say "Using existing build-eips at $BUILD_EIPS" +else + install_build_eips +fi + +ensure_zola + +say "Theme repo: $THEME_ROOT" +say "Workspace root: $WORKSPACE_ROOT" +say "Active proposal repo: $ACTIVE_REPO_ROOT" + +say "Bootstrapping workspace at $WORKSPACE_ROOT" +if ! "$BUILD_EIPS" -C "$ACTIVE_REPO_ROOT" init "$WORKSPACE_ROOT" $WORKSPACE_FLAGS; then + die "build-eips init failed" +fi + +say "Running build-eips doctor" +if ! "$BUILD_EIPS" -C "$ACTIVE_REPO_ROOT" doctor; then + say "Warning: build-eips doctor reported issues above. Fix them before relying on direct build-eips commands." +fi + +WORKSPACE_DOC=$WORKSPACE_ROOT/WORKSPACE.md +say "" +if [ -f "$WORKSPACE_DOC" ]; then + say "Workspace docs: $WORKSPACE_DOC (../WORKSPACE.md from this repo)" +else + say "Warning: workspace docs were not found at $WORKSPACE_DOC after build-eips init" +fi + +if [ -n "$PATH_NOTES" ]; then + say "" + say "Updated PATH for this script process only:" + old_ifs=$IFS + IFS=' +' + for path_note in $PATH_NOTES; do + say " $path_note" + done + say "To make this permanent in your shell, add:" + for path_note in $PATH_NOTES; do + say " export PATH=$(shell_quote "$path_note"):\$PATH" + done + IFS=$old_ifs +fi + +say "" +say "Next commands:" +say " cd $(shell_quote "$THEME_ROOT")" +say " build-eips -C $(shell_quote "$ACTIVE_REPO_ROOT") serve" +say " build-eips -C $(shell_quote "$ACTIVE_REPO_ROOT") check" +say " build-eips -C $(shell_quote "$ACTIVE_REPO_ROOT") doctor" diff --git a/scripts/dev-setup.ps1 b/scripts/dev-setup.ps1 new file mode 100644 index 0000000..1399417 --- /dev/null +++ b/scripts/dev-setup.ps1 @@ -0,0 +1,609 @@ +param( + [switch]$Template, + [switch]$PlatformDev +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$DefaultActiveRepoUrl = "https://github.com/ethereum/EIPs.git" + +function Say { + param([string]$Message) + + Write-Host $Message +} + +function Die { + param([string]$Message) + + throw "error: $Message" +} + +function ConvertTo-NormalizedDirectoryPath { + param([string]$Directory) + + $trimmed = $Directory.Trim('"') + $trimChars = [char[]]@([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + + try { + return ([System.IO.Path]::GetFullPath($trimmed)).TrimEnd($trimChars) + } catch { + return $trimmed.TrimEnd($trimChars) + } +} + +function Test-DirectoryOnPath { + param([string]$Directory) + + if ([string]::IsNullOrWhiteSpace($env:Path)) { + return $false + } + + $target = ConvertTo-NormalizedDirectoryPath -Directory $Directory + foreach ($entry in ($env:Path -split ";")) { + if ([string]::IsNullOrWhiteSpace($entry)) { + continue + } + + $candidate = ConvertTo-NormalizedDirectoryPath -Directory $entry + if ([string]::Equals($candidate, $target, [System.StringComparison]::OrdinalIgnoreCase)) { + return $true + } + } + + return $false +} + +function Add-PathNote { + param([string]$InstallDir) + + $target = ConvertTo-NormalizedDirectoryPath -Directory $InstallDir + foreach ($pathNote in $script:PathNotes) { + $candidate = ConvertTo-NormalizedDirectoryPath -Directory $pathNote + if ([string]::Equals($candidate, $target, [System.StringComparison]::OrdinalIgnoreCase)) { + return + } + } + + $script:PathNotes += $InstallDir +} + +function Move-DirectoryToFrontOfSessionPath { + param([string]$InstallDir) + + $target = ConvertTo-NormalizedDirectoryPath -Directory $InstallDir + $remainingEntries = @() + + if (-not [string]::IsNullOrWhiteSpace($env:Path)) { + foreach ($entry in ($env:Path -split ";")) { + if ([string]::IsNullOrWhiteSpace($entry)) { + continue + } + + $candidate = ConvertTo-NormalizedDirectoryPath -Directory $entry + if ([string]::Equals($candidate, $target, [System.StringComparison]::OrdinalIgnoreCase)) { + continue + } + + $remainingEntries += $entry + } + } + + $newEntries = @($InstallDir) + if ($remainingEntries.Count -gt 0) { + $newEntries += $remainingEntries + } + + $updatedPath = [string]::Join(";", $newEntries) + if (-not [string]::Equals($env:Path, $updatedPath, [System.StringComparison]::Ordinal)) { + $env:Path = $updatedPath + + Add-PathNote -InstallDir $InstallDir + } +} + +function Find-BuildEipsOnPath { + foreach ($commandName in @("build-eips", "build-eips.exe")) { + $commands = @(Get-Command -Name $commandName -CommandType Application -ErrorAction SilentlyContinue) + if ($commands.Count -gt 0) { + return $commands[0].Source + } + } + + return $null +} + +function Find-ZolaOnPath { + foreach ($commandName in @("zola", "zola.exe")) { + $commands = @(Get-Command -Name $commandName -CommandType Application -ErrorAction SilentlyContinue) + if ($commands.Count -gt 0) { + return $commands[0].Source + } + } + + return $null +} + +function Assert-InstallDirWritable { + param([string]$InstallDir) + + $probePath = $null + + try { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + $probeName = ".build-eips-write-test-{0}.tmp" -f ([System.Guid]::NewGuid().ToString("N")) + $probePath = Join-Path -Path $InstallDir -ChildPath $probeName + [System.IO.File]::WriteAllText($probePath, "") + Remove-Item -LiteralPath $probePath -Force + $probePath = $null + } catch { + Die ("install directory cannot be created or written ({0}): {1}" -f $InstallDir, $_.Exception.Message) + } finally { + if (($null -ne $probePath) -and (Test-Path -LiteralPath $probePath)) { + Remove-Item -LiteralPath $probePath -Force -ErrorAction SilentlyContinue + } + } +} + +function Invoke-ReleaseDownload { + param( + [string]$Url, + [string]$Destination + ) + + $previousProgressPreference = $ProgressPreference + $ProgressPreference = "SilentlyContinue" + try { + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing + } catch { + Die ("failed to download {0}: {1}" -f $Url, $_.Exception.Message) + } finally { + $ProgressPreference = $previousProgressPreference + } +} + +function Test-AsciiHexHash { + param([string]$Hash) + + return $Hash -match "^[0-9a-fA-F]{64}$" +} + +function Assert-ArchiveChecksum { + param( + [string]$ArchivePath, + [string]$SidecarPath, + [string]$ArchiveName + ) + + if (-not (Test-Path -LiteralPath $SidecarPath -PathType Leaf)) { + Die "missing checksum sidecar: $SidecarPath" + } + + $sidecarText = [System.IO.File]::ReadAllText($SidecarPath) + $checksumLines = @($sidecarText -split "\r?\n" | Where-Object { $_.Trim().Length -gt 0 }) + if ($checksumLines.Count -ne 1) { + Die "checksum sidecar must contain exactly one checksum line" + } + + $fields = @($checksumLines[0].Trim() -split "\s+") + if ($fields.Count -ne 2) { + Die "checksum sidecar must contain only a hash and archive filename" + } + + $expectedHash = $fields[0].ToLowerInvariant() + $expectedName = $fields[1] + + if (-not (Test-AsciiHexHash -Hash $expectedHash)) { + Die "checksum sidecar hash must be 64 hex characters" + } + if ($expectedName -match '[/\\]') { + Die "checksum sidecar filename must be a basename" + } + if ($expectedName -ne $ArchiveName) { + Die ("checksum sidecar filename '{0}' does not match '{1}'" -f $expectedName, $ArchiveName) + } + + $actualHash = (Get-FileHash -Algorithm SHA256 -Path $ArchivePath).Hash.ToLowerInvariant() + if ($actualHash -ne $expectedHash) { + Die "checksum mismatch for $ArchiveName" + } +} + +function Assert-FileSha256 { + param( + [string]$ArchivePath, + [string]$ExpectedHash, + [string]$ArchiveName + ) + + $actualHash = (Get-FileHash -Algorithm SHA256 -Path $ArchivePath).Hash.ToLowerInvariant() + if ($actualHash -ne $ExpectedHash.ToLowerInvariant()) { + Die "checksum mismatch for $ArchiveName" + } +} + +function Install-BuildEips { + param( + [string]$InstallDir, + [string]$BuildEipsPath + ) + + $archiveName = "build-eips-windows.zip" + $releaseBaseUrl = "https://github.com/eips-wg/preprocessor/releases/latest/download" + $tmpRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("build-eips-" + [System.Guid]::NewGuid().ToString("N")) + $archivePath = Join-Path -Path $tmpRoot -ChildPath $archiveName + $sidecarPath = Join-Path -Path $tmpRoot -ChildPath "$archiveName.sha256" + $extractDir = Join-Path -Path $tmpRoot -ChildPath "extract" + + try { + Assert-InstallDirWritable -InstallDir $InstallDir + + New-Item -ItemType Directory -Path $extractDir -Force | Out-Null + + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + + Say "Installing build-eips from $releaseBaseUrl/$archiveName" + Invoke-ReleaseDownload -Url "$releaseBaseUrl/$archiveName" -Destination $archivePath + Invoke-ReleaseDownload -Url "$releaseBaseUrl/$archiveName.sha256" -Destination $sidecarPath + Assert-ArchiveChecksum -ArchivePath $archivePath -SidecarPath $sidecarPath -ArchiveName $archiveName + + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDir -Force + + $extractedBuildEips = Join-Path -Path $extractDir -ChildPath "build-eips.exe" + if (-not (Test-Path -LiteralPath $extractedBuildEips -PathType Leaf)) { + Die "release archive did not contain expected build-eips.exe" + } + + try { + Move-Item -LiteralPath $extractedBuildEips -Destination $BuildEipsPath -Force + } catch { + Die ("build-eips.exe is in use. Close any running build-eips process and re-run this script. Details: {0}" -f $_.Exception.Message) + } + + return $BuildEipsPath + } catch { + Die ("failed to install build-eips: {0}" -f $_.Exception.Message) + } finally { + if (Test-Path -LiteralPath $tmpRoot) { + Remove-Item -LiteralPath $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +function Get-ZolaReleaseAsset { + $architecture = $env:PROCESSOR_ARCHITECTURE + if ([string]::IsNullOrWhiteSpace($architecture)) { + $architecture = "unknown" + } + if (($architecture -eq "x86") -and (-not [string]::IsNullOrWhiteSpace($env:PROCESSOR_ARCHITEW6432))) { + $architecture = $env:PROCESSOR_ARCHITEW6432 + } + + switch ($architecture.ToUpperInvariant()) { + "AMD64" { + return @{ + ArchiveName = "zola-v0.22.1-x86_64-pc-windows-msvc.zip" + Hash = "2c8b368f5abdf2b2478748f9549a761fd6599238e18948eccb76a7cae51f5dc1" + } + } + default { + Die "Unsupported platform Windows/$architecture for automatic Zola install. Install Zola 0.22.1 manually from https://github.com/getzola/zola/releases and ensure it is on PATH." + } + } +} + +function Install-Zola { + param( + [string]$InstallDir, + [string]$ZolaPath + ) + + $asset = Get-ZolaReleaseAsset + $archiveName = $asset.ArchiveName + $archiveHash = $asset.Hash + $releaseBaseUrl = "https://github.com/getzola/zola/releases/download/v0.22.1" + $tmpRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("zola-" + [System.Guid]::NewGuid().ToString("N")) + $archivePath = Join-Path -Path $tmpRoot -ChildPath $archiveName + $extractDir = Join-Path -Path $tmpRoot -ChildPath "extract" + + try { + Assert-InstallDirWritable -InstallDir $InstallDir + + New-Item -ItemType Directory -Path $extractDir -Force | Out-Null + + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + + Say "Installing zola 0.22.1 from $releaseBaseUrl/$archiveName" + Invoke-ReleaseDownload -Url "$releaseBaseUrl/$archiveName" -Destination $archivePath + Assert-FileSha256 -ArchivePath $archivePath -ExpectedHash $archiveHash -ArchiveName $archiveName + + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDir -Force + + $extractedZola = Join-Path -Path $extractDir -ChildPath "zola.exe" + if (-not (Test-Path -LiteralPath $extractedZola -PathType Leaf)) { + Die "zola release archive did not contain expected zola.exe" + } + + try { + Move-Item -LiteralPath $extractedZola -Destination $ZolaPath -Force + } catch { + Die ("zola.exe is in use. Close any running zola process and re-run this script. Details: {0}" -f $_.Exception.Message) + } + + return $ZolaPath + } catch { + Die ("failed to install zola: {0}" -f $_.Exception.Message) + } finally { + if (Test-Path -LiteralPath $tmpRoot) { + Remove-Item -LiteralPath $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +function Get-ZolaVersionInfo { + param([string]$ZolaPath) + + try { + $output = & $ZolaPath --version 2>$null + if ($LASTEXITCODE -ne 0) { + return $null + } + } catch { + return $null + } + + $fields = @(($output -join " ") -split "\s+" | Where-Object { $_.Length -gt 0 }) + if ($fields.Count -lt 2) { + return $null + } + + $versionToken = $fields[1] + if ($versionToken -notmatch "^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$") { + return $null + } + + return @{ + VersionToken = $versionToken + Version = [version]("{0}.{1}.{2}" -f $Matches[1], $Matches[2], $Matches[3]) + Suffix = $Matches[4] + } +} + +function Get-ZolaVersionRelation { + param([hashtable]$VersionInfo) + + $minimumVersion = [version]"0.22.1" + if ($VersionInfo.Version -lt $minimumVersion) { + return "below" + } + if ($VersionInfo.Version -gt $minimumVersion) { + return "newer" + } + if (-not [string]::IsNullOrEmpty($VersionInfo.Suffix)) { + return "below" + } + + return "equal" +} + +function Install-PinnedZola { + $defaultPaths = Get-DefaultInstallPaths + $installedZola = Install-Zola -InstallDir $defaultPaths.InstallDir -ZolaPath $defaultPaths.ZolaPath + Move-DirectoryToFrontOfSessionPath -InstallDir $defaultPaths.InstallDir + + return $installedZola +} + +function Initialize-Zola { + $zolaPath = Find-ZolaOnPath + if ($null -eq $zolaPath) { + $defaultPaths = Get-DefaultInstallPaths + if (Test-Path -LiteralPath $defaultPaths.ZolaPath -PathType Leaf) { + $zolaPath = $defaultPaths.ZolaPath + Move-DirectoryToFrontOfSessionPath -InstallDir $defaultPaths.InstallDir + } + } + + if ($null -eq $zolaPath) { + return (Install-PinnedZola) + } + + $versionInfo = Get-ZolaVersionInfo -ZolaPath $zolaPath + if ($null -eq $versionInfo) { + Say "Found zola with unparseable version output. Installing zola 0.22.1." + return (Install-PinnedZola) + } + + $relation = Get-ZolaVersionRelation -VersionInfo $versionInfo + switch ($relation) { + "below" { + Say ("Found zola {0} below supported 0.22.1. Installing zola 0.22.1." -f $versionInfo.VersionToken) + return (Install-PinnedZola) + } + "equal" { + Say ("Using existing zola {0} at {1}" -f $versionInfo.VersionToken, $zolaPath) + return $zolaPath + } + "newer" { + Say ("Found zola {0}. build-eips is tested with zola 0.22.1 or newer. Continuing with the installed version." -f $versionInfo.VersionToken) + return $zolaPath + } + } +} + +function Resolve-ActiveRepoRoot { + param( + [string]$InvocationDir, + [string]$WorkspaceRoot + ) + + $activeRepoExplicit = $false + if (-not [string]::IsNullOrWhiteSpace($env:ACTIVE_REPO_ROOT)) { + $activeRepoExplicit = $true + if ([System.IO.Path]::IsPathRooted($env:ACTIVE_REPO_ROOT)) { + $activeRepoCandidate = $env:ACTIVE_REPO_ROOT + } else { + $activeRepoCandidate = Join-Path -Path $InvocationDir -ChildPath $env:ACTIVE_REPO_ROOT + } + } else { + $activeRepoCandidate = Join-Path -Path $WorkspaceRoot -ChildPath "EIPs" + } + + if ($activeRepoExplicit) { + try { + $resolved = (Resolve-Path -LiteralPath $activeRepoCandidate).ProviderPath + } catch { + Die "configured ACTIVE_REPO_ROOT does not exist: $activeRepoCandidate. Fix ACTIVE_REPO_ROOT or unset it to let this script clone EIPs." + } + + if (-not (Test-Path -LiteralPath $resolved -PathType Container)) { + Die "configured ACTIVE_REPO_ROOT is not a directory: $resolved. Fix ACTIVE_REPO_ROOT or unset it to let this script clone EIPs." + } + if (-not (Test-Path -LiteralPath (Join-Path -Path $resolved -ChildPath ".git"))) { + Die "configured ACTIVE_REPO_ROOT is not a git checkout: $resolved. Fix ACTIVE_REPO_ROOT or unset it to let this script clone EIPs." + } + + return @{ + Path = $resolved + Explicit = $true + } + } + + if (-not (Test-Path -LiteralPath $activeRepoCandidate)) { + $gitCommand = Get-Command git -CommandType Application -ErrorAction SilentlyContinue + if ($null -eq $gitCommand) { + Die "need git to clone default proposal repo from $DefaultActiveRepoUrl" + } + + Say "No active proposal repo found at $activeRepoCandidate." + Say "Cloning default proposal repo from $DefaultActiveRepoUrl" + Say "This may take a few minutes..." + & git clone $DefaultActiveRepoUrl $activeRepoCandidate + $GitCloneExitCode = $LASTEXITCODE + if ($GitCloneExitCode -ne 0) { + Die "failed to clone default proposal repo from $DefaultActiveRepoUrl to $activeRepoCandidate" + } + Say "Cloned default proposal repo to $activeRepoCandidate" + } + + try { + $resolved = (Resolve-Path -LiteralPath $activeRepoCandidate).ProviderPath + } catch { + Die "default active proposal repo path does not exist after clone: $activeRepoCandidate" + } + + if (-not (Test-Path -LiteralPath $resolved -PathType Container)) { + Die "default active proposal repo path exists but is not a directory: $resolved. Remove it or set ACTIVE_REPO_ROOT." + } + if (-not (Test-Path -LiteralPath (Join-Path -Path $resolved -ChildPath ".git"))) { + Die "default active proposal repo path exists but is not a git checkout: $resolved. Remove it or set ACTIVE_REPO_ROOT." + } + + return @{ + Path = $resolved + Explicit = $false + } +} + +function ConvertTo-PowerShellQuotedPath { + param([string]$Path) + + return "'{0}'" -f ($Path -replace "'", "''") +} + +function Get-DefaultInstallPaths { + if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { + Die "LOCALAPPDATA is not set; cannot determine the user-local install directory" + } + + $installDir = Join-Path -Path (Join-Path -Path $env:LOCALAPPDATA -ChildPath "build-eips") -ChildPath "bin" + $buildEipsPath = Join-Path -Path $installDir -ChildPath "build-eips.exe" + $zolaPath = Join-Path -Path $installDir -ChildPath "zola.exe" + + return @{ + InstallDir = $installDir + BuildEipsPath = $buildEipsPath + ZolaPath = $zolaPath + } +} + +$PathNotes = @() +$WorkspaceFlags = @() +if ($Template) { + $WorkspaceFlags += "--template" +} +if ($PlatformDev) { + $WorkspaceFlags += "--platform-dev" +} + +$InvocationDir = (Get-Location).ProviderPath +$ScriptDir = (Resolve-Path -LiteralPath $PSScriptRoot).ProviderPath +$ThemeRoot = (Resolve-Path -LiteralPath (Split-Path -Path $ScriptDir -Parent)).ProviderPath +$WorkspaceRoot = (Resolve-Path -LiteralPath (Split-Path -Path $ThemeRoot -Parent)).ProviderPath +$ActiveRepoInfo = Resolve-ActiveRepoRoot -InvocationDir $InvocationDir -WorkspaceRoot $WorkspaceRoot +$ActiveRepoRoot = $ActiveRepoInfo["Path"] +$ActiveRepoExplicit = $ActiveRepoInfo["Explicit"] + +$BuildEipsPath = Find-BuildEipsOnPath +if ($null -ne $BuildEipsPath) { + Say "Using existing build-eips at $BuildEipsPath" +} else { + $defaultPaths = Get-DefaultInstallPaths + $DefaultInstallDir = $defaultPaths.InstallDir + $DefaultBuildEipsPath = $defaultPaths.BuildEipsPath + + if (Test-Path -LiteralPath $DefaultBuildEipsPath -PathType Leaf) { + $BuildEipsPath = $DefaultBuildEipsPath + Move-DirectoryToFrontOfSessionPath -InstallDir $DefaultInstallDir + Say "Using existing build-eips at $BuildEipsPath" + } else { + $BuildEipsPath = Install-BuildEips -InstallDir $DefaultInstallDir -BuildEipsPath $DefaultBuildEipsPath + Move-DirectoryToFrontOfSessionPath -InstallDir $DefaultInstallDir + } +} + +$ZolaPath = Initialize-Zola + +Say "Theme repo: $ThemeRoot" +Say "Workspace root: $WorkspaceRoot" +Say "Active proposal repo: $ActiveRepoRoot" +Say "If PowerShell blocks this script, run:" +Say " powershell -ExecutionPolicy Bypass -File .\scripts\dev-setup.ps1" + +Say "Bootstrapping workspace at $WorkspaceRoot" +& $BuildEipsPath -C $ActiveRepoRoot init $WorkspaceRoot @WorkspaceFlags +$WorkspaceInitExitCode = $LASTEXITCODE +if ($WorkspaceInitExitCode -ne 0) { + Die "build-eips init failed with exit code $WorkspaceInitExitCode" +} + +Say "Running build-eips doctor" +& $BuildEipsPath -C $ActiveRepoRoot doctor +$WorkspaceDoctorExitCode = $LASTEXITCODE +if ($WorkspaceDoctorExitCode -ne 0) { + Say "Warning: build-eips doctor reported issues above. Fix them before relying on direct build-eips commands." +} + +$WorkspaceDocPath = Join-Path -Path $WorkspaceRoot -ChildPath "WORKSPACE.md" +Say "" +if (Test-Path -LiteralPath $WorkspaceDocPath -PathType Leaf) { + Say "Workspace docs: $WorkspaceDocPath (../WORKSPACE.md from this repo)" +} else { + Say "Warning: workspace docs were not found at $WorkspaceDocPath after build-eips init" +} + +if ($PathNotes.Count -gt 0) { + Say "" + Say 'Updated PATH for this PowerShell session only:' + foreach ($pathNote in $PathNotes) { + Say " $pathNote" + } + Say "To make this permanent, add the listed directory or directories to your user Path in Windows Environment Variables." +} + +Say "" +Say "Next commands:" +Say (" cd {0}" -f (ConvertTo-PowerShellQuotedPath -Path $ThemeRoot)) +Say (" build-eips -C {0} serve" -f (ConvertTo-PowerShellQuotedPath -Path $ActiveRepoRoot)) +Say (" build-eips -C {0} check" -f (ConvertTo-PowerShellQuotedPath -Path $ActiveRepoRoot)) +Say (" build-eips -C {0} doctor" -f (ConvertTo-PowerShellQuotedPath -Path $ActiveRepoRoot)) From 90097e642f1801802ef283d91db37169c731859c Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 18 May 2026 00:24:16 -0400 Subject: [PATCH 3/8] Add search route templates and Pagefind hooks Add the generated search route page and inert search state fallback chain. The theme ships data/build_eips_search.toml with enabled = false and a [extra.search] block in theme.toml. base.html prefers the project data file when build-eips has written one and falls back to the theme-level config for standalone theme consumption. The search route renders the mount, embeds search state as JSON for later runtime code, and shows a disabled-state message when search is off. No Pagefind browser assets are loaded here. Add the generated search route to the header navigation with a normal text link so desktop and mobile navigation have the route as soon as the route exists. Add the Pagefind indexing contract on proposal rendering. Mark the proposal article as the data-pagefind-body region and mark repeated chrome as data-pagefind-ignore, so the index covers proposal prose rather than taxonomy badges, preamble tables, the table of contents, status callouts, links, citations, or other repeated UI. Emit Pagefind filters for derived proposal_category, status, name-only authors, and created_year; meta for proposal id, title, description, status, type, category, author display strings, and created date; and sort keys for number and created date. Read the canonical proposal id from page.extra.proposal_id on rendered proposal pages, and add proposal_id values to the committed theme fixtures so standalone theme rendering produces the same ids that build-eips will write. Add two validation scripts. scripts/check-search-template covers the fallback chain by building downstream Zola fixtures for missing project data and project override cases. scripts/check-pagefind-indexing-hooks runs real Pagefind against rendered output and asserts that /search/, home, taxonomy pages, generic pages, and repeated chrome do not appear in the index. --- content/00020/index.md | 1 + content/00165.md | 1 + content/00214.md | 1 + content/00721.md | 1 + content/01155.md | 1 + content/03475/index.md | 1 + content/07201.md | 1 + data/build_eips_search.toml | 2 + scripts/check-pagefind-indexing-hooks | 397 ++++++++++++++++++++++++++ scripts/check-search-template | 143 ++++++++++ templates/base.html | 7 + templates/eip.html | 105 +++++-- templates/search.html | 28 ++ theme.toml | 4 + 14 files changed, 673 insertions(+), 20 deletions(-) create mode 100644 data/build_eips_search.toml create mode 100755 scripts/check-pagefind-indexing-hooks create mode 100755 scripts/check-search-template create mode 100644 templates/search.html diff --git a/content/00020/index.md b/content/00020/index.md index 768f9a1..679fc14 100644 --- a/content/00020/index.md +++ b/content/00020/index.md @@ -13,6 +13,7 @@ status = ["Final"] category = ["ERC"] [extra] +proposal_id = "ERC-20" status = "Final" type = "Standards Track" number = 20 diff --git a/content/00165.md b/content/00165.md index 9acf881..c8000ec 100644 --- a/content/00165.md +++ b/content/00165.md @@ -14,6 +14,7 @@ type = ["Standards Track"] category = ["ERC"] [extra] +proposal_id = "ERC-165" status = "Final" type = "Standards Track" number = 165 diff --git a/content/00214.md b/content/00214.md index 8ef9afc..568d1df 100644 --- a/content/00214.md +++ b/content/00214.md @@ -14,6 +14,7 @@ status = ["Final"] category = ["Core"] [extra] +proposal_id = "EIP-214" category = "Core" type = "Standards Track" number = 214 diff --git a/content/00721.md b/content/00721.md index 4c8a4c5..998a452 100644 --- a/content/00721.md +++ b/content/00721.md @@ -14,6 +14,7 @@ type = ["Standards Track"] category = ["ERC"] [extra] +proposal_id = "ERC-721" status = "Draft" discussions_to = "https://github.com/ethereum/eips/issues/721" category = "ERC" diff --git a/content/01155.md b/content/01155.md index c520eda..1555a03 100644 --- a/content/01155.md +++ b/content/01155.md @@ -14,6 +14,7 @@ status = ["Final"] category = ["ERC"] [extra] +proposal_id = "ERC-1155" requires = ["@/00165.md"] discussions_to = "https://github.com/ethereum/EIPs/issues/1155" category = "ERC" diff --git a/content/03475/index.md b/content/03475/index.md index 357107b..f8a8978 100644 --- a/content/03475/index.md +++ b/content/03475/index.md @@ -14,6 +14,7 @@ status = ["Final"] type = ["Standards Track"] [extra] +proposal_id = "ERC-3475" requires = ["@/00020/index.md", "@/00721.md", "@/01155.md"] discussions_to = "https://ethereum-magicians.org/t/eip-3475-multiple-callable-bonds-standard/8691" type = "Standards Track" diff --git a/content/07201.md b/content/07201.md index 2ef91a6..ee17661 100644 --- a/content/07201.md +++ b/content/07201.md @@ -14,6 +14,7 @@ category = ["ERC"] status = ["Final"] [extra] +proposal_id = "ERC-7201" status = "Final" number = 7201 discussions_to = "https://ethereum-magicians.org/t/eip-7201-namespaced-storage-layout/14796" diff --git a/data/build_eips_search.toml b/data/build_eips_search.toml new file mode 100644 index 0000000..f01055f --- /dev/null +++ b/data/build_eips_search.toml @@ -0,0 +1,2 @@ +enabled = false +base_path = "/" diff --git a/scripts/check-pagefind-indexing-hooks b/scripts/check-pagefind-indexing-hooks new file mode 100755 index 0000000..a3b30e1 --- /dev/null +++ b/scripts/check-pagefind-indexing-hooks @@ -0,0 +1,397 @@ +#!/bin/sh +set -eu + +die() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +say() { + printf '%s\n' "$*" +} + +assert_contains() { + file=$1 + expected=$2 + message=$3 + + if ! grep -Fq "$expected" "$file"; then + die "$message" + fi +} + +assert_not_contains() { + file=$1 + unexpected=$2 + message=$3 + + if grep -Fq "$unexpected" "$file"; then + die "$message" + fi +} + +assert_matches() { + file=$1 + expected_pattern=$2 + message=$3 + + if ! grep -Eq "$expected_pattern" "$file"; then + die "$message" + fi +} + +assert_html_attr_count() { + html_root=$1 + attr=$2 + expected=$3 + message=$4 + + count=$( + find "$html_root" -name '*.html' -exec grep -h -o "$attr" {} + \ + | wc -l \ + | tr -d ' ' + ) + if [ "$count" != "$expected" ]; then + die "$message; expected $expected, found $count" + fi +} + +assert_pagefind_artifact() { + path_pattern=$1 + message=$2 + + if ! find "$pagefind_dir" -path "$path_pattern" -type f | grep -q .; then + die "$message" + fi +} + +write_fixture_project() { + project_dir=$1 + + mkdir -p "$project_dir/content" "$project_dir/themes" + ln -s "$theme_root" "$project_dir/themes/eips-theme" + + cat >"$project_dir/config.toml" <<'EOF' +base_url = "https://example.test/" +title = "Pagefind Hook Fixture" +description = "Pagefind hook fixture" +theme = "eips-theme" +compile_sass = false +build_search_index = false +generate_feeds = false +taxonomies = [ + { name = "status" }, + { name = "category" }, + { name = "type" }, +] + +[extra.github] +eip_repository = "https://github.com/example/eips" + +[extra.taxonomies.status.final] +tooltip = "Final" + +[extra.taxonomies.category.core] +tooltip = "Core" + +[extra.taxonomies.type.standards-track] +tooltip = "Standards Track" +EOF + + cat >"$project_dir/content/_index.md" <<'EOF' ++++ +title = "Home" ++++ + +# Home + +UniqueHomeMarker +EOF + + cat >"$project_dir/content/search.md" <<'EOF' ++++ +title = "Search" +template = "search.html" ++++ + +UniqueSearchMarker +EOF + + cat >"$project_dir/content/about.md" <<'EOF' ++++ +title = "About" ++++ + +UniqueGenericMarker +EOF + + cat >"$project_dir/content/01559.md" <<'EOF' ++++ +title = "Fee Market Change" +description = "A transaction pricing mechanism." +date = 2021-04-13 +slug = "1559" +authors = ["Alice", "Bob", "Carol", "Dan"] +template = "eip.html" + +[taxonomies] +status = ["Final"] +type = ["Standards Track"] +category = ["Core"] + +[extra] +proposal_id = "EIP-1559" +prefix = "EIP" +number = 1559 +status = "Final" +type = "Standards Track" +category = "Core" +withdrawal_reason = "UniquePreambleMarker" + +[[extra.author_details]] +name = "Alice" +email = "alice@example.com" +display = "Alice " + +[[extra.author_details]] +name = "Bob" +github = "bob" +display = "Bob (@bob)" + +[[extra.author_details]] +name = "Carol" +github = "carol" +email = "carol@example.com" +display = "Carol (@carol) " + +[[extra.author_details]] +name = "Dan" +display = "Dan" ++++ + +## Abstract + +PhaseFourBodyMarker + +## Motivation + +This document should be searchable. +EOF +} + +build_fixture_project() { + project_dir=$1 + output_dir=$2 + + (cd "$project_dir" && "$zola" build --force -o "$output_dir" >/dev/null) +} + +assert_no_pagefind_browser_assets() { + html_file=$1 + + assert_not_contains "$html_file" "pagefind.js" "Phase 4 must not import pagefind.js from rendered pages" + assert_not_contains "$html_file" "pagefind-ui.js" "Phase 4 must not import Pagefind UI JavaScript" + assert_not_contains "$html_file" "pagefind-modular-ui.js" "Phase 4 must not import Pagefind modular UI JavaScript" + assert_not_contains "$html_file" "pagefind-highlight.js" "Phase 4 must not import Pagefind highlight JavaScript" + assert_not_contains "$html_file" "pagefind-ui.css" "Phase 4 must not import Pagefind UI CSS" + assert_not_contains "$html_file" "pagefind-highlight.css" "Phase 4 must not import Pagefind highlight CSS" +} + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +theme_root=$(CDPATH= cd -- "$script_dir/.." && pwd) +zola=${ZOLA:-zola} +pagefind=${PAGEFIND:-pagefind} +node=${NODE:-node} + +command -v "$zola" >/dev/null 2>&1 || die "zola is required on PATH" +command -v "$pagefind" >/dev/null 2>&1 || die "pagefind is required on PATH" +command -v "$node" >/dev/null 2>&1 || die "node is required on PATH" + +tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/eips-theme-pagefind-hooks.XXXXXX") +trap 'rm -rf "$tmp_root"' EXIT INT HUP TERM + +fixture_project=$tmp_root/project +fixture_output=$tmp_root/output +write_fixture_project "$fixture_project" +build_fixture_project "$fixture_project" "$fixture_output" + +proposal_html=$fixture_output/1559/index.html +search_html=$fixture_output/search/index.html +home_html=$fixture_output/index.html +generic_html=$fixture_output/about/index.html +taxonomy_html=$fixture_output/category/core/index.html + +[ -f "$proposal_html" ] || die "proposal fixture did not render /1559/" +[ -f "$search_html" ] || die "search fixture did not render /search/" +[ -f "$home_html" ] || die "home fixture did not render /" +[ -f "$generic_html" ] || die "generic fixture did not render /about/" +[ -f "$taxonomy_html" ] || die "taxonomy fixture did not render /category/core/" + +assert_html_attr_count "$fixture_output" 'data-pagefind-body' 1 "only the proposal page should carry data-pagefind-body" +assert_contains "$proposal_html" '
' "proposal page must put data-pagefind-body on the proposal article" +assert_contains "$proposal_html" 'EIP-1559: Fee Market Change' "proposal body must include the proposal id and title" +assert_contains "$proposal_html" 'PhaseFourBodyMarker' "proposal body must include rendered proposal markdown content" +assert_not_contains "$search_html" 'data-pagefind-body' "/search/ must not carry data-pagefind-body" +assert_not_contains "$home_html" 'data-pagefind-body' "home must not carry data-pagefind-body" +assert_not_contains "$generic_html" 'data-pagefind-body' "generic pages must not carry data-pagefind-body" +assert_not_contains "$taxonomy_html" 'data-pagefind-body' "taxonomy pages must not carry data-pagefind-body" + +assert_contains "$proposal_html" 'class="h5" data-pagefind-ignore' "proposal badge row must be ignored" +assert_contains "$proposal_html" 'class="table table-borderless preamble" data-pagefind-ignore' "proposal preamble table must be ignored" +assert_contains "$proposal_html" '