Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "xpkg"
version = "0.0.45"
version = "0.0.46"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
160 changes: 144 additions & 16 deletions src/xpkg-index.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,94 @@ import std;

export namespace mcpplibs::xpkg {

namespace index_detail_ {

std::string namespace_of(std::string_view entryKey, const IndexEntry& entry) {
if (!entry.identity.namespaceName.empty()) {
return entry.identity.namespaceName;
}
auto colon = entryKey.find(':');
if (colon == std::string_view::npos) return {};
return std::string(entryKey.substr(0, colon));
}

std::string short_name_of(std::string_view entryKey, const IndexEntry& entry) {
if (!entry.identity.name.empty()) return entry.identity.name;
if (!entry.name.empty()) {
auto at = entry.name.find('@');
return at == std::string::npos ? entry.name : entry.name.substr(0, at);
}

auto colon = entryKey.find(':');
auto base = colon == std::string_view::npos
? entryKey
: entryKey.substr(colon + 1);
auto at = base.find('@');
return std::string(at == std::string_view::npos ? base : base.substr(0, at));
}

std::string canonical_name_of(std::string_view entryKey,
const IndexEntry& entry) {
if (!entry.canonicalName.empty()) return entry.canonicalName;
auto at = entryKey.find('@');
return std::string(at == std::string_view::npos
? entryKey
: entryKey.substr(0, at));
}

std::vector<std::string>
identity_entries(const PackageIndex& index, std::string_view canonicalName) {
if (auto it = index.identityEntries.find(std::string(canonicalName));
it != index.identityEntries.end()) {
return it->second;
}

std::vector<std::string> candidates;
auto prefix = std::string(canonicalName) + "@";
for (auto& [entryKey, entry] : index.entries) {
if (canonical_name_of(entryKey, entry) == canonicalName
|| entryKey.starts_with(prefix)) {
candidates.push_back(entryKey);
}
}
std::ranges::sort(candidates);
return candidates;
}

} // namespace index_detail_

std::vector<std::string>
find_candidates(const PackageIndex& index,
std::string_view name,
std::optional<std::string_view> namespaceName = std::nullopt) {
if (namespaceName) {
auto canonicalName = namespaceName->empty()
? std::string(name)
: std::string(*namespaceName) + ":" + std::string(name);
auto entries = index_detail_::identity_entries(index, canonicalName);
return entries.empty()
? std::vector<std::string> {}
: std::vector<std::string> { canonicalName };
}

if (auto it = index.shortNames.find(std::string(name));
it != index.shortNames.end()) {
return it->second;
}

std::vector<std::string> candidates;
for (auto& [entryKey, entry] : index.entries) {
if (index_detail_::short_name_of(entryKey, entry) == name) {
candidates.push_back(
index_detail_::canonical_name_of(entryKey, entry));
}
}
std::ranges::sort(candidates);
auto uniqueEnd = std::ranges::unique(candidates).begin();
candidates.erase(uniqueEnd, candidates.end());
return candidates;
}

// Fuzzy search: returns names of entries whose name or description contains
// query (case-insensitive). Results are sorted.
std::vector<std::string>
Expand All @@ -15,16 +103,22 @@ search(const PackageIndex& index, const std::string& query) {
std::transform(q.begin(), q.end(), q.begin(),
[](unsigned char c){ return std::tolower(c); });

for (auto& [name, entry] : index.entries) {
std::string n = name;
for (auto& [entryKey, entry] : index.entries) {
std::string n = entryKey;
std::transform(n.begin(), n.end(), n.begin(),
[](unsigned char c){ return std::tolower(c); });
std::string shortName = index_detail_::short_name_of(entryKey, entry);
std::transform(shortName.begin(), shortName.end(), shortName.begin(),
[](unsigned char c){ return std::tolower(c); });
std::string d = entry.description;
std::transform(d.begin(), d.end(), d.begin(),
[](unsigned char c){ return std::tolower(c); });

if (n.find(q) != std::string::npos || d.find(q) != std::string::npos)
results.push_back(name);
if (n.find(q) != std::string::npos
|| shortName.find(q) != std::string::npos
|| d.find(q) != std::string::npos) {
results.push_back(entryKey);
}
}
std::sort(results.begin(), results.end());
return results;
Expand All @@ -37,7 +131,16 @@ resolve(const PackageIndex& index, const std::string& name) {
auto it = index.entries.find(name);
if (it == index.entries.end())
return name;
return it->second.ref.empty() ? name : it->second.ref;
if (it->second.ref.empty()) return name;

auto ref = it->second.ref;
auto refAt = ref.find('@');
auto refHead = refAt == std::string::npos ? ref : ref.substr(0, refAt);
if (refHead.find(':') != std::string::npos) return ref;

auto namespaceName = index_detail_::namespace_of(name, it->second);
if (namespaceName.empty()) return ref;
return namespaceName + ":" + ref;
}

// Find the best-matching entry for a base package name.
Expand All @@ -49,13 +152,7 @@ match_version(const PackageIndex& index, const std::string& name) {
if (index.entries.count(name))
return name;

// Collect all versioned entries whose name@ prefix matches
std::string prefix = name + "@";
std::vector<std::string> candidates;
for (auto& [key, entry] : index.entries) {
if (key.starts_with(prefix))
candidates.push_back(key);
}
auto candidates = index_detail_::identity_entries(index, name);
if (candidates.empty())
return std::nullopt;

Expand Down Expand Up @@ -92,16 +189,47 @@ mutex_packages(const PackageIndex& index, const std::string& pkg_name) {
PackageIndex
merge(PackageIndex base, const PackageIndex& overlay,
const std::string& namespace_ = "") {
for (auto& [name, entry] : overlay.entries) {
std::string key = namespace_.empty() ? name : namespace_ + "-x-" + name;
for (auto& [entryKey, entry] : overlay.entries) {
auto e = entry;
e.name = key;
base.entries[key] = std::move(e);
auto shortName = index_detail_::short_name_of(entryKey, e);
if (e.identity.name.empty()) e.identity.name = shortName;
if (e.identity.namespaceName.empty()) {
e.identity.namespaceName = namespace_;
}
if (e.version.empty()) {
auto versionSeparator = entryKey.find('@');
if (versionSeparator != std::string::npos) {
e.version = entryKey.substr(versionSeparator + 1);
}
}
auto canonicalName = e.identity.canonical_name();
auto suffix = e.version.empty() ? std::string {} : "@" + e.version;
auto key = canonicalName + suffix;
e.canonicalName = canonicalName;
e.entryKey = key;
e.name = shortName;
if (base.entries.contains(key)) {
throw std::invalid_argument(
"duplicate package identity in index merge: '" + key + "'");
}
base.entries.emplace(key, std::move(e));
base.identityEntries[canonicalName].push_back(key);
base.shortNames[shortName].push_back(canonicalName);
}
for (auto& [gkey, gmembers] : overlay.mutex_groups) {
auto& dest = base.mutex_groups[gkey];
dest.insert(dest.end(), gmembers.begin(), gmembers.end());
}
for (auto& [_, candidates] : base.identityEntries) {
std::ranges::sort(candidates);
auto uniqueEnd = std::ranges::unique(candidates).begin();
candidates.erase(uniqueEnd, candidates.end());
}
for (auto& [_, candidates] : base.shortNames) {
std::ranges::sort(candidates);
auto uniqueEnd = std::ranges::unique(candidates).begin();
candidates.erase(uniqueEnd, candidates.end());
}
return base;
}

Expand Down
64 changes: 52 additions & 12 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ load_package(const fs::path& pkg_path) {
}

std::expected<PackageIndex, std::string>
build_index(const fs::path& repo_dir, const std::string& namespace_ = "") {
build_index(const fs::path& repo_dir, const std::string& defaultNamespace = "") {
PackageIndex index;
auto pkgs_dir = repo_dir / "pkgs";
if (!fs::is_directory(pkgs_dir))
Expand All @@ -691,23 +691,63 @@ build_index(const fs::path& repo_dir, const std::string& namespace_ = "") {
// Run pkgindex-build.lua if present (generates complete package files)
loader_detail::run_pkgindex_build(repo_dir);

std::vector<fs::path> packagePaths;
for (auto& letter_dir : fs::directory_iterator(pkgs_dir)) {
if (!letter_dir.is_directory()) continue;
for (auto& entry : fs::directory_iterator(letter_dir)) {
if (entry.path().extension() != ".lua") continue;
auto result = load_package(entry.path());
if (!result) continue; // skip malformed packages
auto& pkg = *result;
std::string key = (namespace_.empty() ? "" : namespace_ + "-x-")
+ pkg.name;
IndexEntry ie;
ie.name = key;
ie.path = entry.path();
ie.type = pkg.type;
ie.description = pkg.description;
index.entries[key] = std::move(ie);
packagePaths.push_back(entry.path().lexically_normal());
}
}
std::ranges::sort(packagePaths);

for (auto& packagePath : packagePaths) {
auto result = load_package(packagePath);
if (!result) continue; // skip malformed packages
auto& pkg = *result;

PackageIdentity identity {
.namespaceName = pkg.namespace_.empty()
? defaultNamespace
: pkg.namespace_,
.name = pkg.name,
};
auto canonicalName = identity.canonical_name();

IndexEntry indexEntry;
indexEntry.identity = std::move(identity);
indexEntry.canonicalName = canonicalName;
indexEntry.entryKey = canonicalName;
indexEntry.name = pkg.name;
indexEntry.path = packagePath;
indexEntry.type = pkg.type;
indexEntry.description = pkg.description;

auto existing = index.entries.find(indexEntry.entryKey);
if (existing != index.entries.end()) {
return std::unexpected(std::format(
"duplicate package identity '{}': '{}' conflicts with '{}'",
canonicalName,
existing->second.path.string(),
packagePath.string()));
}

index.entries.emplace(indexEntry.entryKey, std::move(indexEntry));
index.identityEntries[canonicalName].push_back(canonicalName);
index.shortNames[pkg.name].push_back(canonicalName);
}

for (auto& [_, candidates] : index.identityEntries) {
std::ranges::sort(candidates);
auto uniqueEnd = std::ranges::unique(candidates).begin();
candidates.erase(uniqueEnd, candidates.end());
}
for (auto& [_, candidates] : index.shortNames) {
std::ranges::sort(candidates);
auto uniqueEnd = std::ranges::unique(candidates).begin();
candidates.erase(uniqueEnd, candidates.end());
}

return index;
}

Expand Down
19 changes: 18 additions & 1 deletion src/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,18 @@ struct Package {
~Package();
};

struct PackageIdentity {
std::string namespaceName;
std::string name;

std::string canonical_name() const;
};

struct IndexEntry {
std::string name; // e.g. "vscode@1.85.0"
PackageIdentity identity;
std::string canonicalName;
std::string entryKey;
std::string name; // descriptor package.name
std::string version;
std::filesystem::path path;
PackageType type = PackageType::Package;
Expand All @@ -143,6 +153,8 @@ struct IndexEntry {

struct PackageIndex {
std::unordered_map<std::string, IndexEntry> entries;
std::unordered_map<std::string, std::vector<std::string>> identityEntries;
std::unordered_map<std::string, std::vector<std::string>> shortNames;
std::unordered_map<std::string, std::vector<std::string>> mutex_groups;
~PackageIndex();
};
Expand Down Expand Up @@ -172,6 +184,11 @@ Package::~Package() = default;
PackageIndex::~PackageIndex() = default;
IndexRepos::~IndexRepos() = default;

std::string PackageIdentity::canonical_name() const {
if (namespaceName.empty()) return name;
return namespaceName + ":" + name;
}

std::string normalize_arch(std::string_view raw) {
std::string s;
s.reserve(raw.size());
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/pkgindex-duplicate/pkgs/a/implicit.demo.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package = {
spec = "1",
name = "demo",
description = "implicit alpha demo",
type = "package",
}
7 changes: 7 additions & 0 deletions tests/fixtures/pkgindex-duplicate/pkgs/b/explicit.demo.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package = {
spec = "1",
namespace = "alpha",
name = "demo",
description = "explicit alpha demo",
type = "package",
}
7 changes: 7 additions & 0 deletions tests/fixtures/pkgindex-namespaces/pkgs/a/alpha.demo.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package = {
spec = "1",
namespace = "alpha",
name = "demo",
description = "alpha demo",
type = "package",
}
7 changes: 7 additions & 0 deletions tests/fixtures/pkgindex-namespaces/pkgs/b/beta.demo.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package = {
spec = "1",
namespace = "beta",
name = "demo",
description = "beta demo",
type = "package",
}
Loading
Loading