Skip to content
Open
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
6 changes: 6 additions & 0 deletions hooks/backend_exec_env.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ function PLUGIN:BackendExecEnv(ctx)
{ key = "PATH", value = bin_path },
}

local runtime_deps = require("lib.runtime_deps")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Bug

The runtime_deps module has a critical bug where library environment variables (LD_LIBRARY_PATH/DYLD_LIBRARY_PATH) are commented out in lib/runtime_deps.lua at lines 31-33. This means the get_env_vars() function will only return PKG_CONFIG_PATH variables, not the library paths needed for runtime dependencies to work properly. The library environment variables must be uncommented to expose shared libraries at runtime.

🤖 Prompt for AI Agents:
In hooks/backend_exec_env.lua at line 17, Uncomment lines 31-33 in lib/runtime_deps.lua to enable\nLD_LIBRARY_PATH/DYLD_LIBRARY_PATH environment variables\nso that runtime dependencies are properly exposed.

local runtime_dep_env_vars = runtime_deps.get_env_vars(install_path, tool, version, RUNTIME.osType)
for _, env_var in ipairs(runtime_dep_env_vars) do
table.insert(env_vars, env_var)
end

if tool == "ruby" then
-- Nix store is read-only, gems should be installed to the state dir
table.insert(env_vars, { key = "GEM_HOME", value = file.join_path(install_path, "state/gems") })
Expand Down
108 changes: 108 additions & 0 deletions lib/runtime_deps.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
local M = {}

-- The main Nix package has runtime deps (other Nix store objects), which provide the usual
-- C headers, shared libraries and pkgconfig files. This function queries those runtime deps
-- and constructs the necessary env vars to make those dependencies available at runtime.
-- In practice, this is important when running package manager binaries that try to build native code
-- (e.g. Ruby gems that compile C extensions).
function M.get_env_vars(install_path, tool, version, os_type)
local file = require("file")
local cmd = require("cmd")

local result_path = file.join_path(install_path, "result")
local requisites = cmd.exec("nix-store --query --requisites " .. result_path)

if requisites:match("error") or requisites:match("failed") then
error("Failed to query runtime deps for " .. tool .. "@" .. version .. ": " .. requisites)
end

local store_paths = {}
for store_path in string.gmatch(requisites, "[^%s]+") do -- poor man's strings.Split(requisites, "\n")
table.insert(store_paths, store_path)
end

local lib_paths = M.find_lib_paths(store_paths, file, cmd)
local pkgconfig_paths = M.find_pkgconfig_paths(store_paths, file, cmd)

local env_vars = {}
local lib_env_vars = M.create_library_env_vars(lib_paths, os_type)
local pkgconfig_env_vars = M.create_pkgconfig_env_vars(pkgconfig_paths)

for _, var in ipairs(lib_env_vars) do
table.insert(env_vars, var)
end
for _, var in ipairs(pkgconfig_env_vars) do
table.insert(env_vars, var)
end

return env_vars
end

-- TODO: file.exists() doesn't exist 🤷
-- https://github.com/jdx/mise/pull/6754
-- Can be replaced once our pinned mise version is bumped again
function M.dir_exists(path, cmd_module)
local check_cmd = "if [ -d " .. path .. " ]; then echo 'exists'; fi"
local check_output = cmd_module.exec(check_cmd)
return check_output:match("exists") ~= nil
end

-- Finds all lib directories in the given store paths
function M.find_lib_paths(store_paths, file_module, cmd_module)
local lib_paths = {}
for _, store_path in ipairs(store_paths) do
local lib_path = file_module.join_path(store_path, "lib")
if M.dir_exists(lib_path, cmd_module) then
table.insert(lib_paths, lib_path)
end
end
return lib_paths
end

-- Finds all lib/pkgconfig directories in the given store paths
function M.find_pkgconfig_paths(store_paths, file_module, cmd_module)
local pkgconfig_paths = {}
for _, store_path in ipairs(store_paths) do
local pkgconfig_path = file_module.join_path(store_path, "lib/pkgconfig")
if M.dir_exists(pkgconfig_path, cmd_module) then
table.insert(pkgconfig_paths, pkgconfig_path)
end
end
return pkgconfig_paths
end

-- Exposes shared library paths for the (platform-specific) dynamic linker
function M.create_library_env_vars(lib_paths, os_type)
local env_vars = {}
local key = nil

if os_type == "linux" then
key = "LD_LIBRARY_PATH"
elseif os_type == "darwin" then
key = "DYLD_LIBRARY_PATH"
else
error("Unsupported architecture: " .. os_type)
end

for _, lib_path in ipairs(lib_paths) do
local is_linux_glibc = os_type == "linux" and lib_path:match("glibc")
local is_darwin_llvm = os_type == "darwin" and (lib_path:match("llvm") or lib_path:match("clang"))
local is_linux_libyaml = os_type == "linux" and lib_path:match("libyaml")
-- if not is_linux_glibc and not is_darwin_llvm then
if is_linux_libyaml then
table.insert(env_vars, { key = key, value = lib_path })
end
end

return env_vars
end

function M.create_pkgconfig_env_vars(pkgconfig_paths)
local env_vars = {}
for _, pkgconfig_path in ipairs(pkgconfig_paths) do
table.insert(env_vars, { key = "PKG_CONFIG_PATH", value = pkgconfig_path })
end
return env_vars
end

