diff options
| author | zzbaron <[email protected]> | 2025-09-24 23:37:51 -0400 |
|---|---|---|
| committer | zzbaron <[email protected]> | 2025-09-24 23:37:51 -0400 |
| commit | 70cb5582cf6a4cfef1616b9e336dac55dd0b70dc (patch) | |
| tree | aac12746b2360e72334c64ad01de05bb6a5cdf56 | |
| parent | 543b3a907ccecf24abc89b2d46dcbf0291d4f26a (diff) | |
nix: cache fixes, rework store path extraction, etc.
| -rw-r--r-- | xmake/modules/package/manager/nix/find_package.lua | 1289 |
1 files 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 "<hash>-<name>-<version>" or "<hash>-<name>-<version>-<output>" + -- 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.<package> # 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 + + -- 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 - if force_nix and opt and (opt.verbose or option.get("verbose")) then - print("Nix: Package " .. actual_name .. " not found in any nix environment") + + -- 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 |
