From 313bd95d9009dc7f0d082b502d75449e1bbb611d Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 30 Aug 2026 18:10:03 -0700 Subject: [PATCH 01/12] added support for interpolation --- .../roles/lessons/tasks/install_step.yml | 13 +- .../cookbooks/lessons/libraries/helpers.rb | 11 ++ .../lessons/manifests/install_step.pp | 10 ++ scriptbox/config/macos.yml | 136 +++++++++++------- scriptbox/scripts/generate_chef_databag.rb | 11 ++ scriptbox/scripts/generate_install_script.rb | 55 +++++-- 6 files changed, 167 insertions(+), 69 deletions(-) diff --git a/configbox/ansible/provision/roles/lessons/tasks/install_step.yml b/configbox/ansible/provision/roles/lessons/tasks/install_step.yml index b7fd3b4..50a0ccd 100644 --- a/configbox/ansible/provision/roles/lessons/tasks/install_step.yml +++ b/configbox/ansible/provision/roles/lessons/tasks/install_step.yml @@ -68,8 +68,19 @@ owner: "{{ lessons_user }}" mode: '0644' +# interpolate: true (see generate_install_script.rb's own append_lines +# comment) has no meaning here - lineinfile writes `line` as plain +# Python data, no shell involved, so there's nothing to evaluate a +# manifest's own `$(...)`/`$VAR` against. Fail loudly rather than +# silently write that text out literally - a step needing real +# interpolation has to become a 'script' step instead. +- name: "append: reject interpolate" + when: step.type == 'append' and step.interpolate | default(false) + ansible.builtin.fail: + msg: "lessons: append '{{ step.name }}' sets interpolate: true, which this role's 'append' task can't honor (lineinfile has no shell to interpolate through) - use a 'script' step instead" + - name: "append" - when: step.type == 'append' + when: step.type == 'append' and not (step.interpolate | default(false)) become: true vars: lessons_append_dests: "{{ ([step.dest] if step.dest is string else step.dest) | map('replace', '$HOME', lessons_home) | list }}" diff --git a/configbox/chef/cookbooks/lessons/libraries/helpers.rb b/configbox/chef/cookbooks/lessons/libraries/helpers.rb index 1b2d4fd..fadaf21 100644 --- a/configbox/chef/cookbooks/lessons/libraries/helpers.rb +++ b/configbox/chef/cookbooks/lessons/libraries/helpers.rb @@ -145,6 +145,17 @@ def lessons_install(pkg) # directory gap as 'file' above - append_if_no_line creates the # destination file itself if missing, which needs the directory # to already exist. + # + # interpolate: true (see generate_install_script.rb's own + # append_lines comment) has no meaning here - append_if_no_line + # writes `line` as plain Ruby content, no shell involved, so + # there's nothing to evaluate a manifest's own `$(...)`/`$VAR` + # against. Fail loudly rather than silently write that text out + # literally - a step needing real interpolation has to become a + # 'script' step instead, the same way it was before append: + # existed at all. + raise "lessons: append '#{pkg['name']}' sets interpolate: true, which the Chef 'append' case can't honor (append_if_no_line has no shell to interpolate through) - use a 'script' step instead" if pkg['interpolate'] + home = Etc.getpwnam(node['lessons']['user']).dir Array(pkg['dest']).each do |dest| real_dest = dest.sub('$HOME', home) diff --git a/configbox/puppet/shared_modules/lessons/manifests/install_step.pp b/configbox/puppet/shared_modules/lessons/manifests/install_step.pp index c4d4e70..c8ebc3c 100644 --- a/configbox/puppet/shared_modules/lessons/manifests/install_step.pp +++ b/configbox/puppet/shared_modules/lessons/manifests/install_step.pp @@ -125,6 +125,16 @@ } 'append': { + # interpolate: true (see generate_install_script.rb's own + # append_lines comment) has no meaning here - file_line writes + # `line` as plain Puppet data, no shell involved, so there's + # nothing to evaluate a manifest's own `$(...)`/`$VAR` against. + # Fail loudly rather than silently write that text out literally - + # a step needing real interpolation has to become a 'script' step + # instead. + if 'interpolate' in $step and $step['interpolate'] { + fail("lessons::install_step '${title}': append '${step_name}' sets interpolate: true, which this case can't honor (file_line has no shell to interpolate through) - use a 'script' step instead") + } $raw_dests = $step['dest'] =~ String ? { true => [$step['dest']], default => $step['dest'] } $dests = $raw_dests.map |$d| { regsubst($d, '\$HOME', $home) } $dests.each |$d| { diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index f6e4cfe..acf8212 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -1,8 +1,11 @@ macos: global: packages: + - script: macos_rosetta - script: macos_xcode_cli_tools - script: macos_homebrew + - append: macos_homebrew_zsh + - append: macos_homebrew_bash lessons: gen_scripts: awk: @@ -18,6 +21,7 @@ macos: - brew: perl meets: perl script: macos_cpan_local_setup + append: macos_cpan_local_setup - cpan: App::cpanminus - cpanm: Switch php: @@ -39,7 +43,7 @@ macos: bash: packages: - brew: bash - script: macos_setup_homebrew_bash + append: macos_setup_homebrew_bash csh: packages: - brew: tcsh @@ -53,7 +57,7 @@ macos: zsh: packages: - brew: zsh - script: macos_setup_homebrew_zsh + append: macos_setup_homebrew_zsh compiled_lang: cs: packages: @@ -67,6 +71,7 @@ macos: - cask: corretto@17 meets: java script: macos_java_17_home + append: macos_java_17_home rust: packages: - brew: rust @@ -90,44 +95,33 @@ macos: - brew: powershell - brew: ruby scripts: + macos_rosetta: + type: bash + cmd: | + [[ "$(uname -m)" == "arm64" ]] && sudo softwareupdate --install-rosetta --agree-to-license + macos_homebrew: type: bash cmd: | script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" /bin/bash -c "$(curl -fsSL "$script_url")" - # Applied before the PATH check below, not after - Homebrew's own - # installer does not add itself to the *current* process's PATH, and - # on Apple Silicon (/opt/homebrew) unlike Intel (/usr/local) that - # directory isn't on the default PATH either way, so `command -v - # brew` would otherwise fail here even on a successful install. - eval "$(brew --prefix)/bin/brew shellenv" + # Bootstrap Homebrew into existing environment + BREWCMD="/usr/local/bin/brew" + [[ "$(uname -m)" == "arm64" ]] && BREWCMD="/opt/homebrew/bin/brew" + eval "$($BREWCMD shellenv zsh)" if ! command -v brew >/dev/null 2>&1; then echo "macos_homebrew: brew not found on PATH after install - stopping" >&2 exit 1 fi - - grep -qxF 'eval "$(brew --prefix)/bin/brew shellenv"' ~/.bashrc 2>/dev/null || echo 'eval "$(brew --prefix)/bin/brew shellenv"' >> ~/.bashrc - grep -qxF 'eval "$(brew --prefix)/bin/brew shellenv"' ~/.zshrc 2>/dev/null || echo 'eval "$(brew --prefix)/bin/brew shellenv"' >> ~/.zshrc macos_cpan_local_setup: type: bash cmd: | # Force ExtUtils::MakeMaker's own prompt() to silently accept # every default answer instead of blocking on stdin. PERL_MM_USE_DEFAULT=1 PERL_MM_OPT="INSTALL_BASE=$HOME/perl5" cpan local::lib - grep -qxF 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' ~/.bashrc 2>/dev/null || echo 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' >> ~/.bashrc - grep -qxF 'export PATH=$HOME/perl5/bin:$PATH' ~/.bashrc 2>/dev/null || echo 'export PATH=$HOME/perl5/bin:$PATH' >> ~/.bashrc - grep -qxF 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' ~/.zshrc 2>/dev/null || echo 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)"' >> ~/.zshrc - grep -qxF 'export PATH=$HOME/perl5/bin:$PATH' ~/.zshrc 2>/dev/null || echo 'export PATH=$HOME/perl5/bin:$PATH' >> ~/.zshrc - # Direct, not `source ~/.bashrc` - a script run non-interactively - # (any provisioner, or even a human running this file with `bash - # script.sh`) hits nearly every default .bashrc's own "return if - # not interactive" guard before ever reaching the lines just - # appended above, so they'd never take effect in this process - - # and the very next steps in this same script (cpan: - # App::cpanminus, cpanm: Switch) need cpanm on PATH already. eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)" export PATH=$HOME/perl5/bin:$PATH macos_wine_fix: @@ -135,48 +129,42 @@ scripts: cmd: | ln -s "/Applications/Wine Stable.app/Contents/Resources/wine/share/wine" /usr/local/share/wine ln -s "/Applications/Wine Stable.app/Contents/Resources/wine/lib/wine" /usr/local/lib/wine - grep -qxF 'export MVK_CONFIG_LOG_LEVEL=0' ~/.bashrc 2>/dev/null || echo 'export MVK_CONFIG_LOG_LEVEL=0' >> ~/.bashrc - grep -qxF 'export WINEDEBUG=-all' ~/.bashrc 2>/dev/null || echo 'export WINEDEBUG=-all' >> ~/.bashrc - grep -qxF 'export MVK_CONFIG_LOG_LEVEL=0' ~/.zshrc 2>/dev/null || echo 'export MVK_CONFIG_LOG_LEVEL=0' >> ~/.zshrc - grep -qxF 'export WINEDEBUG=-all' ~/.zshrc 2>/dev/null || echo 'export WINEDEBUG=-all' >> ~/.zshrc - # Applied immediately too - macos_winetricks_wsh57 runs wine-based - # commands later in this same script and benefits from the + # macos_winetricks_wsh57 runs wine-basedvcommands later in this same script and benefits from the # quieter debug output just as much as a future session would. export MVK_CONFIG_LOG_LEVEL=0 export WINEDEBUG=-all - macos_setup_homebrew_bash: - type: bash - cmd: | - grep -qxF "$(brew --prefix)/bin/bash" /etc/shells 2>/dev/null || echo "$(brew --prefix)/bin/bash" | sudo tee -a /etc/shells - macos_setup_homebrew_zsh: - type: bash - cmd: | - grep -qxF "$(brew --prefix)/bin/zsh" /etc/shells 2>/dev/null || echo "$(brew --prefix)/bin/zsh" | sudo tee -a /etc/shells macos_xcode_cli_tools: type: bash cmd: | - touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress - label=$(softwareupdate -l 2>&1 \ - | grep -E '^[[:space:]]*\* Command Line Tools' \ - | sed 's/^[[:space:]]*\* //' \ - | head -1) - if softwareupdate --help 2>&1 | grep -q -- '--agree-to-license'; then - sudo softwareupdate -i "$label" --agree-to-license --verbose - else - sudo softwareupdate -i "$label" --verbose - fi - rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress + # Run interactive GUI installer + # CLT packages now appear in list with sfotwareupdate -l + xcode-select --install + + # new packages available + CLT_LABEL=$( + softwareupdate -l 2>&1 | + awk -F': ' ' + /Label: Command Line Tools/ { + label = $2 + version = label + sub(/^Command Line Tools for Xcode /, "", version) + sub(/-.*/, "", version) + + if (version + 0 > max) { + max = version + 0 + newest = label + } + } + END { + print newest + }' + ) + + sudo softwareupdate --install "$CLT_LABEL" macos_java_17_home: type: bash cmd: | - grep -qxF 'export JAVA_HOME=$(/usr/libexec/java_home -v 17)' ~/.bashrc 2>/dev/null || echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 17)' >> ~/.bashrc - grep -qxF 'export PATH=$JAVA_HOME/bin:$PATH' ~/.bashrc 2>/dev/null || echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc - grep -qxF 'export JAVA_HOME=$(/usr/libexec/java_home -v 17)' ~/.zshrc 2>/dev/null || echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 17)' >> ~/.zshrc - grep -qxF 'export PATH=$JAVA_HOME/bin:$PATH' ~/.zshrc 2>/dev/null || echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.zshrc - - # Applied immediately too - groovy (needs: java) installs right - # after this in the same script and needs java on PATH already. export JAVA_HOME=$(/usr/libexec/java_home -v 17) export PATH=$JAVA_HOME/bin:$PATH macos_winetricks_wsh57: @@ -213,6 +201,44 @@ scripts: popd +appends: + # Configure Brew CLI for default zsh + macos_homebrew_zsh: + dest: $HOME/.zprofile + lines: + - BREWCMD=$([[ "$(uname -m)" == "arm64" ]] && echo "/opt/homebrew/bin/brew" || echo "/usr/local/bin/brew") + - eval "$($BREWCMD shellenv zsh)" + # Configure Brew CLI for default bash + macos_homebrew_bash: + dest: $HOME/.bash_profile + lines: + - BREWCMD=$([[ "$(uname -m)" == "arm64" ]] && echo "/opt/homebrew/bin/brew" || echo "/usr/local/bin/brew") + - eval "$($BREWCMD shellenv bash)" + macos_setup_homebrew_bash: + dest: /etc/shells + interpolate: true + lines: + - $(brew --prefix)/bin/bash + macos_setup_homebrew_zsh: + dest: /etc/shells + interpolate: true + lines: + - $(brew --prefix)/bin/zsh + macos_java_17_home: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - export JAVA_HOME=$(/usr/libexec/java_home -v 17) + - export PATH=$JAVA_HOME/bin:$PATH + macos_cpan_local_setup: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib=$HOME/perl5)" + - export PATH=$HOME/perl5/bin:$PATH + macos_wine_fix: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - export MVK_CONFIG_LOG_LEVEL=0 + - export WINEDEBUG=-all + - diff --git a/scriptbox/scripts/generate_chef_databag.rb b/scriptbox/scripts/generate_chef_databag.rb index ec69926..3a339b5 100644 --- a/scriptbox/scripts/generate_chef_databag.rb +++ b/scriptbox/scripts/generate_chef_databag.rb @@ -97,6 +97,17 @@ def step_to_entry(step, tree) append = (tree['appends'] || {})[step[:name]] entry[:dest] = append['dest'] entry[:lines] = append['lines'] + # Passed through, not acted on - Chef's own append_if_no_line, + # Ansible's lineinfile, and Puppet's file_line all write `line` as + # inert Ruby/Python/Puppet data with no shell involved at all (see + # generate_install_script.rb's own append_lines comment on why this + # flag exists in the first place - it's meaningless without a real + # shell downstream to do the evaluating). Carried into the data bag + # anyway so a consumer that reaches an interpolate: true step it + # can't honor has something to check and reject loudly against, + # rather than silently writing the manifest's own literal `$(...)` + # text into the target file. + entry[:interpolate] = true if append['interpolate'] end entry diff --git a/scriptbox/scripts/generate_install_script.rb b/scriptbox/scripts/generate_install_script.rb index 34bd0a0..7cfc191 100755 --- a/scriptbox/scripts/generate_install_script.rb +++ b/scriptbox/scripts/generate_install_script.rb @@ -297,20 +297,38 @@ def file_write(step, tree) # grep-guard/`|| true` logic itself lives once in append_line() now, # not re-emitted per line here - see common.yml's own comment for why # each of those pieces matters. +# +# interpolate: true (default false, per entry - not per line) switches +# from single- to double-quoting, so a line's own `$(...)`/`$VAR` gets +# evaluated by the shell running append_line - a live homebrew prefix +# (`$(brew --prefix)/bin/bash`, /etc/shells needs the real resolved +# path, not the literal text) has no other way to reach the target +# machine's own actual value. Off by default because most appends - +# msys2/cygwin_purge_windows_path's own tr/grep pipelines, anything +# with embedded quotes - need the opposite: written exactly as typed, +# none of it evaluated as shell syntax. def append_lines(step, tree) entry = tree['appends'][step[:name]] Array(entry['dest']).flat_map do |dest| Array(entry['lines']).map do |line| - # Lines like msys2/cygwin_purge_windows_path embed their own single - # quotes (tr ':' '\n', grep -vE '^/[a-zA-Z]/', ...). Naively - # interpolating `line` inside a '...' wrapper lets those embedded - # quotes toggle bash's own quote-parsing mid-string, silently - # corrupting what gets written (confirmed directly: produced - # `tr : n` instead of `tr ':' '\n'` in a real generated .bashrc). - # Standard bash single-quote escaping - close, escaped literal - # quote, reopen - keeps the whole line literal regardless of - # what it contains. - quoted = "'" + line.gsub("'") { "'\\''" } + "'" + quoted = if entry['interpolate'] + # Double-quote escaping - only \ and " need it here; + # $ and ` are deliberately left alone, that's the + # entire point of this branch existing. + '"' + line.gsub('\\') { '\\\\' }.gsub('"') { '\\"' } + '"' + else + # Lines like msys2/cygwin_purge_windows_path embed + # their own single quotes (tr ':' '\n', grep -vE + # '^/[a-zA-Z]/', ...). Naively interpolating `line` + # inside a '...' wrapper lets those embedded quotes + # toggle bash's own quote-parsing mid-string, silently + # corrupting what gets written (confirmed directly: + # produced `tr : n` instead of `tr ':' '\n'` in a real + # generated .bashrc). Standard bash single-quote + # escaping - close, escaped literal quote, reopen - + # keeps the whole line literal regardless of content. + "'" + line.gsub("'") { "'\\''" } + "'" + end %(append_line "#{dest}" #{quoted}) end end.join("\n") @@ -326,13 +344,24 @@ def append_lines(step, tree) # variables the same way bash's do, so append_line's own $Dest # parameter receives the already-expanded path, not the literal text # "$PROFILE". Line values are escaped for a PowerShell single-quoted -# string (a literal quote there is just doubled, not the close/escape/ -# reopen dance bash needs). +# string by default (a literal quote there is just doubled, not the +# close/escape/reopen dance bash needs) - unless interpolate: true +# (see append_lines' own comment on why this exists at all), which +# switches to a double-quoted line the same way, so $variable/$(...) +# in the manifest's own text gets evaluated by the shell running +# append_line instead of written out literally. def powershell_append_lines(step, tree) entry = tree['appends'][step[:name]] Array(entry['dest']).flat_map do |dest| Array(entry['lines']).map do |line| - quoted = "'" + line.gsub("'") { "''" } + "'" + quoted = if entry['interpolate'] + # ` is PowerShell's own escape character - has to be + # escaped first, before ", or a line containing both + # would double-escape the quote's own backtick. + '"' + line.gsub('`') { '``' }.gsub('"') { '`"' } + '"' + else + "'" + line.gsub("'") { "''" } + "'" + end %(append_line "#{dest}" #{quoted}) end end.join("\n") From a9a85a1a336835526ca489c9a470dc1ce1c00507 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 30 Aug 2026 22:57:08 -0700 Subject: [PATCH 02/12] support for erb parsing at variable setting stage --- scriptbox/config/macos.yml | 69 +++++++++++++++++------------- scriptbox/scripts/resolve_order.rb | 24 ++++++++++- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index acf8212..0d3a57a 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -1,4 +1,6 @@ macos: + variables: + brew_prefix: "<%= RbConfig::CONFIG['host_cpu'] == 'x86_64' ? '/usr/local' : '/opt/homebrew' %>" global: packages: - script: macos_rosetta @@ -11,6 +13,7 @@ macos: awk: packages: - brew: gawk + append: macos_brew_gawk groovy: packages: - brew: groovy @@ -39,7 +42,9 @@ macos: shell_scripts: packages: - brew: bc - - brew: getopt + append: macos_brew_bc + - brew: gnu-getopt + append: macos_brew_gnu-getopt bash: packages: - brew: bash @@ -181,49 +186,44 @@ scripts: macos_wine_coreutils: type: bash cmd: | - pushd $HOME/Downloads - - # Download coreutils - curl -LO https://github.com/microsoft/coreutils/releases/download/v2026.6.16/coreutils-2026.6.16-x64.exe - - # Verify checksum - ACTUAL_SHA=$(shasum -a 256 coreutils-2026.6.16-x64.exe | cut -f1 -d' ') - EXPECTED_SHA="f862b1aa433310420ae20f9b1384f3f974a26ba98ae37ac548061116a3ef6c62" - - if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then - echo "ERROR: CHECKSUM MISMATCH - DO NOT USE THIS FILE" >&2 - exit 1 - fi - echo "CHECKSUM OK" - - # install coreutils - wine coreutils-2026.6.16-x64.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- - - popd + pushd $HOME/Downloads + # Download coreutils + curl -LO https://github.com/microsoft/coreutils/releases/download/v2026.6.16/coreutils-2026.6.16-x64.exe + + # Verify checksum + ACTUAL_SHA=$(shasum -a 256 coreutils-2026.6.16-x64.exe | cut -f1 -d' ') + EXPECTED_SHA="f862b1aa433310420ae20f9b1384f3f974a26ba98ae37ac548061116a3ef6c62" + + if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then + echo "ERROR: CHECKSUM MISMATCH - DO NOT USE THIS FILE" >&2 + exit 1 + fi + echo "CHECKSUM OK" + + # install coreutils + wine coreutils-2026.6.16-x64.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- + + popd appends: # Configure Brew CLI for default zsh macos_homebrew_zsh: dest: $HOME/.zprofile lines: - - BREWCMD=$([[ "$(uname -m)" == "arm64" ]] && echo "/opt/homebrew/bin/brew" || echo "/usr/local/bin/brew") - - eval "$($BREWCMD shellenv zsh)" + - eval "$(<%= $brew_prefix %>/bin/brew shellenv zsh)" # Configure Brew CLI for default bash macos_homebrew_bash: dest: $HOME/.bash_profile lines: - - BREWCMD=$([[ "$(uname -m)" == "arm64" ]] && echo "/opt/homebrew/bin/brew" || echo "/usr/local/bin/brew") - - eval "$($BREWCMD shellenv bash)" + - eval "$(<%= $brew_prefix %>/bin/brew shellenv bash)" macos_setup_homebrew_bash: dest: /etc/shells - interpolate: true lines: - - $(brew --prefix)/bin/bash + - <%= $brew_prefix %>/bin/bash macos_setup_homebrew_zsh: dest: /etc/shells - interpolate: true lines: - - $(brew --prefix)/bin/zsh + - <%= $brew_prefix %>/bin/zsh macos_java_17_home: dest: ["$HOME/.bashrc", "$HOME/.zshrc"] lines: @@ -239,6 +239,17 @@ appends: lines: - export MVK_CONFIG_LOG_LEVEL=0 - export WINEDEBUG=-all - + macos_brew_gawk: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - PATH="<%= $brew_prefix %>/opt/gawk/libexec/gnubin:$PATH" + macos_brew_bc: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - PATH="<%= $brew_prefix %>/opt/bc/bin:$PATH" + macos_brew_gnu-getopt: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - PATH="<%= $brew_prefix %>/opt/gnu-getopt/bin:$PATH" diff --git a/scriptbox/scripts/resolve_order.rb b/scriptbox/scripts/resolve_order.rb index 50ec570..02fa3fa 100644 --- a/scriptbox/scripts/resolve_order.rb +++ b/scriptbox/scripts/resolve_order.rb @@ -1,4 +1,5 @@ require 'yaml' +require 'erb' PACKAGE_TYPES = %w[brew cask tap cpan cpanm system apt pyenv rbenv rvm sdkman asdf asdf_plugin choco choco_cyg choco_local feature gem pacman path cyg cmd sysctl salt_formula apt_pin pipx powershell_package_provider powershell_module powershell_cmd noop].freeze @@ -568,6 +569,27 @@ def parse_condition(condition) # since it has more than one dot; but `retries: 3` would parse as an # Integer) - always coerced to a String on substitution since it's # being spliced into one. +# +# Two unrelated things both spelled `<%= ... %>`, on purpose: a +# *reference* (`<%= $name %>`, VARIABLE_REF's own syntax, matched by +# the gsub below) is never templated - just a literal name lookup, so +# `$PATH` sitting right next to one in the same string (e.g. `PATH="<%= +# $brew_prefix %>/opt/gawk/libexec/gnubin:$PATH"`) is never touched, +# whatever it contains. A variable's own *definition* in the +# variables: block, on the other hand, is run through a real +# ERB.new(...).result here, every time it's looked up - genuine Ruby, +# evaluated on whatever machine runs this generator (RbConfig::CONFIG, +# ENV, anything else in scope), not the eventual target - e.g. +# `brew_prefix: "<%= RbConfig::CONFIG['host_cpu'] == 'x86_64' ? '/usr/ +# local/' : '/opt/homebrew/' %>"` resolves once per reference to a +# plain, already-final string before VARIABLE_REF ever splices it in. +# A value with no ERB tags at all (`ruby_ver: 4.0.6`) passes through +# ERB unchanged - it's a no-op for plain text, not just for expressions. +# Re-evaluated on every reference rather than cached once - harmless +# (a pure function of the string plus this process's own stable state, +# same result every time within one generator run) and far simpler +# than threading a resolved-vars cache through every one of this +# function's own recursive calls for no real benefit. def substitute_variables(value, vars) case value when String @@ -575,7 +597,7 @@ def substitute_variables(value, vars) name = Regexp.last_match(1) raise "unknown variable '#{name}' referenced as '<%= $#{name} %>' - not defined in this manifest's own variables: block" unless vars.key?(name) - vars[name].to_s + ERB.new(vars[name].to_s).result end when Hash value.transform_values { |v| substitute_variables(v, vars) } From 17cfb1a1d66392fc9007adc36afd46136bf1a6a9 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 30 Aug 2026 23:09:27 -0700 Subject: [PATCH 03/12] support for using sudo for append --- scriptbox/config/helpers/common.yml | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/scriptbox/config/helpers/common.yml b/scriptbox/config/helpers/common.yml index beeb86c..cf414db 100644 --- a/scriptbox/config/helpers/common.yml +++ b/scriptbox/config/helpers/common.yml @@ -6,10 +6,39 @@ common: append_line() { local DEST=$1 local LINE=$2 + local DIR + DIR="$(dirname "$DEST")" - mkdir -p "$(dirname "$DEST")" - [ -f "$DEST" ] || touch "$DEST" - grep -qxF "$LINE" "$DEST" 2>/dev/null || echo "$LINE" >> "$DEST" + # Auto-detected, not a manifest-level flag: a destination like + # /etc/shells is root-owned on every platform this runs + # against, while every $HOME-relative dest (.bashrc, .zshrc, + # ...) is writable by whoever's running this script - no + # reason to make a manifest author remember to flag the rare + # case when the real filesystem already knows the answer. + # Existing dest checks itself (a read-only file in an + # otherwise-writable dir); a not-yet-existing dest checks its + # parent dir instead, since that's what actually has to accept + # the mkdir/touch/write. + local SUDO="" + if [ -e "$DEST" ]; then + [ -w "$DEST" ] || SUDO="sudo" + else + [ -w "$DIR" ] || SUDO="sudo" + fi + + $SUDO mkdir -p "$DIR" + [ -f "$DEST" ] || $SUDO touch "$DEST" + if ! grep -qxF "$LINE" "$DEST" 2>/dev/null; then + # sudo echo ... >> "$DEST" would NOT actually gain root for + # the redirection - >> is set up by *this* unprivileged + # shell before sudo ever runs. sudo tee -a is the real fix, + # piped rather than prefixed, only when actually needed. + if [ -n "$SUDO" ]; then + echo "$LINE" | sudo tee -a "$DEST" >/dev/null + else + echo "$LINE" >> "$DEST" + fi + fi } cmd_powershell: | function append_line { From b4238b6f242c8f67d47d578bac4551de00390f67 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 30 Aug 2026 23:16:36 -0700 Subject: [PATCH 04/12] update --- scriptbox/config/helpers/common.yml | 27 ++++++++------------ scriptbox/config/macos.yml | 2 ++ scriptbox/scripts/generate_install_script.rb | 8 +++++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/scriptbox/config/helpers/common.yml b/scriptbox/config/helpers/common.yml index cf414db..054fb3e 100644 --- a/scriptbox/config/helpers/common.yml +++ b/scriptbox/config/helpers/common.yml @@ -3,28 +3,23 @@ common: helpers: append_line: cmd: | + # $3 (SUDO_NEEDED, "true"/empty) - a manifest's own appends: entry + # says so explicitly (sudo: true), decided once at generation + # time - see generate_install_script.rb's own append_lines. + # `sudo append_line ...` from the call site can't work instead: + # sudo execs a fresh subprocess and looks up its argument as an + # external program on PATH - it has no visibility into this + # shell's own function table, so it would just fail outright + # with "command not found". The elevation has to happen on + # append_line's own internal touch/tee calls, from inside. append_line() { local DEST=$1 local LINE=$2 + local SUDO_NEEDED=$3 local DIR DIR="$(dirname "$DEST")" - - # Auto-detected, not a manifest-level flag: a destination like - # /etc/shells is root-owned on every platform this runs - # against, while every $HOME-relative dest (.bashrc, .zshrc, - # ...) is writable by whoever's running this script - no - # reason to make a manifest author remember to flag the rare - # case when the real filesystem already knows the answer. - # Existing dest checks itself (a read-only file in an - # otherwise-writable dir); a not-yet-existing dest checks its - # parent dir instead, since that's what actually has to accept - # the mkdir/touch/write. local SUDO="" - if [ -e "$DEST" ]; then - [ -w "$DEST" ] || SUDO="sudo" - else - [ -w "$DIR" ] || SUDO="sudo" - fi + [ "$SUDO_NEEDED" = "true" ] && SUDO="sudo" $SUDO mkdir -p "$DIR" [ -f "$DEST" ] || $SUDO touch "$DEST" diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index 0d3a57a..f62cd68 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -218,10 +218,12 @@ appends: - eval "$(<%= $brew_prefix %>/bin/brew shellenv bash)" macos_setup_homebrew_bash: dest: /etc/shells + sudo: true lines: - <%= $brew_prefix %>/bin/bash macos_setup_homebrew_zsh: dest: /etc/shells + sudo: true lines: - <%= $brew_prefix %>/bin/zsh macos_java_17_home: diff --git a/scriptbox/scripts/generate_install_script.rb b/scriptbox/scripts/generate_install_script.rb index 7cfc191..f490a37 100755 --- a/scriptbox/scripts/generate_install_script.rb +++ b/scriptbox/scripts/generate_install_script.rb @@ -307,8 +307,14 @@ def file_write(step, tree) # msys2/cygwin_purge_windows_path's own tr/grep pipelines, anything # with embedded quotes - need the opposite: written exactly as typed, # none of it evaluated as shell syntax. +# +# sudo: true (default false, per entry) - a destination like /etc/shells +# is root-owned; append_line's own internal touch/tee calls need it +# passed through as a third argument (see common.yml's own comment on +# why this can't just be `sudo append_line ...` from out here instead). def append_lines(step, tree) entry = tree['appends'][step[:name]] + sudo_arg = entry['sudo'] ? ' true' : '' Array(entry['dest']).flat_map do |dest| Array(entry['lines']).map do |line| quoted = if entry['interpolate'] @@ -329,7 +335,7 @@ def append_lines(step, tree) # keeps the whole line literal regardless of content. "'" + line.gsub("'") { "'\\''" } + "'" end - %(append_line "#{dest}" #{quoted}) + %(append_line "#{dest}" #{quoted}#{sudo_arg}) end end.join("\n") end From 9083f23380439f5173aacbf52b9546f22528b4a7 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 30 Aug 2026 23:27:37 -0700 Subject: [PATCH 05/12] small fix --- scriptbox/config/macos.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index f62cd68..e68d9c0 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -112,9 +112,7 @@ scripts: /bin/bash -c "$(curl -fsSL "$script_url")" # Bootstrap Homebrew into existing environment - BREWCMD="/usr/local/bin/brew" - [[ "$(uname -m)" == "arm64" ]] && BREWCMD="/opt/homebrew/bin/brew" - eval "$($BREWCMD shellenv zsh)" + eval "$(<%= $brew_prefix %>/bin/brew shellenv bash)" if ! command -v brew >/dev/null 2>&1; then echo "macos_homebrew: brew not found on PATH after install - stopping" >&2 From 3f6da35500568f55047b211b807d05325204393f Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Tue, 1 Sep 2026 13:13:43 -0700 Subject: [PATCH 06/12] macos.yml update --- scriptbox/config/macos.yml | 111 ++++++++++++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index e68d9c0..f375734 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -1,6 +1,11 @@ macos: variables: brew_prefix: "<%= RbConfig::CONFIG['host_cpu'] == 'x86_64' ? '/usr/local' : '/opt/homebrew' %>" + python3_ver: 3.14 + ruby4_ver: 4.0 + php_ver: 8.5 + tcl_ver: 9 + go_ver: 1.27 global: packages: - script: macos_rosetta @@ -8,11 +13,16 @@ macos: - script: macos_homebrew - append: macos_homebrew_zsh - append: macos_homebrew_bash + - brew: grep + append: macos_brew_gnu_grep + script: macos_brew_gnu_grep + meets: gnu_grep lessons: gen_scripts: awk: packages: - brew: gawk + script: macos_brew_gawk append: macos_brew_gawk groovy: packages: @@ -29,21 +39,47 @@ macos: - cpanm: Switch php: packages: - - brew: php + # Dependencies + # - apr + # - apr-util + # - argon2 + # - autoconf + # - curl + # - freetds + # - gd + # - gmp + # - icu4c@78 + # - libpq + # - libsodium + # - libzip + # - net-snmp + # - oniguruma + # - openldap + # - openssl@3 + # - pcre2 + # - sqlite + # - tidy-html5 + # - unixodbc + # - gettext + - brew: php@<%= $php_ver %> python3: packages: - - brew: python3 + - brew: python@<%= $python3_ver %> ruby: packages: - - brew: ruby + - brew: ruby@<%= $ruby4_ver %> + - append: macos_brew_ruby4 + meets: ruby4 tcl: packages: - - brew: tcl-tk + - brew: tcl-tk@<%= $tcl_ver %> shell_scripts: packages: - brew: bc + script: macos_brew_bc append: macos_brew_bc - brew: gnu-getopt + script: macos_brew_gnu-getopt append: macos_brew_gnu-getopt bash: packages: @@ -54,7 +90,7 @@ macos: - brew: tcsh ksh: packages: - - brew: ksh + - brew: ksh93 posix: packages: - brew: dash-shell @@ -69,7 +105,8 @@ macos: - cask: dotnet-sdk go: packages: - - brew: go + - brew: go@<%= $go_ver %> + append: macos_go java: packages: - tap: homebrew/cask-versions @@ -79,26 +116,49 @@ macos: append: macos_java_17_home rust: packages: + # Dependencies + # - libgit2 + # - libssh2 + # - llvm@22 + # - openssl@3 + # - pkgconf + # - sqlite + # Zsh Completions: + # /opt/homebrew/share/zsh/site-functions + # Link this toolchain with `rustup` under the name `system` with: + # rustup toolchain link system "$(brew --prefix rust)" - brew: rust win_scripts: packages: - cask: wine-stable script: macos_wine_fix + tag: wine - brew: winetricks script: macos_winetricks_wsh57 + tag: wine - script: macos_wine_coreutils + tag: wine powershell: packages: + # * Bottle requires building from source (downloads dotnet 10.x in source form) + # * Cask has the official, self-contained Microsoft version of PowerShell that has the embedded .NET runtime - brew: powershell cibox: packages: - brew: act - brew: colima - brew: docker + scriptbox: + packages: + - script: macos_gem_update + needs: ruby4, gnu_grep + - gem: ratatui_ruby + needs: ruby >= 3.2 testbox: packages: - brew: powershell - brew: ruby + scripts: macos_rosetta: type: bash @@ -203,6 +263,30 @@ scripts: wine coreutils-2026.6.16-x64.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- popd + macos_gem_update: + type: bash + cmd: | + chmod -R u+w <%= $brew_prefix %>/lib/ruby/gems/4.0.0/plugins/ + + URL="https://rubygems.org/api/v1/versions/rubygems-update/latest.json" + if [[ "$(curl -s $URL | grep -oP '"version"[: ]*"\K[^"]+')" != "$(gem --version)" ]]; then + gem update --system + fi + macos_brew_gnu_grep: + cmd: | + PATH="<%= $brew_prefix %>/opt/grep/libexec/gnubin:$PATH" + macos_brew_gawk: + cmd: | + PATH="<%= $brew_prefix %>/opt/gawk/libexec/gnubin:$PATH" + macos_brew_bc: + cmd: | + PATH="<%= $brew_prefix %>/opt/bc/bin:$PATH" + macos_brew_gnu-getopt: + cmd: | + PATH="<%= $brew_prefix %>/opt/gnu-getopt/bin:$PATH" + + + appends: # Configure Brew CLI for default zsh macos_homebrew_zsh: @@ -251,5 +335,16 @@ appends: dest: ["$HOME/.bashrc", "$HOME/.zshrc"] lines: - PATH="<%= $brew_prefix %>/opt/gnu-getopt/bin:$PATH" - - + macos_brew_ruby4: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - PATH="<%= $brew_prefix %>/lib/ruby/gems/4.0.0/bin:$PATH" + macos_brew_gnu_grep: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - PATH="<%= $brew_prefix %>/opt/grep/libexec/gnubin:$PATH" + macos_go: + dest: ["$HOME/.bashrc", "$HOME/.zshrc"] + lines: + - export GOPATH=$(go env GOPATH) + - export PATH=$GOPATH/bin:$PATH From 4f99540785041e03caa7555b1652aac2de326291 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Tue, 1 Sep 2026 22:01:24 -0700 Subject: [PATCH 07/12] wip --- .gitmodules | 3 +++ pkgbox/tap/darkn3rd/homebrew-tools | 1 + scriptbox/config/macos.yml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 160000 pkgbox/tap/darkn3rd/homebrew-tools diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8dfd10f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pkgbox/tap/darkn3rd/homebrew-tools"] + path = pkgbox/tap/darkn3rd/homebrew-tools + url = git@github.com:darkn3rd/homebrew-tools.git diff --git a/pkgbox/tap/darkn3rd/homebrew-tools b/pkgbox/tap/darkn3rd/homebrew-tools new file mode 160000 index 0000000..f65527f --- /dev/null +++ b/pkgbox/tap/darkn3rd/homebrew-tools @@ -0,0 +1 @@ +Subproject commit f65527f190a94fea186a27e2393e6f0923227789 diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index f375734..05d81eb 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -142,7 +142,7 @@ macos: packages: # * Bottle requires building from source (downloads dotnet 10.x in source form) # * Cask has the official, self-contained Microsoft version of PowerShell that has the embedded .NET runtime - - brew: powershell + - cask: darkn3rd/tools/powershell cibox: packages: - brew: act From 3d9234044db56dd099883fa5e18e5451665a4d98 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Tue, 1 Sep 2026 22:03:09 -0700 Subject: [PATCH 08/12] remove redundant repo content --- pkgbox/tap/darkn3rd/homebrew-tools | 1 - 1 file changed, 1 deletion(-) delete mode 160000 pkgbox/tap/darkn3rd/homebrew-tools diff --git a/pkgbox/tap/darkn3rd/homebrew-tools b/pkgbox/tap/darkn3rd/homebrew-tools deleted file mode 160000 index f65527f..0000000 --- a/pkgbox/tap/darkn3rd/homebrew-tools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f65527f190a94fea186a27e2393e6f0923227789 From 0630d056e31c25c197e229079f6fa9cf57a26841 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Tue, 1 Sep 2026 22:14:16 -0700 Subject: [PATCH 09/12] Re-add pkgbox/tap/darkn3rd/homebrew-tools submodule The prior "remove redundant repo content" commit had only removed the gitlink, leaving .gitmodules and .git/config with an orphaned submodule entry pointing at nothing. Cleaned that up (deinit, removed cached .git/modules data, dropped the stale .gitmodules entry) and re-added the submodule properly, same commit (f65527f) as before. --- pkgbox/tap/darkn3rd/homebrew-tools | 1 + 1 file changed, 1 insertion(+) create mode 160000 pkgbox/tap/darkn3rd/homebrew-tools diff --git a/pkgbox/tap/darkn3rd/homebrew-tools b/pkgbox/tap/darkn3rd/homebrew-tools new file mode 160000 index 0000000..f65527f --- /dev/null +++ b/pkgbox/tap/darkn3rd/homebrew-tools @@ -0,0 +1 @@ +Subproject commit f65527f190a94fea186a27e2393e6f0923227789 From da4a1e9ec1f58e30e792b3462f204db754cf399b Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Tue, 1 Sep 2026 22:22:57 -0700 Subject: [PATCH 10/12] update new powershell-pkg name --- pkgbox/tap/darkn3rd/homebrew-tools | 2 +- scriptbox/config/macos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgbox/tap/darkn3rd/homebrew-tools b/pkgbox/tap/darkn3rd/homebrew-tools index f65527f..450694f 160000 --- a/pkgbox/tap/darkn3rd/homebrew-tools +++ b/pkgbox/tap/darkn3rd/homebrew-tools @@ -1 +1 @@ -Subproject commit f65527f190a94fea186a27e2393e6f0923227789 +Subproject commit 450694f15a47aff940d638a26bc37a661b455c03 diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index 05d81eb..d5ff360 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -142,7 +142,7 @@ macos: packages: # * Bottle requires building from source (downloads dotnet 10.x in source form) # * Cask has the official, self-contained Microsoft version of PowerShell that has the embedded .NET runtime - - cask: darkn3rd/tools/powershell + - cask: darkn3rd/tools/powershell-pkg cibox: packages: - brew: act From 2323988b64fae399609860d83ceb7146458d2154 Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 6 Sep 2026 22:28:12 -0700 Subject: [PATCH 11/12] update clt routine --- scriptbox/config/macos.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scriptbox/config/macos.yml b/scriptbox/config/macos.yml index d5ff360..42109cb 100644 --- a/scriptbox/config/macos.yml +++ b/scriptbox/config/macos.yml @@ -168,8 +168,8 @@ scripts: macos_homebrew: type: bash cmd: | - script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" - /bin/bash -c "$(curl -fsSL "$script_url")" + SCRIPT_URL="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + /bin/bash -c "$(curl -fsSL $SCRIPT_URL)" # Bootstrap Homebrew into existing environment eval "$(<%= $brew_prefix %>/bin/brew shellenv bash)" @@ -225,6 +225,7 @@ scripts: ) sudo softwareupdate --install "$CLT_LABEL" + pkill -9 -f 'Install Command Line Developer Tools' macos_java_17_home: type: bash cmd: | From f9e9cd513a0fa22de7b4b08db4c1a87bbe05158e Mon Sep 17 00:00:00 2001 From: Joaquin Menchaca Date: Sun, 13 Sep 2026 15:20:51 -0700 Subject: [PATCH 12/12] submodule version bump --- pkgbox/tap/darkn3rd/homebrew-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgbox/tap/darkn3rd/homebrew-tools b/pkgbox/tap/darkn3rd/homebrew-tools index 450694f..472890b 160000 --- a/pkgbox/tap/darkn3rd/homebrew-tools +++ b/pkgbox/tap/darkn3rd/homebrew-tools @@ -1 +1 @@ -Subproject commit 450694f15a47aff940d638a26bc37a661b455c03 +Subproject commit 472890b67216957925e693e47be166e8b6b44e82