Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/build_container.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,12 @@

require "digest"
require "pathname"
require "securerandom"
require "tmpdir"
require "yaml"

require "build_watcher"

# Content-addressed Docker image management for build containers.
#
# Computes a tag from the hash of Dockerfile + .dockerignore + lockfiles
Expand DownExpand Up@@ -194,7 +198,6 @@ def build_contexts_from_lockfile(project_root)
path = Pathname(project_root) / BUILD_DEPS_LOCK
return {} unless path.exist?

require "yaml"
yaml = YAML.safe_load(path.read, permitted_classes: [Symbol]) || {}

contexts = {}
Expand DownExpand Up@@ -398,7 +401,6 @@ def prewarm_commit!(base_tag, final_tag, volumes:, prewarm:, secrets:)
# @param container [String] the run's --name, so a stall can be killed
# @return [Boolean] whether a run succeeded within the retry budget
def run_watched(argv, container:)
require "build_watcher"
BuildWatcher.new(container_name: container).run(argv)
end

Expand All@@ -408,8 +410,6 @@ def run_watched(argv, container:)
# @param secrets [Hash{String => String}]
# @return [Hash{String => String}] secret id => temp file path
def write_secret_files(secrets)
require "tmpdir"
require "securerandom"
secrets.each_with_object({}) do |(id, value), files|
path = File.join(Dir.tmpdir, "dev-secret-#{SecureRandom.hex(8)}")
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |f| f.write(value) }
Expand Down
23 changes: 18 additions & 5 deletions lib/dev/deps/bundler_integration.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,16 +44,29 @@ def install_all(dependencies)

private

# Ensure a bundler executable is available. Bundler ships with modern Ruby,
# so this is normally a no-op; install it on demand if missing.
# Every subprocess below runs through `shadowenv exec` in the project
# root: the dev process inherits the invoking shell's PATH — headless
# services (CI, runners) have no shadowenv hook — so a bare `bundle`
# or `gem` would resolve to whatever Ruby the host carries instead of
# the provisioned toolchain the installed gems must target.

# Ensure a bundler executable is available in the provisioned Ruby.
# Bundler ships with modern Ruby, so this is normally a no-op; install
# it on demand if missing.
#
# @raise [BundlerMissingError] if bundler cannot be made available
# @return [void]
def ensure_bundler!
_out, _err, status = Open3.capture3("bundle", "--version")
_out, _err, status = Open3.capture3(
"shadowenv", "exec", "--", "bundle", "--version",
chdir: @project_root.to_s,
)
return if status.success?

_out, err, status = Open3.capture3("gem", "install", "bundler", "--no-document")
_out, err, status = Open3.capture3(
"shadowenv", "exec", "--", "gem", "install", "bundler", "--no-document",
chdir: @project_root.to_s,
)
raise BundlerMissingError, "failed to install bundler: #{err}" unless status.success?
end

Expand All@@ -64,7 +77,7 @@ def ensure_bundler!
def run_bundle_install
_out, err, status = Open3.capture3(
{ "BUNDLE_GEMFILE" => gemfile_path.to_s, "BUNDLE_FROZEN" => "true" },
"bundle", "install",
"shadowenv", "exec", "--", "bundle", "install",
chdir: @project_root.to_s,
)
raise InstallError, "bundle install failed: #{err}" unless status.success?
Expand Down
2 changes: 1 addition & 1 deletion lib/dev/deps/cache_gc.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@

require "set"
require "fileutils"
require "open3"
require "pathname"
require_relative "lockfile"

Expand DownExpand Up@@ -166,7 +167,6 @@ def running_image_refs
#
# @return [String]
def capture(*argv)
require "open3"
out, _err, status = Open3.capture3(*argv)
status.success? ? out : ""
rescue StandardError
Expand Down
6 changes: 5 additions & 1 deletion lib/dev/deps/gem_skill_linker.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,11 +82,15 @@ def gem_roots
end
end

# Runs under the project's shadowenv for the same reason as
# BundlerIntegration: the dev process's own PATH is the invoking
# service's, which on headless boxes carries the wrong Ruby.
#
# @return [Array<Pathname>] install paths of every gem in the bundle
def bundled_gem_paths
out, err, status = Open3.capture3(
{ "BUNDLE_GEMFILE" => gemfile_path.to_s },
"bundle", "list", "--paths",
"shadowenv", "exec", "--", "bundle", "list", "--paths",
chdir: @project_root.to_s,
)
unless status.success?
Expand Down
2 changes: 1 addition & 1 deletion lib/dev/deps/pip_integration.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@

require "open3"
require "pathname"
require "shadowenv_python"
require_relative "integration"
require_relative "dependency"

