--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file find_package.lua -- -- imports 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") import("core.base.object") -- 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_info" -- 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 table.orderpairs(env_data) do table.insert(key_parts, k .. "=" .. v) end return table.concat(key_parts, "|") end -- 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 caches first local cached = memory_cache:get2(DERIVATION_CACHE, store_path) if cached then 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.current_output end cached = cache:get2(DERIVATION_CACHE, store_path) if cached then if opt and (opt.verbose or option.get("verbose")) then 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.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 wprint("Nix: Required tools not found: " .. table.concat(missing, ", ")) end return nil end -- Get the derivation path local drv_output = try {function() 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 if opt and (opt.verbose or option.get("verbose")) then print("Nix: Could not get derivation for: " .. store_path) end return nil 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() local outdata = os.iorunv(nix.program, { "derivation", "show", drv_path, "--extra-experimental-features", "nix-command flakes" }) if outdata then return outdata:trim() end return outdata 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 print("Nix: Could not show derivation for: " .. drv_output) end return nil end -- Parse the JSON output 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") end return nil end -- Extract the derivation info (should be a single key-value pair) local drv_info = nil for _, info in pairs(derivation_data) do drv_info = info break end 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 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 {} 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 -- 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 -- 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 -- 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 -- 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 function package_info:add_store_path(p, output_name) table.insert(self._store_paths, p) if output_name then self._outputs[output_name] = p end end function package_info:add_includedir(d) table.insert(self._includedirs, d) end function package_info:add_bindir(d) table.insert(self._bindirs, d) end function package_info:add_linkdir(d) table.insert(self._linkdirs, d) end function package_info:add_link(l) table.insert(self._links, l) end function package_info:add_libfile(f) table.insert(self._libfiles, f) end function package_info:set_version(v) if not self._version and v then self._version = v end end function package_info:set_pkgconfig_available() self._pkgconfig_available = true end function package_info:finalize() -- remove duplicates 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 } end -- follow propagated build inputs recursively with caching local function follow_propagated_inputs(store_paths, opt) local cache = get_nix_cache() local visited = {} -- Add initial paths local all_paths = table.unique(store_paths) 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 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") 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/") then table.insert(prop_paths, prop_path) end end end end -- 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 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 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() local outdata = os.iorunv(command, args) if outdata then return outdata:trim() end return outdata 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 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 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 -- 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 -- STORE PATH EXTRACTION FUNCTIONS -- 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 -- 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 return {} end return get_store_paths_from_command( nix.program, {"profile", "list", "--extra-experimental-features", "nix-command flakes"}, opt ) end -- 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 return {} end return get_store_paths_from_command( home_manager.program, {"packages"}, opt ) end -- 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 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 -- 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 return {} end local user = os.getenv("USER") or "unknown" local output = try {function() local outdata = os.iorunv(nixos_option.program, {"users.users." .. user .. ".packages"}) if outdata then return outdata:trim() end return outdata 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 return {} end -- 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 output = try {function() local outdata = os.iorunv(nixos_option.program, {"environment.systemPackages"}) if outdata then return outdata:trim() end return outdata 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 return {} end -- 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 return {} end if not os.isdir("/run/current-system") then return {} 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 -- 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 -- 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 end -- 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 -- check if store path has the package name (substring search) local function path_matches_package(store_path, package_name) local path_name_lower = path.basename(store_path):lower() local package_name_lower = package_name:lower() return path_name_lower:find(package_name_lower, 1, true) ~= nil end -- extract package information from store paths with caching 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:" .. (package_name or "all") .. ":" .. 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: Using session cached package info") end 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 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 -- 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) 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) end end if opt and (opt.verbose or option.get("verbose")) then print("Nix: Extracting package info for " .. #filtered_paths .. " store paths") end local packages = {} -- map: package_name -> package_info local function ensure_pkg(name) if not name then name = "" end local p = packages[name] if not p then -- Create the package_info object p = package_info:new(name) packages[name] = p end return p end 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 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: Could not extract package info from: " .. store_path) end goto continue end local pkg = ensure_pkg(parsed_name) pkg:add_store_path(store_path, current_output) if parsed_version then pkg:set_version(parsed_version) 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 -- 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 -- bin local bindir = path.join(store_path, "bin") if os.isdir(bindir) then pkg:add_bindir(bindir) 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 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 end ::continue:: end -- finalize all package_info instances into plain tables local result = {} for name, pkgobj in pairs(packages) do local plain = pkgobj:finalize() result[name] = plain 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 keys = table.keys(result) print("Nix: Extracted " .. #keys .. " packages from store paths") end return result 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 -- 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 -- 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 filtered_paths = follow_propagated_inputs(filtered_paths, opt) -- 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") } 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 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 end end end -- Cache negative result memory_cache:set2(PKGCONFIG_CACHE, cache_key, false) cache:set2(PKGCONFIG_CACHE, cache_key, false) 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") 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 -- Get all store paths from all nix environments (cached) local store_paths = get_all_store_paths(opt) if #store_paths == 0 then if 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, name, opt) 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 return nil end -- Try to find the package by name match first local name_lower = name:lower() local found_package = packages[name_lower] -- Try pkg-config if package found in store paths local pkgconfig_result = nil 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: " .. 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: " .. name .. " (" .. found_package.name .. ")") end local result = { name = found_package.name, version = found_package.version } local fields_to_copy = {"includedirs", "linkdirs", "links", "libfiles", "bindirs"} -- Add directories and links if they exist 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 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 end