feat: split branding into a checker and a helper over one canonical file

check-branding.sh could say what disagreed but not fix it, and its
canonical block lived in its own header — so renaming meant hand-editing
the checker, which is exactly the fragile step it exists to catch.

branding.conf now holds the identity once. Both scripts read it and
neither contains a branding literal anywhere, so a rename never has to
rewrite its own tooling.

brand-plugin.sh derives the package name and Rust types from the display
name, validates them against what Cargo actually accepts, rewrites every
tracked file longest-value-first (a short name can be a substring of a
longer one), appends the retired spellings to STALE, and finishes by
running the checker. A run that ends green is a rename that landed. Dry
run unless --apply.

It refuses three things worth refusing:

- Package names Cargo rejects — digit-leading (`206 Prototype` derives to
  `206_prototype`), Rust keywords, built-in crate collisions. Verified
  against cargo init rather than assumed.
- Moving CLAP_ID or VST3_CLASS_ID without --allow-identity-change. Hosts
  key saved projects off those; changing either orphans every existing
  session silently. The 16-byte class ID is *proposed*, never derived —
  it is a fixed-width frozen identity, so the choice stays human.
- Development history in CLAP_DESCRIPTION. Hosts show it to users who
  never knew the codename, so any retired name or the repo codename
  appearing there is rejected.

The checker gains the other half of its job: asserting canonical values
are present where they belong, not only that retired ones are gone.
Absence alone would pass a rename to the *wrong* new spelling. New
positive assertions cover Cargo authors, Cargo.lock, the CI bundle
command and NOTICE — every one of which the recent Bari 206 rename had to
touch. Structural checks read their file locations from branding.conf so
a plugin whose params outgrow src/lib.rs can point them elsewhere.