Expand DownExpand Up@@ -40,7 +41,6 @@ def install_all(dependencies)
version = @python_version.to_s.strip
raise MissingVersionError, "pip dependencies declared but no `python` version set in dependencies.rb" if version.empty?

require "shadowenv_python"
ShadowenvPython.ensure_venv!(python_version: version, project_root: @project_root)

# Invoke `python -m pip` (not the `pip` console script): ensurepip always
Expand Down
2 changes: 1 addition & 1 deletion lib/dev/deps/xcode_integration.rb
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "pathname"
require "shadowenv_xcode"
require_relative "integration"

module Dev
Expand DownExpand Up@@ -167,7 +168,6 @@ def install_failure_message(version)
def publish_developer_dir(version)
return unless project_root

require "shadowenv_xcode"
developer_dir = self.class.developer_dir(version, root: install_root)
return if ShadowenvXcode.provisioned?(developer_dir, project_root: project_root)

Expand Down
9 changes: 9 additions & 0 deletions lib/shadowenv_ruby.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,15 @@ def detect_homebrew_ruby_version
(v && !v.empty?) ? v : nil
end

# Guarded provisioning: the O(1) provisioned? check first, so callers on
# every-command paths (CommandRunner, the up/install-deps builtins) pay
# nothing after the first run.
def ensure!(ruby_version:, project_root:)
return if provisioned?(ruby_version, project_root: project_root)

setup!(ruby_version: ruby_version, project_root: project_root)
end

# Returns true when .shadowenv.d/510_ruby.lisp exists and already
# provisions the requested version. This is the fast-path check.
def provisioned?(ruby_version, project_root:)
Expand Down
16 changes: 6 additions & 10 deletions src/dev/command_runner.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@

require "dev/cli/ui"
require "dev/command"
require "dev/credentials"
require "build_container"
require "shadowenv_llvm"
require "shadowenv_python"
require "shadowenv_ruby"

module Dev
# Runs dev commands by exec-ing into the child process. Dev prints a colored
Expand DownExpand Up@@ -81,7 +86,6 @@ def publish_image?

sig { params(_cmd: ShellCommand, shell_command: String).void }
def run_in_container(_cmd, shell_command)
require "build_container"
config = T.must(@build_container)
image_tag = BuildContainer.ensure_image!(
config,
Expand DownExpand Up@@ -141,7 +145,6 @@ def container_command(config, image_tag, shell_command)
# @return [Hash{String => String}]
sig { params(config: Dev::BuildContainerConfig).returns(T::Hash[String, String]) }
def resolve_build_args(config)
require "dev/credentials"
Dev::Credentials.resolve_build_args(config.build_args)
end

Expand All@@ -153,7 +156,6 @@ def resolve_build_args(config)
# @return [Hash{String => String}]
sig { params(config: Dev::BuildContainerConfig).returns(T::Hash[String, String]) }
def resolve_build_secrets(config)
require "dev/credentials"
Dev::Credentials.resolve_build_args(config.build_secrets)
end

Expand All@@ -174,7 +176,6 @@ def resolve_build_secrets(config)
def resolve_run_env(config)
return {} if config.run_env.empty?

require "dev/credentials"
config.run_env.each_with_object({}) do |(name, credential_ref), resolved|
namespace, key = credential_ref.split("/", 2)
value = ENV[name] || Dev::Credentials.load(T.must(namespace), T.must(key))
Expand DownExpand Up@@ -202,11 +203,8 @@ def child_env
# a list; three explicit, guarded steps stay readable for now.)
sig { void }
def ensure_shadowenv_provisioned!
require "shadowenv_ruby"
project_root = @project_root
unless ShadowenvRuby.provisioned?(@ruby_version, project_root: project_root)
ShadowenvRuby.setup!(ruby_version: @ruby_version, project_root: project_root)
end
ShadowenvRuby.ensure!(ruby_version: @ruby_version, project_root: project_root)

ensure_llvm_provisioned!(project_root)
ensure_python_provisioned!(project_root)
Expand All@@ -219,15 +217,13 @@ def ensure_python_provisioned!(project_root)
version = @python_version
return if version.nil? || version.empty?

require "shadowenv_python"
return if ShadowenvPython.provisioned?(version, project_root: project_root)

ShadowenvPython.setup!(python_version: version, project_root: project_root)
end

sig { params(project_root: Pathname).void }
def ensure_llvm_provisioned!(project_root)
require "shadowenv_llvm"
return if ShadowenvLlvm.ci_or_linux?
return unless ShadowenvLlvm.project_needs_llvm?(project_root)

Expand Down
Loading
Loading