From a892e7e262967664b3c836435b04a368a06e226c Mon Sep 17 00:00:00 2001 From: nitin sanghi Date: Thu, 30 Jul 2026 15:13:51 +0530 Subject: [PATCH 1/3] CHEF-37441: Add ~/.chef/ruby/VERSION/gems to GEM_PATH for dynamic plugin loading - Update binstub_patch.rb to prepend vendor dir, Gem.user_dir, and ~/.chef/ruby/VERSION/gems to GEM_PATH at runtime so that plugins installed via 'chef gem install knife-' are immediately available without additional configuration. Uses RbConfig::CONFIG['ruby_version'] to resolve the correct API version path at runtime. - Update habitat/plan.sh wrapper script to include ${HOME}/.gem/ruby/VERSION and ${HOME}/.chef/ruby/VERSION/gems in GEM_PATH so plugins are discoverable when knife runs in a Habitat environment on Linux. - Update habitat/aarch64-darwin/plan.sh wrapper script with same GEM_PATH additions for macOS ARM64 Habitat builds. - Update habitat/plan.ps1 to pass --ignore-dependencies to gem install knife*.gem (avoids appbundler ambiguous specs from duplicate dep entries) and inject binstub_patch.rb into Windows binstubs after appbundler generation, matching the Linux/macOS behavior. Signed-off-by: nitin sanghi --- binstub_patch.rb | 18 +++++++++++++++++- habitat/aarch64-darwin/plan.sh | 7 +++++-- habitat/plan.ps1 | 12 ++++++++++++ habitat/plan.sh | 7 +++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/binstub_patch.rb b/binstub_patch.rb index 0059942..b13a715 100644 --- a/binstub_patch.rb +++ b/binstub_patch.rb @@ -1,4 +1,20 @@ unless ENV["APPBUNDLER_ALLOW_RVM"] ENV["APPBUNDLER_ALLOW_RVM"] = "true" - ENV["GEM_PATH"] = [File.expand_path(File.join(__dir__, "..", "vendor")), ENV["GEM_PATH"]].compact.join(File::PATH_SEPARATOR) end + +# Prepend the package vendor dir, the current user's gem dir, and the Chef gem dir to +# GEM_PATH so that: +# 1. Knife's vendored dependencies are always found. +# 2. Knife plugins installed via `gem install knife-` are discoverable on both +# Linux (~/.gem/ruby/VERSION) and Windows (%USERPROFILE%\.gem\ruby\VERSION). +# 3. Gems installed via `chef gem install` (~/.chef/gems) are immediately available. +# +# This block runs before appbundler's env_sanitizer (which calls Gem.clear_paths and +# re-reads GEM_PATH from ENV), so these additions are always picked up correctly. +_knife_vendor = File.expand_path(File.join(__dir__, "..", "vendor")) +# chef-cli gem install places gems under ~/.chef/ruby/RUBY_API_VERSION/gems +# (e.g. ~/.chef/ruby/3.4.0/gems). RbConfig::CONFIG["ruby_version"] returns the same +# API version string that Ruby/Bundler use for the gem directory layout. +_chef_gem_dir = File.join(Dir.home, ".chef", "ruby", RbConfig::CONFIG["ruby_version"], "gems") +_existing_paths = ENV["GEM_PATH"]&.split(File::PATH_SEPARATOR) || [] +ENV["GEM_PATH"] = ([_knife_vendor, Gem.user_dir, _chef_gem_dir] + _existing_paths).uniq.join(File::PATH_SEPARATOR) diff --git a/habitat/aarch64-darwin/plan.sh b/habitat/aarch64-darwin/plan.sh index 92e255b..177a67c 100644 --- a/habitat/aarch64-darwin/plan.sh +++ b/habitat/aarch64-darwin/plan.sh @@ -137,8 +137,11 @@ do_install() { set -e # GEM_HOME points to the bundler-managed gem tree (where 'chef' and Gemfile deps live) export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" -# GEM_PATH also includes the flat vendor tree (where gem install puts knife runtime deps) -export GEM_PATH="$pkg_prefix/vendor" +# GEM_PATH includes the flat vendor tree (knife runtime deps), the standard user gem dir +# (~/.gem/ruby/VERSION), and the Chef gem dir (~/.chef/gems) so that plugins installed +# via 'gem install knife-' or 'chef gem install knife-' are found at +# runtime without any additional configuration. +export GEM_PATH="$pkg_prefix/vendor:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" export DYLD_LIBRARY_PATH="$(pkg_path_for core/libarchive)/lib:\$DYLD_LIBRARY_PATH" # SSL certificate verification - point OpenSSL to CA certificates export SSL_CERT_FILE="$(pkg_path_for core/cacerts)/ssl/certs/cacert.pem" diff --git a/habitat/plan.ps1 b/habitat/plan.ps1 index 00a2fe1..39cec0f 100644 --- a/habitat/plan.ps1 +++ b/habitat/plan.ps1 @@ -93,6 +93,18 @@ function Invoke-Install { Write-BuildLine "** generating binstubs for knife" Invoke-Expression -Command "appbundler.bat $project_root $pkg_prefix/bin knife" If ($lastexitcode -ne 0) { Exit $lastexitcode } + + Write-BuildLine "** patching binstubs for direct execution and dynamic plugin loading" + $binstubPatch = Get-Content -Path "$project_root\binstub_patch.rb" + Get-ChildItem "$pkg_prefix\bin" | Where-Object { $_.Extension -notin @(".bat", ".ps1") } | ForEach-Object { + $lines = Get-Content -Path $_.FullName + $matchLine = $lines | Select-String -Pattern 'require "rubygems"' | Select-Object -First 1 + if ($matchLine) { + $lineNum = $matchLine.LineNumber # 1-based; insert patch after this line + $newLines = $lines[0..($lineNum - 1)] + $binstubPatch + $lines[$lineNum..($lines.Count - 1)] + Set-Content -Path $_.FullName -Value $newLines + } + } } finally { Pop-Location } diff --git a/habitat/plan.sh b/habitat/plan.sh index 2902d43..17074ab 100644 --- a/habitat/plan.sh +++ b/habitat/plan.sh @@ -143,8 +143,11 @@ do_install() { set -e # GEM_HOME points to the bundler-managed gem tree (where 'chef' and Gemfile deps live) export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" -# GEM_PATH also includes the flat vendor tree (where gem install puts knife runtime deps) -export GEM_PATH="$pkg_prefix/vendor" +# GEM_PATH includes the flat vendor tree (knife runtime deps), the standard user gem dir +# (~/.gem/ruby/VERSION), and the Chef gem dir (~/.chef/gems) so that plugins installed +# via 'gem install knife-' or 'chef gem install knife-' are found at +# runtime without any additional configuration. +export GEM_PATH="$pkg_prefix/vendor:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" exec $(pkg_path_for ${ruby_pkg})/bin/ruby $pkg_prefix/libexec/knife "\$@" EOF chmod -v 755 "$pkg_prefix/bin/knife" From 612c2f7b99e29f4848f4a07c5113d209b42104b5 Mon Sep 17 00:00:00 2001 From: nitin sanghi Date: Thu, 30 Jul 2026 15:16:32 +0530 Subject: [PATCH 2/3] CHEF-37441: Fix Lint/UnderscorePrefixedVariableName warnings in binstub_patch.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove leading underscore from knife_vendor, chef_gem_dir, and existing_paths variables — underscore prefix is reserved for intentionally unused variables in Ruby/RuboCop convention. Signed-off-by: nitin sanghi --- binstub_patch.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/binstub_patch.rb b/binstub_patch.rb index b13a715..cc8bb45 100644 --- a/binstub_patch.rb +++ b/binstub_patch.rb @@ -11,10 +11,10 @@ # # This block runs before appbundler's env_sanitizer (which calls Gem.clear_paths and # re-reads GEM_PATH from ENV), so these additions are always picked up correctly. -_knife_vendor = File.expand_path(File.join(__dir__, "..", "vendor")) +knife_vendor = File.expand_path(File.join(__dir__, "..", "vendor")) # chef-cli gem install places gems under ~/.chef/ruby/RUBY_API_VERSION/gems # (e.g. ~/.chef/ruby/3.4.0/gems). RbConfig::CONFIG["ruby_version"] returns the same # API version string that Ruby/Bundler use for the gem directory layout. -_chef_gem_dir = File.join(Dir.home, ".chef", "ruby", RbConfig::CONFIG["ruby_version"], "gems") -_existing_paths = ENV["GEM_PATH"]&.split(File::PATH_SEPARATOR) || [] -ENV["GEM_PATH"] = ([_knife_vendor, Gem.user_dir, _chef_gem_dir] + _existing_paths).uniq.join(File::PATH_SEPARATOR) +chef_gem_dir = File.join(Dir.home, ".chef", "ruby", RbConfig::CONFIG["ruby_version"], "gems") +existing_paths = ENV["GEM_PATH"]&.split(File::PATH_SEPARATOR) || [] +ENV["GEM_PATH"] = ([knife_vendor, Gem.user_dir, chef_gem_dir] + existing_paths).uniq.join(File::PATH_SEPARATOR) From ee14e836d4bc1a9d3bc4d65488171eaacd5b7c56 Mon Sep 17 00:00:00 2001 From: nitin sanghi Date: Thu, 30 Jul 2026 17:09:52 +0530 Subject: [PATCH 3/3] Added ruby version with hab ruby version Signed-off-by: nitin sanghi --- habitat/aarch64-darwin/plan.sh | 16 ++++++++++++---- habitat/plan.sh | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/habitat/aarch64-darwin/plan.sh b/habitat/aarch64-darwin/plan.sh index 177a67c..5875163 100644 --- a/habitat/aarch64-darwin/plan.sh +++ b/habitat/aarch64-darwin/plan.sh @@ -5,9 +5,9 @@ pkg_name=knife pkg_origin=chef ruby_pkg="core/ruby3_4" -# Ruby version for gem directory structure -# NOTE: Bundler normalizes Ruby versions to major.minor.0 format for gem compatibility. -ruby_gem_version="3.4.0" +# ruby_gem_version is set in do_before() once pkg_path_for is available. +# Declared here so it is in scope for all build functions. +ruby_gem_version="" pkg_description="knife is a command-line tool that provides an interface between a local chef-repo and the Chef Infra Server." pkg_deps=(${ruby_pkg} core/coreutils core/libarchive core/cacerts) @@ -42,6 +42,11 @@ pkg_version() { do_before() { update_pkg_version + # Resolve the Ruby API version (e.g. 3.4.0) from the actual Habitat ruby binary. + # Must be done here rather than at plan top-level because pkg_path_for is not + # available until after package dependencies have been resolved. + ruby_gem_version="$($(pkg_path_for ${ruby_pkg})/bin/ruby -e 'print RbConfig::CONFIG["ruby_version"]')" + build_line "Resolved ruby_gem_version=${ruby_gem_version}" } do_unpack() { @@ -141,7 +146,10 @@ export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" # (~/.gem/ruby/VERSION), and the Chef gem dir (~/.chef/gems) so that plugins installed # via 'gem install knife-' or 'chef gem install knife-' are found at # runtime without any additional configuration. -export GEM_PATH="$pkg_prefix/vendor:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" +# vendor/ruby/VERSION — bundler-managed gem tree (train-core and all runtime deps) +# vendor — flat gem install tree (knife gem itself) +# ~/.gem/ruby/VERSION and ~/.chef/ruby/VERSION/gems — user-installed plugins +export GEM_PATH="$pkg_prefix/vendor:$pkg_prefix/vendor/ruby/${ruby_gem_version}:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" export DYLD_LIBRARY_PATH="$(pkg_path_for core/libarchive)/lib:\$DYLD_LIBRARY_PATH" # SSL certificate verification - point OpenSSL to CA certificates export SSL_CERT_FILE="$(pkg_path_for core/cacerts)/ssl/certs/cacert.pem" diff --git a/habitat/plan.sh b/habitat/plan.sh index 17074ab..961b692 100644 --- a/habitat/plan.sh +++ b/habitat/plan.sh @@ -3,11 +3,9 @@ export HAB_BLDR_CHANNEL="base-2025" export HAB_REFRESH_CHANNEL="base-2025" ruby_pkg="core/ruby3_4" -# Ruby version for gem directory structure -# NOTE: Bundler normalizes Ruby versions to major.minor.0 format for gem compatibility. -# Even if running Ruby 3.4.2, Bundler creates ruby/3.4.0 directory structure. -# This is standard behavior - gems built for 3.4.0 are compatible with 3.4.x patch versions. -ruby_gem_version="3.4.0" +# ruby_gem_version is set in do_before() once pkg_path_for is available. +# Declared here so it is in scope for all build functions. +ruby_gem_version="" pkg_name="knife" pkg_origin="chef" @@ -36,6 +34,11 @@ pkg_version() { do_before() { update_pkg_version + # Resolve the Ruby API version (e.g. 3.4.0) from the actual Habitat ruby binary. + # Must be done here rather than at plan top-level because pkg_path_for is not + # available until after package dependencies have been resolved. + ruby_gem_version="$($(pkg_path_for ${ruby_pkg})/bin/ruby -e 'print RbConfig::CONFIG["ruby_version"]')" + build_line "Resolved ruby_gem_version=${ruby_gem_version}" } # Directories that contain executable binaries @@ -147,7 +150,10 @@ export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" # (~/.gem/ruby/VERSION), and the Chef gem dir (~/.chef/gems) so that plugins installed # via 'gem install knife-' or 'chef gem install knife-' are found at # runtime without any additional configuration. -export GEM_PATH="$pkg_prefix/vendor:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" +# vendor/ruby/VERSION — bundler-managed gem tree (train-core and all runtime deps) +# vendor — flat gem install tree (knife gem itself) +# ~/.gem/ruby/VERSION and ~/.chef/ruby/VERSION/gems — user-installed plugins +export GEM_PATH="$pkg_prefix/vendor:$pkg_prefix/vendor/ruby/${ruby_gem_version}:\${HOME}/.gem/ruby/${ruby_gem_version}:\${HOME}/.chef/ruby/${ruby_gem_version}/gems" exec $(pkg_path_for ${ruby_pkg})/bin/ruby $pkg_prefix/libexec/knife "\$@" EOF chmod -v 755 "$pkg_prefix/bin/knife"