return M
21 changes: 21 additions & 0 deletions mise-tasks/test-integration
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,33 @@ run_test "PATH is set correctly" \
run_test "GEM_HOME is set for ruby" \
"mise exec nixpkgs:ruby@latest -- bash -c 'test -n \"\$GEM_HOME\"'" || exit 1

echo ""
echo "=== Gem Install Tests ==="

run_test "ruby: gem install fastlane" \
"mise exec nixpkgs:ruby@latest -- gem install fastlane" || exit 1

run_test "ruby: gem install cocoapods" \
"mise exec nixpkgs:ruby@latest -- gem install cocoapods" || exit 1

# Needs libyaml from system libraries
run_test "ruby: gem install psych" \
"mise exec nixpkgs:ruby@latest -- gem install psych" || exit 1

run_test "ruby: gem install psych: build from source" \
"mise exec nixpkgs:ruby@latest -- gem install psych --platform ruby" || exit 1

cleanup_installs

echo ""
echo "=== Smoke Tests ==="

run_test "fastlane actions" \
"mise exec nixpkgs:ruby@latest -- bash -c 'gem install fastlane && fastlane actions'" || exit 1

run_test "pod plugins" \
"mise exec nixpkgs:ruby@latest -- bash -c 'gem install cocoapods && pod plugins --allow-root'" || exit 1

cleanup_installs

echo ""
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ run = "command -v busted >/dev/null 2>&1 || mise exec lua -- luarocks install bu

[tasks.format]
description = "Format Lua scripts"
run = "stylua metadata.lua hooks/"
run = "stylua metadata.lua hooks/ lib/ spec/"

[tasks.lint]
description = "Lint Lua scripts and GitHub Actions using hk"
Expand Down
64 changes: 32 additions & 32 deletions spec/helpers/fixtures.lua
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
local M = {}

M.sample_index = {
pkgs = {
ruby = {
["3.3.9"] = {
nixpkgs_commit = "abc123def456",
commit_timestamp = "2025-01-15T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash1-ruby-3.3.9",
["aarch64-darwin"] = "/nix/store/hash2-ruby-3.3.9",
},
},
["3.3.8"] = {
nixpkgs_commit = "def456abc789",
commit_timestamp = "2025-01-14T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash3-ruby-3.3.8",
-- Note: aarch64-darwin entry is intentionally missing for testing
},
},
},
python = {
["3.12.1"] = {
nixpkgs_commit = "ghi789jkl012",
commit_timestamp = "2025-01-16T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash4-python-3.12.1",
["aarch64-darwin"] = "/nix/store/hash5-python-3.12.1",
},
},
},
},
pkgs = {
ruby = {
["3.3.9"] = {
nixpkgs_commit = "abc123def456",
commit_timestamp = "2025-01-15T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash1-ruby-3.3.9",
["aarch64-darwin"] = "/nix/store/hash2-ruby-3.3.9",
},
},
["3.3.8"] = {
nixpkgs_commit = "def456abc789",
commit_timestamp = "2025-01-14T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash3-ruby-3.3.8",
-- Note: aarch64-darwin entry is intentionally missing for testing
},
},
},
python = {
["3.12.1"] = {
nixpkgs_commit = "ghi789jkl012",
commit_timestamp = "2025-01-16T12:00:00+00:00",
store_paths = {
["x86_64-linux"] = "/nix/store/hash4-python-3.12.1",
["aarch64-darwin"] = "/nix/store/hash5-python-3.12.1",
},
},
},
},
}

M.sample_json = function()
local json = require("json")
return json.encode(M.sample_index)
local json = require("json")
return json.encode(M.sample_index)
end

return M
43 changes: 22 additions & 21 deletions spec/helpers/mock_modules.lua
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
local M = {}

function M.mock_file_module()
return {
read = function(path)
return '{"pkgs":{}}'
end,
join_path = function(...)
return table.concat({ ... }, "/")
end,
}
return {
read = function(path)
return '{"pkgs":{}}'
end,
join_path = function(...)
return table.concat({ ... }, "/")
end,
}
end

function M.mock_cmd_module()
return {
exec = function(cmd)
return "success"
end,
}
return {
exec = function(cmd)
return "success"
end,
}
end

function M.inject_modules(mocks)
for name, mock in pairs(mocks) do
package.loaded[name] = mock
end
for name, mock in pairs(mocks) do
package.loaded[name] = mock
end
end

function M.clear_modules()
package.loaded["file"] = nil
package.loaded["json"] = nil
package.loaded["cmd"] = nil
package.loaded["lib.nix"] = nil
package.loaded["lib.nixpkgs_mapping"] = nil
package.loaded["file"] = nil
package.loaded["json"] = nil
package.loaded["cmd"] = nil
package.loaded["lib.nix"] = nil
package.loaded["lib.nixpkgs_mapping"] = nil
package.loaded["lib.runtime_deps"] = nil
end

return M
16 changes: 8 additions & 8 deletions spec/helpers/mock_runtime.lua
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
local M = {}

function M.create_runtime(opts)
opts = opts or {}
return {
osType = opts.osType or "linux",
archType = opts.archType or "amd64",
pluginDirPath = opts.pluginDirPath or "/tmp/test-plugin",
}
opts = opts or {}
return {
osType = opts.osType or "linux",
archType = opts.archType or "amd64",
pluginDirPath = opts.pluginDirPath or "/tmp/test-plugin",
}
end

function M.inject_runtime(runtime)
_G.RUNTIME = runtime
_G.RUNTIME = runtime
end

function M.restore_runtime()
_G.RUNTIME = nil
_G.RUNTIME = nil
end

return M
Loading