diff --git a/binstub_patch.rb b/binstub_patch.rb index 0059942..cc8bb45 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..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() { @@ -137,8 +142,14 @@ 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. +# 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.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..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 @@ -143,8 +146,14 @@ 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. +# 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"