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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ Custom integrations implement `Dev::Deps::Integration` (with `install_all(pins,
- **`dev install-deps`** — install locked deps handled on the host (gh releases, steam apps) into their version-keyed install dirs, filtered to the detected env and host OS. Finishes by refreshing agent skill links (see [Agent skills & org learnings](#agent-skills--org-learnings)).
- **`dev up`** — first converges the host layer (self-update + org Brewfile, see [Host tooling: the Brewfile contract](#host-tooling-the-brewfile-contract)), then auto-installs all deps from lockfiles (build group first), then runs the project's `up:` command from `dev.yml` if defined. On success, stamps the installed lockfile digest (see `dev check`). Finishes by refreshing agent skill links, like `install-deps`. Also valid outside any project: converges the host layer only — the fresh-box bootstrap (`brew install <org>/<tap>/dev` → `dev up` → ready).
- **`dev check`** — report dependency-state staleness explicitly: `dependencies.rb` vs lockfiles (digest recorded by `update-deps`), and lockfiles vs the per-machine installed stamp (`~/.dev/state/<project>/installed-digest`, written after a fully-successful `up`/`install-deps`). The same two O(1) checks run at every command start — warning on workstations, erroring in CI.
- **`dev deps path <integration> <name> <platform>`** — print the absolute path of a locked artifact (e.g. `dev deps path ficsit SML LinuxServer`, or `dev deps path xcode` for the pinned DEVELOPER_DIR) so scripts don't reconstruct cache keys or layout conventions.
- **`dev deps path <integration> <name> <platform>`** — print the absolute path of a locked artifact (e.g. `dev deps path ficsit SML LinuxServer`, `dev deps path xcode` for the pinned DEVELOPER_DIR, or `dev deps path gh UnrealEngineMac` for a gh release's version-keyed install dir under the data root) so scripts don't reconstruct cache keys or layout conventions.
- **`dev config list | get <key> | set <key> <value>`** — manage dev's settings (see [Org configuration & deployment](#org-configuration--deployment)). `list` shows every known key with its resolved value and source layer (`env` / `user` / `system` / unset) — the settings debugging tool; `get` prints the resolved value (exit 1 when unset); `set` writes the user file (`~/.config/dev/config.yml`), creating it if missing. Known keys only. Global: works without a `dev.yml`.
- **`dev cred get <namespace> <key>`** — resolve a credential through the provider chain (ENV → keychain → file → prompt) and print it. A non-interactive miss errors with `gh secret set` guidance. Mirrors `dev deps path` for shell consumers (e.g. a staging sync). Global: works without a `dev.yml`.
- **`dev cd <repo>`** — jump to a checkout under `$DEV_CD_ROOT` (default `~/src`) by fuzzy name, with Tab completion (see [dev cd](#dev-cd--jump-between-checkouts)). Global: works without a `dev.yml`.
Expand Down
52 changes: 44 additions & 8 deletions lib/dev/deps/accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
require_relative "cache"
require_relative "ficsit_integration"
require_relative "xcode_integration"
require_relative "gh_integration"
require_relative "../data_root"

module Dev
module Deps
# Read-only accessor over the lockfile + content cache, surfaced as
# `dev deps <subcommand>`. It answers "where is a locked dep's artifact?"
# — the cached zip for a ficsit mod platform, the DEVELOPER_DIR for the
# pinned Xcode — so consumers (deploy, build scripts, CI) resolve paths
# from the lockfile instead of reconstructing dev's layout conventions.
# pinned Xcode, the version-keyed install dir of a gh release — so
# consumers (deploy, build scripts, CI) resolve paths from the lockfile
# instead of reconstructing dev's layout conventions.
class Accessor
extend T::Sig

Expand All @@ -24,16 +27,25 @@ class PlatformNotLockedError < StandardError; end
class NotCachedError < StandardError; end
class NotInstalledError < StandardError; end

USAGE = "usage: dev deps path ficsit <mod> <platform> | dev deps path xcode"
USAGE = "usage: dev deps path ficsit <mod> <platform> | dev deps path xcode | dev deps path gh <name>"

# @param lockfile [Lockfile]
# @param cache [Cache]
# @param xcode_install_root [String] where Xcode bundles live (tests use a tmpdir)
sig { params(lockfile: Lockfile, cache: Cache, xcode_install_root: String).void }
def initialize(lockfile:, cache:, xcode_install_root: XcodeIntegration::INSTALL_ROOT)
# @param data_root [String] where `~/.dev` install_dirs re-root (tests use a tmpdir)
sig do
params(lockfile: Lockfile, cache: Cache, xcode_install_root: String, data_root: String).void
end
def initialize(
lockfile:,
cache:,
xcode_install_root: XcodeIntegration::INSTALL_ROOT,
data_root: Dev::DataRoot.path
)
@lockfile = lockfile
@cache = cache
@xcode_install_root = xcode_install_root
@data_root = data_root
end

# Dispatch a `dev deps …` invocation and print the result.
Expand All @@ -56,15 +68,15 @@ def run(args, out: $stdout)

# Resolve the artifact path for a locked dependency.
#
# @param integration [String] integration name ("ficsit" or "xcode")
# @param name [String, nil] dependency name (e.g. "SML"; unused for xcode)
# @param integration [String] integration name ("ficsit", "xcode" or "gh")
# @param name [String, nil] dependency name (e.g. "SML", "UnrealEngineMac"; unused for xcode)
# @param platform [String, nil] ficsit target name (e.g. "LinuxServer")
# @return [Pathname] absolute path to the artifact
# @raise [UsageError] for a missing/unsupported integration
# @raise [NotLockedError] if the dep isn't in the lockfile
# @raise [PlatformNotLockedError] if the platform isn't locked for the dep
# @raise [NotCachedError] if the zip isn't in the cache (run dev up)
# @raise [NotInstalledError] if the pinned Xcode isn't installed (run dev up)
# @raise [NotInstalledError] if the pinned Xcode or gh release isn't installed (run dev up)
sig do
params(
integration: T.nilable(String),
Expand All @@ -76,12 +88,36 @@ def path(integration = nil, name = nil, platform = nil)
case integration
when "ficsit" then ficsit_path(name, platform)
when "xcode" then xcode_developer_dir
when "gh" then gh_install_dir(name)
else raise UsageError, USAGE
end
end

private

# The version-keyed install dir of a locked gh release — the immutable
# directory GhIntegration publishes under the dep's install_dir, re-rooted
# onto the data root. Resolved from the lock rather than the `current`
# pointer so a checkout always gets the tag its lockfile names, not
# whatever this machine installed last.
#
# @param name [String, nil]
# @return [Pathname]
sig { params(name: T.nilable(String)).returns(Pathname) }
def gh_install_dir(name)
raise UsageError, USAGE unless name

dep = find_dep(:gh, name)
base_dir = Dev::DataRoot.expand(dep.metadata.fetch("install_dir"), root: @data_root)
version_dir = Pathname(base_dir) / dep.version
marker = version_dir / GhIntegration::MARKER_FILE
unless marker.file? && marker.read.strip == dep.version
raise NotInstalledError, "#{name}@#{dep.version} is not installed at #{version_dir} — run dev up"
end

version_dir
end

# @param name [String, nil]
# @param platform [String, nil]
# @return [Pathname]
Expand Down
2 changes: 1 addition & 1 deletion src/dev/builtins/deps_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def initialize(
end

sig { override.returns(String) }
def desc = "Inspect locked dependencies (e.g. deps path ficsit <mod> <platform>)"
def desc = "Inspect locked dependencies (e.g. deps path ficsit <mod> <platform>, deps path gh <name>)"

sig { override.returns(Command::Category) }
def category = Command::Category::Lifecycle
Expand Down
123 changes: 123 additions & 0 deletions test/dev/deps/accessor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "dev/deps/dependency"
require "dev/deps/ficsit_integration"
require "dev/deps/xcode_integration"
require "dev/deps/gh_integration"
require "pathname"
require "tmpdir"
require "stringio"
Expand Down Expand Up @@ -64,6 +65,128 @@ def setup_locked_xcode(dir, version: "26.1.1", installed: true)
[accessor, developer_dir]
end

GH_ENGINE_TAG = "5.8.0-mac-editor-1"

# Lock a gh engine dep whose `~/.dev` install_dir re-roots onto dir (the
# injected data root), optionally publishing the version dir + marker.
def setup_locked_gh(dir, installed: true, marker_version: GH_ENGINE_TAG)
lockfile = Dev::Deps::Lockfile.new(dir: dir)
lockfile.lock([
Dev::Deps::Dependency.new(
name: "UnrealEngineMac", integration: :gh, group: :editor,
version: GH_ENGINE_TAG, hash: nil,
metadata: { "repo" => "d3mlabs/unreal-engine", "install_dir" => "~/.dev/engines/ue5-mac" },
),
])
version_dir = File.join(dir, "engines", "ue5-mac", GH_ENGINE_TAG)
if installed
FileUtils.mkdir_p(version_dir)
File.write(File.join(version_dir, Dev::Deps::GhIntegration::MARKER_FILE), marker_version)
end

accessor = Dev::Deps::Accessor.new(
lockfile: lockfile,
cache: Dev::Deps::Cache.new(cache_dir: File.join(dir, "cache")),
data_root: dir,
)
[accessor, version_dir]
end

test "path gh returns the locked version dir under the data root" do
Given "a locked and installed gh engine"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, version_dir = setup_locked_gh(dir)

When "asking for the engine path"
result = accessor.path("gh", "UnrealEngineMac")

Then "it is the version-keyed dir, not a current pointer"
result == Pathname(version_dir)

Cleanup
FileUtils.rm_rf(dir)
end

test "path gh raises NotInstalledError when the version dir is absent" do
Given "a locked but uninstalled gh engine"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, = setup_locked_gh(dir, installed: false)

When "asking for the engine path"
error = assert_raises(Dev::Deps::Accessor::NotInstalledError) do
accessor.path("gh", "UnrealEngineMac")
end

Then "the fix is dev up"
error.message.include?("run dev up")

Cleanup
FileUtils.rm_rf(dir)
end

test "path gh raises NotInstalledError when the marker records another version" do
Given "a version dir whose marker was stamped by a different tag (half-published)"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, = setup_locked_gh(dir, marker_version: "5.8.0-mac-editor-0")

When "asking for the engine path"
accessor.path("gh", "UnrealEngineMac")

Then
raises Dev::Deps::Accessor::NotInstalledError

Cleanup
FileUtils.rm_rf(dir)
end

test "path gh raises NotLockedError without the dep in the lockfile" do
Given "a lockfile without any gh dep"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, = setup_locked_sml(dir, platforms: linux_platforms)

When "asking for an engine path"
error = assert_raises(Dev::Deps::Accessor::NotLockedError) do
accessor.path("gh", "UnrealEngineMac")
end

Then "the fix is update-deps"
error.message.include?("run dev update-deps")

Cleanup
FileUtils.rm_rf(dir)
end

test "path gh raises UsageError without a dep name" do
Given "an accessor"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, = setup_locked_gh(dir)

When "asking for a gh path with no name"
accessor.path("gh")

Then
raises Dev::Deps::Accessor::UsageError

Cleanup
FileUtils.rm_rf(dir)
end

test "run path gh prints the version dir to the output stream" do
Given "a locked and installed gh engine"
dir = Dir.mktmpdir("dev-accessor-test-")
accessor, version_dir = setup_locked_gh(dir)
out = StringIO.new

When "running deps path gh UnrealEngineMac"
accessor.run(["path", "gh", "UnrealEngineMac"], out: out)

Then
out.string.strip == version_dir

Cleanup
FileUtils.rm_rf(dir)
end

test "path xcode returns the pinned DEVELOPER_DIR" do
Given "a locked and installed xcode pin"
dir = Dir.mktmpdir("dev-accessor-test-")
Expand Down
Loading