From 8c24b83ea38088c6047eecad8f80a4de575d74b3 Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Tue, 1 Sep 2026 10:15:34 +0900 Subject: [PATCH] refactor: move branding into tools/ and drive the checker from a table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes that only make sense together. **tools/**. check-branding.sh, brand-plugin.sh and branding.conf were accumulating in the repo root, and a deploy.sh/bat/ps1 set would land there too. They now live in tools/ and resolve the repo root explicitly rather than assuming their own directory is it, so they behave the same however they are invoked. **One name drives the rest.** branding.conf now derives the package name, the Rust types and the display name from PLUGIN_NAME, each overridable. Derivation-by-default removes a class of drift; the override matters because the derivation is sometimes invalid — "206 Prototype" derives to "206_prototype", which cargo rejects outright — and because PLUGIN_NAME is cosmetic while the package name is structural. A display tweak should not silently rename the crate, its bundle lookup, the CI command and the lockfile. This template overrides three of them, which a real plugin normally will not: its names are the template's own rather than derived from a product name. An overridden PKG that no longer matches the derivation is reported at info level, never failed. The override is usually deliberate; a display rename that forgot the package looks identical from here, and is not. **The site table is data.** branding-sites.tsv lists every place a name must appear and in what form; check-branding.sh iterates it instead of carrying 29 check calls in shell. The scripts are now identical across every plugin built from this template, with only the table and conf differing — which matters because the template and codename-206 had already diverged by two checks, so every sync was a shell merge. brand-plugin.sh regenerates the rows it owns and preserves the rest, so a hand-added row for a file this template does not ship survives a rename. Custom rows are value-substituted like any other tracked file, so they track the rename rather than going stale — the first cut excluded the whole table from substitution and got that wrong. brand-plugin.sh also gained an interactive mode: run it with no arguments and it prompts for each value with the current one as the default, so Enter keeps it and a run that answers nothing changes nothing. It only engages when there is a terminal to ask at, so CI gets the usage message and exit 2 rather than a hang. Verified by applying a full rename to a sandboxed copy: 8 files rewritten, 29 owned rows regenerated, 1 custom row preserved and substituted, checker green at 48/0/7, and the result compiles as bari_206. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn --- .github/workflows/ci.yaml | 2 +- README.md | 66 ++++++----- branding.conf | 76 ------------- brand-plugin.sh => tools/brand-plugin.sh | 100 ++++++++++++++-- tools/branding-sites.tsv | 45 ++++++++ tools/branding.conf | 90 +++++++++++++++ check-branding.sh => tools/check-branding.sh | 114 ++++++++----------- 7 files changed, 307 insertions(+), 186 deletions(-) delete mode 100644 branding.conf rename brand-plugin.sh => tools/brand-plugin.sh (63%) create mode 100644 tools/branding-sites.tsv create mode 100644 tools/branding.conf rename check-branding.sh => tools/check-branding.sh (57%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6cd636a..26ddd1f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: # 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. - name: Branding consistency - run: sh check-branding.sh + run: sh tools/check-branding.sh # The default build is CLAP-only; the vst3 feature is opt-in, so both # configurations have to be built to know the cfg gates are still correct. diff --git a/README.md b/README.md index b50b7ae..e9cde5c 100644 --- a/README.md +++ b/README.md @@ -24,41 +24,49 @@ Identity is duplicated across `Cargo.toml`, `bundler.toml`, `src/lib.rs`, `NOTIC `Cargo.lock`, the CI workflow and the docs. Nothing makes those agree, so a half-finished rename builds a bundle under one name while CI bundles another. -`branding.conf` holds the canonical identity once. Two scripts read it: +Everything for it lives in `tools/`: -```bash -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 +``` +tools/branding.conf the names, and what derives from what +tools/branding-sites.tsv where each name has to appear, and in what form +tools/brand-plugin.sh changes names +tools/check-branding.sh verifies them ``` -**`brand-plugin.sh` changes names.** It derives the package name and Rust types from the -display name, validates them against what Cargo actually accepts, rewrites every tracked -file, appends the retired spellings to `STALE`, and finishes by running the checker — a run -that ends green is a rename that landed. It is a dry run unless you pass `--apply`. +```bash +sh tools/brand-plugin.sh # interactive; Enter keeps a value +sh tools/brand-plugin.sh --display "Fuzz Box" # or non-interactive; dry run +sh tools/brand-plugin.sh --display "Fuzz Box" --apply +sh tools/check-branding.sh # exit 0 = consistent, 1 = drift +``` -**`check-branding.sh` verifies them.** Both directions: every canonical value is asserted -present where it belongs, *and* no retired spelling survives anywhere. Absence alone is not -enough — renaming to the wrong new spelling would pass a stale-sweep and still break CI. +**One name drives the rest.** `PLUGIN_NAME` derives the package name (`Fuzz Box` → +`fuzz_box`), the Rust types (`FuzzBox`, `FuzzBoxParams`) and the display name. Each is +overridable in `branding.conf`, because the derivation is sometimes wrong and sometimes +outright invalid — `206 Prototype` derives to `206_prototype`, which Cargo rejects since a +package name cannot start with a digit. The helper refuses those, along with Rust keywords +and built-in crate names. -Three things it will refuse or warn about, all learned the hard way: +**The checker does both halves.** Every canonical value is asserted *present* where it +belongs, and every retired spelling is swept for. Absence alone is not enough: renaming to +the wrong new spelling would pass a stale-sweep and still break CI. -- **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. +**Two things it will not do for you.** `CLAP_ID` and `VST3_CLASS_ID` are how hosts recognise +saved projects and presets — change either and every existing session silently loses the +plugin, with no error. They need `--allow-identity-change`, and the 16-byte class ID is +*proposed* rather than derived, because that choice is yours. And `CLAP_DESCRIPTION` is +rejected if it carries a retired name or the repo codename: hosts show it to users who never +knew your development history. -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. +**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` 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. +Adding a site the template doesn't ship is one row in `branding-sites.tsv` with owner +`custom` — no shell to edit, and it survives regeneration. Rows the helper owns are rewritten +on each rename; custom rows are carried through and value-substituted like any other file. +Both files can be hand-edited; the helper is what keeps them agreeing, so run the checker +after if you do. ## Build @@ -119,9 +127,7 @@ bundler.toml # Bundle name for `cargo xtask bundle` src/lib.rs # Plugin skeleton — params, DSP, and egui editor, with one example gain param xtask/ # Bundler xtask (cargo xtask bundle) .cargo/config.toml # xtask alias -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 +tools/ # Branding toolkit — see Renaming .gitattributes # Pins *.sh to LF so Windows checkouts keep a valid shebang .github/workflows/ # CI: branding check, both feature sets build, test, bundle ``` diff --git a/branding.conf b/branding.conf deleted file mode 100644 index dff64d3..0000000 --- a/branding.conf +++ /dev/null @@ -1,76 +0,0 @@ -# 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 -> ".vst3" / ".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" diff --git a/brand-plugin.sh b/tools/brand-plugin.sh similarity index 63% rename from brand-plugin.sh rename to tools/brand-plugin.sh index 40fb4ba..44764c9 100755 --- a/brand-plugin.sh +++ b/tools/brand-plugin.sh @@ -22,10 +22,12 @@ # --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; } +# 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 -. ./branding.conf +. ./tools/branding.conf OLD_PKG="$PKG"; OLD_DISPLAY="$DISPLAY"; OLD_PLUGIN_NAME="$PLUGIN_NAME" OLD_VENDOR="$VENDOR"; OLD_EMAIL="$EMAIL"; OLD_HOMEPAGE="$HOMEPAGE" @@ -56,7 +58,28 @@ while [ $# -gt 0 ]; do esac done -[ -n "$new_display" ] || { printf 'nothing to do: pass --display "New Name"\n' >&2; exit 2; } +# 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 " "$OLD_VENDOR" + 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, @@ -184,7 +207,11 @@ trap 'rm -f "$subst_file"' EXIT changed=0 git ls-files 2>/dev/null | while IFS= read -r f; do [ -f "$f" ] || continue - case "$f" in branding.conf) continue ;; esac + # 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 @@ -214,10 +241,63 @@ sed -e "s|^PKG=.*|PKG=\"$new_pkg\"|" \ -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' + tools/branding.conf > tools/branding.conf.new +{ sed '$ s/"$//' tools/branding.conf.new; printf '%s"\n' "$retired"; } > tools/branding.conf +rm -f tools/branding.conf.new +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" +gen="$(mktemp)"; keep="$(mktemp)" +trap 'rm -f "$subst_file" "$gen" "$keep"' 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_vendor" "$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" + printf 'req%sdescription.clap%s%s%sconst CLAP_DESCRIPTION: Option<&'"'"'static str> = Some("%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" + +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 check-branding.sh +sh tools/check-branding.sh diff --git a/tools/branding-sites.tsv b/tools/branding-sites.tsv new file mode 100644 index 0000000..39e9970 --- /dev/null +++ b/tools/branding-sites.tsv @@ -0,0 +1,45 @@ +# branding-sites.tsv - every place a canonical name has to appear, and in what form. +# +# Generated and refreshed by tools/brand-plugin.sh; read by tools/check-branding.sh, +# which asserts each pattern is present in its file. Tab-separated, four columns: +# +# kind req = a missing file is a failure; opt = a missing file is a skip +# owner the brand-plugin.sh rule that manages this row, or "custom" for rows +# you added by hand. Custom rows are preserved across regeneration; +# owned rows are overwritten, so do not hand-edit those. +# file repo-relative path +# pattern literal text that must appear in that file, already resolved +# +# Adding a site for something the template does not ship is a one-line edit here +# with owner "custom" - no shell to change, and it survives the next rename. +# +#kind owner file pattern +req pkg.cargo Cargo.toml name = "nih_plug_template" +req pkg.bundler bundler.toml [nih_plug_template] +opt pkg.lock Cargo.lock name = "nih_plug_template" +opt pkg.ci .github/workflows/ci.yaml cargo xtask bundle nih_plug_template +opt pkg.readme README.md cargo xtask bundle nih_plug_template +req display.bundler bundler.toml name = "Plugin Name" +opt display.notice NOTICE Plugin Name +req name.plugin src/lib.rs const NAME: &'static str = "Plugin Name" +req vendor.plugin src/lib.rs const VENDOR: &'static str = "Your Name" +req email.plugin src/lib.rs const EMAIL: &'static str = "your@email.com" +req authors.cargo Cargo.toml authors = ["Your Name "] +req homepage.cargo Cargo.toml homepage = "https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME" +req url.plugin src/lib.rs const URL: &'static str = env!("CARGO_PKG_HOMEPAGE") +req description.cargo Cargo.toml description = "A minimal NIH-plug audio plugin" +req description.clap src/lib.rs const CLAP_DESCRIPTION: Option<&'static str> = Some("Your plugin description") +req clapid.plugin src/lib.rs const CLAP_ID: &'static str = "com.your-domain.plugin-name" +req vst3id.plugin src/lib.rs const VST3_CLASS_ID: [u8; 16] = *b"Unique16CharId!!" +req struct.def src/lib.rs struct MyPlugin { +req struct.plugin src/lib.rs impl Plugin for MyPlugin { +req struct.clap src/lib.rs impl ClapPlugin for MyPlugin { +req struct.vst3 src/lib.rs impl Vst3Plugin for MyPlugin { +req struct.exportclap src/lib.rs nih_export_clap!(MyPlugin); +req struct.exportvst3 src/lib.rs nih_export_vst3!(MyPlugin); +req params.def src/lib.rs struct PluginParams { +req params.default src/lib.rs impl Default for PluginParams { +req params.field src/lib.rs params: Arc, +opt deploy.pkg deploy.ps1 $Package = "nih_plug_template" +opt deploy.vst3 deploy.ps1 $vst3Name = "Plugin Name.vst3" +opt deploy.clap deploy.ps1 $clapName = "Plugin Name.clap" diff --git a/tools/branding.conf b/tools/branding.conf new file mode 100644 index 0000000..089897c --- /dev/null +++ b/tools/branding.conf @@ -0,0 +1,90 @@ +# 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 ------------------------------------------------------------ +# Defined before the values so the defaults below can use them. +# Package name: lowercased, every run of non-alphanumerics becomes one +# underscore. Cargo accepts hyphens too and maps them to underscores for the +# built artifact, which nih_plug_xtask already handles; underscores are used 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. +_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="Plugin Name" # the one name everything else derives from +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" # -> Plugin::URL +DESCRIPTION="A minimal NIH-plug audio plugin" # Cargo description + +# What the plugin does *now*. No codename, no "prototype", no "v2 rewrite" - +# hosts show this to users who never knew any of that. check-branding.sh +# enforces the mechanical half by rejecting retired names and the repo codename. +CLAP_DESCRIPTION="Your plugin description" + +# --- frozen identity --------------------------------------------------------- +# Hosts key saved projects and presets off these two. Change either and every +# existing session silently loses the plugin: the track comes up empty, with no +# error. Choose once before the first release, then never again. +# brand-plugin.sh refuses to move 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 + +# --- fixed at repo creation -------------------------------------------------- +# The repository's own name. Deliberately NOT renamed when the plugin is: the +# repo keeps its development codename while the plugin gets a product name. +# Recorded only so it can be kept out of user-facing text. +CODENAME="nih-plug-template" + +# Where the two Rust types are defined. A plugin whose params outgrow lib.rs +# points PARAMS_FILE at src/params.rs. +LIB_FILE="src/lib.rs" +PARAMS_FILE="src/lib.rs" + +# --- derived unless overridden ----------------------------------------------- +# Set one only when the derivation is wrong or rejected - a name like +# "206 Prototype" derives to "206_prototype", which cargo refuses because a +# package name cannot start with a digit. +# +# This template overrides three of them, which a real plugin usually will not: +# its package and type names are the template's own rather than derived from a +# product name. Delete these three lines after renaming and the derivations +# from PLUGIN_NAME take over. +PKG="nih_plug_template" +STRUCT="MyPlugin" +PARAMS_STRUCT="PluginParams" +# DISPLAY="Plugin Name" # defaults to PLUGIN_NAME +: "${DISPLAY:=$PLUGIN_NAME}" +: "${PKG:=$(_derive_pkg "$PLUGIN_NAME")}" +: "${STRUCT:=$(_derive_struct "$PLUGIN_NAME")}" +: "${PARAMS_STRUCT:=${STRUCT}Params}" + +# --- retired spellings ------------------------------------------------------- +# Must no longer appear in tracked files. An entry is skipped while it is still +# part of a canonical value above and arms the moment it stops being one, so a +# rename is self-checking in both directions. brand-plugin.sh appends here. +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" diff --git a/check-branding.sh b/tools/check-branding.sh similarity index 57% rename from check-branding.sh rename to tools/check-branding.sh index 8c8df7e..6386030 100755 --- a/check-branding.sh +++ b/tools/check-branding.sh @@ -12,20 +12,22 @@ # with this repo, and it touches every file below. Run this immediately after, # and the STALE machinery will list whatever you missed. # -# Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found) +# 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 -cd "$(dirname "$0")" || exit 1 +# 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 branding.conf ]; then - printf 'branding.conf not found next to this script\n' >&2 +if [ ! -f tools/branding.conf ]; then + printf 'tools/branding.conf not found\n' >&2 exit 1 fi # shellcheck disable=SC1091 -. ./branding.conf +. ./tools/branding.conf # Every canonical spelling concatenated, for the substring test below. CANONICAL_BLOB="$PKG @@ -65,66 +67,56 @@ opt_check() { # opt_check LABEL FILE LITERAL 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 "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 display name" bundler.toml "name = \"$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 -printf '\nsource identity (src/lib.rs)\n' -check "Plugin::NAME" "$LIB_FILE" "const NAME: &'static str = \"$PLUGIN_NAME\"" -check "Plugin::VENDOR" "$LIB_FILE" "const VENDOR: &'static str = \"$VENDOR\"" -check "Plugin::EMAIL" "$LIB_FILE" "const EMAIL: &'static str = \"$EMAIL\"" -check "CLAP_ID" "$LIB_FILE" "const CLAP_ID: &'static str = \"$CLAP_ID\"" -check "CLAP_DESCRIPTION" "$LIB_FILE" "const CLAP_DESCRIPTION: Option<&'static str> = Some(\"$CLAP_DESCRIPTION\")" -check "VST3_CLASS_ID" "$LIB_FILE" "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\"" +# --- validity: rules the table cannot express -------------------------------- +printf '\nvalidity\n' -# 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" "$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 -# compile error, but it is far clearer to say so here than to read rustc on it. +# 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 are compared verbatim by hosts, so -# a stray space or capital is a silent identity change between machines. +# 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 -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" "$LIB_FILE" "struct $STRUCT {" -check "impl Plugin" "$LIB_FILE" "impl Plugin for $STRUCT {" -check "impl ClapPlugin" "$LIB_FILE" "impl ClapPlugin for $STRUCT {" -check "impl Vst3Plugin" "$LIB_FILE" "impl Vst3Plugin for $STRUCT {" -check "nih_export_clap!" "$LIB_FILE" "nih_export_clap!($STRUCT);" -check "nih_export_vst3!" "$LIB_FILE" "nih_export_vst3!($STRUCT);" -check "struct $PARAMS_STRUCT" "$PARAMS_FILE" "struct $PARAMS_STRUCT {" -check "impl Default for params" "$PARAMS_FILE" "impl Default for $PARAMS_STRUCT {" -check "params field type" "$LIB_FILE" "params: Arc<$PARAMS_STRUCT>," +# 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" -printf '\ndocs\n' -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' +# 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 @@ -137,22 +129,6 @@ 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' -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 @@ -186,7 +162,7 @@ while IFS= read -r s; do # 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 check-branding.sh|branding.conf) continue ;; esac + 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\""