#!/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