Merge branch 'feat/branding-toolkit'
CI / Build (push) Successful in 21s

Adopt the template's four-file branding toolkit under tools/, with this plugin's
identity as data rather than forked shell.
This commit is contained in:
2026-09-02 16:39:12 +09:00
6 changed files with 640 additions and 219 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ on:
- 'Cargo.toml' - 'Cargo.toml'
- 'Cargo.lock' - 'Cargo.lock'
- 'bundler.toml' - 'bundler.toml'
- 'check-branding.sh' - 'tools/**'
pull_request: pull_request:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
@@ -40,7 +40,7 @@ jobs:
# Cheap and needs no toolchain, so it runs first: a rename that only # Cheap and needs no toolchain, so it runs first: a rename that only
# landed in half the files is worth catching before spending a build on it. # landed in half the files is worth catching before spending a build on it.
- name: Branding consistency - name: Branding consistency
run: sh check-branding.sh run: sh tools/check-branding.sh
- name: Build - name: Build
run: cargo build --release run: cargo build --release
-217
View File
@@ -1,217 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# check-branding.sh - verify the plugin's branding/identity strings agree everywhere.
#
# Plugin branding is duplicated across the build manifest, the bundler manifest,
# the source, the docs and the deploy script. Nothing makes them agree, so a
# rename drifts silently: the bundle builds under one name and the deploy script
# looks for another. This asserts they match, and that the identity constants
# are legal.
#
# Kept in sync with musiikki/nih-plug-template's copy - fixes belong there
# first, then come back here. The differences are structural, not stylistic:
# this repo keeps its params in src/params.rs and wraps CLAP_DESCRIPTION.
#
# Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found)
#
# ---------------------------------------------------------------------------
# CANONICAL IDENTITY - the single source of truth. Edit these to rename, then
# run this script; every failure it prints is a place still holding the old name.
# ---------------------------------------------------------------------------
PKG="bari_206" # Cargo package name == bundler.toml table key
DISPLAY="Bari 206" # bundle name -> "<DISPLAY>.vst3" / "<DISPLAY>.clap"
PLUGIN_NAME="Bari 206" # Plugin::NAME - what the DAW displays
VENDOR="Novoyuuparosk" # Plugin::VENDOR - the org, not the person
EMAIL="mikkeli@novoyuuparosk.org" # Plugin::EMAIL
HOMEPAGE="https://github.com/mikkeli/codename-206" # Cargo homepage -> Plugin::URL
CLAP_ID="org.novoyuuparosk.bari-206" # CLAP_ID - stable plugin identity
VST3_CLASS_ID="Bari206Multiband" # VST3_CLASS_ID - stable identity, MUST be 16 bytes
DESCRIPTION="A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
CLAP_DESCRIPTION="Multiband compressor/limiter (stage 3: 3-band + 'All' channel)"
# Rust type names. Cosmetic - they never reach a host - but a rename that leaves
# half of them behind is the residue that says nobody read the file.
STRUCT="Bari206" # the Plugin impl target (src/lib.rs)
PARAMS_STRUCT="Bari206Params" # the Params impl target (src/params.rs)
# Old spellings retired by the "Codename 206" -> "Bari 206" rename, plus the new
# ones so the NEXT rename is self-checking too. Each entry is skipped while it is
# still one of the canonical values above and arms the moment it stops being one.
#
# Deliberately NOT listed: the bare "codename-206". The repo, its remote and the
# homepage URL all keep that name on purpose, so it is current branding, not
# residue. The retired CLAP ID is listed in full instead, which cannot collide
# with the homepage.
STALE="codename_206
Codename 206
Codename206
Codename206Params
Codename206Maxi!
206 prototype
com.mikkeli.codename-206
bari_206
Bari 206
Bari206
Bari206Params
Bari206Multiband
org.novoyuuparosk.bari-206"
set -u
cd "$(dirname "$0")" || exit 1
pass=0; fail=0; skip=0
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$1"; [ $# -gt 1 ] && printf ' %s\n' "$2"; return 0; }
note() { skip=$((skip+1)); printf ' skip %s\n' "$1"; }
# Reported for awareness only - never counted, never fails the run.
info() { printf ' -- %s\n' "$1"; }
# has FILE PATTERN -> fixed-string search
has() { grep -qF "$2" "$1" 2>/dev/null; }
check() { # check LABEL FILE LITERAL
if [ ! -f "$2" ]; then bad "$1" "missing file: $2"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
# Same as check, but a missing file is "not applicable yet" rather than drift.
# The template ships without a deploy script; this starts checking once you add one.
opt_check() { # opt_check LABEL FILE LITERAL
if [ ! -f "$2" ]; then note "$1 ($2 not present)"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
printf 'branding check (canonical: %s / "%s")\n\n' "$PKG" "$DISPLAY"
printf 'build manifests\n'
check "Cargo.toml package name" Cargo.toml "name = \"$PKG\""
check "Cargo.toml homepage" Cargo.toml "homepage = \"$HOMEPAGE\""
check "Cargo.toml description" Cargo.toml "description = \"$DESCRIPTION\""
check "bundler.toml table key" bundler.toml "[$PKG]"
check "bundler.toml display name" bundler.toml "name = \"$DISPLAY\""
printf '\nsource identity (src/lib.rs)\n'
check "Plugin::NAME" src/lib.rs "const NAME: &'static str = \"$PLUGIN_NAME\""
check "Plugin::VENDOR" src/lib.rs "const VENDOR: &'static str = \"$VENDOR\""
check "Plugin::EMAIL" src/lib.rs "const EMAIL: &'static str = \"$EMAIL\""
check "CLAP_ID" src/lib.rs "const CLAP_ID: &'static str = \"$CLAP_ID\""
# CLAP_DESCRIPTION wraps onto a second line here, so the const name and its
# value are never on one line. Match the value alone.
check "CLAP_DESCRIPTION" src/lib.rs "Some(\"$CLAP_DESCRIPTION\")"
check "VST3_CLASS_ID" src/lib.rs "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\""
# Plugin::URL is env!("CARGO_PKG_HOMEPAGE"), so the manifest is the only place
# it is spelled. Assert the indirection is still in place rather than hardcoded.
check "Plugin::URL from manifest" src/lib.rs "const URL: &'static str = env!(\"CARGO_PKG_HOMEPAGE\")"
# VST3 class IDs are a fixed 16-byte array. A shorter or longer literal is a
# compile error, but it is far clearer to say so here than to read rustc on it.
n=$(printf '%s' "$VST3_CLASS_ID" | wc -c | tr -d ' ')
if [ "$n" -eq 16 ]; then ok "VST3_CLASS_ID is 16 bytes"
else bad "VST3_CLASS_ID is 16 bytes" "got $n bytes: \"$VST3_CLASS_ID\""; fi
# CLAP IDs are reverse-DNS by convention and are compared verbatim by hosts, so
# a stray space or capital is a silent identity change between machines.
case "$CLAP_ID" in
*\ *|*[A-Z]*) bad "CLAP_ID is reverse-DNS safe" "no spaces or capitals allowed: \"$CLAP_ID\"" ;;
*.*) ok "CLAP_ID is reverse-DNS safe" ;;
*) bad "CLAP_ID is reverse-DNS safe" "expected dotted reverse-DNS, got: \"$CLAP_ID\"" ;;
esac
printf '\nstructural names (src/lib.rs)\n'
# Every site the type names appear. A rename that misses one is a compile error
# rather than silent drift, but naming the site is faster than reading rustc,
# and the Vst3Plugin impl sits behind a cfg so a default build never reports it.
check "struct $STRUCT" src/lib.rs "struct $STRUCT {"
check "impl Plugin" src/lib.rs "impl Plugin for $STRUCT {"
check "impl ClapPlugin" src/lib.rs "impl ClapPlugin for $STRUCT {"
check "impl Vst3Plugin" src/lib.rs "impl Vst3Plugin for $STRUCT {"
check "nih_export_clap!" src/lib.rs "nih_export_clap!($STRUCT);"
check "nih_export_vst3!" src/lib.rs "nih_export_vst3!($STRUCT);"
check "struct $PARAMS_STRUCT" src/params.rs "struct $PARAMS_STRUCT {"
check "impl Default for params" src/params.rs "impl Default for $PARAMS_STRUCT {"
check "params field type" src/lib.rs "params: Arc<$PARAMS_STRUCT>,"
printf '\ndocs\n'
opt_check "README bundle command" README.md "cargo xtask bundle $PKG"
printf '\nci\n'
# CI bundles by package name, so a rename that misses this file breaks the build
# rather than drifting quietly - but it breaks after a full compile, which is a slow
# way to learn about a typo. Checking it here costs nothing and fails in seconds.
opt_check "CI bundle command" .github/workflows/ci.yaml "cargo xtask bundle $PKG"
printf '\ndeploy script\n'
opt_check "deploy.ps1 default package" deploy.ps1 "\$Package = \"$PKG\""
opt_check "deploy.ps1 VST3 bundle" deploy.ps1 "\$vst3Name = \"$DISPLAY.vst3\""
opt_check "deploy.ps1 CLAP bundle" deploy.ps1 "\$clapName = \"$DISPLAY.clap\""
printf '\nstale spellings\n'
# File list for the scan below. Prefer git, so build output and any vendored
# tree stay out of it. Fall back to find when this is not a checkout - an
# exported tarball, or a CI step that fetched without git metadata - because
# `git ls-files` there returns nothing, and an empty list would report every
# stale spelling as absent. A check that passes hardest when it can see least
# is worse than no check.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
file_list() { git ls-files; }
scan_source="git"
else
file_list() { find . -path ./.git -prune -o -path ./target -prune -o -type f -print | sed 's|^\./||'; }
scan_source="find (not a git checkout)"
fi
info "scanning tracked files via $scan_source"
# Every canonical spelling concatenated, for the substring test below.
CANONICAL_BLOB="$PKG
$DESCRIPTION
$CLAP_DESCRIPTION
$STRUCT
$PARAMS_STRUCT
$DISPLAY
$PLUGIN_NAME
$VENDOR
$EMAIL
$HOMEPAGE
$CLAP_ID
$VST3_CLASS_ID"
# Fed by here-doc rather than a pipe on purpose: a piped `while` runs in a
# subshell, and the pass/fail counters it increments would be discarded.
while IFS= read -r s; do
[ -n "$s" ] || continue
# Still part of a canonical value? Then it is current branding, not a leftover.
# This is what lets the list ship pre-seeded: entries arm themselves as you
# rename. Substring, not equality, because placeholders nest - "your-domain"
# lives inside the canonical CLAP_ID, and "YOUR-USERNAME" inside the homepage.
# Over-skipping is the safe direction: a false FAIL on a name you still use
# would train you to ignore this section.
case "$CANONICAL_BLOB" in
*"$s"*) note "\"$s\" (still canonical)"; continue ;;
esac
# This script names every placeholder by definition, so it can never be a hit.
hits=$(file_list 2>/dev/null | while read -r f; do
[ "$f" = "check-branding.sh" ] || { [ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null; }
done)
if [ -z "$hits" ]; then ok "no occurrences of \"$s\""
else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi
done <<STALE_LIST
$STALE
STALE_LIST
# Template scaffolding, not drift: TODOs are legitimate while you are still
# building. Reported so a finished plugin does not quietly ship with them.
# `grep -c` prints 0 AND exits 1 when there are no matches, so `|| echo 0`
# would append a second zero and make the count unparseable. Swallow the
# status instead, and default an empty result (missing file) to zero.
todos=$(grep -c 'TODO' src/lib.rs 2>/dev/null || :)
[ -n "$todos" ] || todos=0
printf '\nscaffolding\n'
if [ "$todos" -eq 0 ]; then ok "no TODO markers left in src/lib.rs"
else info "$todos TODO marker(s) still in src/lib.rs (not a failure)"; fi
printf '\n%d passed, %d failed' "$pass" "$fail"
[ "$skip" -gt 0 ] && printf ', %d skipped' "$skip"
printf '\n'
[ "$fail" -eq 0 ] || exit 1
+355
View File
@@ -0,0 +1,355 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# brand-plugin.sh - rename this plugin, everywhere, in one go.
#
# check-branding.sh tells you what disagrees. This changes it. They share
# branding.conf: this script rewrites that file and every copy of the old values
# in tracked files, then runs the checker and reports what it says. A run that
# ends with the checker green is a rename that landed.
#
# Dry run by default:
# sh brand-plugin.sh --display "Bari 206"
# sh brand-plugin.sh --display "Bari 206" --vendor Novoyuuparosk --apply
#
# Options:
# --display NAME the product name, with spaces and capitals. Everything
# else that can be derived, is derived from this.
# --pkg NAME override the derived Cargo package name.
# --vendor NAME --email ADDR --homepage URL
# --description D --clap-description D
# --struct NAME override the derived Rust type name.
# --allow-identity-change permit CLAP_ID / VST3_CLASS_ID to move. See below.
# --apply actually write. Without it, nothing is modified.
set -u
# Resolve the repo root: these live in tools/, every path they touch is
# repo-relative, and they must behave the same wherever they are invoked from.
cd "$(dirname "$0")/.." || exit 1
[ -f tools/branding.conf ] || { printf 'tools/branding.conf not found\n' >&2; exit 1; }
# shellcheck disable=SC1091
. ./tools/branding.conf
OLD_PKG="$PKG"; OLD_DISPLAY="$DISPLAY"; OLD_PLUGIN_NAME="$PLUGIN_NAME"
OLD_VENDOR="$VENDOR"; OLD_EMAIL="$EMAIL"; OLD_HOMEPAGE="$HOMEPAGE"; OLD_AUTHOR="$AUTHOR"
OLD_DESCRIPTION="$DESCRIPTION"; OLD_CLAP_DESCRIPTION="$CLAP_DESCRIPTION"
OLD_CLAP_ID="$CLAP_ID"; OLD_VST3="$VST3_CLASS_ID"
OLD_STRUCT="$STRUCT"; OLD_PARAMS_STRUCT="$PARAMS_STRUCT"
new_display=""; new_pkg=""; new_vendor=""; new_email=""; new_homepage=""
new_description=""; new_clap_description=""; new_struct=""; new_author=""
new_clap_id=""; new_vst3=""; apply=0; allow_identity=0
while [ $# -gt 0 ]; do
case "$1" in
--display) new_display="${2:?}"; shift 2 ;;
--pkg) new_pkg="${2:?}"; shift 2 ;;
--vendor) new_vendor="${2:?}"; shift 2 ;;
--author) new_author="${2:?}"; shift 2 ;;
--email) new_email="${2:?}"; shift 2 ;;
--homepage) new_homepage="${2:?}"; shift 2 ;;
--description) new_description="${2:?}"; shift 2 ;;
--clap-description) new_clap_description="${2:?}"; shift 2 ;;
--struct) new_struct="${2:?}"; shift 2 ;;
--clap-id) new_clap_id="${2:?}"; shift 2 ;;
--vst3-class-id) new_vst3="${2:?}"; shift 2 ;;
--allow-identity-change) allow_identity=1; shift ;;
--apply) apply=1; shift ;;
-h|--help) sed -n '3,25p' "$0"; exit 0 ;;
*) printf 'unknown option: %s\n' "$1" >&2; exit 2 ;;
esac
done
# Interactive when nothing was passed and there is a terminal to ask at. Every
# prompt offers the current value; Enter keeps it, so a run that answers nothing
# is a no-op rather than a surprise. Optional fields additionally accept "-" to
# clear them.
ask() { # ask VARNAME PROMPT CURRENT
printf ' %s [%s]: ' "$2" "$3" > /dev/tty
IFS= read -r _reply < /dev/tty || _reply=""
[ -n "$_reply" ] || _reply="$3"
[ "$_reply" = "-" ] && _reply=""
eval "$1=\$_reply"
}
if [ -z "$new_display" ] && [ -t 0 ] && [ -c /dev/tty ]; then
printf 'branding this plugin - Enter keeps the current value, "-" clears an optional one\n\n'
ask new_display "display name " "$OLD_DISPLAY"
ask new_vendor "vendor (org) " "$OLD_VENDOR"
ask new_author "cargo author " "$OLD_AUTHOR"
ask new_email "email " "$OLD_EMAIL"
ask new_homepage "homepage " "$OLD_HOMEPAGE"
ask new_description "cargo description" "$OLD_DESCRIPTION"
ask new_clap_description "clap description" "$OLD_CLAP_DESCRIPTION"
printf '\n'
fi
[ -n "$new_display" ] || { printf 'nothing to do: pass --display "New Name" (or run interactively)\n' >&2; exit 2; }
# --- derivation -------------------------------------------------------------
# Package name: lowercase, every run of non-alphanumerics becomes one separator,
# ends trimmed. Cargo accepts both hyphens and underscores in a package name and
# converts to underscores for the built artifact, which nih_plug_xtask already
# accounts for; underscores are used here to match the Rust convention.
derive_pkg() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -e 's/[^a-z0-9]\{1,\}/_/g' -e 's/^_//' -e 's/_$//'
}
# Rust type name: CamelCase, non-alphanumerics dropped entirely.
derive_struct() {
printf '%s' "$1" | sed -e 's/[^A-Za-z0-9]\{1,\}/ /g' \
| awk '{for(i=1;i<=NF;i++) printf "%s%s", toupper(substr($i,1,1)), substr($i,2); print ""}'
}
[ -n "$new_pkg" ] || new_pkg=$(derive_pkg "$new_display")
[ -n "$new_struct" ] || new_struct=$(derive_struct "$new_display")
new_params_struct="${new_struct}Params"
[ -n "$new_vendor" ] || new_vendor="$OLD_VENDOR"
[ -n "$new_author" ] || new_author="$OLD_AUTHOR"
[ -n "$new_email" ] || new_email="$OLD_EMAIL"
[ -n "$new_homepage" ] || new_homepage="$OLD_HOMEPAGE"
[ -n "$new_description" ] || new_description="$OLD_DESCRIPTION"
[ -n "$new_clap_description" ] || new_clap_description="$OLD_CLAP_DESCRIPTION"
# --- validation -------------------------------------------------------------
fail=0
die() { printf ' INVALID %s\n' "$1"; fail=$((fail+1)); }
case "$new_pkg" in
"") die "derived package name is empty" ;;
[0-9]*) die "package name \"$new_pkg\" starts with a digit - cargo rejects this outright" ;;
esac
case "$new_pkg" in
test|core|std|alloc|proc_macro)
die "package name \"$new_pkg\" collides with a built-in crate" ;;
as|break|const|continue|crate|dyn|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|static|struct|super|trait|true|type|unsafe|use|where|while|async|await|box|do|final|macro|override|priv|typeof|unsized|virtual|yield|try)
die "package name \"$new_pkg\" is a Rust keyword - cargo rejects this" ;;
esac
# bundler.toml uses the package name as a TOML bare key.
case "$new_pkg" in *[!a-zA-Z0-9_-]*) die "package name \"$new_pkg\" is not a valid TOML bare key" ;; esac
case "$new_struct" in ""|[0-9]*) die "derived type name \"$new_struct\" is not a valid Rust identifier" ;; esac
# --- frozen identity --------------------------------------------------------
identity_note=""
if [ -n "$new_clap_id" ] || [ -n "$new_vst3" ]; then
if [ "$allow_identity" -eq 0 ]; then
die "CLAP_ID / VST3_CLASS_ID are frozen identity; pass --allow-identity-change to move them"
fi
fi
[ -n "$new_clap_id" ] || new_clap_id="$OLD_CLAP_ID"
[ -n "$new_vst3" ] || new_vst3="$OLD_VST3"
# VST3_CLASS_ID is a fixed 16-byte array, so it cannot simply be derived. Propose
# one from the new name and let a human decide - it is frozen the moment it ships.
proposed_vst3=$(printf '%s' "${new_struct}Plugin000000000000" | cut -c1-16)
n=$(printf '%s' "$new_vst3" | wc -c | tr -d ' ')
[ "$n" -eq 16 ] || die "VST3_CLASS_ID \"$new_vst3\" is $n bytes, must be exactly 16"
case "$new_clap_id" in
*\ *|*[A-Z]*) die "CLAP_ID \"$new_clap_id\" must be lowercase with no spaces" ;;
*.*) ;;
*) die "CLAP_ID \"$new_clap_id\" should be dotted reverse-DNS" ;;
esac
# CLAP_DESCRIPTION is user-facing: keep development history out of it.
for hist in "$CODENAME" "$OLD_DISPLAY" "$OLD_PKG"; do
case "$new_clap_description" in
*"$hist"*) die "CLAP_DESCRIPTION still mentions \"$hist\" - it is shown to users, who never knew the development name" ;;
esac
done
# --- plan -------------------------------------------------------------------
printf 'rename plan\n\n'
row() { [ "$2" = "$3" ] && printf ' %-18s %s (unchanged)\n' "$1" "$2" || printf ' %-18s %s -> %s\n' "$1" "$2" "$3"; }
row "package" "$OLD_PKG" "$new_pkg"
row "display" "$OLD_DISPLAY" "$new_display"
row "Plugin::NAME" "$OLD_PLUGIN_NAME" "$new_display"
row "vendor" "$OLD_VENDOR" "$new_vendor"
row "cargo author" "$OLD_AUTHOR" "$new_author"
row "email" "$OLD_EMAIL" "$new_email"
row "homepage" "$OLD_HOMEPAGE" "$new_homepage"
row "description" "$OLD_DESCRIPTION" "$new_description"
row "clap desc" "$OLD_CLAP_DESCRIPTION" "$new_clap_description"
row "struct" "$OLD_STRUCT" "$new_struct"
row "params struct" "$OLD_PARAMS_STRUCT" "$new_params_struct"
row "CLAP_ID" "$OLD_CLAP_ID" "$new_clap_id"
row "VST3_CLASS_ID" "$OLD_VST3" "$new_vst3"
printf '\n repo codename stays %s - fixed at creation, never renamed\n' "$CODENAME"
if [ "$new_vst3" = "$OLD_VST3" ]; then
printf ' suggested VST3_CLASS_ID for this name (16 bytes): "%s"\n' "$proposed_vst3"
printf ' apply with --vst3-class-id "%s" --allow-identity-change, and only before first release\n' "$proposed_vst3"
fi
if [ "$fail" -ne 0 ]; then
printf '\n%d problem(s); nothing written.\n' "$fail"
exit 1
fi
if [ "$apply" -eq 0 ]; then
printf '\nDry run. Re-run with --apply to write these changes.\n'
exit 0
fi
# --- apply ------------------------------------------------------------------
# Three scratch files, one trap: the regenerated site table, the rows carried
# through from the old one, and the value-substitution pairs.
gen="$(mktemp)"; keep="$(mktemp)"; subst_file="$(mktemp)"
trap 'rm -f "$gen" "$keep" "$subst_file"' EXIT
T="$(printf '\t')"
{
printf 'req%spkg.cargo%sCargo.toml%sname = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'req%spkg.bundler%sbundler.toml%s[%s]\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.lock%sCargo.lock%sname = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.ci%s.github/workflows/ci.yaml%scargo xtask bundle %s\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.readme%sREADME.md%scargo xtask bundle %s\n' "$T" "$T" "$T" "$new_pkg"
printf 'req%sdisplay.bundler%sbundler.toml%sname = "%s"\n' "$T" "$T" "$T" "$new_display"
printf 'opt%sdisplay.notice%sNOTICE%s%s\n' "$T" "$T" "$T" "$new_display"
printf 'req%sname.plugin%s%s%sconst NAME: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_display"
printf 'req%svendor.plugin%s%s%sconst VENDOR: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_vendor"
printf 'req%semail.plugin%s%s%sconst EMAIL: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_email"
printf 'req%sauthors.cargo%sCargo.toml%sauthors = ["%s <%s>"]\n' "$T" "$T" "$T" "$new_author" "$new_email"
printf 'req%shomepage.cargo%sCargo.toml%shomepage = "%s"\n' "$T" "$T" "$T" "$new_homepage"
printf 'req%surl.plugin%s%s%sconst URL: &'"'"'static str = env!("CARGO_PKG_HOMEPAGE")\n' "$T" "$T" "$LIB_FILE" "$T"
printf 'req%sdescription.cargo%sCargo.toml%sdescription = "%s"\n' "$T" "$T" "$T" "$new_description"
# Narrowest distinctive fragment, not the whole statement: rustfmt wraps a long
# CLAP_DESCRIPTION onto its own line, and a pattern spanning the assignment
# would then never match a correctly formatted file.
printf 'req%sdescription.clap%s%s%sSome("%s")\n' "$T" "$T" "$LIB_FILE" "$T" "$new_clap_description"
printf 'req%sclapid.plugin%s%s%sconst CLAP_ID: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_clap_id"
printf 'req%svst3id.plugin%s%s%sconst VST3_CLASS_ID: [u8; 16] = *b"%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_vst3"
printf 'req%sstruct.def%s%s%sstruct %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.plugin%s%s%simpl Plugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.clap%s%s%simpl ClapPlugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.vst3%s%s%simpl Vst3Plugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.exportclap%s%s%snih_export_clap!(%s);\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.exportvst3%s%s%snih_export_vst3!(%s);\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sparams.def%s%s%sstruct %s {\n' "$T" "$T" "$PARAMS_FILE" "$T" "$new_params_struct"
printf 'req%sparams.default%s%s%simpl Default for %s {\n' "$T" "$T" "$PARAMS_FILE" "$T" "$new_params_struct"
printf 'req%sparams.field%s%s%sparams: Arc<%s>,\n' "$T" "$T" "$LIB_FILE" "$T" "$new_params_struct"
printf 'opt%sdeploy.pkg%sdeploy.ps1%s$Package = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%sdeploy.vst3%sdeploy.ps1%s$vst3Name = "%s.vst3"\n' "$T" "$T" "$T" "$new_display"
printf 'opt%sdeploy.clap%sdeploy.ps1%s$clapName = "%s.clap"\n' "$T" "$T" "$T" "$new_display"
} > "$gen"
# Pass 1 - site-directed. Each owned row in the existing table carries the OLD
# resolved literal for its site; the regenerated row carries the new one. Rewriting
# by that pair is unambiguous, which plain value substitution is not: AUTHOR
# defaults to VENDOR, so both fields can hold the same old string and a global
# replace cannot tell which occurrence belongs to which. This must run BEFORE
# pass 2, or the global replace consumes the old literals first and leaves the
# precise rewrite with nothing to match.
sites="tools/branding-sites.tsv"
T="$(printf '\t')"
if [ -f "$sites" ]; then
while IFS="$T" read -r ok_ oo_ of_ op_; do
case "$ok_" in ''|\#*) continue ;; esac
np_=$(awk -F"$T" -v o="$oo_" '$2==o {print $4; exit}' "$gen")
[ -n "$np_" ] || continue
[ "$np_" != "$op_" ] || continue
[ -f "$of_" ] || continue
esc_o=$(printf '%s' "$op_" | sed 's/[][\.*^$/|&]/\\&/g')
esc_n=$(printf '%s' "$np_" | sed 's/[\\/&|]/\\&/g')
tmp_=$(mktemp)
sed "s|$esc_o|$esc_n|g" "$of_" > "$tmp_" && cat "$tmp_" > "$of_"
rm -f "$tmp_"
done < "$sites"
printf ' pass 1: rewrote known sites from the table\n'
fi
# Pass 2 - value substitution, for everything the table does not enumerate:
# doc comments, use sites in other modules, prose in the README.
# Longest values first: a shorter spelling can be a substring of a longer one
# (the package name inside a URL, say), and replacing it first would corrupt the
# longer match. branding.conf is excluded and rewritten wholesale below, so its
# STALE list is never rewritten into meaninglessness.
{
printf '%s\t%s\n' "$OLD_CLAP_ID" "$new_clap_id"
printf '%s\t%s\n' "$OLD_HOMEPAGE" "$new_homepage"
printf '%s\t%s\n' "$OLD_CLAP_DESCRIPTION" "$new_clap_description"
printf '%s\t%s\n' "$OLD_DESCRIPTION" "$new_description"
printf '%s\t%s\n' "$OLD_PARAMS_STRUCT" "$new_params_struct"
printf '%s\t%s\n' "$OLD_VST3" "$new_vst3"
printf '%s\t%s\n' "$OLD_PLUGIN_NAME" "$new_display"
printf '%s\t%s\n' "$OLD_DISPLAY" "$new_display"
printf '%s\t%s\n' "$OLD_STRUCT" "$new_struct"
printf '%s\t%s\n' "$OLD_EMAIL" "$new_email"
printf '%s\t%s\n' "$OLD_AUTHOR" "$new_author"
printf '%s\t%s\n' "$OLD_VENDOR" "$new_vendor"
printf '%s\t%s\n' "$OLD_PKG" "$new_pkg"
} | awk -F'\t' '$1 != $2 { print length($1)"\t"$0 }' | sort -rn | cut -f2- > "$subst_file"
changed=0
git ls-files 2>/dev/null | while IFS= read -r f; do
[ -f "$f" ] || continue
# branding.conf alone is excluded: it is rewritten wholesale below, and its
# STALE list names every retired spelling by design. The site table IS
# substituted, so hand-added custom rows track the rename; owned rows are
# regenerated afterwards regardless.
case "$f" in tools/branding.conf) continue ;; esac
before=$(cat "$f")
after="$before"
while IFS="$(printf '\t')" read -r old new; do
[ -n "$old" ] || continue
after=$(printf '%s' "$after" | sed "s|$(printf '%s' "$old" | sed 's/[][\.*^$/|&]/\\&/g')|$(printf '%s' "$new" | sed 's/[\\/&|]/\\&/g')|g")
done < "$subst_file"
if [ "$before" != "$after" ]; then printf '%s\n' "$after" > "$f"; printf ' rewrote %s\n' "$f"; fi
done
# branding.conf last, with the retired spellings appended to STALE so the next
# run of the checker sweeps for anything this pass missed.
# Deduplicated: two fields can retire to the same spelling - DISPLAY and
# PLUGIN_NAME are commonly identical - and listing it twice makes the checker
# report the same drift twice. Note that neither this script nor the checker
# contains a branding literal anywhere: both read every name from
# branding.conf, so a rename never has to rewrite its own tooling.
retired=$(cut -f1 "$subst_file" | awk '!seen[$0]++')
sed -e "s|^PKG=.*|PKG=\"$new_pkg\"|" \
-e "s|^DISPLAY=.*|DISPLAY=\"$new_display\"|" \
-e "s|^PLUGIN_NAME=.*|PLUGIN_NAME=\"$new_display\"|" \
-e "s|^VENDOR=.*|VENDOR=\"$new_vendor\"|" \
-e "s|^# *AUTHOR=.*|AUTHOR=\"$new_author\"|" \
-e "s|^AUTHOR=.*|AUTHOR=\"$new_author\"|" \
-e "s|^EMAIL=.*|EMAIL=\"$new_email\"|" \
-e "s|^HOMEPAGE=.*|HOMEPAGE=\"$new_homepage\"|" \
-e "s|^DESCRIPTION=.*|DESCRIPTION=\"$new_description\"|" \
-e "s|^CLAP_DESCRIPTION=.*|CLAP_DESCRIPTION=\"$new_clap_description\"|" \
-e "s|^CLAP_ID=.*|CLAP_ID=\"$new_clap_id\"|" \
-e "s|^VST3_CLASS_ID=.*|VST3_CLASS_ID=\"$new_vst3\"|" \
-e "s|^STRUCT=.*|STRUCT=\"$new_struct\"|" \
-e "s|^PARAMS_STRUCT=.*|PARAMS_STRUCT=\"$new_params_struct\"|" \
tools/branding.conf > tools/branding.conf.new
if [ -n "$retired" ]; then
{ sed '$ s/"$//' tools/branding.conf.new; printf '%s"\n' "$retired"; } > tools/branding.conf
else
# Nothing retired (a no-op rename, or a regeneration): leave STALE untouched
# rather than appending a blank line to it on every run.
cat tools/branding.conf.new > tools/branding.conf
fi
rm -f tools/branding.conf.new
if [ "$new_author" = "$new_vendor" ]; then
# Identical to VENDOR, so re-comment it: the conf should keep showing that this
# value derives rather than pinning a duplicate that then has to be maintained.
tmp_c=$(mktemp)
sed "s|^AUTHOR=.*|# AUTHOR=\"$new_author\"|" tools/branding.conf > "$tmp_c" && cat "$tmp_c" > tools/branding.conf
rm -f "$tmp_c"
fi
printf ' rewrote tools/branding.conf (retired spellings appended to STALE)\n'
# Regenerate the site table. Rows whose owner this script recognises are
# rewritten with the new resolved literals; every other row - anything marked
# custom, or added by hand for a file this template does not ship - is carried
# through untouched. Clobbering those silently is how a tool stops being trusted.
sites="tools/branding-sites.tsv"
owned=$(cut -f2 "$gen")
if [ -f "$sites" ]; then
grep '^#' "$sites" > "$keep" 2>/dev/null || : > "$keep"
while IFS="$T" read -r k o f pat; do
case "$k" in ''|\#*) continue ;; esac
printf '%s\n' "$owned" | grep -qxF "$o" || printf '%s%s%s%s%s%s%s\n' "$k" "$T" "$o" "$T" "$f" "$T" "$pat" >> "$keep"
done < "$sites"
else
: > "$keep"
fi
{ cat "$keep"; cat "$gen"; } > "$sites"
printf ' rewrote %s (%s owned rows, %s preserved)\n' "$sites" "$(wc -l < "$gen" | tr -d ' ')" "$(grep -cv '^#' "$keep" 2>/dev/null || echo 0)"
printf '\nverifying with check-branding.sh\n\n'
sh tools/check-branding.sh
+29
View File
@@ -0,0 +1,29 @@
req pkg.cargo Cargo.toml name = "bari_206"
req pkg.bundler bundler.toml [bari_206]
opt pkg.lock Cargo.lock name = "bari_206"
opt pkg.ci .github/workflows/ci.yaml cargo xtask bundle bari_206
opt pkg.readme README.md cargo xtask bundle bari_206
req display.bundler bundler.toml name = "Bari 206"
opt display.notice NOTICE Bari 206
req name.plugin src/lib.rs const NAME: &'static str = "Bari 206"
req vendor.plugin src/lib.rs const VENDOR: &'static str = "Novoyuuparosk"
req email.plugin src/lib.rs const EMAIL: &'static str = "mikkeli@novoyuuparosk.org"
req authors.cargo Cargo.toml authors = ["Mikkeli Matlock <mikkeli@novoyuuparosk.org>"]
req homepage.cargo Cargo.toml homepage = "https://github.com/mikkeli/codename-206"
req url.plugin src/lib.rs const URL: &'static str = env!("CARGO_PKG_HOMEPAGE")
req description.cargo Cargo.toml description = "A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
req description.clap src/lib.rs Some("Multiband compressor/limiter (stage 3: 3-band + 'All' channel)")
req clapid.plugin src/lib.rs const CLAP_ID: &'static str = "org.novoyuuparosk.bari-206"
req vst3id.plugin src/lib.rs const VST3_CLASS_ID: [u8; 16] = *b"Bari206Multiband"
req struct.def src/lib.rs struct Bari206 {
req struct.plugin src/lib.rs impl Plugin for Bari206 {
req struct.clap src/lib.rs impl ClapPlugin for Bari206 {
req struct.vst3 src/lib.rs impl Vst3Plugin for Bari206 {
req struct.exportclap src/lib.rs nih_export_clap!(Bari206);
req struct.exportvst3 src/lib.rs nih_export_vst3!(Bari206);
req params.def src/params.rs struct Bari206Params {
req params.default src/params.rs impl Default for Bari206Params {
req params.field src/lib.rs params: Arc<Bari206Params>,
opt deploy.pkg deploy.ps1 $Package = "bari_206"
opt deploy.vst3 deploy.ps1 $vst3Name = "Bari 206.vst3"
opt deploy.clap deploy.ps1 $clapName = "Bari 206.clap"
Can't render this file because it contains an unexpected character in line 1 and column 33.
+66
View File
@@ -0,0 +1,66 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# branding.conf - this plugin's canonical identity.
#
# Managed by tools/brand-plugin.sh, read by tools/check-branding.sh. Hand-editing
# works and is sometimes the quickest thing, but the helper is what keeps this
# file, the source tree and tools/branding-sites.tsv agreeing with each other.
# Edit here and you are on the hook for running the checker afterwards.
# --- derivations ------------------------------------------------------------
_derive_pkg() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -e 's/[^a-z0-9]\{1,\}/_/g' -e 's/^_//' -e 's/_$//'
}
_derive_struct() {
printf '%s' "$1" | sed -e 's/[^A-Za-z0-9]\{1,\}/ /g' \
| awk '{for(i=1;i<=NF;i++) printf "%s%s", toupper(substr($i,1,1)), substr($i,2); print ""}'
}
# --- the names ---------------------------------------------------------------
PLUGIN_NAME="Bari 206"
VENDOR="Novoyuuparosk"
EMAIL="mikkeli@novoyuuparosk.org"
AUTHOR="Mikkeli Matlock"
HOMEPAGE="https://github.com/mikkeli/codename-206"
DESCRIPTION="A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
# What the plugin does *now*. No codename, no development stage.
CLAP_DESCRIPTION="Multiband compressor/limiter (stage 3: 3-band + 'All' channel)"
# --- frozen identity ---------------------------------------------------------
# Hosts key saved projects and presets off these two. Never move them after the
# first release: every existing session silently loses the plugin.
CLAP_ID="org.novoyuuparosk.bari-206"
VST3_CLASS_ID="Bari206Multiband"
# --- fixed at repo creation --------------------------------------------------
# The repo keeps its development codename while the plugin carries the product
# name. Recorded only so it can be kept out of user-facing text.
CODENAME="codename-206"
LIB_FILE="src/lib.rs"
PARAMS_FILE="src/params.rs"
# --- derived unless overridden -----------------------------------------------
# PKG, STRUCT and PARAMS_STRUCT all derive correctly from PLUGIN_NAME here, so
# none is pinned. DISPLAY defaults to PLUGIN_NAME.
: "${AUTHOR:=$VENDOR}"
: "${DISPLAY:=$PLUGIN_NAME}"
: "${PKG:=$(_derive_pkg "$PLUGIN_NAME")}"
: "${STRUCT:=$(_derive_struct "$PLUGIN_NAME")}"
: "${PARAMS_STRUCT:=${STRUCT}Params}"
# --- retired spellings -------------------------------------------------------
STALE="codename_206
Codename 206
Codename206
Codename206Params
Codename206Maxi!
206 prototype
com.mikkeli.codename-206
bari_206
Bari 206
Bari206
Bari206Params
Bari206Multiband
org.novoyuuparosk.bari-206"
+188
View File
@@ -0,0 +1,188 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# check-branding.sh - verify the plugin's branding/identity strings agree everywhere.
#
# Plugin branding is duplicated across the build manifest, the bundler manifest,
# the source, the docs and (once you add one) the deploy script. Nothing makes
# them agree, so a rename drifts silently: the bundle builds under one name and
# the deploy script looks for another. This asserts they match, and that the
# identity constants are legal.
#
# In a template this matters more than usual: renaming is the first thing you do
# with this repo, and it touches every file below. Run this immediately after,
# and the STALE machinery will list whatever you missed.
#
# Usage: sh tools/check-branding.sh (exit 0 = consistent, 1 = drift found)
#
# The canonical identity lives in branding.conf, shared with brand-plugin.sh.
# Edit it there, then run this to find every copy that still holds the old value.
set -u
# Resolve the repo root: these live in tools/, every path they touch is
# repo-relative, and they must behave the same wherever they are invoked from.
cd "$(dirname "$0")/.." || exit 1
if [ ! -f tools/branding.conf ]; then
printf 'tools/branding.conf not found\n' >&2
exit 1
fi
# shellcheck disable=SC1091
. ./tools/branding.conf
# Every canonical spelling concatenated, for the substring test below.
CANONICAL_BLOB="$PKG
$DESCRIPTION
$CLAP_DESCRIPTION
$STRUCT
$PARAMS_STRUCT
$DISPLAY
$PLUGIN_NAME
$VENDOR
$EMAIL
$HOMEPAGE
$CLAP_ID
$VST3_CLASS_ID"
pass=0; fail=0; skip=0
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$1"; [ $# -gt 1 ] && printf ' %s\n' "$2"; return 0; }
note() { skip=$((skip+1)); printf ' skip %s\n' "$1"; }
# Reported for awareness only - never counted, never fails the run.
info() { printf ' -- %s\n' "$1"; }
# has FILE PATTERN -> fixed-string search
has() { grep -qF "$2" "$1" 2>/dev/null; }
check() { # check LABEL FILE LITERAL
if [ ! -f "$2" ]; then bad "$1" "missing file: $2"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
# Same as check, but a missing file is "not applicable yet" rather than drift.
# The template ships without a deploy script; this starts checking once you add one.
opt_check() { # opt_check LABEL FILE LITERAL
if [ ! -f "$2" ]; then note "$1 ($2 not present)"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
printf 'branding check (canonical: %s / "%s")\n\n' "$PKG" "$DISPLAY"
# --- consistency: every site in the table holds its resolved literal ----------
# The table is data, so adding a site is a row rather than a shell edit, and the
# same script runs unmodified across every plugin built from this template.
SITES="tools/branding-sites.tsv"
if [ ! -f "$SITES" ]; then
bad "site table" "$SITES not found - run tools/brand-plugin.sh to generate it"
else
printf 'sites\n'
while IFS="$(printf '\t')" read -r kind owner file pattern; do
case "$kind" in ''|\#*) continue ;; esac
[ -n "$pattern" ] || continue
label="$owner"
if [ ! -f "$file" ]; then
case "$kind" in
opt) note "$label ($file not present)" ;;
*) bad "$label" "missing file: $file" ;;
esac
continue
fi
if has "$file" "$pattern"; then ok "$label"
else bad "$label" "$file does not contain: $pattern"; fi
done < "$SITES"
fi
# --- validity: rules the table cannot express --------------------------------
printf '\nvalidity\n'
# VST3 class IDs are a fixed 16-byte array. A wrong length is a compile error,
# but saying so here is clearer than reading rustc on it.
n=$(printf '%s' "$VST3_CLASS_ID" | wc -c | tr -d ' ')
if [ "$n" -eq 16 ]; then ok "VST3_CLASS_ID is 16 bytes"
else bad "VST3_CLASS_ID is 16 bytes" "got $n bytes: \"$VST3_CLASS_ID\""; fi
# CLAP IDs are reverse-DNS by convention and compared verbatim by hosts, so a
# stray space or capital is a silent identity change between machines.
case "$CLAP_ID" in
*\ *|*[A-Z]*) bad "CLAP_ID is reverse-DNS safe" "no spaces or capitals allowed: \"$CLAP_ID\"" ;;
*.*) ok "CLAP_ID is reverse-DNS safe" ;;
*) bad "CLAP_ID is reverse-DNS safe" "expected dotted reverse-DNS, got: \"$CLAP_ID\"" ;;
esac
# An overridden package name is legitimate - the derivation is sometimes invalid
# - but a display rename that forgot the package is not, and looks identical
# from here. Reported, never failed.
derived_pkg=$(_derive_pkg "$PLUGIN_NAME")
[ "$PKG" = "$derived_pkg" ] || info "PKG \"$PKG\" differs from the derivation of PLUGIN_NAME (\"$derived_pkg\") - fine if deliberate"
# CLAP_DESCRIPTION is shown to users in a host's plugin browser. "Free of
# development history" is a judgement a script cannot make, but the failure it
# produces is mechanical: a retired name or the repo codename surviving in it.
hygiene_hits=""
while IFS= read -r s_entry; do
[ -n "$s_entry" ] || continue
case "$CANONICAL_BLOB" in *"$s_entry"*) continue ;; esac
case "$CLAP_DESCRIPTION" in *"$s_entry"*) hygiene_hits="$hygiene_hits \"$s_entry\"" ;; esac
done <<HYGIENE_LIST
$STALE
$CODENAME
HYGIENE_LIST
if [ -z "$hygiene_hits" ]; then ok "CLAP_DESCRIPTION carries no retired name or codename"
else bad "CLAP_DESCRIPTION carries no retired name or codename" "found:$hygiene_hits"; fi
printf '\nstale spellings\n'
# File list for the scan below. Prefer git, so build output and any vendored
# tree stay out of it. Fall back to find when this is not a checkout - an
# exported tarball, or a CI step that fetched without git metadata - because
# `git ls-files` there returns nothing, and an empty list would report every
# stale spelling as absent. A check that passes hardest when it can see least
# is worse than no check.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
file_list() { git ls-files; }
scan_source="git"
else
file_list() { find . -path ./.git -prune -o -path ./target -prune -o -type f -print | sed 's|^\./||'; }
scan_source="find (not a git checkout)"
fi
info "scanning tracked files via $scan_source"
# Fed by here-doc rather than a pipe on purpose: a piped `while` runs in a
# subshell, and the pass/fail counters it increments would be discarded.
while IFS= read -r s; do
[ -n "$s" ] || continue
# Still part of a canonical value? Then it is current branding, not a leftover.
# This is what lets the list ship pre-seeded: entries arm themselves as you
# rename. Substring, not equality, because placeholders nest - "your-domain"
# lives inside the canonical CLAP_ID, and "YOUR-USERNAME" inside the homepage.
# Over-skipping is the safe direction: a false FAIL on a name you still use
# would train you to ignore this section.
case "$CANONICAL_BLOB" in
*"$s"*) note "\"$s\" (still canonical)"; continue ;;
esac
# These two name every retired spelling by definition - the STALE list lives in
# branding.conf and this script reads it - so neither can ever be a real hit.
hits=$(file_list 2>/dev/null | while read -r f; do
case "$f" in tools/check-branding.sh|tools/branding.conf) continue ;; esac
[ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null
done)
if [ -z "$hits" ]; then ok "no occurrences of \"$s\""
else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi
done <<STALE_LIST
$STALE
STALE_LIST
# Template scaffolding, not drift: TODOs are legitimate while you are still
# building. Reported so a finished plugin does not quietly ship with them.
# `grep -c` prints 0 AND exits 1 when there are no matches, so `|| echo 0`
# would append a second zero and make the count unparseable. Swallow the
# status instead, and default an empty result (missing file) to zero.
todos=$(grep -c 'TODO' src/lib.rs 2>/dev/null || :)
[ -n "$todos" ] || todos=0
printf '\nscaffolding\n'
if [ "$todos" -eq 0 ]; then ok "no TODO markers left in src/lib.rs"
else info "$todos TODO marker(s) still in src/lib.rs (not a failure)"; fi
printf '\n%d passed, %d failed' "$pass" "$fail"
[ "$skip" -gt 0 ] && printf ', %d skipped' "$skip"
printf '\n'
[ "$fail" -eq 0 ] || exit 1