Module: OpenC3::PythonVenv

Defined in:
lib/openc3/utilities/python_venv.rb

Constant Summary collapse

PLUGIN_VENVS_DIR =
'/gems/plugin_venvs'
DEFAULT_PYTHONUSERBASE =

Shared user site-packages directory used by plugins that predate the per-plugin UV venvs. Every caller that falls back off a plugin venv points PYTHONUSERBASE here, so keep it in one place rather than repeating the literal in script.rb, running_script.rb, microservice_operator.rb and microservice_model.rb.

'/gems/python_packages'
RESERVED_PACKAGES =

Packages a plugin venv must never supply itself. The scripts and microservices that use a plugin venv still run the base venv's Python binary, and CPython searches every PYTHONPATH entry ahead of the running venv's own site-packages. A plugin that declares openc3 as a dependency would therefore shadow the system library with its own copy, silently swapping the API client the script talks to COSMOS with. Reordering PYTHONPATH cannot fix this - the whole variable outranks the base venv - so the copy is removed at install time instead.

['openc3'].freeze

Class Method Summary collapse

Class Method Details

.configure_environment(environment, venv_dir) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/openc3/utilities/python_venv.rb', line 50

def self.configure_environment(environment, venv_dir)
  environment['VIRTUAL_ENV'] = venv_dir
  environment['PATH'] = "#{venv_dir}/bin:#{ENV.fetch('PATH', '')}"
  environment['PYTHONUSERBASE'] = venv_dir

  site_packages = Dir.glob("#{venv_dir}/lib/python*/site-packages").first
  # Prefer a PYTHONPATH the caller already seeded on `environment` over the
  # ambient one. Script.process_suite sets it to ENV['PYTHONPATH'] || '.'
  # before calling here, and reading ENV directly would drop that value.
  existing_pythonpath = environment['PYTHONPATH'] || ENV.fetch('PYTHONPATH', '')
  if site_packages
    environment['PYTHONPATH'] = existing_pythonpath.empty? ? site_packages : "#{site_packages}:#{existing_pythonpath}"
  else
    environment['PYTHONPATH'] = existing_pythonpath.empty? ? nil : existing_pythonpath
  end

  environment
end

.configure_for_script(environment, name:, scope:, python_venv: nil) ⇒ Object

Resolve a script's plugin venv and expose its packages to a child process. Returns the venv path when configured, or nil when the system environment should be used.



42
43
44
45
46
47
48
# File 'lib/openc3/utilities/python_venv.rb', line 42

def self.configure_for_script(environment, name:, scope:, python_venv: nil)
  venv_dir = resolve_script_venv(name: name, scope: scope, python_venv: python_venv)
  return nil unless venv_dir

  configure_environment(environment, venv_dir)
  venv_dir
end

.plugin_venv_path(scope:, plugin_name:) ⇒ Object



99
100
101
102
103
# File 'lib/openc3/utilities/python_venv.rb', line 99

def self.plugin_venv_path(scope:, plugin_name:)
  sanitized_name = "#{scope}__#{plugin_name}".tr('^a-zA-Z0-9_-', '_')
  candidate = File.join(PLUGIN_VENVS_DIR, sanitized_name, '.venv')
  candidate if File.directory?(candidate)
end

.purge_reserved_packages(venv_dir) ⇒ Object

Remove any RESERVED_PACKAGES a plugin installed into its own venv. Returns the names that were removed so callers can report them.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/openc3/utilities/python_venv.rb', line 71

def self.purge_reserved_packages(venv_dir)
  return [] if venv_dir.nil? || venv_dir.empty?

  purged = []
  RESERVED_PACKAGES.each do |package|
    next unless package_installed?(venv_dir, package)

    Logger.warn("Plugin venv #{venv_dir} declares '#{package}', which would shadow the " \
                "system library. Removing it; the plugin will use the system #{package}.")
    _output, status = Open3.capture2e('uv', 'pip', 'uninstall', '--python', venv_dir, package)
    if status.success?
      purged << package
    else
      Logger.error("Failed to remove '#{package}' from plugin venv #{venv_dir}")
    end
  end
  purged
rescue => e
  Logger.error("Could not purge reserved packages from #{venv_dir}: #{e.message}")
  []
end