From dc2942b9f48795c8172295f7379724382a7406ea Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Tue, 28 Jul 2026 11:51:31 -0400 Subject: [PATCH 1/3] Provision the shadowenv Ruby in install-deps and run bundler under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless boxes (CI, runner services) reach `dev install-deps` before any dev.yml command has run CommandRunner's provisioning, and their service PATH has no shadowenv hook — so the builtin neither installed the pinned Ruby nor targeted it: bundle/gem resolved to whatever Ruby the host carried (macOS system 2.6 in the reproducing case). - ShadowenvRuby.ensure!: the guarded provisioned?-then-setup! step, shared by CommandRunner and the up/install-deps builtins. - install_locked_deps provisions the pinned Ruby before installing. - BundlerIntegration and GemSkillLinker run bundle/gem through `shadowenv exec` in the project root, the same env dev.yml commands get. Co-authored-by: Cursor --- lib/dev/deps/bundler_integration.rb | 23 ++++++++--- lib/dev/deps/gem_skill_linker.rb | 6 ++- lib/shadowenv_ruby.rb | 9 +++++ src/dev/command_runner.rb | 4 +- src/dev/runner.rb | 6 +++ test/dev/deps/bundler_integration_test.rb | 47 ++++++++++++++++++++--- test/dev/deps/gem_skill_linker_test.rb | 6 ++- test/dev/runner_test.rb | 27 +++++++++++++ test/lib/shadowenv_ruby_test.rb | 36 +++++++++++++++++ 9 files changed, 148 insertions(+), 16 deletions(-) diff --git a/lib/dev/deps/bundler_integration.rb b/lib/dev/deps/bundler_integration.rb index fac7f65..e65b6b3 100644 --- a/lib/dev/deps/bundler_integration.rb +++ b/lib/dev/deps/bundler_integration.rb @@ -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 @@ -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? diff --git a/lib/dev/deps/gem_skill_linker.rb b/lib/dev/deps/gem_skill_linker.rb index d06d9f5..366466a 100644 --- a/lib/dev/deps/gem_skill_linker.rb +++ b/lib/dev/deps/gem_skill_linker.rb @@ -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] 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? diff --git a/lib/shadowenv_ruby.rb b/lib/shadowenv_ruby.rb index 36ff227..32cd82a 100644 --- a/lib/shadowenv_ruby.rb +++ b/lib/shadowenv_ruby.rb @@ -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:) diff --git a/src/dev/command_runner.rb b/src/dev/command_runner.rb index 69a0586..d8317c4 100644 --- a/src/dev/command_runner.rb +++ b/src/dev/command_runner.rb @@ -204,9 +204,7 @@ def child_env 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) diff --git a/src/dev/runner.rb b/src/dev/runner.rb index 25b7939..8820eff 100644 --- a/src/dev/runner.rb +++ b/src/dev/runner.rb @@ -422,6 +422,12 @@ def build_repositories(project_root:, ruby_version_requirement: nil) # @param context [ExecutionContext] sig { params(context: ExecutionContext).void } def install_locked_deps(context) + # Headless boxes (CI, runner services) reach install-deps before any + # dev.yml command has run CommandRunner's provisioning, so the builtin + # must provision the pinned Ruby itself — bundler installs against it. + require "shadowenv_ruby" + ShadowenvRuby.ensure!(ruby_version: context.ruby_version, project_root: context.project_root) + lockfile = Dev::Deps::Lockfile.new(dir: context.project_root) installer = Dev::Deps::DependencyInstaller.new( lockfile: lockfile, diff --git a/test/dev/deps/bundler_integration_test.rb b/test/dev/deps/bundler_integration_test.rb index d713277..5970d7f 100644 --- a/test/dev/deps/bundler_integration_test.rb +++ b/test/dev/deps/bundler_integration_test.rb @@ -24,20 +24,51 @@ def gem_dep(name) version: "1.0.0", hash: nil, metadata: {}) end - test "install_all runs a frozen bundle install against the generated Gemfile" do + # Every bundler subprocess goes through `shadowenv exec` in the project + # root: the dev process inherits the invoking service's PATH (headless + # boxes have no shadowenv shell hook), so a bare `bundle` would resolve + # to whatever Ruby the host carries, not the provisioned toolchain. + test "install_all runs a frozen bundle install under the project's shadowenv" do Given "a bundler integration with one locked gem" dir = Dir.mktmpdir("dev-bundler-int-test-") integration = build_integration(dir) gemfile = (Pathname(dir) / "Gemfile").to_s - Open3.stubs(:capture3).with("bundle", "--version").returns(["Bundler version 2.5.0", "", stub(success?: true)]) + Open3.stubs(:capture3) + .with("shadowenv", "exec", "--", "bundle", "--version", chdir: dir) + .returns(["Bundler version 2.5.0", "", stub(success?: true)]) Open3.expects(:capture3) - .with({ "BUNDLE_GEMFILE" => gemfile, "BUNDLE_FROZEN" => "true" }, "bundle", "install", chdir: dir) + .with({ "BUNDLE_GEMFILE" => gemfile, "BUNDLE_FROZEN" => "true" }, + "shadowenv", "exec", "--", "bundle", "install", chdir: dir) .returns(["", "", stub(success?: true)]) When "installing all dependencies" integration.install_all([gem_dep("ffi")]) - Then "bundle install was dispatched against the project root" + Then "bundle install was dispatched under shadowenv against the project root" + true + + Cleanup + FileUtils.rm_rf(dir) + end + + test "install_all installs bundler under the project's shadowenv when missing" do + Given "a bundler integration whose shadowenv Ruby has no bundler" + dir = Dir.mktmpdir("dev-bundler-int-test-") + integration = build_integration(dir) + Open3.stubs(:capture3) + .with("shadowenv", "exec", "--", "bundle", "--version", chdir: dir) + .returns(["", "command not found", stub(success?: false)]) + Open3.expects(:capture3) + .with("shadowenv", "exec", "--", "gem", "install", "bundler", "--no-document", chdir: dir) + .returns(["", "", stub(success?: true)]) + Open3.stubs(:capture3) + .with(anything, "shadowenv", "exec", "--", "bundle", "install", chdir: dir) + .returns(["", "", stub(success?: true)]) + + When "installing all dependencies" + integration.install_all([gem_dep("ffi")]) + + Then "bundler was installed into the provisioned Ruby, not the host one" true Cleanup @@ -64,8 +95,12 @@ def gem_dep(name) Given "a bundler integration whose install fails" dir = Dir.mktmpdir("dev-bundler-int-test-") integration = build_integration(dir) - Open3.stubs(:capture3).with("bundle", "--version").returns(["Bundler version 2.5.0", "", stub(success?: true)]) - Open3.stubs(:capture3).with(anything, "bundle", "install", chdir: dir).returns(["", "frozen mismatch", stub(success?: false)]) + Open3.stubs(:capture3) + .with("shadowenv", "exec", "--", "bundle", "--version", chdir: dir) + .returns(["Bundler version 2.5.0", "", stub(success?: true)]) + Open3.stubs(:capture3) + .with(anything, "shadowenv", "exec", "--", "bundle", "install", chdir: dir) + .returns(["", "frozen mismatch", stub(success?: false)]) When "installing all dependencies" error = assert_raises(Dev::Deps::BundlerIntegration::InstallError) do diff --git a/test/dev/deps/gem_skill_linker_test.rb b/test/dev/deps/gem_skill_linker_test.rb index e27c040..87d5ad7 100644 --- a/test/dev/deps/gem_skill_linker_test.rb +++ b/test/dev/deps/gem_skill_linker_test.rb @@ -48,9 +48,13 @@ def build_gem(gems_root, dir_name, skills: []) root end + # `bundle list` must run under the project's shadowenv — same reasoning as + # BundlerIntegration: the dev process's PATH is the invoking service's, + # which on headless boxes carries the wrong Ruby. def stub_bundle_list(project, paths) Open3.stubs(:capture3) - .with({ "BUNDLE_GEMFILE" => (project / "Gemfile").to_s }, "bundle", "list", "--paths", chdir: project.to_s) + .with({ "BUNDLE_GEMFILE" => (project / "Gemfile").to_s }, + "shadowenv", "exec", "--", "bundle", "list", "--paths", chdir: project.to_s) .returns([paths.map { |p| "#{p}\n" }.join, "", stub(success?: true)]) end diff --git a/test/dev/runner_test.rb b/test/dev/runner_test.rb index f021ca3..7db8e1f 100644 --- a/test/dev/runner_test.rb +++ b/test/dev/runner_test.rb @@ -6,6 +6,7 @@ require "dev/runner" require "dev/credentials" require "dev/deps/cmake_integration" +require "shadowenv_ruby" require "stringio" require "tempfile" require "tmpdir" @@ -351,6 +352,7 @@ class RunnerTest < Minitest::Test Dev.stubs(:target_project_root).returns(root) runner = build_runner(commands: {}) runner.stubs(:resolve_ruby_version).returns("4.0.1") + ShadowenvRuby.stubs(:ensure!) Dev::Deps::DependencyInstaller.any_instance.stubs(:install) Dev::Deps::GemSkillLinker.any_instance.expects(:link_all).once Dev::Knowledge::Synchronizer.any_instance.expects(:sync).with(project_root: root).once @@ -365,6 +367,31 @@ class RunnerTest < Minitest::Test FileUtils.rm_rf(root) end + # Headless boxes (CI, runner services) reach install-deps before any + # dev.yml command has run CommandRunner's provisioning — the builtin must + # provision the toolchain itself or bundler installs against whatever + # Ruby the service PATH happens to carry. + test "install-deps provisions the shadowenv Ruby before installing" do + Given "a Runner pinned to an empty project root, with the installer stubbed" + root = Pathname.new(Dir.mktmpdir("runner-install-deps-ruby-")) + Dev.stubs(:target_project_root).returns(root) + runner = build_runner(commands: {}) + runner.stubs(:resolve_ruby_version).returns("4.0.1") + ShadowenvRuby.expects(:ensure!).with(ruby_version: "4.0.1", project_root: root).once + Dev::Deps::DependencyInstaller.any_instance.stubs(:install) + Dev::Deps::GemSkillLinker.any_instance.stubs(:link_all) + Dev::Knowledge::Synchronizer.any_instance.stubs(:sync) + + When "we run install-deps" + runner.run(["install-deps"], ui: fake_ui) + + Then "the expectation on the provisioning step holds" + true + + Cleanup + FileUtils.rm_rf(root) + end + test "declared_ruby_version returns the dependencies.rb ruby directive" do Given "a project whose dependencies.rb declares ruby" root = Pathname.new(Dir.mktmpdir("runner-ruby-deps-")) diff --git a/test/lib/shadowenv_ruby_test.rb b/test/lib/shadowenv_ruby_test.rb index 14e8a5e..bd4cfe3 100644 --- a/test/lib/shadowenv_ruby_test.rb +++ b/test/lib/shadowenv_ruby_test.rb @@ -57,6 +57,42 @@ class ShadowenvRubyTest < Minitest::Test 1 * Kernel.abort("dev: Resolved Ruby 2.6.0 is below dev's minimum (>= 2.7.0). Pin a newer version in dependencies.rb or run: brew upgrade ruby") end + # --- ensure! --- + + test "ensure! skips setup! when the project is already provisioned" do + Given "a project root whose lisp already provides the version" + tmpdir = Dir.mktmpdir("shadowenv-ensure-test-") + shadowenv_d = File.join(tmpdir, ".shadowenv.d") + FileUtils.mkdir_p(shadowenv_d) + File.write( + File.join(shadowenv_d, "510_ruby.lisp"), + ShadowenvRuby.generate_ruby_lisp("/opt/ruby/4.0.1", "4.0.1") + ) + + When "we ensure the version" + ShadowenvRuby.ensure!(ruby_version: "4.0.1", project_root: tmpdir) + + Then "setup! is never reached (the fast path won)" + 0 * ShadowenvRuby.setup! + + Cleanup + FileUtils.rm_rf(tmpdir) + end + + test "ensure! runs setup! when the project is not provisioned" do + Given "a project root with no .shadowenv.d" + tmpdir = Dir.mktmpdir("shadowenv-ensure-test-") + + When "we ensure the version" + ShadowenvRuby.ensure!(ruby_version: "4.0.1", project_root: tmpdir) + + Then "setup! runs with the same version and root" + 1 * ShadowenvRuby.setup!(ruby_version: "4.0.1", project_root: tmpdir) + + Cleanup + FileUtils.rm_rf(tmpdir) + end + # --- provisioned? --- test "provisioned? returns true when lisp file matches version" do From 2e53f69d287777183fb5d61faa2812ae964dc9db Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Tue, 28 Jul 2026 12:13:29 -0400 Subject: [PATCH 2/3] Cover CommandRunner's provisioning delegation to ShadowenvRuby.ensure! The suite stubbed ensure_shadowenv_provisioned! everywhere, leaving the switch to the shared guarded helper unexercised (the codecov patch miss). Co-authored-by: Cursor --- test/dev/command_runner_test.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/dev/command_runner_test.rb b/test/dev/command_runner_test.rb index 2592047..9f00653 100644 --- a/test/dev/command_runner_test.rb +++ b/test/dev/command_runner_test.rb @@ -6,6 +6,7 @@ require "dev/build_container_config" require "dev/credentials" require "build_container" +require "shadowenv_ruby" transform!(RSpock::AST::Transformation) class CommandRunnerTest < Minitest::Test @@ -28,6 +29,26 @@ def teardown FileUtils.rm_rf(@project_root) if @project_root&.exist? end + # --- Toolchain provisioning --- + + test "run routes Ruby provisioning through the shared guarded ensure!" do + Given "a runner whose provisioning step is not stubbed" + runner = Dev::CommandRunner.new(ui: @ui, ruby_version: "4.0.1", project_root: @project_root) + runner.stubs(:ensure_llvm_provisioned!) + runner.stubs(:ensure_python_provisioned!) + Kernel.stubs(:exec) + cmd = Dev::ShellCommand.new(run: "./bin/console", repl: true) + + When "we run a command" + runner.run(cmd) + + Then "the declared Ruby is ensured for the project root" + 1 * ShadowenvRuby.ensure!(ruby_version: "4.0.1", project_root: @project_root) + + Cleanup + Dir.chdir(@original_cwd) + end + # --- Local execution (no container) --- test "run prints header and execs directly when repl" do From 7272267e943496a0a953763b5dfac35d14a5ff3a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Tue, 28 Jul 2026 14:37:22 -0400 Subject: [PATCH 3/3] Hoist lazy requires to file tops per top-level-requires The mid-body requires were a hand-rolled cli-kit-style lazy-loading pattern; none of the loaded files are expensive, so the dispatch-cost argument doesn't hold and the org's top-level-requires skill applies. The one deliberate exception stays: Deps::CliUI's `require "cli/ui"` is an optional-gem probe (rescue LoadError with plain-text fallbacks) on the stdlib-only bootstrap chain. Co-authored-by: Cursor --- lib/build_container.rb | 8 +++---- lib/dev/deps/cache_gc.rb | 2 +- lib/dev/deps/pip_integration.rb | 2 +- lib/dev/deps/xcode_integration.rb | 2 +- src/dev/command_runner.rb | 12 ++++------ src/dev/runner.rb | 39 ++++++++++++------------------- 6 files changed, 27 insertions(+), 38 deletions(-) diff --git a/lib/build_container.rb b/lib/build_container.rb index a5693c7..7126fda 100644 --- a/lib/build_container.rb +++ b/lib/build_container.rb @@ -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 @@ -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 = {} @@ -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 @@ -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) } diff --git a/lib/dev/deps/cache_gc.rb b/lib/dev/deps/cache_gc.rb index 4765b35..fcf1da7 100644 --- a/lib/dev/deps/cache_gc.rb +++ b/lib/dev/deps/cache_gc.rb @@ -2,6 +2,7 @@ require "set" require "fileutils" +require "open3" require "pathname" require_relative "lockfile" @@ -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 diff --git a/lib/dev/deps/pip_integration.rb b/lib/dev/deps/pip_integration.rb index 2072abc..3303604 100644 --- a/lib/dev/deps/pip_integration.rb +++ b/lib/dev/deps/pip_integration.rb @@ -2,6 +2,7 @@ require "open3" require "pathname" +require "shadowenv_python" require_relative "integration" require_relative "dependency" @@ -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 diff --git a/lib/dev/deps/xcode_integration.rb b/lib/dev/deps/xcode_integration.rb index f92a413..661fa1a 100644 --- a/lib/dev/deps/xcode_integration.rb +++ b/lib/dev/deps/xcode_integration.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "pathname" +require "shadowenv_xcode" require_relative "integration" module Dev @@ -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) diff --git a/src/dev/command_runner.rb b/src/dev/command_runner.rb index d8317c4..8d9d2fa 100644 --- a/src/dev/command_runner.rb +++ b/src/dev/command_runner.rb @@ -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 @@ -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, @@ -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 @@ -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 @@ -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)) @@ -202,7 +203,6 @@ 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 ShadowenvRuby.ensure!(ruby_version: @ruby_version, project_root: project_root) @@ -217,7 +217,6 @@ 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) @@ -225,7 +224,6 @@ def ensure_python_provisioned!(project_root) 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) diff --git a/src/dev/runner.rb b/src/dev/runner.rb index 8820eff..ee4533e 100644 --- a/src/dev/runner.rb +++ b/src/dev/runner.rb @@ -1,19 +1,31 @@ # typed: strict # frozen_string_literal: true +require 'digest' require 'pathname' require 'dev/config_parser' require 'dev/command_registry' +require 'dev/credential_accessor' +require 'dev/credentials' require 'dev/execution_context' require 'dev/deps' -require 'dev/deps/repository' +require 'dev/deps/accessor' +require 'dev/deps/cache' +require 'dev/deps/cache_gc' +require 'dev/deps/gem_skill_linker' require 'dev/deps/integration' -require 'dev/deps/resolver' require 'dev/deps/lockfile' -require 'dev/deps/gem_skill_linker' +require 'dev/deps/registry' +require 'dev/deps/repository' +require 'dev/deps/resolver' +require 'dev/deps/staleness' require 'dev/knowledge' +require 'dev/plan' +require 'dev/runner_setup' require 'dev/cli/ui' require 'dev/cd' +require 'build_container' +require 'shadowenv_ruby' module Dev # Main entry: find repo, load config, build registry, parse argv, show usage or run command. @@ -92,7 +104,6 @@ def run(argv, ui:, out: $stdout) def guard_staleness(cmd_name, project_root) return if STALENESS_EXEMPT_COMMANDS.include?(cmd_name) - require "dev/deps/staleness" messages = Dev::Deps::Staleness.new(project_root:).messages return if messages.empty? @@ -111,7 +122,6 @@ def guard_staleness(cmd_name, project_root) def stamp_installed(cmd_name, project_root) return unless ["up", "install-deps"].include?(cmd_name) - require "dev/deps/staleness" Dev::Deps::Staleness.new(project_root:).stamp_installed! end @@ -124,7 +134,6 @@ def provision_build_credentials config = @config.build_container return if config.nil? || config.build_args.empty? - require "dev/credentials" Dev::Credentials.resolve_build_args(config.build_args) end @@ -156,7 +165,6 @@ def register_runner_builtins(registry, config) registry.register("runner-setup", BuiltinCommand.new( desc: "Register this host as a self-hosted GitHub Actions runner (repo-scoped, or org-wide with --org)", ) do |args, context| - require "dev/runner_setup" cfg = context.runner raise ArgumentError, "no `runner:` block in dev.yml" if cfg.nil? @@ -186,8 +194,6 @@ def register_container_builtins(registry, config) desc: "Resolve the build container image (local/pull/build) and print its tag", hidden: true, ) do |_args, context| - require "build_container" - require "dev/credentials" cfg = context.build_container image_tag = BuildContainer.ensure_image!( cfg, @@ -206,7 +212,6 @@ def register_container_builtins(registry, config) registry.register("reset-container", BuiltinCommand.new( desc: "Remove the persistent build container (clears its incremental cache)", ) do |_args, context| - require "build_container" cfg = context.build_container image_tag = BuildContainer.image_with_tag(cfg, project_root: context.project_root) removed = BuildContainer.reset_service!(image_tag, context.project_root) @@ -234,7 +239,6 @@ def register_builtins(registry) resolved = resolver.resolve(deps_config.declarations) # Record the manifest digest so the staleness check can tell whether # dependencies.rb changed after this resolution (Dev::Deps::Staleness). - require "digest" manifest_digest = deps_rb.exist? ? Digest::SHA256.file(deps_rb.to_s).hexdigest : nil lockfile.lock(resolved, manifest_digest:) puts "dev: lockfiles updated — now run dev up to install." @@ -278,7 +282,6 @@ def register_builtins(registry) registry.register("check", BuiltinCommand.new( desc: "Check dependency state freshness (manifest vs lockfiles vs installed)", ) do |args, context| - require "dev/deps/staleness" messages = Dev::Deps::Staleness.new(project_root: context.project_root).messages if messages.empty? puts "dev: dependency state is in sync (manifest, lockfiles, installed stamp)." @@ -291,8 +294,6 @@ def register_builtins(registry) registry.register("deps", BuiltinCommand.new( desc: "Inspect locked dependencies (e.g. deps path ficsit )", ) do |args, context| - require "dev/deps/accessor" - require "dev/deps/cache" Dev::Deps::Accessor.new( lockfile: Dev::Deps::Lockfile.new(dir: context.project_root), cache: Dev::Deps::Cache.new, @@ -302,7 +303,6 @@ def register_builtins(registry) registry.register("cache", BuiltinCommand.new( desc: "Manage host caches (e.g. cache gc --keep 2)", ) do |args, context| - require "dev/deps/cache_gc" subcommand, *rest = args raise ArgumentError, "usage: dev cache gc [--keep N]" unless subcommand == "gc" @@ -312,7 +312,6 @@ def register_builtins(registry) image_ref = nil live_tag = nil if (cfg = context.build_container) - require "build_container" image_ref = cfg.image_ref live_tag = BuildContainer.image_with_tag(cfg, project_root: context.project_root) end @@ -322,15 +321,12 @@ def register_builtins(registry) registry.register("cred", BuiltinCommand.new( desc: "Resolve a stored credential (e.g. cred get )", ) do |args, _context| - require "dev/credentials" - require "dev/credential_accessor" Dev::CredentialAccessor.new.run(args) end) registry.register("plan", BuiltinCommand.new( desc: "Sync Cursor plans with GitHub issues (new/link/pull/push/status)", ) do |args, context| - require "dev/plan" Dev::Plan::Accessor.new(project_root: context.project_root).run(args) end) end @@ -404,7 +400,6 @@ def parse_value_flag(args, flag) ).returns(T::Hash[Symbol, Dev::Deps::Repository]) end def build_repositories(project_root:, ruby_version_requirement: nil) - require "dev/deps/registry" Dev::Deps::Registry.repositories(project_root:, ruby_version_requirement:) end @@ -425,7 +420,6 @@ def install_locked_deps(context) # Headless boxes (CI, runner services) reach install-deps before any # dev.yml command has run CommandRunner's provisioning, so the builtin # must provision the pinned Ruby itself — bundler installs against it. - require "shadowenv_ruby" ShadowenvRuby.ensure!(ruby_version: context.ruby_version, project_root: context.project_root) lockfile = Dev::Deps::Lockfile.new(dir: context.project_root) @@ -453,8 +447,6 @@ def install_locked_deps(context) # @return [Hash{Symbol => Dev::Deps::Integration}] sig { params(project_root: Pathname, python_version: T.nilable(String)).returns(T::Hash[Symbol, Dev::Deps::Integration]) } def build_host_integrations(project_root:, python_version: nil) - require "dev/deps/cache" - require "dev/deps/registry" Dev::Deps::Registry.host_integrations( project_root:, cache: Dev::Deps::Cache.new, @@ -486,7 +478,6 @@ def print_usage(name, registry, out:) sig { params(explicit_version: T.nilable(String)).returns(String) } def resolve_ruby_version(explicit_version) - require "shadowenv_ruby" ShadowenvRuby.resolve_ruby_version(explicit_version) end