diff options
| author | ruki <[email protected]> | 2026-01-08 09:27:30 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-08 09:27:30 +0800 |
| commit | 5ec8f01cf2e953e1a9536aba194e4cfd43d123f3 (patch) | |
| tree | a01144df574b439c244281a85bebd1906d3ac09d | |
| parent | 6eff9dd8b660e8a0b0d93d423871b4625f02a596 (diff) | |
| parent | d0269216873d13127c164ea8aaafa9cc00911ffa (diff) | |
Merge pull request #7190 from ZZBaron/nix/fix-version-handling
Nix Package Manager: Add Semantic Versioning and Improve Version Selection
| -rw-r--r-- | xmake/modules/package/manager/nix/find_package.lua | 496 | ||||
| -rw-r--r-- | xmake/modules/package/manager/nix/install_package.lua | 2 | ||||
| -rw-r--r-- | xmake/modules/package/manager/nix/search_package.lua | 2 |
3 files changed, 322 insertions, 178 deletions
diff --git a/xmake/modules/package/manager/nix/find_package.lua b/xmake/modules/package/manager/nix/find_package.lua index 5e5762beb..79fb236d4 100644 --- a/xmake/modules/package/manager/nix/find_package.lua +++ b/xmake/modules/package/manager/nix/find_package.lua @@ -14,11 +14,11 @@ -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- --- @author ruki +-- @author ZZBaron -- @file find_package.lua -- --- imports +--imports import("core.base.option") import("lib.detect.find_tool") import("private.core.base.is_cross") @@ -27,6 +27,7 @@ import("core.cache.globalcache") import("core.cache.memcache") import("core.base.json") import("core.base.object") +import("core.base.semver") -- cache keys local STORE_PATHS_CACHE = "nix_store_paths" @@ -88,6 +89,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.current_output end @@ -96,6 +98,7 @@ local function extract_package_info_from_path(store_path, opt) 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 @@ -109,7 +112,9 @@ 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 - wprint("Nix: Required tools not found: " .. table.concat(missing, ", ")) + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Required tools not found: " .. table.concat(missing, ", ")) + end end return nil end @@ -193,10 +198,8 @@ local function extract_package_info_from_path(store_path, opt) 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 + if not drv_info and opt and (opt.verbose or option.get("verbose")) then + print("Nix: No derivation info found in JSON") end -- Extract package information @@ -246,15 +249,15 @@ local function extract_package_info_from_path(store_path, opt) 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"}} +local package_info = object {_init = {"name", "version"}} -function package_info:new(name) +function package_info:new(name, version) self._name = name + self._version = version self._includedirs = {} self._bindirs = {} self._linkdirs = {} @@ -262,9 +265,8 @@ function package_info:new(name) self._libfiles = {} self._store_paths = {} self._outputs = {} - self._version = nil self._pkgconfig_available = false - return package_info {self, name} + return package_info {self, name, version} end function package_info:add_store_path(p, output_name) @@ -294,12 +296,6 @@ 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 @@ -422,9 +418,6 @@ local function parse_store_paths_from_env(env_vars, opt) -- 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 @@ -432,9 +425,6 @@ local function parse_store_paths_from_env(env_vars, opt) 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) @@ -637,18 +627,12 @@ local function get_all_store_paths(opt) -- 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 @@ -697,6 +681,50 @@ local function path_matches_package(store_path, package_name) return path_name_lower:find(package_name_lower, 1, true) ~= nil end +local function group_paths_by_version(store_paths, package_name, opt) + local version_groups = {} -- { "package:version" -> [store_paths] } + + for _, store_path in ipairs(store_paths) do + if not store_path or store_path == "" then goto continue end + + -- Extract package info + local parsed_name, parsed_version, output_paths, current_output = extract_package_info_from_path(store_path, opt) + + if not parsed_name then + goto continue + end + + -- Only include matching package names (if filtering) + if package_name then + local name_matches = parsed_name:lower() == package_name:lower() + if not name_matches then + goto continue + end + end + + -- Create version key + local version_key = parsed_name .. ":" .. (parsed_version or "unknown") + + if not version_groups[version_key] then + version_groups[version_key] = { + name = parsed_name, + version = parsed_version, + paths = {} + } + end + + table.insert(version_groups[version_key].paths, { + path = store_path, + outputs = output_paths, + current_output = current_output + }) + + ::continue:: + end + + return version_groups +end + -- extract package information from store paths with caching local function extract_package_info(store_paths, package_name, opt) opt = opt or {} @@ -708,18 +736,12 @@ local function extract_package_info(store_paths, package_name, opt) -- 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 @@ -758,111 +780,93 @@ 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 -> package_info - - local function ensure_pkg(name) - if not name then - name = "<unknown>" - 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) + local version_groups = group_paths_by_version(filtered_paths, package_name, opt) - pkg:add_store_path(store_path, current_output) - if parsed_version then - pkg:set_version(parsed_version) - end + local packages = {} -- map: "package_name:version" -> package_info - -- 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 + for version_key, version_data in pairs(version_groups) do + local pkg_name = version_data.name + local pkg_version = version_data.version + local pkg = package_info:new(pkg_name, pkg_version) + + -- Process all store paths for this version only + for _, path_data in ipairs(version_data.paths) do + local store_path = path_data.path + local output_paths = path_data.outputs + local current_output = path_data.current_output + + pkg:add_store_path(store_path, current_output) + + -- 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 - 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) + -- 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 - end - -- bin - local bindir = path.join(store_path, "bin") - if os.isdir(bindir) then - pkg:add_bindir(bindir) - 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) + -- 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 - end - return files - end } or {} + 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 + 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 end - - ::continue:: + + packages[version_key] = pkg end -- finalize all package_info instances into plain tables local result = {} - for name, pkgobj in pairs(packages) do + for version_key, pkgobj in pairs(packages) do local plain = pkgobj:finalize() - result[name] = plain + result[version_key] = plain end -- cache result @@ -872,40 +876,40 @@ local function extract_package_info(store_paths, package_name, opt) if opt and (opt.verbose or option.get("verbose")) then local keys = table.keys(result) - print("Nix: Extracted " .. #keys .. " packages from store paths") + print("Nix: Extracted " .. #keys .. " package versions 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 function find_all_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, ";")) + local cache_key = package_name .. ":all:" .. (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) + local status = (type(memo) == "table" and #memo > 0) and "found" or "not found" + print("Nix: Using session cached pkg-config results (" .. status .. ") for: " .. package_name) end - return memo or nil + return (type(memo) == "table") and memo or {} 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) + local status = (type(cached_result) == "table" and #cached_result > 0) and "found" or "not found" + print("Nix: Using persistent cached pkg-config results (" .. status .. ") for: " .. package_name) end - memory_cache:set2(PKGCONFIG_CACHE, cache_key, cached_result) - return cached_result or nil + local result_table = (type(cached_result) == "table") and cached_result or {} + memory_cache:set2(PKGCONFIG_CACHE, cache_key, result_table) + return result_table end -- Filter store paths to only relevant ones @@ -922,7 +926,7 @@ local function find_with_pkgconfig(package_name, store_paths, opt) -- Add dependencies of matching packages filtered_paths = follow_propagated_inputs(filtered_paths, opt) - -- Search filtered paths + local all_results = {} for _, store_path in ipairs(filtered_paths) do local pkgconfig_dirs = { path.join(store_path, "lib", "pkgconfig"), @@ -932,22 +936,132 @@ local function find_with_pkgconfig(package_name, store_paths, opt) 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 result and result.version then + table.insert(all_results, result) if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Found package via pkg-config: " .. package_name) + print("Nix: Found pkg-config version: " .. package_name .. " " .. result.version) 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 + memory_cache:set2(PKGCONFIG_CACHE, cache_key, all_results) + cache:set2(PKGCONFIG_CACHE, cache_key, all_results) + + return all_results +end + +local function select_best_version(packages, package_name, require_version, opt) + -- Collect all versions of the target package + local candidates = {} + local name_lower = package_name:lower() + + for version_key, pkg_data in pairs(packages) do + local pkg_name = pkg_data.name or "" + if pkg_name:lower() == name_lower then + table.insert(candidates, pkg_data) + end + end + + -- Handle empty candidates + if not candidates or #candidates == 0 then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: No versions found for package: " .. package_name) + end + return nil + end + + -- If only one candidate, return it + if #candidates == 1 then + if opt and (opt.verbose or option.get("verbose")) then + local ver = candidates[1].version or "unknown" + print("Nix: Found single version: " .. package_name .. " " .. ver) + end + return candidates[1] + end + + -- Multiple candidates - select best one + local best_match = nil + local best_version = nil + + for _, candidate in ipairs(candidates) do + local pkg_version = candidate.version + + if not pkg_version then + -- No version info, use as fallback if nothing better found + if not best_match then + best_match = candidate + end + goto continue + end + + -- Check if version satisfies constraints + local satisfies = false + if not require_version or require_version == "latest" then + satisfies = true + else + satisfies = try { + function() + return semver.satisfies(pkg_version, require_version) + end, + catch = function() + -- If semver parsing fails, try exact match + return (pkg_version == require_version) + end + } + end + + if satisfies then + -- Try to parse version for comparison + local current_ver = try { + function() + return semver.new(pkg_version) + end, + catch = function() + return nil + end + } + + -- Select highest version among satisfying candidates + if not best_version then + best_match = candidate + best_version = current_ver + elseif current_ver and best_version then + -- Both versions are valid semver, compare them + if current_ver:gt(best_version) then + best_match = candidate + best_version = current_ver + end + elseif current_ver and not best_version then + -- Current has valid semver but previous didn't, prefer current + best_match = candidate + best_version = current_ver + end + end + + ::continue:: + end + + -- Log results if verbose + if opt and (opt.verbose or option.get("verbose")) then + if best_match then + local ver = best_match.version or "unknown" + local constraint_msg = (require_version and require_version ~= "") + and (" matching constraint: " .. require_version) or "" + print("Nix: Selected version: " .. package_name .. " " .. ver .. constraint_msg) + else + local constraint_msg = (require_version and require_version ~= "") + and (" matching constraint: " .. require_version) or "" + print("Nix: No version found for: " .. package_name .. constraint_msg) + end + end + + return best_match +end + +local function nonempty_string(v) + return type(v) == "string" and v ~= "" and v end -- find package using the nix package manager @@ -961,7 +1075,9 @@ function main(name, opt) -- 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()) - + + local require_version = nonempty_string(opt.require_version) or nonempty_string(opt.version) + -- Skip cross-compilation scenarios if is_cross(opt.plat, opt.arch) then return @@ -986,48 +1102,76 @@ function main(name, opt) return nil end - -- Try to find the package by name match first - local name_lower = name:lower() - local found_package = packages[name_lower] + -- Find best version from extracted packages + local found_package = select_best_version(packages, name, require_version, opt) - -- 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) + if not found_package then + if opt and (opt.verbose or option.get("verbose")) then + print("Nix: Package " .. name .. " not found or no matching version") + end + return nil + end + + local pkgconfig_results = find_all_with_pkgconfig(name, store_paths, opt) + local pkgconfig_result = select_best_version(pkgconfig_results, name, require_version, opt) + + -- Decide which result to use - prefer whichever has the highest version + local use_pkgconfig = false + if pkgconfig_result then + if not found_package.version then + -- found_package has no version, prefer pkg-config + use_pkgconfig = true + elseif not pkgconfig_result.version then + -- pkg-config has no version, prefer found_package + use_pkgconfig = false + else + -- Both have versions, compare them + local pkg_ver = try { + function() return semver.new(found_package.version) end + } + local pkgconfig_ver = try { + function() return semver.new(pkgconfig_result.version) end + } + + if pkg_ver and pkgconfig_ver then + -- Prefer whichever is higher + if pkgconfig_ver:gt(pkg_ver) then + use_pkgconfig = true + end + elseif pkgconfig_ver then + -- Only pkg-config has valid semver + use_pkgconfig = true end - return pkgconfig_result + -- else: only pkg_ver is valid or neither is valid, use found_package end end - -- If we found package info directly, return it - if found_package then + if use_pkgconfig and pkgconfig_result then + local ver = pkgconfig_result.version or "unknown" 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 + print("Nix: Returning pkg-config package: " .. name .. " (" .. ver .. ")") end - - return result + return pkgconfig_result end - -- Package not found + -- Return the selected version from extracted packages + local ver = found_package.version or "unknown" if opt and (opt.verbose or option.get("verbose")) then - print("Nix: Package " .. name .. " not found in any nix environment") + print("Nix: Returning package: " .. name .. " (" .. ver .. ")") end - return nil + 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
\ No newline at end of file diff --git a/xmake/modules/package/manager/nix/install_package.lua b/xmake/modules/package/manager/nix/install_package.lua index 507cf6d13..15084dfcd 100644 --- a/xmake/modules/package/manager/nix/install_package.lua +++ b/xmake/modules/package/manager/nix/install_package.lua @@ -14,7 +14,7 @@ -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- --- @author ruki +-- @author ZZBaron -- @file install_package.lua -- diff --git a/xmake/modules/package/manager/nix/search_package.lua b/xmake/modules/package/manager/nix/search_package.lua index b95978736..3ad66610c 100644 --- a/xmake/modules/package/manager/nix/search_package.lua +++ b/xmake/modules/package/manager/nix/search_package.lua @@ -14,7 +14,7 @@ -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- --- @author ruki +-- @author ZZBaron -- @file search_package.lua -- |
