From 975a7c38894f6a8536c34df3c285a4f7bfb88e0d Mon Sep 17 00:00:00 2001 From: zzbaron Date: Sun, 21 Sep 2025 14:01:13 -0400 Subject: nix: rewrite of package detection --- xmake/modules/package/manager/nix/find_package.lua | 774 ++++++++++++++------- 1 file changed, 521 insertions(+), 253 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 8f3a909e8..cc04bc898 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -24,321 +24,605 @@ import("lib.detect.find_tool") import("private.core.base.is_cross") import("package.manager.pkgconfig.find_package", {alias = "find_package_from_pkgconfig"}) --- 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 - --- extract store paths from nix environment variables with better filtering -function _extract_nix_store_paths(env_var_name, env_var_value) - local paths = {} +-- recursively follow propagated build inputs +function _follow_propagated_inputs(store_paths, opt, visited) + visited = visited or {} + local all_paths = {} local seen = {} - if env_var_value == "" then - return paths + -- 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 - -- Handle different environment variable formats - local separators = { - PATH = ":", - PKG_CONFIG_PATH = ":", - LIBRARY_PATH = ":", - LD_LIBRARY_PATH = ":", - C_INCLUDE_PATH = ":", - CPLUS_INCLUDE_PATH = ":", - NIX_LDFLAGS = "%s", -- space separated, may contain -L flags - NIX_CFLAGS_COMPILE = "%s" -- space separated, may contain -I flags - } - - local separator = separators[env_var_name] or ":" - local pattern = separator == ":" and "[^:]+" or "[^%s]+" - - for item in env_var_value:gmatch(pattern) do - local clean_item = item - - -- Remove flag prefixes for compiler/linker flags - if env_var_name == "NIX_LDFLAGS" then - clean_item = item:gsub("^%-L", "") - elseif env_var_name == "NIX_CFLAGS_COMPILE" then - clean_item = item:gsub("^%-[iI]system%s*", ""):gsub("^%-I", "") - end + -- Process each path + local i = 1 + while i <= #all_paths do + local store_path = all_paths[i] - if clean_item:startswith("/nix/store/") then - local store_path = clean_item:match("(/nix/store/[^/]+)") - if store_path and not seen[store_path] then - seen[store_path] = true - table.insert(paths, store_path) + 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() + 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 + table.insert(all_paths, prop_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added propagated: " .. prop_path) + end + end + end + end end end + + i = i + 1 end - return paths + return all_paths end --- get current shell buildInputs from environment with improved detection -function _get_shell_build_inputs() - local all_paths = {} +-- parse store paths from environment variables +function _parse_store_paths_from_env(env_vars, opt) + local paths = {} local seen = {} - if not _in_nix_shell() then - return all_paths - end - - -- Environment variables to check for nix store paths - local env_vars = { - "PATH", - "PKG_CONFIG_PATH", - "LIBRARY_PATH", - "LD_LIBRARY_PATH", - "C_INCLUDE_PATH", - "CPLUS_INCLUDE_PATH", - "NIX_LDFLAGS", - "NIX_CFLAGS_COMPILE" - } + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Parsing store paths from environment variables") + end - for _, env_var in ipairs(env_vars) do - local env_value = os.getenv(env_var) or "" - local paths = _extract_nix_store_paths(env_var, env_value) + for _, var_name in ipairs(env_vars) do + local env_value = os.getenv(var_name) or "" - for _, path in ipairs(paths) do - if not seen[path] then - seen[path] = true - table.insert(all_paths, path) + 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/[^/]+)") + if store_path and not seen[store_path] then + seen[store_path] = true + table.insert(paths, store_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found store path: " .. store_path) + end + end + end end end end - return all_paths + -- Follow propagated build inputs + paths = _follow_propagated_inputs(paths, opt) + + return paths end --- get all nix store paths currently available in environment -function _get_available_nix_paths() - local paths = {} - local seen = {} +-- 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 + +-- group store paths by package base name +function _group_store_paths_by_package(store_paths, opt) + local packages = {} - -- First, get paths from current shell if we're in nix-shell - if _in_nix_shell() then - local shell_paths = _get_shell_build_inputs() - for _, path in ipairs(shell_paths) do - if not seen[path] then - seen[path] = true - table.insert(paths, path) + 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 - -- Get paths from environment PATH (additional check) - local env_path = os.getenv("PATH") or "" + return packages +end + +-- pkg-config search that handles all outputs +function _find_with_pkgconfig(package_name, store_paths, opt) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: pkg-config search for " .. package_name) + end - for dir in env_path:gmatch("[^:]+") do - if dir:startswith("/nix/store/") then - local store_path = dir:match("(/nix/store/[^/]+)") - if store_path and not seen[store_path] then - seen[store_path] = true - table.insert(paths, store_path) + -- Collect all pkg-config directories from all store paths + local all_pkgconfig_dirs = {} + local pkgconfig_env_additions = {} + + for _, store_path in ipairs(store_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) + table.insert(pkgconfig_env_additions, pkgconfig_dir) end end end - -- Get paths from common Nix environment locations - local env_locations = { - os.getenv("NIX_PROFILES") or "", - (os.getenv("HOME") or "") .. "/.nix-profile", - "/nix/var/nix/profiles/default", - "/run/current-system/sw" -- NixOS - } + if #all_pkgconfig_dirs == 0 then + return nil + end - for _, location in ipairs(env_locations) do - if location ~= "" and os.isdir(location) then - - -- Check if it's a symlink to store path - local target = try {function() - return os.iorunv("readlink", {"-f", location}):trim() - end} - - if target and target:startswith("/nix/store/") then - local store_path = target:match("(/nix/store/[^/]+)") - if store_path and not seen[store_path] then - seen[store_path] = true - table.insert(paths, store_path) - end - 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) + + 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 {} - -- Also check for manifest (generation info) - local manifest = path.join(location, "manifest.nix") - if os.isfile(manifest) then - local manifest_content = io.readfile(manifest) - - if manifest_content then - -- Extract store paths from manifest - for store_path in manifest_content:gmatch('(/nix/store/[^"\'%s]+)') do - if not seen[store_path] then - seen[store_path] = true - table.insert(paths, store_path) + 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 + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found via pkg-config: " .. pc_name) + end + break end end end end + if result then break end end end - - return paths + + return result end --- check if a store path actually contains the requested package -function _validate_package_in_store_path(store_path, name) +-- extract package info from all outputs of a package +function _extract_package_info(store_paths, package_name, opt) + local result = { + includedirs = {}, + bindirs = {}, + linkdirs = {}, + links = {}, + libfiles = {} + } - -- Check if the store path name contains the package name - local store_name = path.basename(store_path):lower() - local search_name = name:lower() + -- Group paths by package + local packages = _group_store_paths_by_package(store_paths, opt) - -- Look for exact match, or package name in the store path - local name_match = store_name:find(search_name, 1, true) or - store_name:find((search_name:gsub("%-", "%%-"))) -- handle hyphens + -- Find matching package + local matching_outputs = nil + local search_name = package_name:lower() - if name_match then - return true + for pkg_name, outputs in pairs(packages) do + local pkg_lower = pkg_name:lower() + local name_in_pkg = pkg_lower:find(search_name, 1, true) + local pkg_in_name = search_name:find(pkg_lower, 1, true) + + if name_in_pkg or pkg_in_name then + matching_outputs = outputs + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found package match: " .. pkg_name .. " (" .. #outputs .. " outputs)") + end + break + end end - -- Check for libraries with the package name - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - local libfiles = os.files(path.join(libdir, "lib" .. name .. ".*")) - if #libfiles > 0 then - return true + -- Also check direct path name matches for cases where grouping fails + if not matching_outputs then + matching_outputs = {} + for _, store_path in ipairs(store_paths) do + local path_name = path.basename(store_path):lower() + if path_name:find(search_name, 1, true) then + table.insert(matching_outputs, store_path) + end end end - -- Check for pkg-config files - local pkgconfigdirs = { - path.join(store_path, "lib", "pkgconfig"), - path.join(store_path, "share", "pkgconfig") - } + if not matching_outputs or #matching_outputs == 0 then + return nil + end - for _, pcdir in ipairs(pkgconfigdirs) do - if os.isdir(pcdir) then - local pcfiles = os.files(path.join(pcdir, name .. ".pc")) - if #pcfiles > 0 then - return true + -- Try pkg-config search first + local pkgconfig_result = _find_with_pkgconfig(package_name, matching_outputs, opt) + + -- Process all outputs of the package + for _, store_path in ipairs(matching_outputs) do + -- Add include directories from any output that has them + 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 include dir: " .. includedir) + end + end + + -- Add bin directories from any output that has them + local bindir = path.join(store_path, "bin") + if os.isdir(bindir) then + table.insert(result.bindirs, bindir) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found bin dir: " .. bindir) + end + end + + -- Add lib directories and scan for libraries from any output that has them + 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(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 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 + filename:match("^lib(.+)%.dylib") + + if linkname then + table.insert(result.links, linkname) + table.insert(result.libfiles, libfile) + 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 + print("Nix: Found lib dir: " .. libdir .. " (cmake/pkgconfig only)") + end + end end end end - - return false + + -- Merge pkg-config results if available (prioritize pkg-config results) + if pkgconfig_result then + -- Use pkg-config results preferentially, but supplement with discovered paths + for _, incdir in ipairs(pkgconfig_result.includedirs or {}) do + table.insert(result.includedirs, incdir) + end + for _, linkdir in ipairs(pkgconfig_result.linkdirs or {}) do + table.insert(result.linkdirs, linkdir) + end + for _, link in ipairs(pkgconfig_result.links or {}) do + table.insert(result.links, link) + end + + -- Add any additional paths we found that pkg-config might have missed + if pkgconfig_result.syslinks then + for _, link in ipairs(pkgconfig_result.syslinks) do + table.insert(result.links, link) + end + end + end + + -- Remove duplicates + local function remove_duplicates(arr) + local seen = {} + local clean = {} + for _, item in ipairs(arr) do + if not seen[item] then + seen[item] = true + table.insert(clean, item) + end + 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 if we found anything useful + if (#result.includedirs > 0) or (#result.bindirs > 0) or (#result.links > 0) or (#result.linkdirs > 0) then + return result + end + + return nil end --- find package in a specific nix store path with validation -function _find_in_store_path(store_path, name) - - if not os.isdir(store_path) then +-- priority 1: nix shell (flake or legacy) +function _find_in_nix_shell(package_name, opt) + if not _in_nix_shell() then return nil end - -- First validate that this store path actually contains our package - if not _validate_package_in_store_path(store_path, name) then + -- Parse buildInputs environment variables + local build_env_vars = { + "buildInputs", + "nativeBuildInputs", + "propagatedBuildInputs", + "propagatedNativeBuildInputs" + } + + local store_paths = _parse_store_paths_from_env(build_env_vars, opt) + if #store_paths > 0 then + local result = _extract_package_info(store_paths, package_name, opt) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Found " .. package_name .. " in nix-shell environment with " .. #store_paths .. " paths") + end + 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 result = {} + 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 + if opt and (opt.verbose or option.get("verbose")) then + print("Found " .. package_name .. " in nix profile with " .. #store_paths .. " paths") + end + return result + end + end + end - -- Find include directories - local includedir = path.join(store_path, "include") - if os.isdir(includedir) then - result.includedirs = {includedir} + 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 - -- Find libraries - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - result.linkdirs = {libdir} - result.links = {} - result.libfiles = {} - - -- Scan for library files related to our package - local libfiles = os.files(path.join(libdir, "*.so*"), - path.join(libdir, "*.a"), - path.join(libdir, "*.dylib*")) + 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 + local store_path = line:match("(/nix/store/[^%s]+)") + if store_path then + table.insert(store_paths, store_path) + end + end - for _, libfile in ipairs(libfiles) do - local filename = path.filename(libfile) - local linkname = filename:match("^lib(.+)%.so") or - filename:match("^lib(.+)%.a") or - filename:match("^lib(.+)%.dylib") - - if linkname then - if linkname == name or linkname:find(name, 1, true) then - table.insert(result.links, linkname) - table.insert(result.libfiles, libfile) - end - - if filename:endswith(".a") then - result.static = true - else - result.shared = true + if #store_paths > 0 then + store_paths = _follow_propagated_inputs(store_paths, opt) + local result = _extract_package_info(store_paths, package_name, opt) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Found " .. package_name .. " in home-manager with " .. #store_paths .. " paths") end + return result end end end - -- Find pkg-config files - local pkgconfigdirs = { - path.join(store_path, "lib", "pkgconfig"), - path.join(store_path, "share", "pkgconfig") - } + 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 - for _, pcdir in ipairs(pkgconfigdirs) do - if os.isdir(pcdir) then - local pcfiles = os.files(path.join(pcdir, name .. ".pc")) - if #pcfiles > 0 then - -- Use pkg-config with configdirs - local pcresult = find_package_from_pkgconfig(name, {configdirs = pcdir}) - - if pcresult then - return pcresult + 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 + if line:startswith("/nix/store/") then + 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 + print("Found " .. package_name .. " in home-manager profile with " .. #store_paths .. " paths") end + return result end end end - -- Return result if we found anything useful - if result.includedirs or (result.links and #result.links > 0) then - return result + 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) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Found " .. package_name .. " in NixOS user packages with " .. #store_paths .. " paths") + end + return result + end + end end return nil end --- try to build package with modern nix (flakes) -function _try_modern_nix_build(name) - local nix = find_tool("nix") - if not nix then +-- 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 - -- Try with flakes syntax - local storepath = try {function() - return os.iorunv(nix.program, {"build", "nixpkgs#" .. name, "--print-out-paths", "--no-link"}):trim() + local system_packages = try {function() + return os.iorunv(nixos_option.program, {"environment.systemPackages"}):trim() end} - return storepath + 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) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Found " .. package_name .. " in NixOS system packages with " .. #store_paths .. " paths") + end + return result + end + end + end + + return nil end --- try to build package with legacy nix -function _try_legacy_nix_build(name) - local nix_build = find_tool("nix-build") - if not nix_build then +-- 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 - -- Try legacy nix-build - local storepath = try {function() - return os.iorunv(nix_build.program, {"", "-A", name, "--no-out-link"}):trim() + 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} - return storepath + if requisites then + local store_paths = {} + for line in requisites:gmatch("[^\n]+") do + if line:startswith("/nix/store/") then + 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 + print("Found " .. package_name .. " in NixOS current system with " .. #store_paths .. " paths") + end + return result + end + end + end + + return nil end + -- main find function function main(name, opt) opt = opt or {} @@ -356,43 +640,27 @@ function main(name, opt) force_nix = true end - -- Get all available Nix store paths - local nix_paths = _get_available_nix_paths() + -- Search priority chain + local search_functions = { + _find_in_nix_shell, + _find_in_nix_profile, + _find_in_home_manager_tool, + _find_in_home_manager_profile, + _find_in_nixos_user_packages, + _find_in_nixos_system_packages, + _find_in_nixos_current_system + } - -- Search through available paths first (prioritize shell environment) - if #nix_paths > 0 then - for i, store_path in ipairs(nix_paths) do - local result = _find_in_store_path(store_path, actual_name) - if result then - if opt.verbose or option.get("verbose") then - print("Found " .. actual_name .. " in: " .. store_path) - end - return result - end + for _, search_func in ipairs(search_functions) do + local result = search_func(actual_name, opt) + if result then + return result end end - -- If not found in available paths and not in nix-shell, try building - if not _in_nix_shell() or force_nix then - local storepath = nil - - -- Try modern nix first - storepath = _try_modern_nix_build(actual_name) - - -- Fallback to legacy nix-build - if not storepath then - storepath = _try_legacy_nix_build(actual_name) - end - - if storepath and os.isdir(storepath) then - local result = _find_in_store_path(storepath, actual_name) - if result then - if opt.verbose or option.get("verbose") then - print("Built and found " .. actual_name .. " in: " .. storepath) - end - 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 -- cgit v1.3.1 From 5c25b34bed9b9e1fb6a5bb1f2ce1c10c94d9cb6f Mon Sep 17 00:00:00 2001 From: zzbaron Date: Sun, 21 Sep 2025 14:20:03 -0400 Subject: fixed nix prefix removal --- xmake/modules/package/manager/nix/find_package.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index cc04bc898..2989930f0 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -636,7 +636,7 @@ function main(name, opt) local actual_name = name local force_nix = false if name:startswith("nix::") then - actual_name = name:sub(6) -- Remove "nix::" prefix + actual_name = name:sub(5) -- Remove "nix::" prefix force_nix = true end -- cgit v1.3.1 From 32468fdcccd6c29cfc4ac29e078da14bd27fc2d2 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Sun, 21 Sep 2025 19:30:04 -0400 Subject: nix: adjusted pkg config logic and include dirs. --- xmake/modules/package/manager/nix/find_package.lua | 106 +++++++++++---------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 2989930f0..3c9c961d5 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -224,54 +224,31 @@ function _extract_package_info(store_paths, package_name, opt) libfiles = {} } - -- Group paths by package - local packages = _group_store_paths_by_package(store_paths, opt) - - -- Find matching package - local matching_outputs = nil - local search_name = package_name:lower() - - for pkg_name, outputs in pairs(packages) do - local pkg_lower = pkg_name:lower() - local name_in_pkg = pkg_lower:find(search_name, 1, true) - local pkg_in_name = search_name:find(pkg_lower, 1, true) - - if name_in_pkg or pkg_in_name then - matching_outputs = outputs - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found package match: " .. pkg_name .. " (" .. #outputs .. " outputs)") - end - break - end - end - - -- Also check direct path name matches for cases where grouping fails - if not matching_outputs then - matching_outputs = {} - for _, store_path in ipairs(store_paths) do - local path_name = path.basename(store_path):lower() - if path_name:find(search_name, 1, true) then - table.insert(matching_outputs, store_path) - end - end - end - - if not matching_outputs or #matching_outputs == 0 then - return nil - end - - -- Try pkg-config search first - local pkgconfig_result = _find_with_pkgconfig(package_name, matching_outputs, opt) - - -- Process all outputs of the package - for _, store_path in ipairs(matching_outputs) do - -- Add include directories from any output that has them + -- Process store paths, not just the ones that match the package name + -- This ensures we get include directories from propagated dependencies + for _, store_path in ipairs(store_paths) do + -- Add include directories from ALL store paths 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 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, "*")) + end} or {} + + for _, subdir in ipairs(subdirs) do + if os.isdir(subdir) then + table.insert(result.includedirs, subdir) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found include subdir: " .. subdir) + end + end + end end -- Add bin directories from any output that has them @@ -330,25 +307,58 @@ function _extract_package_info(store_paths, package_name, opt) end end + -- Try pkg-config search with all store paths + local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt) + -- Merge pkg-config results if available (prioritize pkg-config results) if pkgconfig_result then - -- Use pkg-config results preferentially, but supplement with discovered paths + -- Create new result starting with pkg-config data (highest priority) + local merged_result = { + includedirs = {}, + bindirs = result.bindirs, -- Keep discovered bin dirs + linkdirs = {}, + links = {}, + libfiles = result.libfiles -- Keep discovered lib files + } + + -- Add pkg-config include dirs first (highest priority) for _, incdir in ipairs(pkgconfig_result.includedirs or {}) do - table.insert(result.includedirs, incdir) + table.insert(merged_result.includedirs, incdir) end + + -- Add our discovered include dirs after pkg-config ones + for _, incdir in ipairs(result.includedirs) do + table.insert(merged_result.includedirs, incdir) + end + + -- Add pkg-config link dirs first for _, linkdir in ipairs(pkgconfig_result.linkdirs or {}) do - table.insert(result.linkdirs, linkdir) + table.insert(merged_result.linkdirs, linkdir) + end + + -- Add our discovered link dirs after pkg-config ones + for _, linkdir in ipairs(result.linkdirs) do + table.insert(merged_result.linkdirs, linkdir) end + + -- Add pkg-config links first for _, link in ipairs(pkgconfig_result.links or {}) do - table.insert(result.links, link) + table.insert(merged_result.links, link) + end + + -- Add our discovered links after pkg-config ones + for _, link in ipairs(result.links) do + table.insert(merged_result.links, link) end - -- Add any additional paths we found that pkg-config might have missed + -- Add any system links from pkg-config if pkgconfig_result.syslinks then for _, link in ipairs(pkgconfig_result.syslinks) do - table.insert(result.links, link) + table.insert(merged_result.links, link) end end + + result = merged_result end -- Remove duplicates -- cgit v1.3.1 From f1a64ad50e22c55a08a2553669aba152f70d9eb6 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Sun, 21 Sep 2025 23:48:21 -0400 Subject: nix: improve package filtering and search accuracy --- xmake/modules/package/manager/nix/find_package.lua | 243 +++++++++++++++------ 1 file changed, 171 insertions(+), 72 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 3c9c961d5..fb14a6b90 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -115,6 +115,31 @@ function _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 = {} @@ -140,15 +165,24 @@ end -- pkg-config search that handles all outputs function _find_with_pkgconfig(package_name, store_paths, opt) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: pkg-config search for " .. package_name) + + -- 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 - -- Collect all pkg-config directories from all store paths + 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(store_paths) do + for _, store_path in ipairs(relevant_paths) do local pkgconfig_dirs = { path.join(store_path, "lib", "pkgconfig"), path.join(store_path, "share", "pkgconfig") @@ -182,6 +216,10 @@ function _find_with_pkgconfig(package_name, store_paths, opt) -- First try direct package name 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 @@ -199,9 +237,6 @@ function _find_with_pkgconfig(package_name, store_paths, opt) 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 - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found via pkg-config: " .. pc_name) - end break end end @@ -214,8 +249,9 @@ function _find_with_pkgconfig(package_name, store_paths, opt) return result end --- extract package info from all outputs of a package +-- extract package info from relevant store paths only function _extract_package_info(store_paths, package_name, opt) + local result = { includedirs = {}, bindirs = {}, @@ -224,19 +260,43 @@ function _extract_package_info(store_paths, package_name, opt) libfiles = {} } - -- Process store paths, not just the ones that match the package name - -- This ensures we get include directories from propagated dependencies + -- 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 - -- Add include directories from ALL store paths + if _path_matches_package(store_path, package_name, opt) then + table.insert(main_package_paths, store_path) + found_main_package = true + else + 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 include dir: " .. includedir) + print("Nix: Found main package include dir: " .. includedir) end -- Recursively add one level of subdirectories to handle cases where - -- headers are organized in subdirectories + -- headers are organized in subdirectories (openexr, etc.) local subdirs = try {function() return os.dirs(path.join(includedir, "*")) end} or {} @@ -245,22 +305,22 @@ function _extract_package_info(store_paths, package_name, opt) if os.isdir(subdir) then table.insert(result.includedirs, subdir) if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found include subdir: " .. subdir) + print("Nix: Found main package include subdir: " .. subdir) end end end end - -- Add bin directories from any output that has them + -- Add bin directories local bindir = path.join(store_path, "bin") if os.isdir(bindir) then table.insert(result.bindirs, bindir) if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found bin dir: " .. bindir) + print("Nix: Found main package bin dir: " .. bindir) end end - -- Add lib directories and scan for libraries from any output that has them + -- 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) @@ -278,7 +338,7 @@ function _extract_package_info(store_paths, package_name, opt) if #libfiles > 0 then table.insert(result.linkdirs, libdir) if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found lib dir: " .. libdir .. " (" .. #libfiles .. " libraries)") + print("Nix: Found main package lib dir: " .. libdir .. " (" .. #libfiles .. " libraries)") end for _, libfile in ipairs(libfiles) do @@ -290,6 +350,9 @@ function _extract_package_info(store_paths, package_name, opt) if linkname then table.insert(result.links, linkname) table.insert(result.libfiles, libfile) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found main package library: " .. linkname .. " -> " .. libfile) + end end end else @@ -300,65 +363,88 @@ function _extract_package_info(store_paths, package_name, opt) if has_cmake or has_pkgconfig then table.insert(result.linkdirs, libdir) if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found lib dir: " .. libdir .. " (cmake/pkgconfig only)") + print("Nix: Found main package lib dir: " .. libdir .. " (cmake/pkgconfig only)") end end end end end - -- Try pkg-config search with all store paths - local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt) - - -- Merge pkg-config results if available (prioritize pkg-config results) - if pkgconfig_result then - -- Create new result starting with pkg-config data (highest priority) - local merged_result = { - includedirs = {}, - bindirs = result.bindirs, -- Keep discovered bin dirs - linkdirs = {}, - links = {}, - libfiles = result.libfiles -- Keep discovered lib files - } - - -- Add pkg-config include dirs first (highest priority) - for _, incdir in ipairs(pkgconfig_result.includedirs or {}) do - table.insert(merged_result.includedirs, incdir) - end - - -- Add our discovered include dirs after pkg-config ones - for _, incdir in ipairs(result.includedirs) do - table.insert(merged_result.includedirs, incdir) - end - - -- Add pkg-config link dirs first - for _, linkdir in ipairs(pkgconfig_result.linkdirs or {}) do - table.insert(merged_result.linkdirs, linkdir) - end - - -- Add our discovered link dirs after pkg-config ones - for _, linkdir in ipairs(result.linkdirs) do - table.insert(merged_result.linkdirs, linkdir) - end - - -- Add pkg-config links first - for _, link in ipairs(pkgconfig_result.links or {}) do - table.insert(merged_result.links, link) - end + -- Process dependency paths (include dirs and libs only, no bins) + for _, store_path in ipairs(dependency_paths) do - -- Add our discovered links after pkg-config ones - for _, link in ipairs(result.links) do - table.insert(merged_result.links, link) + -- 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, "*")) + end} or {} + + for _, subdir in ipairs(subdirs) do + if os.isdir(subdir) then + table.insert(result.includedirs, subdir) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found dependency include subdir: " .. subdir) + end + end + end end - -- Add any system links from pkg-config - if pkgconfig_result.syslinks then - for _, link in ipairs(pkgconfig_result.syslinks) do - table.insert(merged_result.links, link) + -- 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(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 + filename:match("^lib(.+)%.dylib") + + if linkname then + table.insert(result.links, linkname) + table.insert(result.libfiles, libfile) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found dependency library: " .. linkname .. " -> " .. libfile) + end + 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 + print("Nix: Found dependency lib dir: " .. libdir .. " (cmake/pkgconfig only)") + end + end end end - - result = merged_result end -- Remove duplicates @@ -380,12 +466,22 @@ function _extract_package_info(store_paths, package_name, opt) result.links = remove_duplicates(result.links) result.libfiles = remove_duplicates(result.libfiles) - -- Return result if we found anything useful + -- 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, " .. + #result.links .. " links") + print("Nix: DEBUG: Main package paths: " .. #main_package_paths .. ", Dependency paths: " .. #dependency_paths) + end return result + else + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: DEBUG: Package info extraction found no useful information for '" .. package_name .. "'") + end + return nil end - - return nil end -- priority 1: nix shell (flake or legacy) @@ -404,10 +500,14 @@ function _find_in_nix_shell(package_name, opt) 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 - print("Found " .. package_name .. " in nix-shell environment with " .. #store_paths .. " paths") + print("Nix: Found " .. package_name .. " in nix-shell environment") end return result end @@ -628,11 +728,10 @@ 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 {} -- cgit v1.3.1 From 998b9990ee6141106cebca48f919ed82ba591550 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Mon, 22 Sep 2025 12:50:03 -0400 Subject: nix: simplifying package detection --- xmake/modules/package/manager/nix/find_package.lua | 244 +++------------------ 1 file 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 -- cgit v1.3.1 From 22babced10601540af85375b1c31e72b5d881c73 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Mon, 22 Sep 2025 14:48:32 -0400 Subject: nix: fixed pkgconfig finding/dependencies --- xmake/modules/package/manager/nix/find_package.lua | 190 +++++---------------- 1 file changed, 39 insertions(+), 151 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 2fec59de9..d17dcca7e 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -118,62 +118,28 @@ function _path_matches_package(store_path, package_name, opt) end function _find_with_pkgconfig(package_name, store_paths, opt) - 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 - 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) - table.insert(pkgconfig_env_additions, pkgconfig_dir) - end - end - end - if #all_pkgconfig_dirs == 0 then - return nil - end - 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 - os.setenv("PKG_CONFIG_PATH", new_pkg_config_path) - local result = find_package_from_pkgconfig(package_name) - if result then - return result - 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 + + for _, pcdir in ipairs(pkgconfig_dirs) do + if os.isdir(pcdir) then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Attempting pkg-config lookup: " .. package_name .. " (configdirs=" .. pcdir .. ")") + end + local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found package via pkg-config: " .. package_name) end + return result end end end - if result then break end end - return result + return nil end function _extract_package_info(store_paths, package_name, opt) @@ -184,110 +150,37 @@ function _extract_package_info(store_paths, package_name, opt) links = {}, libfiles = {} } + + -- Try pkg-config first local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt) if pkgconfig_result then return pkgconfig_result end + 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) - found_main_package = true - else - table.insert(dependency_paths, store_path) end end - if not found_main_package then - return nil - end - for _, store_path in ipairs(main_package_paths) do + + -- Collect includedirs and linkdirs from all store paths + for _, store_path in ipairs(store_paths) do 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 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) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found main package include subdir: " .. subdir) - end end end end local bindir = path.join(store_path, "bin") if os.isdir(bindir) then table.insert(result.bindirs, bindir) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found main package bin dir: " .. bindir) - end - end - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - 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(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 - filename:match("^lib(.+)%.dylib") - if linkname then - table.insert(result.links, linkname) - table.insert(result.libfiles, libfile) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found main package library: " .. linkname .. " -> " .. libfile) - end - end - end - else - 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 - print("Nix: Found main package lib dir: " .. libdir .. " (cmake/pkgconfig only)") - end - end - end - end - end - for _, store_path in ipairs(dependency_paths) do - 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 - 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) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found dependency include subdir: " .. subdir) - end - end - end end local libdir = path.join(store_path, "lib") if os.isdir(libdir) then @@ -303,19 +196,16 @@ function _extract_package_info(store_paths, package_name, opt) 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 - filename:match("^lib(.+)%.dylib") - if linkname then - table.insert(result.links, linkname) - table.insert(result.libfiles, libfile) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found dependency library: " .. linkname .. " -> " .. libfile) + -- Only add links/libfiles for main package paths + if _path_matches_package(store_path, package_name, opt) then + for _, libfile in ipairs(libfiles) do + local filename = path.filename(libfile) + 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) end end end @@ -324,13 +214,11 @@ function _extract_package_info(store_paths, package_name, opt) 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 - print("Nix: Found dependency lib dir: " .. libdir .. " (cmake/pkgconfig only)") - end end end end end + local function remove_duplicates(arr) local seen = {} local clean = {} @@ -348,18 +236,8 @@ function _extract_package_info(store_paths, package_name, opt) result.links = remove_duplicates(result.links) result.libfiles = remove_duplicates(result.libfiles) 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, " .. - #result.links .. " links") - print("Nix: DEBUG: Main package paths: " .. #main_package_paths .. ", Dependency paths: " .. #dependency_paths) - end return result else - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: DEBUG: Package info extraction found no useful information for '" .. package_name .. "'") - end return nil end end @@ -390,6 +268,12 @@ function _find_in_nix_shell(package_name, opt) return nil end +-- Find package from current user's nix profile, includes nix-env installed packages +-- Note: nix-env only lists one output in the profile list +-- $ nix-env -iA nixpkgs. # installs multiple outputs, but only one is listed in the profile +-- this can cause issues if the main output does not contain the necessary files +-- Example: zlib.dev contains the headers, but zlib only contains the library +-- there does not seem to be an straight-forward way to find all outputs... function _find_in_nix_profile(package_name, opt) local nix = find_tool("nix") if not nix then @@ -420,6 +304,7 @@ function _find_in_nix_profile(package_name, opt) return nil end +-- Popular nix-community tool to declaratively manage user environments (NixOS and non-NixOS) function _find_in_home_manager_tool(package_name, opt) local home_manager = find_tool("home-manager") if not home_manager then @@ -450,6 +335,7 @@ function _find_in_home_manager_tool(package_name, opt) return nil end +-- Home manager can be installed as a module in nixos, in which case the home-manager tool is missing. function _find_in_home_manager_profile(package_name, opt) local nix_store = find_tool("nix-store") if not nix_store then @@ -483,6 +369,7 @@ function _find_in_home_manager_profile(package_name, opt) return nil end +-- nixos-option is not always configured properly, but if it is, we can find user/system packages function _find_in_nixos_user_packages(package_name, opt) local nixos_option = find_tool("nixos-option") if not nixos_option then @@ -538,6 +425,7 @@ function _find_in_nixos_system_packages(package_name, opt) return nil end +-- Includes all system/user/home-manager packages function _find_in_nixos_current_system(package_name, opt) local nix_store = find_tool("nix-store") if not nix_store then -- cgit v1.3.1 From e6b9720e26937265820d03a6aac1acc30d029d4f Mon Sep 17 00:00:00 2001 From: zzbaron Date: Mon, 22 Sep 2025 14:59:05 -0400 Subject: nix: fix false positives --- xmake/modules/package/manager/nix/find_package.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index d17dcca7e..40868bf29 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -235,7 +235,7 @@ function _extract_package_info(store_paths, package_name, opt) result.linkdirs = remove_duplicates(result.linkdirs) result.links = remove_duplicates(result.links) result.libfiles = remove_duplicates(result.libfiles) - if (#result.includedirs > 0) or (#result.bindirs > 0) or (#result.links > 0) or (#result.linkdirs > 0) then + if #main_package_paths > 0 and ((#result.links > 0) or (#result.libfiles > 0)) then return result else return nil -- cgit v1.3.1 From b1ceca439f15b04689e553a5165437511d66fe85 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Mon, 22 Sep 2025 15:47:21 -0400 Subject: nix: various changes --- xmake/modules/package/manager/nix/find_package.lua | 103 +++++++++++++++++---- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 40868bf29..de56e8f76 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -117,6 +117,7 @@ function _path_matches_package(store_path, package_name, opt) return false end +-- find package with pkg-config in a specific directory function _find_with_pkgconfig(package_name, store_paths, opt) for _, store_path in ipairs(store_paths) do local pkgconfig_dirs = { @@ -150,22 +151,31 @@ function _extract_package_info(store_paths, package_name, opt) links = {}, libfiles = {} } - + -- Try pkg-config first local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt) if pkgconfig_result then return pkgconfig_result end - + local main_package_paths = {} + local propagated_paths = {} + 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) + else + -- Add non-matching paths as propagated dependencies + table.insert(propagated_paths, store_path) end end - - -- Collect includedirs and linkdirs from all store paths - for _, store_path in ipairs(store_paths) do + + if #main_package_paths == 0 then + return nil + end + + -- Process main package paths + for _, store_path in ipairs(main_package_paths) do local includedir = path.join(store_path, "include") if os.isdir(includedir) then table.insert(result.includedirs, includedir) @@ -196,17 +206,14 @@ function _extract_package_info(store_paths, package_name, opt) end} or {} if #libfiles > 0 then table.insert(result.linkdirs, libdir) - -- Only add links/libfiles for main package paths - if _path_matches_package(store_path, package_name, opt) then - for _, libfile in ipairs(libfiles) do - local filename = path.filename(libfile) - 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) - end + for _, libfile in ipairs(libfiles) do + local filename = path.filename(libfile) + 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) end end else @@ -218,7 +225,63 @@ function _extract_package_info(store_paths, package_name, opt) end end end - + + -- Process all propagated paths for include directories, bin directories, and libraries + for _, store_path in ipairs(propagated_paths) do + local includedir = path.join(store_path, "include") + if os.isdir(includedir) then + table.insert(result.includedirs, includedir) + -- Also add subdirectories of include for propagated deps + 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) + end + end + end + + local bindir = path.join(store_path, "bin") + if os.isdir(bindir) then + table.insert(result.bindirs, bindir) + end + + local libdir = path.join(store_path, "lib") + if os.isdir(libdir) then + 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(dylib_files) do table.insert(files, f) end + return files + end} or {} + if #libfiles > 0 then + table.insert(result.linkdirs, libdir) + for _, libfile in ipairs(libfiles) do + local filename = path.filename(libfile) + 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) + end + end + else + -- Also check for cmake/pkgconfig dirs in propagated deps + 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) + end + end + end + end + local function remove_duplicates(arr) local seen = {} local clean = {} @@ -235,7 +298,7 @@ function _extract_package_info(store_paths, package_name, opt) result.linkdirs = remove_duplicates(result.linkdirs) result.links = remove_duplicates(result.links) result.libfiles = remove_duplicates(result.libfiles) - if #main_package_paths > 0 and ((#result.links > 0) or (#result.libfiles > 0)) then + if (#result.includedirs > 0) or (#result.bindirs > 0) or (#result.links > 0) or (#result.linkdirs > 0) then return result else return nil @@ -280,7 +343,7 @@ function _find_in_nix_profile(package_name, opt) return nil end local profile_list = try {function() - return os.iorunv(nix.program, {"profile", "list", "--extra-experimental-features 'nix-command flakes'"}):trim() + return os.iorunv(nix.program, {"profile", "list", "--extra-experimental-features", "nix-command flakes"}):trim() end} if profile_list then local store_paths = {} @@ -425,7 +488,7 @@ function _find_in_nixos_system_packages(package_name, opt) return nil end --- Includes all system/user/home-manager packages +-- Includes all system/current user/home-manager packages function _find_in_nixos_current_system(package_name, opt) local nix_store = find_tool("nix-store") if not nix_store then -- cgit v1.3.1 From 543b3a907ccecf24abc89b2d46dcbf0291d4f26a Mon Sep 17 00:00:00 2001 From: zzbaron Date: Tue, 23 Sep 2025 15:55:59 -0400 Subject: nix: added store path cache --- xmake/modules/package/manager/nix/find_package.lua | 68 +++++++++++++++++----- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index de56e8f76..b3ff9b331 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -24,6 +24,11 @@ import("lib.detect.find_tool") import("private.core.base.is_cross") import("package.manager.pkgconfig.find_package", {alias = "find_package_from_pkgconfig"}) +-- static caches +local _store_paths_cache = {} +local _propagated_inputs_cache = {} +local _pkgconfig_cache = {} + -- recursively follow propagated build inputs function _follow_propagated_inputs(store_paths, opt, visited) visited = visited or {} @@ -42,22 +47,40 @@ function _follow_propagated_inputs(store_paths, opt, visited) local store_path = all_paths[i] if not visited[store_path] then visited[store_path] = true - local prop_file = path.join(store_path, "nix-support", "propagated-build-inputs") - if os.isfile(prop_file) then - local content = try {function() - return io.readfile(prop_file):trim() - end} - if content and content ~= "" then - for prop_path in content:gmatch("%S+") do - if prop_path:startswith("/nix/store/") and not seen[prop_path] then - seen[prop_path] = true - table.insert(all_paths, prop_path) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added propagated: " .. prop_path) + -- cache propagated inputs per store_path + if _propagated_inputs_cache[store_path] then + for _, prop_path in ipairs(_propagated_inputs_cache[store_path]) do + if prop_path:startswith("/nix/store/") and not seen[prop_path] then + seen[prop_path] = true + table.insert(all_paths, prop_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added propagated (cached): " .. prop_path) + end + end + end + else + local prop_file = path.join(store_path, "nix-support", "propagated-build-inputs") + local prop_paths = {} + if os.isfile(prop_file) then + local content = try {function() + return io.readfile(prop_file):trim() + end} + if content and content ~= "" then + for prop_path in content:gmatch("%S+") do + if prop_path:startswith("/nix/store/") and not seen[prop_path] then + seen[prop_path] = true + table.insert(all_paths, prop_path) + table.insert(prop_paths, prop_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added propagated: " .. prop_path) + end + else + table.insert(prop_paths, prop_path) end end end end + _propagated_inputs_cache[store_path] = prop_paths end end i = i + 1 @@ -67,6 +90,14 @@ end -- parse store paths from environment variables function _parse_store_paths_from_env(env_vars, opt) + local cache_key = table.concat(env_vars, "|") + if _store_paths_cache[cache_key] then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using cached store paths for env_vars: " .. cache_key) + end + return _store_paths_cache[cache_key] + end + local paths = {} local seen = {} @@ -93,6 +124,7 @@ function _parse_store_paths_from_env(env_vars, opt) end paths = _follow_propagated_inputs(paths, opt) + _store_paths_cache[cache_key] = paths return paths end @@ -119,12 +151,20 @@ end -- find package with pkg-config in a specific directory function _find_with_pkgconfig(package_name, store_paths, opt) + local cache_key = package_name .. ":" .. table.concat(store_paths, ";") + if _pkgconfig_cache[cache_key] then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using cached pkg-config result for: " .. cache_key) + end + return _pkgconfig_cache[cache_key] + end + for _, store_path in ipairs(store_paths) do local pkgconfig_dirs = { path.join(store_path, "lib", "pkgconfig"), path.join(store_path, "share", "pkgconfig") } - + for _, pcdir in ipairs(pkgconfig_dirs) do if os.isdir(pcdir) then if opt and (opt.verbose or option.get("verbose")) then @@ -135,11 +175,13 @@ function _find_with_pkgconfig(package_name, store_paths, opt) if opt and (opt.verbose or option.get("verbose")) then print("Nix: Found package via pkg-config: " .. package_name) end + _pkgconfig_cache[cache_key] = result return result end end end end + _pkgconfig_cache[cache_key] = nil return nil end -- cgit v1.3.1 From 70cb5582cf6a4cfef1616b9e336dac55dd0b70dc Mon Sep 17 00:00:00 2001 From: zzbaron Date: Wed, 24 Sep 2025 23:37:51 -0400 Subject: nix: cache fixes, rework store path extraction, etc. --- xmake/modules/package/manager/nix/find_package.lua | 1289 +++++++++++++------- 1 file changed, 870 insertions(+), 419 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index b3ff9b331..0508d6239 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -23,88 +23,453 @@ import("core.base.option") import("lib.detect.find_tool") import("private.core.base.is_cross") import("package.manager.pkgconfig.find_package", {alias = "find_package_from_pkgconfig"}) +import("core.cache.globalcache") +import("core.cache.memcache") +import("core.base.json") --- static caches -local _store_paths_cache = {} -local _propagated_inputs_cache = {} -local _pkgconfig_cache = {} +-- cache keys +local STORE_PATHS_CACHE = "nix_store_paths" +local PACKAGE_INFO_CACHE = "nix_package_info" +local PROPAGATED_CACHE = "nix_propagated" +local PKGCONFIG_CACHE = "nix_pkgconfig" +local DERIVATION_CACHE = "nix_derivation" --- recursively follow propagated build inputs -function _follow_propagated_inputs(store_paths, opt, visited) - visited = visited or {} - local all_paths = {} +-- get nix cache instance +local function get_nix_cache() + return globalcache.cache("nix_packages") +end + +-- get memory cache for current session +local function get_memory_cache() + return memcache.cache("nix_session") +end + +-- check if we're in a nix shell +local function is_in_nix_shell() + local in_nix_shell = os.getenv("IN_NIX_SHELL") + return in_nix_shell == "pure" or in_nix_shell == "impure" +end + +-- generate cache key for environment state +local function generate_env_cache_key() + local env_vars = { + "buildInputs", + "nativeBuildInputs", + "propagatedBuildInputs", + "propagatedNativeBuildInputs" + } + + local env_data = {} + for _, var in ipairs(env_vars) do + env_data[var] = os.getenv(var) or "" + end + + -- Include nix shell state and user + env_data.in_nix_shell = tostring(is_in_nix_shell()) + env_data.user = os.getenv("USER") or "unknown" + + -- Create a hash-like key from the environment + local key_parts = {} + for k, v in pairs(env_data) do + table.insert(key_parts, k .. "=" .. v) + end + table.sort(key_parts) + return table.concat(key_parts, "|") +end + + + +-- get derivation info for a store path with caching +local function get_derivation_info(store_path, opt) + local cache = get_nix_cache() + local memory_cache = get_memory_cache() + + -- Check memory cache first + local cached = memory_cache:get2(DERIVATION_CACHE, store_path) + if cached ~= nil then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using session cached derivation for: " .. store_path) + end + return cached + end + + -- Check persistent cache + cached = cache:get2(DERIVATION_CACHE, store_path) + if cached ~= nil then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using persistent cached derivation for: " .. store_path) + end + memory_cache:set2(DERIVATION_CACHE, store_path, cached) + return cached + end + + -- Get derivation path + local nix_store = find_tool("nix-store") + if not nix_store then + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + local drv_path = try {function() + return os.iorunv(nix_store.program, {"-q", store_path, "--deriver"}):trim() + end} + + if not drv_path or drv_path == "" then + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + -- Get derivation info using nix derivation show + local nix = find_tool("nix") + if not nix then + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + local drv_json = try {function() + return os.iorunv(nix.program, { + "derivation", "show", + "--extra-experimental-features", "nix-command flakes", + drv_path + }):trim() + end} + + if not drv_json or drv_json == "" then + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + -- Parse the JSON using xmake's JSON parser + local drv_data, parse_error = json.decode(drv_json) + if not drv_data or parse_error then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Failed to parse derivation JSON for " .. store_path .. ": " .. (parse_error or "unknown error")) + end + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + -- Extract the first (and usually only) derivation + local drv_info = nil + for _, info in pairs(drv_data) do + drv_info = info + break + end + + if not drv_info or type(drv_info) ~= "table" or not drv_info.env then + local empty = {} + cache:set2(DERIVATION_CACHE, store_path, empty) + memory_cache:set2(DERIVATION_CACHE, store_path, empty) + return empty + end + + -- Extract relevant information + local result = { + name = drv_info.env.pname or drv_info.env.name or "", + version = drv_info.env.version or "", + outputs = drv_info.outputs or {}, + env = drv_info.env or {} + } + + -- Cache the result + cache:set2(DERIVATION_CACHE, store_path, result) + memory_cache:set2(DERIVATION_CACHE, store_path, result) + cache:save() + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Cached derivation info for: " .. store_path .. " (name=" .. result.name .. ", version=" .. result.version .. ")") + end + + return result +end + +-- parse store basename to extract name, version, and output (fallback method) +local function parse_store_basename(path_name) + -- parse "--" or "---" + -- remove leading hash (up to first '-') + local first_dash = path_name:find("-", 1, true) + if not first_dash then + return nil, nil, nil + end + local rest = path_name:sub(first_dash + 1) + + -- find last dash and second-last dash in rest + local last_dash = nil + for i = #rest, 1, -1 do + if rest:sub(i, i) == "-" then + last_dash = i + break + end + end + if not last_dash then + return rest, nil, nil -- Just name, no version + end + + -- try to find second-last dash + local second_last = nil + for i = last_dash - 1, 1, -1 do + if rest:sub(i, i) == "-" then + second_last = i + break + end + end + + if second_last then + -- form: name (may have dashes) = rest[1..second_last-1], version = rest[second_last+1..last_dash-1], output = rest[last_dash+1..] + local name = rest:sub(1, second_last - 1) + local version = rest:sub(second_last + 1, last_dash - 1) + local output = rest:sub(last_dash + 1) + return name, version, output + else + -- form: name = rest[1..last_dash-1], version = rest[last_dash+1..] + local name = rest:sub(1, last_dash - 1) + local version = rest:sub(last_dash + 1) + return name, version, nil + end +end + +-- remove duplicates from array +local function remove_duplicates(arr) local seen = {} + local clean = {} + for _, item in ipairs(arr) do + if not seen[item] then + seen[item] = true + table.insert(clean, item) + end + end + return clean +end + +-- PackageInfo class +local PackageInfo = {} +PackageInfo.__index = PackageInfo + +function PackageInfo:new(package_name) + local o = { + name = package_name, + includedirs = {}, + bindirs = {}, + linkdirs = {}, + links = {}, + libfiles = {}, + store_paths = {}, + version = nil, + pkgconfig_available = false, + outputs = {} + } + + table.inherit2(o, self) + return o +end + +function PackageInfo:add_store_path(p) + table.insert(self.store_paths, p) +end + +function PackageInfo:add_includedir(d) + table.insert(self.includedirs, d) +end + +function PackageInfo:add_bindir(d) + table.insert(self.bindirs, d) +end + +function PackageInfo:add_linkdir(d) + table.insert(self.linkdirs, d) +end + +function PackageInfo:add_link(l) + table.insert(self.links, l) +end + +function PackageInfo:add_libfile(f) + table.insert(self.libfiles, f) +end + +function PackageInfo:set_version(v) + if not self.version and v and v ~= "" then + self.version = v + end +end + +function PackageInfo:set_pname(p) + if not self.name and p and p ~= "" then + self.name = p + end +end + +function PackageInfo:set_outputs(o) + if o and type(o) == "table" then + self.outputs = o + end +end + +function PackageInfo:set_pkgconfig_available() + self.pkgconfig_available = true +end +function PackageInfo:finalize() + -- remove duplicates + self.includedirs = remove_duplicates(self.includedirs) + self.bindirs = remove_duplicates(self.bindirs) + self.linkdirs = remove_duplicates(self.linkdirs) + self.links = remove_duplicates(self.links) + self.libfiles = remove_duplicates(self.libfiles) + self.store_paths = remove_duplicates(self.store_paths) + + -- return plain table (so cache stores normal table) + return { + name = self.name, + includedirs = self.includedirs, + bindirs = self.bindirs, + linkdirs = self.linkdirs, + links = self.links, + libfiles = self.libfiles, + store_paths = self.store_paths, + version = self.version, + outputs = self.outputs, + pkgconfig_available = self.pkgconfig_available + } +end + +-- follow propagated build inputs recursively with caching +local function follow_propagated_inputs(store_paths, opt) + local cache = get_nix_cache() + local all_paths = {} + local seen = {} + local visited = {} + + -- 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 - + local i = 1 while i <= #all_paths do local store_path = all_paths[i] if not visited[store_path] then visited[store_path] = true - -- cache propagated inputs per store_path - if _propagated_inputs_cache[store_path] then - for _, prop_path in ipairs(_propagated_inputs_cache[store_path]) do - if prop_path:startswith("/nix/store/") and not seen[prop_path] then - seen[prop_path] = true - table.insert(all_paths, prop_path) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added propagated (cached): " .. prop_path) - end - end + + -- Check cache first + local cached_props = cache:get2(PROPAGATED_CACHE, store_path) + local prop_paths + + if cached_props then + prop_paths = cached_props + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using cached propagated inputs for: " .. store_path) end else + -- Read from filesystem + prop_paths = {} local prop_file = path.join(store_path, "nix-support", "propagated-build-inputs") - local prop_paths = {} if os.isfile(prop_file) then local content = try {function() return io.readfile(prop_file):trim() end} if content and content ~= "" then for prop_path in content:gmatch("%S+") do - if prop_path:startswith("/nix/store/") and not seen[prop_path] then - seen[prop_path] = true - table.insert(all_paths, prop_path) - table.insert(prop_paths, prop_path) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added propagated: " .. prop_path) - end - else + if prop_path:startswith("/nix/store/") then table.insert(prop_paths, prop_path) end end end end - _propagated_inputs_cache[store_path] = prop_paths + + -- Cache the result + cache:set2(PROPAGATED_CACHE, store_path, prop_paths) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Cached propagated inputs for: " .. store_path) + end + end + + -- Add new paths + for _, prop_path in ipairs(prop_paths) do + if not seen[prop_path] then + seen[prop_path] = true + table.insert(all_paths, prop_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added propagated: " .. prop_path) + end + end end end i = i + 1 end + return all_paths end --- parse store paths from environment variables -function _parse_store_paths_from_env(env_vars, opt) - local cache_key = table.concat(env_vars, "|") - if _store_paths_cache[cache_key] then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Using cached store paths for env_vars: " .. cache_key) +-- get store paths from nix command output +local function get_store_paths_from_command(command, args, opt) + local output = try {function() + return os.iorunv(command, args):trim() + end} + + if not output then + return {} + end + + local store_paths = {} + for line in output:gmatch("[^\n]+") do + local store_path = line:match("(/nix/store/[^%s]+)") + if store_path then + table.insert(store_paths, store_path) end - return _store_paths_cache[cache_key] end + + return follow_propagated_inputs(store_paths, opt) +end +-- parse store paths from environment variables with caching +local function parse_store_paths_from_env(env_vars, opt) + local cache_key = generate_env_cache_key() + local memory_cache = get_memory_cache() + + -- Check memory cache first (session cache) + local cached_paths = memory_cache:get2(STORE_PATHS_CACHE, cache_key) + if cached_paths then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using session cached store paths") + end + return cached_paths + end + + -- Check persistent cache + local cache = get_nix_cache() + cached_paths = cache:get2(STORE_PATHS_CACHE, cache_key) + if cached_paths then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using persistent cached store paths") + end + -- Also cache in memory for faster access + memory_cache:set2(STORE_PATHS_CACHE, cache_key, cached_paths) + memory_cache:set("last_env_key", cache_key) + return cached_paths + end + + -- Parse from environment 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 @@ -122,474 +487,560 @@ function _parse_store_paths_from_env(env_vars, opt) end end end - - paths = _follow_propagated_inputs(paths, opt) - _store_paths_cache[cache_key] = paths + + -- Follow propagated inputs + paths = follow_propagated_inputs(paths, opt) + + -- Cache the result + cache:set2(STORE_PATHS_CACHE, cache_key, paths) + memory_cache:set2(STORE_PATHS_CACHE, cache_key, paths) + memory_cache:set("last_env_key", cache_key) + cache:save() + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Cached " .. #paths .. " store paths") + end + return paths end -function _in_nix_shell() - local in_nix_shell = os.getenv("IN_NIX_SHELL") - return in_nix_shell == "pure" or in_nix_shell == "impure" +-- STORE PATH EXTRACTION FUNCTIONS (same as before) + +-- extract store paths from nix shell +local function get_store_paths_nix_shell(opt) + if not is_in_nix_shell() then + return {} + end + + local build_env_vars = { + "buildInputs", + "nativeBuildInputs", + "propagatedBuildInputs", + "propagatedNativeBuildInputs" + } + + return parse_store_paths_from_env(build_env_vars, opt) end -function _path_matches_package(store_path, package_name, opt) - local path_name = path.basename(store_path) - local package_name_lower = package_name:lower() - local package_base = path_name:match("^[^%-]+-([^%-]+)") - if package_base then - local package_base_lower = package_base:lower() - if package_base_lower == package_name_lower then - return true - end - if package_base_lower:find(package_name_lower, 1, true) or package_name_lower:find(package_base_lower, 1, true) then - return true - end +-- extract store paths from nix profile +local function get_store_paths_nix_profile(opt) + local nix = find_tool("nix") + if not nix then + return {} end - return false + + return get_store_paths_from_command( + nix.program, + {"profile", "list", "--extra-experimental-features", "nix-command flakes"}, + opt + ) end --- find package with pkg-config in a specific directory -function _find_with_pkgconfig(package_name, store_paths, opt) - local cache_key = package_name .. ":" .. table.concat(store_paths, ";") - if _pkgconfig_cache[cache_key] then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Using cached pkg-config result for: " .. cache_key) - end - return _pkgconfig_cache[cache_key] +-- extract store paths from home-manager (tool version) +local function get_store_paths_home_manager_tool(opt) + local home_manager = find_tool("home-manager") + if not home_manager then + return {} end + + return get_store_paths_from_command( + home_manager.program, + {"packages"}, + opt + ) +end - for _, store_path in ipairs(store_paths) do - local pkgconfig_dirs = { - path.join(store_path, "lib", "pkgconfig"), - path.join(store_path, "share", "pkgconfig") - } +-- extract store paths from home-manager (profile version) +local function get_store_paths_home_manager_profile(opt) + local nix_store = find_tool("nix-store") + if not nix_store then + return {} + end + + local user = os.getenv("USER") or "unknown" + local user_profile = "/etc/profiles/per-user/" .. user + if not os.isdir(user_profile) then + return {} + end + + return get_store_paths_from_command( + nix_store.program, + {"--query", "--requisites", user_profile}, + opt + ) +end - for _, pcdir in ipairs(pkgconfig_dirs) do - if os.isdir(pcdir) then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Attempting pkg-config lookup: " .. package_name .. " (configdirs=" .. pcdir .. ")") - end - local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found package via pkg-config: " .. package_name) - end - _pkgconfig_cache[cache_key] = result - return result - end - end +-- extract store paths from nixos user packages +local function get_store_paths_nixos_user_packages(opt) + local nixos_option = find_tool("nixos-option") + if not nixos_option then + return {} + end + + local user = os.getenv("USER") or "unknown" + local output = try {function() + return os.iorunv(nixos_option.program, {"users.users." .. user .. ".packages"}):trim() + end} + + if output then + local store_paths = {} + for store_path in output:gmatch('(/nix/store/[^"\'%s]+)') do + table.insert(store_paths, store_path) end + + return follow_propagated_inputs(store_paths, opt) end - _pkgconfig_cache[cache_key] = nil - return nil + return {} end -function _extract_package_info(store_paths, package_name, opt) - local result = { - includedirs = {}, - bindirs = {}, - linkdirs = {}, - links = {}, - libfiles = {} - } - - -- Try pkg-config first - local pkgconfig_result = _find_with_pkgconfig(package_name, store_paths, opt) - if pkgconfig_result then - return pkgconfig_result +-- extract store paths from nixos system packages +local function get_store_paths_nixos_system_packages(opt) + local nixos_option = find_tool("nixos-option") + if not nixos_option then + return {} end - local main_package_paths = {} - local propagated_paths = {} + local output = try {function() + return os.iorunv(nixos_option.program, {"environment.systemPackages"}):trim() + end} - 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) - else - -- Add non-matching paths as propagated dependencies - table.insert(propagated_paths, store_path) + if output then + local store_paths = {} + for store_path in output:gmatch('(/nix/store/[^"\'%s]+)') do + table.insert(store_paths, store_path) end + + return follow_propagated_inputs(store_paths, opt) + end + return {} +end + +-- extract store paths from nixos current system +local function get_store_paths_nixos_current_system(opt) + local nix_store = find_tool("nix-store") + if not nix_store then + return {} end - if #main_package_paths == 0 then - return nil + if not os.isdir("/run/current-system") then + return {} end - -- Process main package paths - for _, store_path in ipairs(main_package_paths) do - local includedir = path.join(store_path, "include") - if os.isdir(includedir) then - table.insert(result.includedirs, 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) - end - end - end - local bindir = path.join(store_path, "bin") - if os.isdir(bindir) then - table.insert(result.bindirs, bindir) - end - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - 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(dylib_files) do table.insert(files, f) end - return files - end} or {} - if #libfiles > 0 then - table.insert(result.linkdirs, libdir) - for _, libfile in ipairs(libfiles) do - local filename = path.filename(libfile) - 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) - end - end - else - 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) - end - end + return get_store_paths_from_command( + nix_store.program, + {"--query", "--requisites", "/run/current-system"}, + opt + ) +end + +-- get all store paths from all nix environments with caching +local function get_all_store_paths(opt) + local cache = get_nix_cache() + local memory_cache = get_memory_cache() + local cache_key = "all_environments:" .. generate_env_cache_key() + + -- Check memory cache first + local cached_paths = memory_cache:get2(STORE_PATHS_CACHE, cache_key) + if cached_paths then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using session cached all store paths") end + return cached_paths end - -- Process all propagated paths for include directories, bin directories, and libraries - for _, store_path in ipairs(propagated_paths) do - local includedir = path.join(store_path, "include") - if os.isdir(includedir) then - table.insert(result.includedirs, includedir) - -- Also add subdirectories of include for propagated deps - 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) - end - end - end - - local bindir = path.join(store_path, "bin") - if os.isdir(bindir) then - table.insert(result.bindirs, bindir) - end - - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - 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(dylib_files) do table.insert(files, f) end - return files - end} or {} - if #libfiles > 0 then - table.insert(result.linkdirs, libdir) - for _, libfile in ipairs(libfiles) do - local filename = path.filename(libfile) - 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) - end - end - else - -- Also check for cmake/pkgconfig dirs in propagated deps - 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) - end - end + -- Check persistent cache + cached_paths = cache:get2(STORE_PATHS_CACHE, cache_key) + if cached_paths then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using persistent cached all store paths") end + memory_cache:set2(STORE_PATHS_CACHE, cache_key, cached_paths) + return cached_paths end - local function remove_duplicates(arr) - local seen = {} - local clean = {} - for _, item in ipairs(arr) do - if not seen[item] then - seen[item] = true - table.insert(clean, item) + -- Extract from all sources + local all_paths = {} + local seen = {} + + local get_store_path_functions = { + get_store_paths_nix_shell, + get_store_paths_nix_profile, + get_store_paths_home_manager_tool, + get_store_paths_home_manager_profile, + get_store_paths_nixos_user_packages, + get_store_paths_nixos_system_packages, + get_store_paths_nixos_current_system + } + + for _, func in ipairs(get_store_path_functions) do + local paths = func(opt) + for _, store_path in ipairs(paths) do + if not seen[store_path] then + seen[store_path] = true + table.insert(all_paths, store_path) end 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) - if (#result.includedirs > 0) or (#result.bindirs > 0) or (#result.links > 0) or (#result.linkdirs > 0) then - return result - else - return nil + + -- Cache the result + cache:set2(STORE_PATHS_CACHE, cache_key, all_paths) + memory_cache:set2(STORE_PATHS_CACHE, cache_key, all_paths) + cache:save() + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found " .. #all_paths .. " total store paths across all environments") end + + return all_paths end -function _find_in_nix_shell(package_name, opt) - if not _in_nix_shell() then - return nil - end - local build_env_vars = { - "buildInputs", - "nativeBuildInputs", - "propagatedBuildInputs", - "propagatedNativeBuildInputs" - } - local store_paths = _parse_store_paths_from_env(build_env_vars, opt) - if #store_paths > 0 then +-- extract package information from store paths with caching and derivation info +local function extract_package_info(store_paths, opt) + opt = opt or {} + local cache = get_nix_cache() + local memory_cache = get_memory_cache() + local paths_key = table.concat(store_paths or {}, ";") + local cache_key = "package_info:" .. paths_key + + -- Check memory cache first + local cached = memory_cache:get2(PACKAGE_INFO_CACHE, cache_key) + if cached ~= nil then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found " .. #store_paths .. " total store paths in nix-shell") + print("Nix: Using session cached package info") end - local result = _extract_package_info(store_paths, package_name, opt) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found " .. package_name .. " in nix-shell environment") - end - return result + return cached + end + + -- Check persistent cache + cached = cache:get2(PACKAGE_INFO_CACHE, cache_key) + if cached ~= nil then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using persistent cached package info") end + memory_cache:set2(PACKAGE_INFO_CACHE, cache_key, cached) + return cached end - return nil -end --- Find package from current user's nix profile, includes nix-env installed packages --- Note: nix-env only lists one output in the profile list --- $ nix-env -iA nixpkgs. # installs multiple outputs, but only one is listed in the profile --- this can cause issues if the main output does not contain the necessary files --- Example: zlib.dev contains the headers, but zlib only contains the library --- there does not seem to be an straight-forward way to find all outputs... -function _find_in_nix_profile(package_name, opt) - local nix = find_tool("nix") - if not nix then - return nil + if not store_paths or #store_paths == 0 then + local empty = {} + cache:set2(PACKAGE_INFO_CACHE, cache_key, empty) + memory_cache:set2(PACKAGE_INFO_CACHE, cache_key, empty) + return empty 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 - local store_path = line:match("(/nix/store/[^%s]+)") - if store_path then - table.insert(store_paths, store_path) + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Extracting package info for " .. #store_paths .. " store paths") + end + + local packages = {} -- map: package_name -> PackageInfo + + for _, store_path in ipairs(store_paths) do + if not store_path or store_path == "" then goto continue end + + -- Get derivation info + local drv_info = get_derivation_info(store_path, opt) + if not drv_info or not drv_info.name or drv_info.name == "" then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Skipping " .. store_path .. " - no derivation info or name available") end + goto continue end - if #store_paths > 0 then - store_paths = _follow_propagated_inputs(store_paths, opt) - local result = _extract_package_info(store_paths, package_name, opt) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in nix profile with " .. #store_paths .. " paths") + + local pkgname = drv_info.name:lower() + + -- Only create one package entry per name (first one wins due to prioritized ordering) + if not packages[pkgname] then + local pkg = PackageInfo:new(pkgname) + packages[pkgname] = pkg + + pkg:add_store_path(store_path) + pkg:set_version(drv_info.version) + pkg:set_pname(drv_info.name) + pkg:set_outputs(drv_info.outputs) + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added package: " .. drv_info.name .. " " .. (drv_info.version or "")) + end + + -- include directories + local includedir = path.join(store_path, "include") + if os.isdir(includedir) then + pkg:add_includedir(includedir) + local subdirs = try { function() return os.dirs(path.join(includedir, "*")) end } or {} + for _, subdir in ipairs(subdirs) do + if os.isdir(subdir) then + pkg:add_includedir(subdir) + end end - return result end - end - end - return nil -end --- Popular nix-community tool to declaratively manage user environments (NixOS and non-NixOS) -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 - local store_path = line:match("(/nix/store/[^%s]+)") - if store_path then - table.insert(store_paths, store_path) + -- bin + local bindir = path.join(store_path, "bin") + if os.isdir(bindir) then + pkg:add_bindir(bindir) 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) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in home-manager with " .. #store_paths .. " paths") + + -- lib + local libdir = path.join(store_path, "lib") + if os.isdir(libdir) then + local libfiles = try { function() + local files = {} + local patterns = {"*.so*", "*.a", "*.dylib*"} + for _, pattern in ipairs(patterns) do + for _, f in ipairs(os.files(path.join(libdir, pattern)) or {}) do + table.insert(files, f) + end + end + return files + end } or {} + + if #libfiles > 0 then + pkg:add_linkdir(libdir) + for _, libfile in ipairs(libfiles) do + local filename = path.filename(libfile) + local linkname = filename:match("^lib(.+)%.so") or + filename:match("^lib(.+)%.a") or + filename:match("^lib(.+)%.dylib") + if linkname then + pkg:add_link(linkname) + pkg:add_libfile(libfile) + end + end + else + -- if no libs, see if cmake/pkgconfig dirs exist and add linkdir + 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 + pkg:add_linkdir(libdir) + end end - return result + end + else + -- Package already exists, just add this store path as additional + packages[pkgname]:add_store_path(store_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added additional store path for " .. pkgname .. ": " .. store_path) end end - end - return nil -end --- Home manager can be installed as a module in nixos, in which case the home-manager tool is missing. -function _find_in_home_manager_profile(package_name, opt) - local nix_store = find_tool("nix-store") - if not nix_store then - return nil + ::continue:: 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 + + -- finalize all PackageInfo instances into plain tables + local result = {} + for name, pkgobj in pairs(packages) do + local plain = pkgobj:finalize() + result[name] = plain 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 - if line:startswith("/nix/store/") then - table.insert(store_paths, line) - end - end - if #store_paths > 0 then - local result = _extract_package_info(store_paths, package_name, opt) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in home-manager profile with " .. #store_paths .. " paths") - end - return result - end - end + + -- cache result + cache:set2(PACKAGE_INFO_CACHE, cache_key, result) + memory_cache:set2(PACKAGE_INFO_CACHE, cache_key, result) + cache:save() + + if opt and (opt.verbose or option.get("verbose")) then + local _, count = table.keys(result) + print("Nix: Extracted " .. count .. " packages from store paths") end - return nil + + return result end --- nixos-option is not always configured properly, but if it is, we can find user/system packages -function _find_in_nixos_user_packages(package_name, opt) - local nixos_option = find_tool("nixos-option") - if not nixos_option then - return nil +-- check if path matches package name using derivation info +local function path_matches_package(store_path, package_name, opt) + local drv_info = get_derivation_info(store_path, opt) + if drv_info and drv_info.name and drv_info.name ~= "" then + return drv_info.name:lower() == package_name:lower() 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) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in NixOS user packages with " .. #store_paths .. " paths") - end - return result - end + return false +end + +-- find package with pkg-config with caching +local function find_with_pkgconfig(package_name, store_paths, opt) + local cache = get_nix_cache() + local memory_cache = get_memory_cache() + + -- prefer the normalized env key stored in session memcache + local env_key = memory_cache:get("last_env_key") + local cache_key = package_name .. ":" .. (env_key or table.concat(store_paths, ";")) + + -- Check session memory cache first + local memo = memory_cache:get2(PKGCONFIG_CACHE, cache_key) + if memo ~= nil then + if opt and (opt.verbose or option.get("verbose")) then + local status = memo and "found" or "not found" + print("Nix: Using session cached pkg-config result (" .. status .. ") for: " .. package_name) end + return memo or nil end - return nil -end -function _find_in_nixos_system_packages(package_name, opt) - local nixos_option = find_tool("nixos-option") - if not nixos_option then - return nil + -- Check persistent cache + local cached_result = cache:get2(PKGCONFIG_CACHE, cache_key) + if cached_result ~= nil then + if opt and (opt.verbose or option.get("verbose")) then + local status = cached_result and "found" or "not found" + print("Nix: Using persistent cached pkg-config result (" .. status .. ") for: " .. package_name) + end + memory_cache:set2(PKGCONFIG_CACHE, cache_key, cached_result) + return cached_result or 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) + + -- Try matching store paths for this package first + local matching_paths = {} + for _, store_path in ipairs(store_paths) do + if path_matches_package(store_path, package_name, opt) then + table.insert(matching_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) - if result then + end + + -- Search matching paths first + for _, store_path in ipairs(matching_paths) do + local pkgconfig_dirs = { + path.join(store_path, "lib", "pkgconfig"), + path.join(store_path, "share", "pkgconfig") + } + + for _, pcdir in ipairs(pkgconfig_dirs) do + if os.isdir(pcdir) then if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in NixOS system packages with " .. #store_paths .. " paths") + print("Nix: Attempting pkg-config lookup: " .. package_name .. " (configdirs=" .. pcdir .. ")") + end + local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) + if result then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found package via pkg-config: " .. package_name) + end + memory_cache:set2(PKGCONFIG_CACHE, cache_key, result) + cache:set2(PKGCONFIG_CACHE, cache_key, result) + return result end - return result end end end - return nil -end --- Includes all system/current user/home-manager packages -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 - if line:startswith("/nix/store/") then - table.insert(store_paths, line) - end - end - if #store_paths > 0 then - local result = _extract_package_info(store_paths, package_name, opt) - if result then - if opt and (opt.verbose or option.get("verbose")) then - print("Found " .. package_name .. " in NixOS current system with " .. #store_paths .. " paths") + -- If no matches found, search all paths + for _, store_path in ipairs(store_paths) do + local pkgconfig_dirs = { + path.join(store_path, "lib", "pkgconfig"), + path.join(store_path, "share", "pkgconfig") + } + + for _, pcdir in ipairs(pkgconfig_dirs) do + if os.isdir(pcdir) then + local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) + if result then + memory_cache:set2(PKGCONFIG_CACHE, cache_key, result) + cache:set2(PKGCONFIG_CACHE, cache_key, result) + return result end - return result end end end + + -- Cache negative result + memory_cache:set2(PKGCONFIG_CACHE, cache_key, false) + cache:set2(PKGCONFIG_CACHE, cache_key, false) return nil end + +-- main entry point function main(name, opt) opt = opt or {} + + -- ensure a stable env cache key is available for the whole run + local memory_cache = get_memory_cache() + memory_cache:set("last_env_key", generate_env_cache_key()) + + -- Skip cross-compilation scenarios 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) + actual_name = name:sub(6) force_nix = true end - local search_functions = { - _find_in_nix_shell, - _find_in_nix_profile, - _find_in_home_manager_tool, - _find_in_home_manager_profile, - _find_in_nixos_user_packages, - _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 + + -- Get all store paths from all nix environments (cached) + local store_paths = get_all_store_paths(opt) + if #store_paths == 0 then + if force_nix and opt and (opt.verbose or option.get("verbose")) then + print("Nix: No store paths found in any nix environment") end + return nil end - if force_nix and opt and (opt.verbose or option.get("verbose")) then - print("Nix: Package " .. actual_name .. " not found in any nix environment") + + -- Extract all package info (cached) + local packages = extract_package_info(store_paths, opt) + local _, count = table.keys(packages) + if not packages or count == 0 then + if force_nix and opt and (opt.verbose or option.get("verbose")) then + print("Nix: No packages extracted from store paths") + end + return nil end + + -- Look for exact name match only + local actual_name_lower = actual_name:lower() + local found_package = packages[actual_name_lower] + + -- Try pkg-config if package found in store paths + local pkgconfig_result = nil + if found_package or force_nix then + pkgconfig_result = find_with_pkgconfig(actual_name, store_paths, opt) + if pkgconfig_result then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found package via pkg-config: " .. actual_name) + end + return pkgconfig_result + end + end + + -- If we found package info directly, return it + if found_package then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Found package: " .. actual_name .. " (" .. found_package.name .. ")") + end + + local result = { + name = found_package.name, + version = found_package.version + } + + -- Add directories and links if they exist + if found_package.includedirs and #found_package.includedirs > 0 then + result.includedirs = found_package.includedirs + end + if found_package.linkdirs and #found_package.linkdirs > 0 then + result.linkdirs = found_package.linkdirs + end + if found_package.links and #found_package.links > 0 then + result.links = found_package.links + end + if found_package.libfiles and #found_package.libfiles > 0 then + result.libfiles = found_package.libfiles + end + if found_package.bindirs and #found_package.bindirs > 0 then + result.bindirs = found_package.bindirs + end + + return result + end + + -- Package not found - alert user + if force_nix then + print("Nix: Package '" .. actual_name .. "' not found in any nix environment") + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Available packages:") + for pkg_name, _ in pairs(packages) do + print(" - " .. pkg_name) + end + end + end + return nil end \ No newline at end of file -- cgit v1.3.1 From 5381e13db135b332308f51302c71361ac2713a77 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Thu, 25 Sep 2025 13:09:45 -0400 Subject: nix: removed unnecessary code + tweaks --- xmake/modules/package/manager/nix/find_package.lua | 465 ++++++++++----------- 1 file changed, 225 insertions(+), 240 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 0508d6239..c25f57e67 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -32,7 +32,7 @@ local STORE_PATHS_CACHE = "nix_store_paths" local PACKAGE_INFO_CACHE = "nix_package_info" local PROPAGATED_CACHE = "nix_propagated" local PKGCONFIG_CACHE = "nix_pkgconfig" -local DERIVATION_CACHE = "nix_derivation" +local DERIVATION_CACHE = "nix_derivation_info" -- get nix cache instance local function get_nix_cache() @@ -77,165 +77,136 @@ local function generate_env_cache_key() return table.concat(key_parts, "|") end - - --- get derivation info for a store path with caching -local function get_derivation_info(store_path, opt) +-- extract package information from store path using derivation data +local function extract_package_info_from_path(store_path, opt) local cache = get_nix_cache() local memory_cache = get_memory_cache() - -- Check memory cache first + -- Check caches first local cached = memory_cache:get2(DERIVATION_CACHE, store_path) - if cached ~= nil then + if cached then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Using session cached derivation for: " .. store_path) + print("Nix: Using session cached derivation info for: " .. store_path) end - return cached + return cached.name, cached.version, cached.outputs, cached.pname end - -- Check persistent cache cached = cache:get2(DERIVATION_CACHE, store_path) - if cached ~= nil then + if cached then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Using persistent cached derivation for: " .. store_path) + print("Nix: Using persistent cached derivation info for: " .. store_path) end memory_cache:set2(DERIVATION_CACHE, store_path, cached) - return cached + return cached.name, cached.version, cached.outputs, cached.pname end - -- Get derivation path + -- Find required tools local nix_store = find_tool("nix-store") - if not nix_store then - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty - end - - local drv_path = try {function() - return os.iorunv(nix_store.program, {"-q", store_path, "--deriver"}):trim() + local nix = find_tool("nix") + -- Get the derivation path + local drv_output = try {function() + return os.iorunv(nix_store.program, {"--query", "--valid-derivers", store_path}):trim() -- not "--deriver" because: + -- The returned deriver is not guaranteed to exist in the local store, for example when paths were substituted from a binary cache. + -- Ref: https://nix.dev/manual/nix/latest/command-ref/nix-store/query.html end} - if not drv_path or drv_path == "" then - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty - end - - -- Get derivation info using nix derivation show - local nix = find_tool("nix") - if not nix then - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty + if not drv_output or drv_output == "" then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Could not get derivation for: " .. store_path) + end + return nil end - - local drv_json = try {function() + + -- drv_output is a list of derivations, cycle through? do all? For now, just take the first one. + drv_output = drv_output:match("(%S+)") + + -- Show the derivation with experimental features + local derivation_json = try {function() return os.iorunv(nix.program, { "derivation", "show", - "--extra-experimental-features", "nix-command flakes", - drv_path + drv_output, + "--extra-experimental-features", "nix-command flakes" }):trim() end} - if not drv_json or drv_json == "" then - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty + if not derivation_json or derivation_json == "" then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Could not show derivation for: " .. drv_output) + end + return nil end - -- Parse the JSON using xmake's JSON parser - local drv_data, parse_error = json.decode(drv_json) - if not drv_data or parse_error then + -- Parse the JSON output + local derivation_data, parse_error = json.decode(derivation_json) + if not derivation_data or parse_error then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Failed to parse derivation JSON for " .. store_path .. ": " .. (parse_error or "unknown error")) + print("Nix: Failed to parse derivation JSON: " .. (parse_error or "unknown error")) end - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty + return nil end - -- Extract the first (and usually only) derivation + -- Extract the derivation info (should be a single key-value pair) local drv_info = nil - for _, info in pairs(drv_data) do + for _, info in pairs(derivation_data) do drv_info = info break end - if not drv_info or type(drv_info) ~= "table" or not drv_info.env then - local empty = {} - cache:set2(DERIVATION_CACHE, store_path, empty) - memory_cache:set2(DERIVATION_CACHE, store_path, empty) - return empty + if not drv_info then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: No derivation info found in JSON") + end end - -- Extract relevant information - local result = { - name = drv_info.env.pname or drv_info.env.name or "", - version = drv_info.env.version or "", - outputs = drv_info.outputs or {}, - env = drv_info.env or {} - } - - -- Cache the result - cache:set2(DERIVATION_CACHE, store_path, result) - memory_cache:set2(DERIVATION_CACHE, store_path, result) - cache:save() - - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Cached derivation info for: " .. store_path .. " (name=" .. result.name .. ", version=" .. result.version .. ")") - end + -- Extract package information + -- structureAttrs vs env: + -- not every nix package has structureAttrs, so we fallback to env + -- Ref: https://nix.dev/manual/nix/latest/language/advanced-attributes.html + local package_name = (drv_info.structuredAttrs and drv_info.structuredAttrs.pname) or (drv_info.env and drv_info.env.pname) or nil + -- not just "name" as that includes version + local version = (drv_info.structuredAttrs and drv_info.structuredAttrs.version) or (drv_info.env and drv_info.env.version) or nil + local outputs = drv_info.outputs or {} - return result -end - --- parse store basename to extract name, version, and output (fallback method) -local function parse_store_basename(path_name) - -- parse "--" or "---" - -- remove leading hash (up to first '-') - local first_dash = path_name:find("-", 1, true) - if not first_dash then - return nil, nil, nil + if not package_name then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: No pname found in derivation: " .. drv_output) + end + return nil end - local rest = path_name:sub(first_dash + 1) - -- find last dash and second-last dash in rest - local last_dash = nil - for i = #rest, 1, -1 do - if rest:sub(i, i) == "-" then - last_dash = i - break + -- Create outputs map (output_name -> store_path) + local output_paths = {} + for output_name, output_info in pairs(outputs) do + if output_info.path then + output_paths[output_name] = output_info.path end end - if not last_dash then - return rest, nil, nil -- Just name, no version - end - -- try to find second-last dash - local second_last = nil - for i = last_dash - 1, 1, -1 do - if rest:sub(i, i) == "-" then - second_last = i + -- Determine which output this store_path represents + local current_output = nil + for output_name, output_path in pairs(output_paths) do + if output_path == store_path then + current_output = output_name break end end - if second_last then - -- form: name (may have dashes) = rest[1..second_last-1], version = rest[second_last+1..last_dash-1], output = rest[last_dash+1..] - local name = rest:sub(1, second_last - 1) - local version = rest:sub(second_last + 1, last_dash - 1) - local output = rest:sub(last_dash + 1) - return name, version, output - else - -- form: name = rest[1..last_dash-1], version = rest[last_dash+1..] - local name = rest:sub(1, last_dash - 1) - local version = rest:sub(last_dash + 1) - return name, version, nil + -- Cache the result + local result = { + name = package_name, + version = version, + outputs = output_paths, + current_output = current_output + } + + cache:set2(DERIVATION_CACHE, store_path, result) + memory_cache:set2(DERIVATION_CACHE, store_path, result) + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Extracted derivation info for " .. package_name .. " (version: " .. (version or "unknown") .. ")") end + + return package_name, version, output_paths, current_output end -- remove duplicates from array @@ -251,30 +222,33 @@ local function remove_duplicates(arr) return clean end --- PackageInfo class +-- PackageInfo data local PackageInfo = {} PackageInfo.__index = PackageInfo function PackageInfo:new(package_name) local o = { - name = package_name, + name = package_name, -- "pname" in nix terms includedirs = {}, bindirs = {}, linkdirs = {}, links = {}, libfiles = {}, store_paths = {}, + outputs = {}, -- output_name -> store_path mapping version = nil, - pkgconfig_available = false, - outputs = {} + pkgconfig_available = false } table.inherit2(o, self) return o end -function PackageInfo:add_store_path(p) +function PackageInfo:add_store_path(p, output_name) table.insert(self.store_paths, p) + if output_name then + self.outputs[output_name] = p + end end function PackageInfo:add_includedir(d) @@ -298,23 +272,11 @@ function PackageInfo:add_libfile(f) end function PackageInfo:set_version(v) - if not self.version and v and v ~= "" then + if not self.version and v then self.version = v end end -function PackageInfo:set_pname(p) - if not self.name and p and p ~= "" then - self.name = p - end -end - -function PackageInfo:set_outputs(o) - if o and type(o) == "table" then - self.outputs = o - end -end - function PackageInfo:set_pkgconfig_available() self.pkgconfig_available = true end @@ -337,8 +299,8 @@ function PackageInfo:finalize() links = self.links, libfiles = self.libfiles, store_paths = self.store_paths, - version = self.version, outputs = self.outputs, + version = self.version, pkgconfig_available = self.pkgconfig_available } end @@ -504,7 +466,7 @@ local function parse_store_paths_from_env(env_vars, opt) return paths end --- STORE PATH EXTRACTION FUNCTIONS (same as before) +-- STORE PATH EXTRACTION FUNCTIONS -- extract store paths from nix shell local function get_store_paths_nix_shell(opt) @@ -522,7 +484,15 @@ local function get_store_paths_nix_shell(opt) return parse_store_paths_from_env(build_env_vars, opt) end --- extract store paths from nix profile +-- Find package from current user's nix profile, includes nix-env installed packages +-- Note: nix-env only lists one output in the profile list +-- $ nix-env -iA nixpkgs. # installs multiple outputs, but only one is listed in the profile +-- this can cause issues if the main output does not contain the necessary files +-- Example: zlib.dev contains the headers, but zlib only contains the library +-- there does not seem to be an straight-forward way to find all outputs... +-- It is better to use nix profile like: +-- $ nix profile install 'nixpkgs#zlib^*'' # installs all outputs +-- $ nix profile install 'nixpkgs#zlib^dev' # installs only the dev output local function get_store_paths_nix_profile(opt) local nix = find_tool("nix") if not nix then @@ -536,7 +506,7 @@ local function get_store_paths_nix_profile(opt) ) end --- extract store paths from home-manager (tool version) +-- Popular nix-community tool to declaratively manage user environments (NixOS and non-NixOS) local function get_store_paths_home_manager_tool(opt) local home_manager = find_tool("home-manager") if not home_manager then @@ -550,7 +520,7 @@ local function get_store_paths_home_manager_tool(opt) ) end --- extract store paths from home-manager (profile version) +-- Home manager can be installed as a module in nixos, in which case the home-manager tool is missing. local function get_store_paths_home_manager_profile(opt) local nix_store = find_tool("nix-store") if not nix_store then @@ -570,7 +540,7 @@ local function get_store_paths_home_manager_profile(opt) ) end --- extract store paths from nixos user packages +-- nixos-option is not always configured properly, but if it is, we can find user/system packages local function get_store_paths_nixos_user_packages(opt) local nixos_option = find_tool("nixos-option") if not nixos_option then @@ -615,7 +585,7 @@ local function get_store_paths_nixos_system_packages(opt) return {} end --- extract store paths from nixos current system +-- Includes all system/user/home-manager packages local function get_store_paths_nixos_current_system(opt) local nix_store = find_tool("nix-store") if not nix_store then @@ -694,7 +664,7 @@ local function get_all_store_paths(opt) return all_paths end --- extract package information from store paths with caching and derivation info +-- extract package information from store paths with caching local function extract_package_info(store_paths, opt) opt = opt or {} local cache = get_nix_cache() @@ -734,92 +704,97 @@ local function extract_package_info(store_paths, opt) local packages = {} -- map: package_name -> PackageInfo + local function ensure_pkg(name) + if not name then + name = "" + end + local p = packages[name] + if not p then + p = PackageInfo:new(name) + packages[name] = p + end + return p + end + for _, store_path in ipairs(store_paths) do if not store_path or store_path == "" then goto continue end - -- Get derivation info - local drv_info = get_derivation_info(store_path, opt) - if not drv_info or not drv_info.name or drv_info.name == "" then + -- Use the enhanced derivation-based extraction + local parsed_name, parsed_version, output_paths, current_output = + extract_package_info_from_path(store_path, opt) + + if not parsed_name then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Skipping " .. store_path .. " - no derivation info or name available") + print("Nix: Could not extract package info from: " .. store_path) end goto continue end - local pkgname = drv_info.name:lower() - - -- Only create one package entry per name (first one wins due to prioritized ordering) - if not packages[pkgname] then - local pkg = PackageInfo:new(pkgname) - packages[pkgname] = pkg - - pkg:add_store_path(store_path) - pkg:set_version(drv_info.version) - pkg:set_pname(drv_info.name) - pkg:set_outputs(drv_info.outputs) + local pkg = ensure_pkg(parsed_name, pname) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added package: " .. drv_info.name .. " " .. (drv_info.version or "")) - end + pkg:add_store_path(store_path, current_output) + if parsed_version then + pkg:set_version(parsed_version) + end - -- include directories - local includedir = path.join(store_path, "include") - if os.isdir(includedir) then - pkg:add_includedir(includedir) - local subdirs = try { function() return os.dirs(path.join(includedir, "*")) end } or {} - for _, subdir in ipairs(subdirs) do - if os.isdir(subdir) then - pkg:add_includedir(subdir) - end - end + -- Add all output paths to the package + if output_paths then + for output_name, output_path in pairs(output_paths) do + pkg.outputs[output_name] = output_path end + end - -- bin - local bindir = path.join(store_path, "bin") - if os.isdir(bindir) then - pkg:add_bindir(bindir) + -- include directories (and their subdirs) + local includedir = path.join(store_path, "include") + if os.isdir(includedir) then + pkg:add_includedir(includedir) + local subdirs = try { function() return os.dirs(path.join(includedir, "*")) end } or {} + for _, subdir in ipairs(subdirs) do + if os.isdir(subdir) then + pkg:add_includedir(subdir) + end end + end - -- lib - local libdir = path.join(store_path, "lib") - if os.isdir(libdir) then - local libfiles = try { function() - local files = {} - local patterns = {"*.so*", "*.a", "*.dylib*"} - for _, pattern in ipairs(patterns) do - for _, f in ipairs(os.files(path.join(libdir, pattern)) or {}) do - table.insert(files, f) - end - end - return files - end } or {} + -- bin + local bindir = path.join(store_path, "bin") + if os.isdir(bindir) then + pkg:add_bindir(bindir) + end - if #libfiles > 0 then - pkg:add_linkdir(libdir) - for _, libfile in ipairs(libfiles) do - local filename = path.filename(libfile) - local linkname = filename:match("^lib(.+)%.so") or - filename:match("^lib(.+)%.a") or - filename:match("^lib(.+)%.dylib") - if linkname then - pkg:add_link(linkname) - pkg:add_libfile(libfile) - end + -- lib and libs + local libdir = path.join(store_path, "lib") + if os.isdir(libdir) then + local libfiles = try { function() + local files = {} + local patterns = {"*.so*", "*.a", "*.dylib*"} + for _, pattern in ipairs(patterns) do + for _, f in ipairs(os.files(path.join(libdir, pattern)) or {}) do + table.insert(files, f) end - else - -- if no libs, see if cmake/pkgconfig dirs exist and add linkdir - 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 - pkg:add_linkdir(libdir) + end + return files + end } or {} + + if #libfiles > 0 then + pkg:add_linkdir(libdir) + for _, libfile in ipairs(libfiles) do + local filename = path.filename(libfile) + local linkname = filename:match("^lib(.+)%.so") or + filename:match("^lib(.+)%.a") or + filename:match("^lib(.+)%.dylib") + if linkname then + pkg:add_link(linkname) + pkg:add_libfile(libfile) end end - end - else - -- Package already exists, just add this store path as additional - packages[pkgname]:add_store_path(store_path) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added additional store path for " .. pkgname .. ": " .. store_path) + else + -- if no libs, see if cmake/pkgconfig dirs exist and add linkdir + 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 + pkg:add_linkdir(libdir) + end end end @@ -846,11 +821,21 @@ local function extract_package_info(store_paths, opt) return result end --- check if path matches package name using derivation info -local function path_matches_package(store_path, package_name, opt) - local drv_info = get_derivation_info(store_path, opt) - if drv_info and drv_info.name and drv_info.name ~= "" then - return drv_info.name:lower() == package_name:lower() +-- check if path matches package name +local function path_matches_package(store_path, package_name) + local path_name = path.basename(store_path) + local package_name_lower = package_name:lower() + + local package_base = path_name:match("^[^%-]+-([^%-]+)") + if package_base then + local package_base_lower = package_base:lower() + if package_base_lower == package_name_lower then + return true + end + 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 @@ -888,7 +873,7 @@ local function find_with_pkgconfig(package_name, store_paths, opt) -- Try matching store paths for this package first local matching_paths = {} for _, store_path in ipairs(store_paths) do - if path_matches_package(store_path, package_name, opt) then + if path_matches_package(store_path, package_name) then table.insert(matching_paths, store_path) end end @@ -943,8 +928,11 @@ local function find_with_pkgconfig(package_name, store_paths, opt) return nil end +-- find package using the nix package manager +-- +-- @param name the package name +-- @param opt the options, e.g. {verbose = true, version = "1.12.x") --- main entry point function main(name, opt) opt = opt or {} @@ -957,18 +945,10 @@ function main(name, opt) return end - -- Handle nix:: prefix - local actual_name = name - local force_nix = false - if name:startswith("nix::") then - actual_name = name:sub(6) - force_nix = true - end - -- Get all store paths from all nix environments (cached) local store_paths = get_all_store_paths(opt) if #store_paths == 0 then - if force_nix and opt and (opt.verbose or option.get("verbose")) then + if opt and (opt.verbose or option.get("verbose")) then print("Nix: No store paths found in any nix environment") end return nil @@ -978,23 +958,34 @@ function main(name, opt) local packages = extract_package_info(store_paths, opt) local _, count = table.keys(packages) if not packages or count == 0 then - if force_nix and opt and (opt.verbose or option.get("verbose")) then + if opt and (opt.verbose or option.get("verbose")) then print("Nix: No packages extracted from store paths") end return nil end - -- Look for exact name match only - local actual_name_lower = actual_name:lower() - local found_package = packages[actual_name_lower] + -- Try to find the package by exact name match first + local name_lower = name:lower() + local found_package = packages[name_lower] + + -- If not found by exact match, try partial matches + if not found_package then + for pkg_name, pkg_info in pairs(packages) do + if pkg_name:find(name_lower, 1, true) or + name_lower:find(pkg_name, 1, true) then + found_package = pkg_info + break + end + end + end -- Try pkg-config if package found in store paths local pkgconfig_result = nil - if found_package or force_nix then - pkgconfig_result = find_with_pkgconfig(actual_name, store_paths, opt) + if found_package then + pkgconfig_result = find_with_pkgconfig(name, store_paths, opt) if pkgconfig_result then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found package via pkg-config: " .. actual_name) + print("Nix: Found package via pkg-config: " .. name) end return pkgconfig_result end @@ -1003,7 +994,7 @@ function main(name, opt) -- If we found package info directly, return it if found_package then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found package: " .. actual_name .. " (" .. found_package.name .. ")") + print("Nix: Found package: " .. name .. " (" .. found_package.name .. ")") end local result = { @@ -1031,15 +1022,9 @@ function main(name, opt) return result end - -- Package not found - alert user - if force_nix then - print("Nix: Package '" .. actual_name .. "' not found in any nix environment") - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Available packages:") - for pkg_name, _ in pairs(packages) do - print(" - " .. pkg_name) - end - end + -- Package not found + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Package " .. name .. " not found in any nix environment") end return nil -- cgit v1.3.1 From 7176e111216c3a79fc292bb9116562b24eb99e91 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Thu, 25 Sep 2025 15:14:16 -0400 Subject: nix: removed fuzzy name matching --- xmake/modules/package/manager/nix/find_package.lua | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index c25f57e67..fe777c7d7 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -964,21 +964,10 @@ function main(name, opt) return nil end - -- Try to find the package by exact name match first + -- Try to find the package by name match first local name_lower = name:lower() local found_package = packages[name_lower] - -- If not found by exact match, try partial matches - if not found_package then - for pkg_name, pkg_info in pairs(packages) do - if pkg_name:find(name_lower, 1, true) or - name_lower:find(pkg_name, 1, true) then - found_package = pkg_info - break - end - end - end - -- Try pkg-config if package found in store paths local pkgconfig_result = nil if found_package then -- cgit v1.3.1 From d342106562174ea48d53b11c540e28e1121ecafc Mon Sep 17 00:00:00 2001 From: zzbaron Date: Fri, 26 Sep 2025 18:42:09 -0400 Subject: nix: added support for multiple derivers --- xmake/modules/package/manager/nix/find_package.lua | 44 ++++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index fe777c7d7..df9923ad5 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -117,17 +117,39 @@ local function extract_package_info_from_path(store_path, opt) return nil end - -- drv_output is a list of derivations, cycle through? do all? For now, just take the first one. - drv_output = drv_output:match("(%S+)") - - -- Show the derivation with experimental features - local derivation_json = try {function() - return os.iorunv(nix.program, { - "derivation", "show", - drv_output, - "--extra-experimental-features", "nix-command flakes" - }):trim() - end} + -- drv_output is a list of derivations + local derivations = {} + for drv_path in drv_output:gmatch("(%S+)") do + if drv_path:match("%.drv$") then + table.insert(derivations, drv_path) + end + end + + if #derivations == 0 then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: No valid derivation paths found for: " .. store_path) + end + return nil + end + + -- Try each derivation until we find one that works + local derivation_json = nil + for _, drv_path in ipairs(derivations) do + derivation_json = try {function() + return os.iorunv(nix.program, { + "derivation", "show", + drv_path, + "--extra-experimental-features", "nix-command flakes" + }):trim() + end} + + if derivation_json and derivation_json ~= "" then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Using derivation: " .. drv_path) + end + break + end + end if not derivation_json or derivation_json == "" then if opt and (opt.verbose or option.get("verbose")) then -- cgit v1.3.1 From 6445e193ab9a689b5af2dbca2a3cfd254583b5c6 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Fri, 26 Sep 2025 19:06:46 -0400 Subject: nix: removed unnecessary matching code --- xmake/modules/package/manager/nix/find_package.lua | 51 +--------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index df9923ad5..ce555dfc6 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -843,25 +843,6 @@ local function extract_package_info(store_paths, opt) return result end --- check if path matches package name -local function path_matches_package(store_path, package_name) - local path_name = path.basename(store_path) - local package_name_lower = package_name:lower() - - local package_base = path_name:match("^[^%-]+-([^%-]+)") - if package_base then - local package_base_lower = package_base:lower() - if package_base_lower == package_name_lower then - return true - end - 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 - -- find package with pkg-config with caching local function find_with_pkgconfig(package_name, store_paths, opt) local cache = get_nix_cache() @@ -892,16 +873,8 @@ local function find_with_pkgconfig(package_name, store_paths, opt) return cached_result or nil end - -- Try matching store paths for this package first - local matching_paths = {} + -- Search all paths (not fully optimal) for _, store_path in ipairs(store_paths) do - if path_matches_package(store_path, package_name) then - table.insert(matching_paths, store_path) - end - end - - -- Search matching paths first - for _, store_path in ipairs(matching_paths) do local pkgconfig_dirs = { path.join(store_path, "lib", "pkgconfig"), path.join(store_path, "share", "pkgconfig") @@ -909,9 +882,6 @@ local function find_with_pkgconfig(package_name, store_paths, opt) for _, pcdir in ipairs(pkgconfig_dirs) do if os.isdir(pcdir) then - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Attempting pkg-config lookup: " .. package_name .. " (configdirs=" .. pcdir .. ")") - end local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) if result then if opt and (opt.verbose or option.get("verbose")) then @@ -925,25 +895,6 @@ local function find_with_pkgconfig(package_name, store_paths, opt) end end - -- If no matches found, search all paths - for _, store_path in ipairs(store_paths) do - local pkgconfig_dirs = { - path.join(store_path, "lib", "pkgconfig"), - path.join(store_path, "share", "pkgconfig") - } - - for _, pcdir in ipairs(pkgconfig_dirs) do - if os.isdir(pcdir) then - local result = find_package_from_pkgconfig(package_name, {configdirs = pcdir}) - if result then - memory_cache:set2(PKGCONFIG_CACHE, cache_key, result) - cache:set2(PKGCONFIG_CACHE, cache_key, result) - return result - end - end - end - end - -- Cache negative result memory_cache:set2(PKGCONFIG_CACHE, cache_key, false) cache:set2(PKGCONFIG_CACHE, cache_key, false) -- cgit v1.3.1 From ada659fd59de79145fe340c664d6d85009b6a171 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Fri, 26 Sep 2025 19:31:31 -0400 Subject: nix: optimized package info extraction --- xmake/modules/package/manager/nix/find_package.lua | 81 ++++++++++++++++++++-- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index ce555dfc6..f0777b839 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -686,13 +686,32 @@ local function get_all_store_paths(opt) return all_paths end +-- check if store path has the package name (fuzzy match) +local function path_matches_package(store_path, package_name) + local path_name = path.basename(store_path) + local package_name_lower = package_name:lower() + + local package_base = path_name:match("^[^%-]+-([^%-]+)") + if package_base then + local package_base_lower = package_base:lower() + if package_base_lower == package_name_lower then + return true + end + 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 + -- extract package information from store paths with caching -local function extract_package_info(store_paths, opt) +local function extract_package_info(store_paths, package_name, opt) opt = opt or {} local cache = get_nix_cache() local memory_cache = get_memory_cache() local paths_key = table.concat(store_paths or {}, ";") - local cache_key = "package_info:" .. paths_key + local cache_key = "package_info:" .. (package_name or "all") .. ":" .. paths_key -- Check memory cache first local cached = memory_cache:get2(PACKAGE_INFO_CACHE, cache_key) @@ -720,8 +739,36 @@ local function extract_package_info(store_paths, opt) return empty end + -- Filter store paths if package_name is provided + local filtered_paths = store_paths + if package_name then + filtered_paths = {} + local seen = {} + + -- First, find direct matches + for _, store_path in ipairs(store_paths) do + if path_matches_package(store_path, package_name) and not seen[store_path] then + seen[store_path] = true + table.insert(filtered_paths, store_path) + end + end + + -- Then find their dependencies + local all_deps = follow_propagated_inputs(filtered_paths, opt) + for _, dep_path in ipairs(all_deps) do + if not seen[dep_path] then + seen[dep_path] = true + table.insert(filtered_paths, dep_path) + end + end + + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Filtered to " .. #filtered_paths .. " relevant store paths for package: " .. package_name) + end + end + if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Extracting package info for " .. #store_paths .. " store paths") + print("Nix: Extracting package info for " .. #filtered_paths .. " store paths") end local packages = {} -- map: package_name -> PackageInfo @@ -738,7 +785,7 @@ local function extract_package_info(store_paths, opt) return p end - for _, store_path in ipairs(store_paths) do + for _, store_path in ipairs(filtered_paths) do if not store_path or store_path == "" then goto continue end -- Use the enhanced derivation-based extraction @@ -752,7 +799,7 @@ local function extract_package_info(store_paths, opt) goto continue end - local pkg = ensure_pkg(parsed_name, pname) + local pkg = ensure_pkg(parsed_name) pkg:add_store_path(store_path, current_output) if parsed_version then @@ -873,8 +920,28 @@ local function find_with_pkgconfig(package_name, store_paths, opt) return cached_result or nil end - -- Search all paths (not fully optimal) + -- Filter store paths to only relevant ones + local filtered_paths = {} + local seen = {} + for _, store_path in ipairs(store_paths) do + if path_matches_package(store_path, package_name) and not seen[store_path] then + seen[store_path] = true + table.insert(filtered_paths, store_path) + end + end + + -- Add dependencies of matching packages + local all_deps = follow_propagated_inputs(filtered_paths, opt) + for _, dep_path in ipairs(all_deps) do + if not seen[dep_path] then + seen[dep_path] = true + table.insert(filtered_paths, dep_path) + end + end + + -- Search filtered paths + for _, store_path in ipairs(filtered_paths) do local pkgconfig_dirs = { path.join(store_path, "lib", "pkgconfig"), path.join(store_path, "share", "pkgconfig") @@ -928,7 +995,7 @@ function main(name, opt) end -- Extract all package info (cached) - local packages = extract_package_info(store_paths, opt) + local packages = extract_package_info(store_paths, name, opt) local _, count = table.keys(packages) if not packages or count == 0 then if opt and (opt.verbose or option.get("verbose")) then -- cgit v1.3.1 From 3031598774da90d690f8bdf8d9e3406999947ece Mon Sep 17 00:00:00 2001 From: zzbaron Date: Fri, 26 Sep 2025 20:26:14 -0400 Subject: nix: fixed bugs and tweaks from gemini --- xmake/modules/package/manager/nix/find_package.lua | 59 +++++++++------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index f0777b839..ea1b93394 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -88,7 +88,7 @@ local function extract_package_info_from_path(store_path, opt) if opt and (opt.verbose or option.get("verbose")) then print("Nix: Using session cached derivation info for: " .. store_path) end - return cached.name, cached.version, cached.outputs, cached.pname + return cached.name, cached.version, cached.outputs, cached.current_output end cached = cache:get2(DERIVATION_CACHE, store_path) @@ -97,12 +97,23 @@ local function extract_package_info_from_path(store_path, opt) print("Nix: Using persistent cached derivation info for: " .. store_path) end memory_cache:set2(DERIVATION_CACHE, store_path, cached) - return cached.name, cached.version, cached.outputs, cached.pname + return cached.name, cached.version, cached.outputs, cached.current_output end -- Find required tools local nix_store = find_tool("nix-store") local nix = find_tool("nix") + + if not nix_store or not nix then + if opt and (opt.verbose or option.get("verbose")) then + local missing = {} + if not nix_store then table.insert(missing, "nix-store") end + if not nix then table.insert(missing, "nix") end + print("Nix: Required tools not found: " .. table.concat(missing, ", ")) + end + return nil + end + -- Get the derivation path local drv_output = try {function() return os.iorunv(nix_store.program, {"--query", "--valid-derivers", store_path}):trim() -- not "--deriver" because: @@ -686,23 +697,12 @@ local function get_all_store_paths(opt) return all_paths end --- check if store path has the package name (fuzzy match) +-- check if store path has the package name (substring search) local function path_matches_package(store_path, package_name) - local path_name = path.basename(store_path) + local path_name_lower = path.basename(store_path):lower() local package_name_lower = package_name:lower() - local package_base = path_name:match("^[^%-]+-([^%-]+)") - if package_base then - local package_base_lower = package_base:lower() - if package_base_lower == package_name_lower then - return true - end - 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 + return path_name_lower:find(package_name_lower, 1, true) ~= nil end -- extract package information from store paths with caching @@ -883,8 +883,8 @@ local function extract_package_info(store_paths, package_name, opt) cache:save() if opt and (opt.verbose or option.get("verbose")) then - local _, count = table.keys(result) - print("Nix: Extracted " .. count .. " packages from store paths") + local keys = table.keys(result) + print("Nix: Extracted " .. #keys .. " packages from store paths") end return result @@ -996,8 +996,8 @@ function main(name, opt) -- Extract all package info (cached) local packages = extract_package_info(store_paths, name, opt) - local _, count = table.keys(packages) - if not packages or count == 0 then + local keys = table.keys(packages) + if not packages or #keys == 0 then if opt and (opt.verbose or option.get("verbose")) then print("Nix: No packages extracted from store paths") end @@ -1031,21 +1031,12 @@ function main(name, opt) version = found_package.version } + local fields_to_copy = {"includedirs", "linkdirs", "links", "libfiles", "bindirs"} -- Add directories and links if they exist - if found_package.includedirs and #found_package.includedirs > 0 then - result.includedirs = found_package.includedirs - end - if found_package.linkdirs and #found_package.linkdirs > 0 then - result.linkdirs = found_package.linkdirs - end - if found_package.links and #found_package.links > 0 then - result.links = found_package.links - end - if found_package.libfiles and #found_package.libfiles > 0 then - result.libfiles = found_package.libfiles - end - if found_package.bindirs and #found_package.bindirs > 0 then - result.bindirs = found_package.bindirs + for _, field in ipairs(fields_to_copy) do + if found_package[field] and #found_package[field] > 0 then + result[field] = found_package[field] + end end return result -- cgit v1.3.1 From c9cc8bbdb105ecb27170ebc795a7688758d1cdae Mon Sep 17 00:00:00 2001 From: zzbaron Date: Fri, 26 Sep 2025 23:30:36 -0400 Subject: nix: adjustments pointed out by ruki --- xmake/modules/package/manager/nix/find_package.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index ea1b93394..41fcb62b0 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -70,10 +70,9 @@ local function generate_env_cache_key() -- Create a hash-like key from the environment local key_parts = {} - for k, v in pairs(env_data) do + for k, v in table.orderpairs(env_data) do table.insert(key_parts, k .. "=" .. v) end - table.sort(key_parts) return table.concat(key_parts, "|") end @@ -109,7 +108,7 @@ local function extract_package_info_from_path(store_path, opt) local missing = {} if not nix_store then table.insert(missing, "nix-store") end if not nix then table.insert(missing, "nix") end - print("Nix: Required tools not found: " .. table.concat(missing, ", ")) + wprint("Nix: Required tools not found: " .. table.concat(missing, ", ")) end return nil end -- cgit v1.3.1 From 154b2e67cf3f239964fb5d1ab0f178faee389bf1 Mon Sep 17 00:00:00 2001 From: zack Date: Sun, 28 Sep 2025 15:48:14 -0400 Subject: nix: adjustments pointed out by ruki --- xmake/modules/package/manager/nix/find_package.lua | 195 ++++++++++----------- 1 file changed, 92 insertions(+), 103 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 41fcb62b0..1c15d5e18 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -26,6 +26,7 @@ import("package.manager.pkgconfig.find_package", {alias = "find_package_from_pkg import("core.cache.globalcache") import("core.cache.memcache") import("core.base.json") +import("core.base.object") -- cache keys local STORE_PATHS_CACHE = "nix_store_paths" @@ -115,9 +116,13 @@ local function extract_package_info_from_path(store_path, opt) -- Get the derivation path local drv_output = try {function() - return os.iorunv(nix_store.program, {"--query", "--valid-derivers", store_path}):trim() -- not "--deriver" because: + local outdata = os.iorunv(nix_store.program, {"--query", "--valid-derivers", store_path}) -- not "--deriver" because: -- The returned deriver is not guaranteed to exist in the local store, for example when paths were substituted from a binary cache. -- Ref: https://nix.dev/manual/nix/latest/command-ref/nix-store/query.html + if outdata then + return outdata:trim() + end + return outdata end} if not drv_output or drv_output == "" then @@ -146,11 +151,15 @@ local function extract_package_info_from_path(store_path, opt) local derivation_json = nil for _, drv_path in ipairs(derivations) do derivation_json = try {function() - return os.iorunv(nix.program, { + local outdata = os.iorunv(nix.program, { "derivation", "show", drv_path, "--extra-experimental-features", "nix-command flakes" - }):trim() + }) + if outdata then + return outdata:trim() + end + return outdata end} if derivation_json and derivation_json ~= "" then @@ -169,10 +178,10 @@ local function extract_package_info_from_path(store_path, opt) end -- Parse the JSON output - local derivation_data, parse_error = json.decode(derivation_json) - if not derivation_data or parse_error then + local derivation_data = json.decode(derivation_json) + if not derivation_data then if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Failed to parse derivation JSON: " .. (parse_error or "unknown error")) + print("Nix: Failed to parse derivation JSON") end return nil end @@ -241,116 +250,91 @@ local function extract_package_info_from_path(store_path, opt) return package_name, version, output_paths, current_output end --- remove duplicates from array -local function remove_duplicates(arr) - local seen = {} - local clean = {} - for _, item in ipairs(arr) do - if not seen[item] then - seen[item] = true - table.insert(clean, item) - end - end - return clean +-- package info data +local package_info = object {_init = {"package_name"}} + +function package_info:new(name) + self._name = name + self._includedirs = {} + self._bindirs = {} + self._linkdirs = {} + self._links = {} + self._libfiles = {} + self._store_paths = {} + self._outputs = {} + self._version = nil + self._pkgconfig_available = false + return package_info {self, name} end --- PackageInfo data -local PackageInfo = {} -PackageInfo.__index = PackageInfo - -function PackageInfo:new(package_name) - local o = { - name = package_name, -- "pname" in nix terms - includedirs = {}, - bindirs = {}, - linkdirs = {}, - links = {}, - libfiles = {}, - store_paths = {}, - outputs = {}, -- output_name -> store_path mapping - version = nil, - pkgconfig_available = false - } - - table.inherit2(o, self) - return o -end - -function PackageInfo:add_store_path(p, output_name) - table.insert(self.store_paths, p) +function package_info:add_store_path(p, output_name) + table.insert(self._store_paths, p) if output_name then - self.outputs[output_name] = p + self._outputs[output_name] = p end end -function PackageInfo:add_includedir(d) - table.insert(self.includedirs, d) +function package_info:add_includedir(d) + table.insert(self._includedirs, d) end -function PackageInfo:add_bindir(d) - table.insert(self.bindirs, d) +function package_info:add_bindir(d) + table.insert(self._bindirs, d) end -function PackageInfo:add_linkdir(d) - table.insert(self.linkdirs, d) +function package_info:add_linkdir(d) + table.insert(self._linkdirs, d) end -function PackageInfo:add_link(l) - table.insert(self.links, l) +function package_info:add_link(l) + table.insert(self._links, l) end -function PackageInfo:add_libfile(f) - table.insert(self.libfiles, f) +function package_info:add_libfile(f) + table.insert(self._libfiles, f) end -function PackageInfo:set_version(v) - if not self.version and v then - self.version = v +function package_info:set_version(v) + if not self._version and v then + self._version = v end end -function PackageInfo:set_pkgconfig_available() - self.pkgconfig_available = true +function package_info:set_pkgconfig_available() + self._pkgconfig_available = true end -function PackageInfo:finalize() +function package_info:finalize() -- remove duplicates - self.includedirs = remove_duplicates(self.includedirs) - self.bindirs = remove_duplicates(self.bindirs) - self.linkdirs = remove_duplicates(self.linkdirs) - self.links = remove_duplicates(self.links) - self.libfiles = remove_duplicates(self.libfiles) - self.store_paths = remove_duplicates(self.store_paths) - + self._includedirs = table.unique(self._includedirs) + self._bindirs = table.unique(self._bindirs) + self._linkdirs = table.unique(self._linkdirs) + self._links = table.unique(self._links) + self._libfiles = table.unique(self._libfiles) + self._store_paths = table.unique(self._store_paths) + -- return plain table (so cache stores normal table) return { - name = self.name, - includedirs = self.includedirs, - bindirs = self.bindirs, - linkdirs = self.linkdirs, - links = self.links, - libfiles = self.libfiles, - store_paths = self.store_paths, - outputs = self.outputs, - version = self.version, - pkgconfig_available = self.pkgconfig_available + name = self._name, + includedirs = self._includedirs, + bindirs = self._bindirs, + linkdirs = self._linkdirs, + links = self._links, + libfiles = self._libfiles, + store_paths = self._store_paths, + outputs = self._outputs, + version = self._version, + pkgconfig_available = self._pkgconfig_available } end -- follow propagated build inputs recursively with caching local function follow_propagated_inputs(store_paths, opt) local cache = get_nix_cache() - local all_paths = {} - local seen = {} local visited = {} -- 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 + local all_paths = table.unique(store_paths) local i = 1 while i <= #all_paths do @@ -393,25 +377,26 @@ local function follow_propagated_inputs(store_paths, opt) -- Add new paths for _, prop_path in ipairs(prop_paths) do - if not seen[prop_path] then - seen[prop_path] = true - table.insert(all_paths, prop_path) - if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Added propagated: " .. prop_path) - end + table.insert(all_paths, prop_path) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Added propagated: " .. prop_path) end end end i = i + 1 end - return all_paths + return table.unique(all_paths) end -- get store paths from nix command output local function get_store_paths_from_command(command, args, opt) local output = try {function() - return os.iorunv(command, args):trim() + local outdata = os.iorunv(command, args) + if outdata then + return outdata:trim() + end + return outdata end} if not output then @@ -581,7 +566,11 @@ local function get_store_paths_nixos_user_packages(opt) local user = os.getenv("USER") or "unknown" local output = try {function() - return os.iorunv(nixos_option.program, {"users.users." .. user .. ".packages"}):trim() + local outdata = os.iorunv(nixos_option.program, {"users.users." .. user .. ".packages"}) + if outdata then + return outdata:trim() + end + return outdata end} if output then @@ -603,7 +592,11 @@ local function get_store_paths_nixos_system_packages(opt) end local output = try {function() - return os.iorunv(nixos_option.program, {"environment.systemPackages"}):trim() + local outdata = os.iorunv(nixos_option.program, {"environment.systemPackages"}) + if outdata then + return outdata:trim() + end + return outdata end} if output then @@ -754,12 +747,7 @@ local function extract_package_info(store_paths, package_name, opt) -- Then find their dependencies local all_deps = follow_propagated_inputs(filtered_paths, opt) - for _, dep_path in ipairs(all_deps) do - if not seen[dep_path] then - seen[dep_path] = true - table.insert(filtered_paths, dep_path) - end - end + filtered_paths = table.unique(all_deps) if opt and (opt.verbose or option.get("verbose")) then print("Nix: Filtered to " .. #filtered_paths .. " relevant store paths for package: " .. package_name) @@ -770,7 +758,7 @@ local function extract_package_info(store_paths, package_name, opt) print("Nix: Extracting package info for " .. #filtered_paths .. " store paths") end - local packages = {} -- map: package_name -> PackageInfo + local packages = {} -- map: package_name -> package_info local function ensure_pkg(name) if not name then @@ -778,7 +766,8 @@ local function extract_package_info(store_paths, package_name, opt) end local p = packages[name] if not p then - p = PackageInfo:new(name) + -- Create the package_info object + p = package_info:new(name) packages[name] = p end return p @@ -808,7 +797,7 @@ local function extract_package_info(store_paths, package_name, opt) -- Add all output paths to the package if output_paths then for output_name, output_path in pairs(output_paths) do - pkg.outputs[output_name] = output_path + pkg._outputs[output_name] = output_path end end @@ -869,7 +858,7 @@ local function extract_package_info(store_paths, package_name, opt) ::continue:: end - -- finalize all PackageInfo instances into plain tables + -- finalize all package_info instances into plain tables local result = {} for name, pkgobj in pairs(packages) do local plain = pkgobj:finalize() -- cgit v1.3.1 From fbb0532761c312c02e29541e6c0de1ac3c9c72a9 Mon Sep 17 00:00:00 2001 From: zzbaron Date: Mon, 29 Sep 2025 00:54:22 -0400 Subject: nix: adjustments pointed out by ruki --- xmake/modules/package/manager/nix/find_package.lua | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 1c15d5e18..5e5762beb 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -920,13 +920,7 @@ local function find_with_pkgconfig(package_name, store_paths, opt) end -- Add dependencies of matching packages - local all_deps = follow_propagated_inputs(filtered_paths, opt) - for _, dep_path in ipairs(all_deps) do - if not seen[dep_path] then - seen[dep_path] = true - table.insert(filtered_paths, dep_path) - end - end + filtered_paths = follow_propagated_inputs(filtered_paths, opt) -- Search filtered paths for _, store_path in ipairs(filtered_paths) do -- cgit v1.3.1