From 929f7ec4952170fcc91e84ee50207025006a7ba8 Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Sun, 3 May 2026 20:23:19 -0400 Subject: [PATCH 1/2] 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 0c959ec..5881b6c 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 e5f4b13b584c8b4befdabcfc312a659faa437b7a Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Sun, 21 Jun 2026 02:44:37 -0400 Subject: [PATCH 2/2] Add Theme Development Setup Add POSIX and PowerShell setup scripts for local theme development. The setup anchors build-eips through a manifest-bearing active proposal checkout, initializes the workspace, runs doctor, and keeps ACTIVE_REPO_ROOT available for ERCs or custom proposal checkouts. The docs describe the split between active repo Build.toml, workspace-local .build-eips.toml, and editable workspace/theme runtime behavior. --- README.md | 150 ++++++++++ scripts/dev-setup | 654 ++++++++++++++++++++++++++++++++++++++++++ scripts/dev-setup.ps1 | 628 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1432 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..9cb0e20 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# 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/ +``` + +Theme has no active proposal identity of its own, so the script anchors setup through `../EIPs` by default and clones `https://github.com/eips-wg/EIPs.git` there when that checkout is missing. The selected active proposal checkout must contain `Build.toml`. + +`Build.toml` is the active proposal repo manifest. It supplies the active and sibling proposal locations, rendered base URL, and `[theme]` source/pin used when `build-eips init` first creates `workspace/theme`. `.build-eips.toml` is workspace-local configuration for settings such as local server binding, base URL overrides, build roots, and targeted rendering. Runtime and editing use the local editable `workspace/theme` checkout; init does not reset an existing theme checkout. + +Fresh default setup depends on the EIPs rollout that adds `Build.toml` to the canonical remote. Until that rollout reaches the default branch, set `ACTIVE_REPO_ROOT` to a local manifest-bearing EIPs or ERCs 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` for that command. + +`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 and the active `Build.toml`. The local theme is `workspace/theme`, and local sibling repos are `workspace/`. The active manifest's `[theme]` supplies the initial theme source and pin when init creates a missing local theme checkout; runtime commands always use the editable local checkout. + +Use `--remote-siblings` when you need to force remote sibling proposal sources for a single command. `--remote-siblings` and `--build-root` are global options, so put them before the command name such as `build`, `serve`, or `preview`. + +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: an editable local build alongside a clean build that uses remote sibling sources. + +```bash +build-eips --build-root /tmp/eips-local -C /work/EIPs-project/EIPs build --base-url http://127.0.0.1:1111 +build-eips --build-root /tmp/eips-clean --remote-siblings -C /work/EIPs-project/EIPs build --clean --base-url http://127.0.0.1:1112 + +build-eips --build-root /tmp/eips-local -C /work/EIPs-project/EIPs preview --port 1111 +build-eips --build-root /tmp/eips-clean -C /work/EIPs-project/EIPs preview --port 1112 + +# Or using serve +build-eips --build-root /tmp/eips-local -C /work/EIPs-project/EIPs serve --port 1111 +build-eips --build-root /tmp/eips-clean --remote-siblings -C /work/EIPs-project/EIPs serve --clean --port 1112 +``` diff --git a/scripts/dev-setup b/scripts/dev-setup new file mode 100755 index 0000000..5f9f5ad --- /dev/null +++ b/scripts/dev-setup @@ -0,0 +1,654 @@ +#!/bin/sh + +set -eu + +DEFAULT_ACTIVE_REPO_URL=https://github.com/eips-wg/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) + + if [ ! -f "$ACTIVE_REPO_ROOT/Build.toml" ]; then + if [ "$ACTIVE_REPO_EXPLICIT" = true ]; then + die "configured ACTIVE_REPO_ROOT does not contain Build.toml: $ACTIVE_REPO_ROOT. Point ACTIVE_REPO_ROOT at a manifest-bearing EIPs or ERCs checkout." + fi + + die "default EIPs checkout at $ACTIVE_REPO_ROOT does not contain Build.toml. The eips-wg/EIPs default branch is awaiting the Build.toml rollout; set ACTIVE_REPO_ROOT to a manifest-bearing EIPs or ERCs checkout until it is available." + fi +} + +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..6eaca83 --- /dev/null +++ b/scripts/dev-setup.ps1 @@ -0,0 +1,628 @@ +param( + [switch]$Template, + [switch]$PlatformDev +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$DefaultActiveRepoUrl = "https://github.com/eips-wg/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 Assert-ActiveRepoManifest { + param( + [string]$ActiveRepoRoot, + [bool]$ActiveRepoExplicit + ) + + $manifestPath = Join-Path -Path $ActiveRepoRoot -ChildPath "Build.toml" + if (Test-Path -LiteralPath $manifestPath -PathType Leaf) { + return + } + + if ($ActiveRepoExplicit) { + Die "configured ACTIVE_REPO_ROOT does not contain Build.toml: $ActiveRepoRoot. Point ACTIVE_REPO_ROOT at a manifest-bearing EIPs or ERCs checkout." + } + + Die "default EIPs checkout at $ActiveRepoRoot does not contain Build.toml. The eips-wg/EIPs default branch is awaiting the Build.toml rollout; set ACTIVE_REPO_ROOT to a manifest-bearing EIPs or ERCs checkout until it is available." +} + +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"] +Assert-ActiveRepoManifest -ActiveRepoRoot $ActiveRepoRoot -ActiveRepoExplicit $ActiveRepoExplicit + +$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))