Also fixes NOTICE naming the repo rather than the product, found by the
new assertion, and excludes branding.conf from the stale sweep — it lists
every retired spelling by definition, the same reason check-branding.sh
was already excluded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
This commit is contained in:
2026-08-31 23:11:47 +09:00
parent b05ad27a24
commit a46e7eb961
5 changed files with 411 additions and 92 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
nih-plug-template Plugin Name
Copyright 2026 Mikkeli Matlock Copyright 2026 Mikkeli Matlock
This product is licensed under the GNU General Public License v3.0 or later. This product is licensed under the GNU General Public License v3.0 or later.
+35 -19
View File
@@ -20,31 +20,45 @@ sh check-branding.sh
## Renaming ## Renaming
Branding is duplicated across `Cargo.toml`, `bundler.toml`, `src/lib.rs` and the docs, Identity is duplicated across `Cargo.toml`, `bundler.toml`, `src/lib.rs`, `NOTICE`,
and nothing makes those agree — so a half-finished rename builds a bundle under one name `Cargo.lock`, the CI workflow and the docs. Nothing makes those agree, so a half-finished
while the docs and deploy script look for another. rename builds a bundle under one name while CI bundles another.
`check-branding.sh` declares the identity once at the top and asserts every copy matches: `branding.conf` holds the canonical identity once. Two scripts read it:
```bash ```bash
sh check-branding.sh # exit 0 = consistent, 1 = drift found sh brand-plugin.sh --display "Fuzz Box" # derive, validate, show the plan (dry run)
sh brand-plugin.sh --display "Fuzz Box" --apply
sh check-branding.sh # exit 0 = consistent, 1 = drift
``` ```
It covers the identity a host actually sees (package and bundle names, `Plugin::NAME`, **`brand-plugin.sh` changes names.** It derives the package name and Rust types from the
`VENDOR`, `EMAIL`, `CLAP_ID`, `VST3_CLASS_ID`, the descriptions) and the Rust type names display name, validates them against what Cargo actually accepts, rewrites every tracked
(`MyPlugin`, `PluginParams`) at every site they appear. Renaming the types is cosmetic — file, appends the retired spellings to `STALE`, and finishes by running the checker — a run
no host ever sees them — but leaving `MyPlugin` in a shipped plugin is the kind of residue that ends green is a rename that landed. It is a dry run unless you pass `--apply`.
that says nobody read the file. Leave them as-is and those checks simply stay green.
To rename: edit the canonical block at the top of the script, run it, and fix everything **`check-branding.sh` verifies them.** Both directions: every canonical value is asserted
it reports. The placeholder spellings are pre-seeded into its `STALE` list, and each one present where it belongs, *and* no retired spelling survives anywhere. Absence alone is not
arms itself as soon as you change the matching canonical value — so leftovers surface enough — renaming to the wrong new spelling would pass a stale-sweep and still break CI.
without you having to track them. It also asserts `VST3_CLASS_ID` is exactly 16 bytes and
that `CLAP_ID` is reverse-DNS safe, both of which are otherwise a compile error or a
silent identity change between machines.
Checks for files the template doesn't ship (e.g. a `deploy.ps1`) report as `skip` and Three things it will refuse or warn about, all learned the hard way:
start enforcing once you add them.
- **A package name Cargo rejects.** Names cannot start with a digit (`206 Prototype` →
`206_prototype`, rejected), be a Rust keyword, or collide with a built-in crate.
- **Moving frozen identity.** `CLAP_ID` and `VST3_CLASS_ID` are how hosts recognise saved
projects and presets. Change either and every existing session silently loses the plugin —
the track comes up empty, with no error. Pick them once before the first release; after
that they never move. `--allow-identity-change` is required to touch them, and the helper
proposes a valid 16-byte class ID rather than deriving one, because that choice is yours.
- **Development history in `CLAP_DESCRIPTION`.** Hosts show it to users who never knew your
codename. Any retired name or the repo's own codename appearing there is rejected.
The repo name is deliberately *not* derived. It is fixed when the repo is created and keeps
its development codename while the plugin gets a product name; `CODENAME` in `branding.conf`
records it so it can be kept out of user-facing text.
Checks for files the template doesn't ship (a `deploy.ps1`, say) report as `skip` and start
enforcing once you add them.
## Build ## Build
@@ -105,7 +119,9 @@ bundler.toml # Bundle name for `cargo xtask bundle`
src/lib.rs # Plugin skeleton — params, DSP, and egui editor, with one example gain param src/lib.rs # Plugin skeleton — params, DSP, and egui editor, with one example gain param
xtask/ # Bundler xtask (cargo xtask bundle) xtask/ # Bundler xtask (cargo xtask bundle)
.cargo/config.toml # xtask alias .cargo/config.toml # xtask alias
check-branding.sh # Asserts the plugin's identity strings agree everywhere (see Renaming) branding.conf # Canonical identity — the one place names are declared
check-branding.sh # Asserts every name is present where it belongs and no old one survives
brand-plugin.sh # Renames the plugin everywhere, then verifies with the checker
.gitattributes # Pins *.sh to LF so Windows checkouts keep a valid shebang .gitattributes # Pins *.sh to LF so Windows checkouts keep a valid shebang
.github/workflows/ # CI: branding check, both feature sets build, test, bundle .github/workflows/ # CI: branding check, both feature sets build, test, bundle
``` ```
+223
View File
@@ -0,0 +1,223 @@
#!/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
cd "$(dirname "$0")" || exit 1
[ -f branding.conf ] || { printf 'branding.conf not found next to this script\n' >&2; exit 1; }
# shellcheck disable=SC1091
. ./branding.conf
OLD_PKG="$PKG"; OLD_DISPLAY="$DISPLAY"; OLD_PLUGIN_NAME="$PLUGIN_NAME"
OLD_VENDOR="$VENDOR"; OLD_EMAIL="$EMAIL"; OLD_HOMEPAGE="$HOMEPAGE"
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_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 ;;
--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
[ -n "$new_display" ] || { printf 'nothing to do: pass --display "New Name"\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_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 "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 ------------------------------------------------------------------
# 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.
subst_file="$(mktemp)"
trap 'rm -f "$subst_file"' EXIT
{
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_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
case "$f" in 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|^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\"|" \
branding.conf > branding.conf.new
{ sed '$ s/"$//' branding.conf.new; printf '%s"\n' "$retired"; } > branding.conf
rm -f branding.conf.new
printf ' rewrote branding.conf (retired spellings appended to STALE)\n'
printf '\nverifying with check-branding.sh\n\n'
sh check-branding.sh
+76
View File
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# branding.conf - the plugin's canonical identity, in one place.
#
# Sourced by both check-branding.sh (which asserts every copy matches) and
# brand-plugin.sh (which rewrites this file and every copy when you rename).
# Editing a value here and running the checker tells you every place that still
# holds the old one; running the helper changes them for you.
# ---------------------------------------------------------------------------
# Names that move when the plugin is renamed
# ---------------------------------------------------------------------------
PKG="nih_plug_template" # Cargo package name == bundler.toml table key
DISPLAY="Plugin Name" # bundle name -> "<DISPLAY>.vst3" / "<DISPLAY>.clap"
PLUGIN_NAME="Plugin Name" # Plugin::NAME - what the DAW displays
VENDOR="Your Name" # Plugin::VENDOR - the org, not the person
EMAIL="your@email.com" # Plugin::EMAIL
HOMEPAGE="https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME" # Cargo homepage -> Plugin::URL
DESCRIPTION="A minimal NIH-plug audio plugin" # Cargo package description
# What the plugin does *now*. Keep development history out of it - no codename,
# no "prototype", no "v2 rewrite". Hosts show this to users, who have no idea
# what any of that meant. check-branding.sh enforces the mechanical half of this
# by rejecting any retired spelling or the repo codename appearing here.
CLAP_DESCRIPTION="Your plugin description"
# ---------------------------------------------------------------------------
# Frozen identity - hosts key saved projects and presets off these two. Changing
# either orphans every existing session silently: the track comes up empty, with
# no error. Choose them once before the first release and never again.
# brand-plugin.sh refuses to touch them without --allow-identity-change.
# ---------------------------------------------------------------------------
CLAP_ID="com.your-domain.plugin-name" # reverse-DNS, lowercase, no spaces
VST3_CLASS_ID="Unique16CharId!!" # MUST be exactly 16 bytes
# ---------------------------------------------------------------------------
# Rust type names. Never reach a host, so renaming them is cosmetic - but
# leaving "MyPlugin" in a shipped plugin is residue that says nobody read the
# file. Leave them alone and the checks simply stay green.
# ---------------------------------------------------------------------------
STRUCT="MyPlugin" # the Plugin impl target
PARAMS_STRUCT="PluginParams" # the Params impl target
# Where those two are defined. A plugin that outgrows one file points these at
# the real locations (e.g. PARAMS_FILE="src/params.rs") - the checker asserts
# definitions here, and the STALE sweep catches every use site elsewhere.
LIB_FILE="src/lib.rs"
PARAMS_FILE="src/lib.rs"
# ---------------------------------------------------------------------------
# The development codename: this repository's own name, fixed when the repo was
# created and deliberately NOT renamed when the plugin is. It keeps its history
# while the plugin gets a product name. Never checked for as branding - it is
# only used to keep it out of user-facing descriptions.
# ---------------------------------------------------------------------------
CODENAME="nih-plug-template"
# ---------------------------------------------------------------------------
# Spellings that must no longer appear in tracked files. Seeded with this
# template's placeholders and with the current values, so renaming is
# self-checking in both directions: an entry is skipped while it is still part
# of a canonical value above, and arms the moment it stops being one.
# brand-plugin.sh appends retired values here automatically.
# ---------------------------------------------------------------------------
STALE="nih_plug_template
MyPlugin
PluginParams
A minimal NIH-plug audio plugin
Your plugin description
Plugin Name
Your Name
your@email.com
com.your-domain.plugin-name
Unique16CharId!!
YOUR-USERNAME
YOUR-PLUGIN-NAME
your-domain"
+76 -72
View File
@@ -14,51 +14,33 @@
# #
# Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found) # Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found)
# #
# --------------------------------------------------------------------------- # The canonical identity lives in branding.conf, shared with brand-plugin.sh.
# CANONICAL IDENTITY - the single source of truth. Edit these to rename, then # Edit it there, then run this to find every copy that still holds the old value.
# run this script; every failure it prints is a place still holding the old name.
# ---------------------------------------------------------------------------
PKG="nih_plug_template" # Cargo package name == bundler.toml table key
DISPLAY="Plugin Name" # bundle name -> "<DISPLAY>.vst3" / "<DISPLAY>.clap"
PLUGIN_NAME="Plugin Name" # Plugin::NAME - what the DAW displays
VENDOR="Your Name" # Plugin::VENDOR
EMAIL="your@email.com" # Plugin::EMAIL
HOMEPAGE="https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME" # Cargo homepage -> Plugin::URL
CLAP_ID="com.your-domain.plugin-name" # CLAP_ID - stable plugin identity
VST3_CLASS_ID="Unique16CharId!!" # VST3_CLASS_ID - stable identity, MUST be 16 bytes
DESCRIPTION="A minimal NIH-plug audio plugin" # Cargo package description
CLAP_DESCRIPTION="Your plugin description" # CLAP_DESCRIPTION - shown in some host browsers
# Rust type names. These never reach a host, so renaming them is cosmetic - but
# leaving "MyPlugin" in a shipped plugin is the sort of residue that says nobody
# read the file. Change these to make the checks below enforce your own names;
# leave them alone and they simply stay green.
STRUCT="MyPlugin" # the Plugin impl target
PARAMS_STRUCT="PluginParams" # the Params impl target
# Spellings that must no longer appear anywhere in tracked files. Seeded with
# this template's own placeholders, so that renaming is self-checking: each entry
# is skipped while it is still one of the canonical values above, and becomes a
# live check the moment you change that value. Add your own old names here after
# a later rename.
STALE="nih_plug_template
MyPlugin
PluginParams
A minimal NIH-plug audio plugin
Your plugin description
Plugin Name
Your Name
your@email.com
com.your-domain.plugin-name
Unique16CharId!!
YOUR-USERNAME
YOUR-PLUGIN-NAME
your-domain"
set -u set -u
cd "$(dirname "$0")" || exit 1 cd "$(dirname "$0")" || exit 1
if [ ! -f branding.conf ]; then
printf 'branding.conf not found next to this script\n' >&2
exit 1
fi
# shellcheck disable=SC1091
. ./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 pass=0; fail=0; skip=0
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; } 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; } bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$1"; [ $# -gt 1 ] && printf ' %s\n' "$2"; return 0; }
@@ -87,20 +69,25 @@ printf 'build manifests\n'
check "Cargo.toml package name" Cargo.toml "name = \"$PKG\"" check "Cargo.toml package name" Cargo.toml "name = \"$PKG\""
check "Cargo.toml homepage" Cargo.toml "homepage = \"$HOMEPAGE\"" check "Cargo.toml homepage" Cargo.toml "homepage = \"$HOMEPAGE\""
check "Cargo.toml description" Cargo.toml "description = \"$DESCRIPTION\"" check "Cargo.toml description" Cargo.toml "description = \"$DESCRIPTION\""
check "Cargo.toml authors" Cargo.toml "authors = [\"$VENDOR <$EMAIL>\"]"
# Cargo regenerates the lockfile on the next build, but only if someone builds
# before pushing. A stale root entry here means CI resolves a package that no
# longer exists under that name.
opt_check "Cargo.lock package" Cargo.lock "name = \"$PKG\""
check "bundler.toml table key" bundler.toml "[$PKG]" check "bundler.toml table key" bundler.toml "[$PKG]"
check "bundler.toml display name" bundler.toml "name = \"$DISPLAY\"" check "bundler.toml display name" bundler.toml "name = \"$DISPLAY\""
printf '\nsource identity (src/lib.rs)\n' printf '\nsource identity (src/lib.rs)\n'
check "Plugin::NAME" src/lib.rs "const NAME: &'static str = \"$PLUGIN_NAME\"" check "Plugin::NAME" "$LIB_FILE" "const NAME: &'static str = \"$PLUGIN_NAME\""
check "Plugin::VENDOR" src/lib.rs "const VENDOR: &'static str = \"$VENDOR\"" check "Plugin::VENDOR" "$LIB_FILE" "const VENDOR: &'static str = \"$VENDOR\""
check "Plugin::EMAIL" src/lib.rs "const EMAIL: &'static str = \"$EMAIL\"" check "Plugin::EMAIL" "$LIB_FILE" "const EMAIL: &'static str = \"$EMAIL\""
check "CLAP_ID" src/lib.rs "const CLAP_ID: &'static str = \"$CLAP_ID\"" check "CLAP_ID" "$LIB_FILE" "const CLAP_ID: &'static str = \"$CLAP_ID\""
check "CLAP_DESCRIPTION" src/lib.rs "const CLAP_DESCRIPTION: Option<&'static str> = Some(\"$CLAP_DESCRIPTION\")" check "CLAP_DESCRIPTION" "$LIB_FILE" "const CLAP_DESCRIPTION: Option<&'static str> = Some(\"$CLAP_DESCRIPTION\")"
check "VST3_CLASS_ID" src/lib.rs "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\"" check "VST3_CLASS_ID" "$LIB_FILE" "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\""
# Plugin::URL is env!("CARGO_PKG_HOMEPAGE"), so the manifest is the only place # 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. # 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\")" check "Plugin::URL from manifest" "$LIB_FILE" "const URL: &'static str = env!(\"CARGO_PKG_HOMEPAGE\")"
# VST3 class IDs are a fixed 16-byte array. A shorter or longer literal is a # 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. # compile error, but it is far clearer to say so here than to read rustc on it.
@@ -120,19 +107,47 @@ printf '\nstructural names (src/lib.rs)\n'
# Every site the type names appear. A rename that misses one is a compile error # 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, # 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. # and the Vst3Plugin impl sits behind a cfg so a default build never reports it.
check "struct $STRUCT" src/lib.rs "struct $STRUCT {" check "struct $STRUCT" "$LIB_FILE" "struct $STRUCT {"
check "impl Plugin" src/lib.rs "impl Plugin for $STRUCT {" check "impl Plugin" "$LIB_FILE" "impl Plugin for $STRUCT {"
check "impl ClapPlugin" src/lib.rs "impl ClapPlugin for $STRUCT {" check "impl ClapPlugin" "$LIB_FILE" "impl ClapPlugin for $STRUCT {"
check "impl Vst3Plugin" src/lib.rs "impl Vst3Plugin for $STRUCT {" check "impl Vst3Plugin" "$LIB_FILE" "impl Vst3Plugin for $STRUCT {"
check "nih_export_clap!" src/lib.rs "nih_export_clap!($STRUCT);" check "nih_export_clap!" "$LIB_FILE" "nih_export_clap!($STRUCT);"
check "nih_export_vst3!" src/lib.rs "nih_export_vst3!($STRUCT);" check "nih_export_vst3!" "$LIB_FILE" "nih_export_vst3!($STRUCT);"
check "struct $PARAMS_STRUCT" src/lib.rs "struct $PARAMS_STRUCT {" check "struct $PARAMS_STRUCT" "$PARAMS_FILE" "struct $PARAMS_STRUCT {"
check "impl Default for params" src/lib.rs "impl Default for $PARAMS_STRUCT {" check "impl Default for params" "$PARAMS_FILE" "impl Default for $PARAMS_STRUCT {"
check "params field type" src/lib.rs "params: Arc<$PARAMS_STRUCT>," check "params field type" "$LIB_FILE" "params: Arc<$PARAMS_STRUCT>,"
printf '\ndocs\n' printf '\ndocs\n'
opt_check "README bundle command" README.md "cargo xtask bundle $PKG" opt_check "README bundle command" README.md "cargo xtask bundle $PKG"
# CLAP_DESCRIPTION is shown to users in a host's plugin browser. It should say
# what the plugin does now, with no development history in it. "Free of history"
# is a judgement a script cannot make, but the failure it produces is mechanical:
# a description still carrying a retired name or the repo's development codename.
printf '\ndescription hygiene\n'
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 '\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.
opt_check "CI bundle command" .github/workflows/ci.yaml "cargo xtask bundle $PKG"
printf '\nattribution\n'
# NOTICE names the product in its first line; a rename that misses it ships an
# artifact crediting a name that no longer exists.
opt_check "NOTICE product name" NOTICE "$DISPLAY"
printf '\ndeploy script\n' printf '\ndeploy script\n'
opt_check "deploy.ps1 default package" deploy.ps1 "\$Package = \"$PKG\"" 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 VST3 bundle" deploy.ps1 "\$vst3Name = \"$DISPLAY.vst3\""
@@ -153,19 +168,6 @@ else
scan_source="find (not a git checkout)" scan_source="find (not a git checkout)"
fi fi
info "scanning tracked files via $scan_source" 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 # 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. # subshell, and the pass/fail counters it increments would be discarded.
while IFS= read -r s; do while IFS= read -r s; do
@@ -181,9 +183,11 @@ while IFS= read -r s; do
*"$s"*) note "\"$s\" (still canonical)"; continue ;; *"$s"*) note "\"$s\" (still canonical)"; continue ;;
esac esac
# This script names every placeholder by definition, so it can never be a hit. # 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 hits=$(file_list 2>/dev/null | while read -r f; do
[ "$f" = "check-branding.sh" ] || { [ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null; } case "$f" in check-branding.sh|branding.conf) continue ;; esac
[ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null
done) done)
if [ -z "$hits" ]; then ok "no occurrences of \"$s\"" if [ -z "$hits" ]; then ok "no occurrences of \"$s\""
else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi