summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzzbaron <[email protected]>2025-09-22 12:50:03 -0400
committerzzbaron <[email protected]>2025-09-22 12:50:47 -0400
commit998b9990ee6141106cebca48f919ed82ba591550 (patch)
tree3379651400859dfd4c1fc99c2d8ffcbc69ce9bd0
parentf1a64ad50e22c55a08a2553669aba152f70d9eb6 (diff)
nix: simplifying package detection
-rw-r--r--xmake/modules/package/manager/nix/find_package.lua244
1 files changed, 35 insertions, 209 deletions
diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua
index fb14a6b90..2fec59de9 100644
--- a/xmake/modules/package/manager/nix/find_package.lua
+++ b/xmake/modules/package/manager/nix/find_package.lua
@@ -29,32 +29,25 @@ function _follow_propagated_inputs(store_paths, opt, visited)
visited = visited or {}
local all_paths = {}
local seen = {}
-
- -- Add initial paths
+
for _, store_path in ipairs(store_paths) do
if not seen[store_path] then
seen[store_path] = true
table.insert(all_paths, store_path)
end
end
-
- -- Process each path
+
local i = 1
while i <= #all_paths do
local store_path = all_paths[i]
-
if not visited[store_path] then
visited[store_path] = true
-
- -- Check for propagated-build-inputs file
local prop_file = path.join(store_path, "nix-support", "propagated-build-inputs")
if os.isfile(prop_file) then
- local content = try {function()
+ local content = try {function()
return io.readfile(prop_file):trim()
end}
-
if content and content ~= "" then
- -- Parse propagated paths
for prop_path in content:gmatch("%S+") do
if prop_path:startswith("/nix/store/") and not seen[prop_path] then
seen[prop_path] = true
@@ -67,10 +60,8 @@ function _follow_propagated_inputs(store_paths, opt, visited)
end
end
end
-
i = i + 1
end
-
return all_paths
end
@@ -78,16 +69,14 @@ end
function _parse_store_paths_from_env(env_vars, opt)
local paths = {}
local seen = {}
-
+
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Parsing store paths from environment variables")
end
-
+
for _, var_name in ipairs(env_vars) do
local env_value = os.getenv(var_name) or ""
-
if env_value ~= "" then
- -- Split by spaces and colons, extract store paths
for item in env_value:gmatch("[^%s:]+") do
if item:startswith("/nix/store/") then
local store_path = item:match("(/nix/store/[^/]+)")
@@ -102,92 +91,49 @@ function _parse_store_paths_from_env(env_vars, opt)
end
end
end
-
- -- Follow propagated build inputs
+
paths = _follow_propagated_inputs(paths, opt)
-
return paths
end
--- check if we're in a nix-shell environment
function _in_nix_shell()
local in_nix_shell = os.getenv("IN_NIX_SHELL")
return in_nix_shell == "pure" or in_nix_shell == "impure"
end
--- check if a store path likely contains the requested package
function _path_matches_package(store_path, package_name, opt)
local path_name = path.basename(store_path)
local package_name_lower = package_name:lower()
-
- -- Extract package name from store path
- -- Format: hash-packagename-version[-output]
local package_base = path_name:match("^[^%-]+-([^%-]+)")
if package_base then
local package_base_lower = package_base:lower()
-
- -- Exact match
if package_base_lower == package_name_lower then
return true
end
-
- -- Partial match (package name contains or is contained in the base name)
if package_base_lower:find(package_name_lower, 1, true) or package_name_lower:find(package_base_lower, 1, true) then
return true
end
end
-
return false
end
--- group store paths by package base name
-function _group_store_paths_by_package(store_paths, opt)
- local packages = {}
-
- for _, store_path in ipairs(store_paths) do
- if os.isdir(store_path) then
- local path_name = path.basename(store_path)
-
- -- Extract package name (everything before version or output suffix)
- -- Format: hash-packagename-version[-output]
- local package_base = path_name:match("^[^%-]+-([^%-]+)")
- if package_base then
- if not packages[package_base] then
- packages[package_base] = {}
- end
- table.insert(packages[package_base], store_path)
- end
- end
- end
-
- return packages
-end
-
--- pkg-config search that handles all outputs
function _find_with_pkgconfig(package_name, store_paths, opt)
-
- -- Only use store paths that might contain the requested package for pkg-config
local relevant_paths = {}
for _, store_path in ipairs(store_paths) do
if _path_matches_package(store_path, package_name, opt) then
table.insert(relevant_paths, store_path)
end
end
-
if #relevant_paths == 0 then
return nil
end
-
- -- Collect all pkg-config directories from relevant store paths
local all_pkgconfig_dirs = {}
local pkgconfig_env_additions = {}
-
for _, store_path in ipairs(relevant_paths) do
local pkgconfig_dirs = {
path.join(store_path, "lib", "pkgconfig"),
path.join(store_path, "share", "pkgconfig")
}
-
for _, pkgconfig_dir in ipairs(pkgconfig_dirs) do
if os.isdir(pkgconfig_dir) then
table.insert(all_pkgconfig_dirs, pkgconfig_dir)
@@ -195,63 +141,42 @@ function _find_with_pkgconfig(package_name, store_paths, opt)
end
end
end
-
if #all_pkgconfig_dirs == 0 then
return nil
end
-
- -- Set up PKG_CONFIG_PATH environment for search
local original_pkg_config_path = os.getenv("PKG_CONFIG_PATH") or ""
local new_pkg_config_path = table.concat(pkgconfig_env_additions, ":")
if original_pkg_config_path ~= "" then
new_pkg_config_path = new_pkg_config_path .. ":" .. original_pkg_config_path
end
-
- -- set PKG_CONFIG_PATH
os.setenv("PKG_CONFIG_PATH", new_pkg_config_path)
-
- -- Try pkg-config with the enhanced path
- local result = nil
-
- -- First try direct package name
- result = find_package_from_pkgconfig(package_name)
-
+ local result = find_package_from_pkgconfig(package_name)
if result then
return result
end
-
- if not result then
- -- Try alternative names - check what .pc files actually exist
- for _, pkgconfig_dir in ipairs(all_pkgconfig_dirs) do
- local pc_files = try {function()
- return os.files(path.join(pkgconfig_dir, "*.pc"))
- end} or {}
-
- for _, pc_file in ipairs(pc_files) do
- local pc_name = path.basename(pc_file):match("^(.+)%.pc$")
- if pc_name then
- local name_lower = package_name:lower()
- local pc_lower = pc_name:lower()
-
- -- Check for partial matches
- if pc_lower:find(name_lower, 1, true) or name_lower:find(pc_lower, 1, true) then
- result = find_package_from_pkgconfig(pc_name)
- if result then
- break
- end
+ for _, pkgconfig_dir in ipairs(all_pkgconfig_dirs) do
+ local pc_files = try {function()
+ return os.files(path.join(pkgconfig_dir, "*.pc"))
+ end} or {}
+ for _, pc_file in ipairs(pc_files) do
+ local pc_name = path.basename(pc_file):match("^(.+)%.pc$")
+ if pc_name then
+ local name_lower = package_name:lower()
+ local pc_lower = pc_name:lower()
+ if pc_lower:find(name_lower, 1, true) or name_lower:find(pc_lower, 1, true) then
+ result = find_package_from_pkgconfig(pc_name)
+ if result then
+ break
end
end
end
- if result then break end
end
+ if result then break end
end
-
return result
end
--- extract package info from relevant store paths only
function _extract_package_info(store_paths, package_name, opt)
-
local result = {
includedirs = {},
bindirs = {},
@@ -259,18 +184,13 @@ function _extract_package_info(store_paths, package_name, opt)
links = {},
libfiles = {}
}
-
- -- First, try pkg-config search with only relevant paths
local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt)
if pkgconfig_result then
return pkgconfig_result
end
-
- -- Find paths that match the requested package
local main_package_paths = {}
local dependency_paths = {}
local found_main_package = false
-
for _, store_path in ipairs(store_paths) do
if _path_matches_package(store_path, package_name, opt) then
table.insert(main_package_paths, store_path)
@@ -279,28 +199,19 @@ function _extract_package_info(store_paths, package_name, opt)
table.insert(dependency_paths, store_path)
end
end
-
if not found_main_package then
return nil
end
-
- -- Process main package paths first (for bins and primary libs)
for _, store_path in ipairs(main_package_paths) do
-
- -- Add include directories
local includedir = path.join(store_path, "include")
if os.isdir(includedir) then
table.insert(result.includedirs, includedir)
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Found main package include dir: " .. includedir)
end
-
- -- Recursively add one level of subdirectories to handle cases where
- -- headers are organized in subdirectories (openexr, etc.)
- local subdirs = try {function()
- return os.dirs(path.join(includedir, "*"))
+ local subdirs = try {function()
+ return os.dirs(path.join(includedir, "*"))
end} or {}
-
for _, subdir in ipairs(subdirs) do
if os.isdir(subdir) then
table.insert(result.includedirs, subdir)
@@ -310,8 +221,6 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
end
-
- -- Add bin directories
local bindir = path.join(store_path, "bin")
if os.isdir(bindir) then
table.insert(result.bindirs, bindir)
@@ -319,34 +228,28 @@ function _extract_package_info(store_paths, package_name, opt)
print("Nix: Found main package bin dir: " .. bindir)
end
end
-
- -- Add lib directories and scan for libraries
local libdir = path.join(store_path, "lib")
if os.isdir(libdir) then
- -- Check if this lib dir actually contains libraries (not just cmake/pkgconfig)
local libfiles = try {function()
local files = {}
local so_files = os.files(path.join(libdir, "*.so*")) or {}
local a_files = os.files(path.join(libdir, "*.a")) or {}
local dylib_files = os.files(path.join(libdir, "*.dylib*")) or {}
for _, f in ipairs(so_files) do table.insert(files, f) end
- for _, f in ipairs(a_files) do table.insert(files, f) end
+ for _, f in ipairs(a_files) do table.insert(files, f) end
for _, f in ipairs(dylib_files) do table.insert(files, f) end
return files
end} or {}
-
if #libfiles > 0 then
table.insert(result.linkdirs, libdir)
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Found main package lib dir: " .. libdir .. " (" .. #libfiles .. " libraries)")
end
-
for _, libfile in ipairs(libfiles) do
local filename = path.filename(libfile)
- local linkname = filename:match("^lib(.+)%.so") or
- filename:match("^lib(.+)%.a") or
+ local linkname = filename:match("^lib(.+)%.so") or
+ filename:match("^lib(.+)%.a") or
filename:match("^lib(.+)%.dylib")
-
if linkname then
table.insert(result.links, linkname)
table.insert(result.libfiles, libfile)
@@ -356,10 +259,8 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
else
- -- If no actual libraries but has cmake/pkgconfig, still add for potential cmake usage
local has_cmake = os.isdir(path.join(libdir, "cmake"))
local has_pkgconfig = os.isdir(path.join(libdir, "pkgconfig"))
-
if has_cmake or has_pkgconfig then
table.insert(result.linkdirs, libdir)
if opt and (opt.verbose or option.get("verbose")) then
@@ -369,24 +270,16 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
end
-
- -- Process dependency paths (include dirs and libs only, no bins)
for _, store_path in ipairs(dependency_paths) do
-
- -- Add include directories from dependencies
local includedir = path.join(store_path, "include")
if os.isdir(includedir) then
table.insert(result.includedirs, includedir)
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Found dependency include dir: " .. includedir)
end
-
- -- Recursively add one level of subdirectories to handle cases where
- -- headers are organized in subdirectories
- local subdirs = try {function()
- return os.dirs(path.join(includedir, "*"))
+ local subdirs = try {function()
+ return os.dirs(path.join(includedir, "*"))
end} or {}
-
for _, subdir in ipairs(subdirs) do
if os.isdir(subdir) then
table.insert(result.includedirs, subdir)
@@ -396,34 +289,28 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
end
-
- -- Add lib directories and scan for libraries from dependencies
local libdir = path.join(store_path, "lib")
if os.isdir(libdir) then
- -- Check if this lib dir actually contains libraries (not just cmake/pkgconfig)
local libfiles = try {function()
local files = {}
local so_files = os.files(path.join(libdir, "*.so*")) or {}
local a_files = os.files(path.join(libdir, "*.a")) or {}
local dylib_files = os.files(path.join(libdir, "*.dylib*")) or {}
for _, f in ipairs(so_files) do table.insert(files, f) end
- for _, f in ipairs(a_files) do table.insert(files, f) end
+ for _, f in ipairs(a_files) do table.insert(files, f) end
for _, f in ipairs(dylib_files) do table.insert(files, f) end
return files
end} or {}
-
if #libfiles > 0 then
table.insert(result.linkdirs, libdir)
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Found dependency lib dir: " .. libdir .. " (" .. #libfiles .. " libraries)")
end
-
for _, libfile in ipairs(libfiles) do
local filename = path.filename(libfile)
- local linkname = filename:match("^lib(.+)%.so") or
- filename:match("^lib(.+)%.a") or
+ local linkname = filename:match("^lib(.+)%.so") or
+ filename:match("^lib(.+)%.a") or
filename:match("^lib(.+)%.dylib")
-
if linkname then
table.insert(result.links, linkname)
table.insert(result.libfiles, libfile)
@@ -433,10 +320,8 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
else
- -- If no actual libraries but has cmake/pkgconfig, still add for potential cmake usage
local has_cmake = os.isdir(path.join(libdir, "cmake"))
local has_pkgconfig = os.isdir(path.join(libdir, "pkgconfig"))
-
if has_cmake or has_pkgconfig then
table.insert(result.linkdirs, libdir)
if opt and (opt.verbose or option.get("verbose")) then
@@ -446,8 +331,6 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
end
-
- -- Remove duplicates
local function remove_duplicates(arr)
local seen = {}
local clean = {}
@@ -459,19 +342,16 @@ function _extract_package_info(store_paths, package_name, opt)
end
return clean
end
-
result.includedirs = remove_duplicates(result.includedirs)
result.bindirs = remove_duplicates(result.bindirs)
result.linkdirs = remove_duplicates(result.linkdirs)
result.links = remove_duplicates(result.links)
result.libfiles = remove_duplicates(result.libfiles)
-
- -- Return result only if we found the main package AND have useful information
if (#result.includedirs > 0) or (#result.bindirs > 0) or (#result.links > 0) or (#result.linkdirs > 0) then
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: DEBUG: Package info extraction succeeded for '" .. package_name .. "'")
- print("Nix: DEBUG: Found " .. #result.includedirs .. " include dirs, " ..
- #result.bindirs .. " bin dirs, " .. #result.linkdirs .. " link dirs, " ..
+ print("Nix: DEBUG: Found " .. #result.includedirs .. " include dirs, " ..
+ #result.bindirs .. " bin dirs, " .. #result.linkdirs .. " link dirs, " ..
#result.links .. " links")
print("Nix: DEBUG: Main package paths: " .. #main_package_paths .. ", Dependency paths: " .. #dependency_paths)
end
@@ -484,26 +364,21 @@ function _extract_package_info(store_paths, package_name, opt)
end
end
--- priority 1: nix shell (flake or legacy)
function _find_in_nix_shell(package_name, opt)
if not _in_nix_shell() then
return nil
end
-
- -- Parse buildInputs environment variables
local build_env_vars = {
"buildInputs",
- "nativeBuildInputs",
+ "nativeBuildInputs",
"propagatedBuildInputs",
"propagatedNativeBuildInputs"
}
-
local store_paths = _parse_store_paths_from_env(build_env_vars, opt)
if #store_paths > 0 then
if opt and (opt.verbose or option.get("verbose")) then
print("Nix: Found " .. #store_paths .. " total store paths in nix-shell")
end
-
local result = _extract_package_info(store_paths, package_name, opt)
if result then
if opt and (opt.verbose or option.get("verbose")) then
@@ -512,33 +387,26 @@ function _find_in_nix_shell(package_name, opt)
return result
end
end
-
return nil
end
--- priority 2: profile installs
function _find_in_nix_profile(package_name, opt)
local nix = find_tool("nix")
if not nix then
return nil
end
-
local profile_list = try {function()
return os.iorunv(nix.program, {"profile", "list", "--extra-experimental-features 'nix-command flakes'"}):trim()
end}
-
if profile_list then
local store_paths = {}
for line in profile_list:gmatch("[^\n]+") do
- -- Parse nix profile list output format
local store_path = line:match("(/nix/store/[^%s]+)")
if store_path then
table.insert(store_paths, store_path)
end
end
-
if #store_paths > 0 then
- -- Follow propagated inputs for profile packages too
store_paths = _follow_propagated_inputs(store_paths, opt)
local result = _extract_package_info(store_paths, package_name, opt)
if result then
@@ -549,21 +417,17 @@ function _find_in_nix_profile(package_name, opt)
end
end
end
-
return nil
end
--- priority 3: home-manager (with tool)
function _find_in_home_manager_tool(package_name, opt)
local home_manager = find_tool("home-manager")
if not home_manager then
return nil
end
-
local hm_packages = try {function()
return os.iorunv(home_manager.program, {"packages"}):trim()
end}
-
if hm_packages then
local store_paths = {}
for line in hm_packages:gmatch("[^\n]+") do
@@ -572,7 +436,6 @@ function _find_in_home_manager_tool(package_name, opt)
table.insert(store_paths, store_path)
end
end
-
if #store_paths > 0 then
store_paths = _follow_propagated_inputs(store_paths, opt)
local result = _extract_package_info(store_paths, package_name, opt)
@@ -584,28 +447,22 @@ function _find_in_home_manager_tool(package_name, opt)
end
end
end
-
return nil
end
--- priority 4: home-manager (without tool)
function _find_in_home_manager_profile(package_name, opt)
local nix_store = find_tool("nix-store")
if not nix_store then
return nil
end
-
local user = os.getenv("USER") or "unknown"
local user_profile = "/etc/profiles/per-user/" .. user
-
if not os.isdir(user_profile) then
return nil
end
-
local requisites = try {function()
return os.iorunv(nix_store.program, {"--query", "--requisites", user_profile}):trim()
end}
-
if requisites then
local store_paths = {}
for line in requisites:gmatch("[^\n]+") do
@@ -613,9 +470,7 @@ function _find_in_home_manager_profile(package_name, opt)
table.insert(store_paths, line)
end
end
-
if #store_paths > 0 then
- -- Note: requisites already includes everything, no need to follow propagated inputs again
local result = _extract_package_info(store_paths, package_name, opt)
if result then
if opt and (opt.verbose or option.get("verbose")) then
@@ -625,28 +480,23 @@ function _find_in_home_manager_profile(package_name, opt)
end
end
end
-
return nil
end
--- priority 5: nixos user packages
function _find_in_nixos_user_packages(package_name, opt)
local nixos_option = find_tool("nixos-option")
if not nixos_option then
return nil
end
-
local user = os.getenv("USER") or "unknown"
local user_packages = try {function()
return os.iorunv(nixos_option.program, {"users.users." .. user .. ".packages"}):trim()
end}
-
if user_packages then
local store_paths = {}
for store_path in user_packages:gmatch('(/nix/store/[^"\'%s]+)') do
table.insert(store_paths, store_path)
end
-
if #store_paths > 0 then
store_paths = _follow_propagated_inputs(store_paths, opt)
local result = _extract_package_info(store_paths, package_name, opt)
@@ -658,27 +508,22 @@ function _find_in_nixos_user_packages(package_name, opt)
end
end
end
-
return nil
end
--- priority 6: nixos system packages
function _find_in_nixos_system_packages(package_name, opt)
local nixos_option = find_tool("nixos-option")
if not nixos_option then
return nil
end
-
local system_packages = try {function()
return os.iorunv(nixos_option.program, {"environment.systemPackages"}):trim()
end}
-
if system_packages then
local store_paths = {}
for store_path in system_packages:gmatch('(/nix/store/[^"\'%s]+)') do
table.insert(store_paths, store_path)
end
-
if #store_paths > 0 then
store_paths = _follow_propagated_inputs(store_paths, opt)
local result = _extract_package_info(store_paths, package_name, opt)
@@ -690,25 +535,20 @@ function _find_in_nixos_system_packages(package_name, opt)
end
end
end
-
return nil
end
--- priority 7: nixos current system
function _find_in_nixos_current_system(package_name, opt)
local nix_store = find_tool("nix-store")
if not nix_store then
return nil
end
-
if not os.isdir("/run/current-system") then
return nil
end
-
local requisites = try {function()
return os.iorunv(nix_store.program, {"--query", "--requisites", "/run/current-system"}):trim()
end}
-
if requisites then
local store_paths = {}
for line in requisites:gmatch("[^\n]+") do
@@ -716,9 +556,7 @@ function _find_in_nixos_current_system(package_name, opt)
table.insert(store_paths, line)
end
end
-
if #store_paths > 0 then
- -- Note: requisites already includes everything, no need to follow propagated inputs again
local result = _extract_package_info(store_paths, package_name, opt)
if result then
if opt and (opt.verbose or option.get("verbose")) then
@@ -728,28 +566,20 @@ function _find_in_nixos_current_system(package_name, opt)
end
end
end
-
return nil
end
--- main find function
function main(name, opt)
opt = opt or {}
-
- -- Check for cross compilation
if is_cross(opt.plat, opt.arch) then
return
end
-
- -- Handle nix:: prefix
local actual_name = name
local force_nix = false
if name:startswith("nix::") then
- actual_name = name:sub(5) -- Remove "nix::" prefix
+ actual_name = name:sub(5)
force_nix = true
end
-
- -- Search priority chain
local search_functions = {
_find_in_nix_shell,
_find_in_nix_profile,
@@ -759,18 +589,14 @@ function main(name, opt)
_find_in_nixos_system_packages,
_find_in_nixos_current_system
}
-
for _, search_func in ipairs(search_functions) do
local result = search_func(actual_name, opt)
if result then
return result
end
end
-
- -- No results found
if force_nix and opt and (opt.verbose or option.get("verbose")) then
print("Nix: Package " .. actual_name .. " not found in any nix environment")
end
-
return nil
end \ No newline at end of file