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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# xmake
.xmake
build
target/
compile_commands.json

# xlings
.xlings
Expand Down
8 changes: 8 additions & 0 deletions mcpp.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Auto-generated by mcpp. Do not edit by hand.
version = 2

[package."mcpplibs.capi.lua"]
namespace = "mcpplibs.capi"
version = "0.0.3"
source = "index+mcpplibs.capi@0.0.3"
hash = "fnv1a:c8e7ef9d60a1a3a6"
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.42"
version = "0.0.43"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
161 changes: 161 additions & 0 deletions src/xpkg-compat.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
module;

export module mcpplibs.xpkg.compat;
import mcpplibs.xpkg;
import std;

export namespace mcpplibs::xpkg {

enum class SourceKind {
None,
ExplicitUrl,
XlingsRes,
UrlTemplate,
};

struct ResourceContext {
std::string name;
std::string version;
std::string platform;
std::string arch;
std::string ext;
};

struct ResolvedResource {
SourceKind kind = SourceKind::None;
std::string version;
std::string url;
std::string sha256;
std::unordered_map<std::string, std::string> mirrors;
};

std::expected<ResolvedResource, std::string> resolve_resource(
const PlatformMatrix& matrix,
const ResourceContext& context);

} // namespace mcpplibs::xpkg

namespace mcpplibs::xpkg {
namespace {

bool is_xlings_res(std::string_view value) {
return value == "xlings-res" || value == "XLINGS_RES";
}

std::string replace_all(
std::string value,
std::string_view needle,
std::string_view replacement) {
std::size_t position = 0;
while ((position = value.find(needle, position)) != std::string::npos) {
value.replace(position, needle.size(), replacement);
position += replacement.size();
}
return value;
}

std::string expand_template(
std::string value,
const ResourceContext& context,
std::string_view version,
std::string_view arch,
std::string_view arch_alias) {
auto ext = context.ext;
if (ext.empty())
ext = context.platform == "windows" ? "zip" : "tar.gz";
value = replace_all(std::move(value), "${name}", context.name);
value = replace_all(std::move(value), "${version}", version);
value = replace_all(std::move(value), "${os}", context.platform);
value = replace_all(std::move(value), "${arch}", arch);
value = replace_all(std::move(value), "${arch_alias}", arch_alias);
return replace_all(std::move(value), "${ext}", ext);
}

} // namespace

std::expected<ResolvedResource, std::string> resolve_resource(
const PlatformMatrix& matrix,
const ResourceContext& context) {
auto platform_it = matrix.entries.find(context.platform);
if (platform_it == matrix.entries.end())
return std::unexpected("unsupported platform: " + context.platform);

const auto& versions = platform_it->second;
auto version_it = versions.find(context.version);
if (version_it == versions.end())
return std::unexpected("unknown version: " + context.version);

std::unordered_set<std::string> visited;
std::string resolved_version = context.version;
const PlatformResource* resource = &version_it->second;
while (!resource->ref.empty()) {
if (!visited.insert(resolved_version).second)
return std::unexpected("resource ref cycle at version: " + resolved_version);
resolved_version = resource->ref;
version_it = versions.find(resolved_version);
if (version_it == versions.end())
return std::unexpected("resource ref target not found: " + resolved_version);
resource = &version_it->second;
}

const auto arch = normalize_arch(context.arch);
const auto arch_it = resource->archs.find(arch);
const ArchResource* arch_resource =
arch_it == resource->archs.end() ? nullptr : &arch_it->second;

ResolvedResource result;
result.version = resolved_version;
if (arch_resource != nullptr) {
result.url = arch_resource->url;
result.sha256 = arch_resource->sha256;
result.mirrors = arch_resource->mirrors;
} else {
result.url = resource->url;
result.sha256 = resource->sha256;
result.mirrors = resource->mirrors;
if (auto hash = resource->sha256_by_arch.find(arch);
hash != resource->sha256_by_arch.end())
result.sha256 = hash->second;
}

auto arch_alias = arch;
if (auto alias = resource->arch_alias.find(arch);
alias != resource->arch_alias.end())
arch_alias = alias->second;

std::string source;
if (auto source_it = matrix.platform_sources.find(context.platform);
source_it != matrix.platform_sources.end())
source = source_it->second;
else
source = matrix.source;

if (result.url.empty() && (resource->is_res || is_xlings_res(source))) {
result.kind = SourceKind::XlingsRes;
} else if (is_xlings_res(result.url)) {
result.kind = SourceKind::XlingsRes;
result.url.clear();
} else {
if (result.url.empty())
result.url = source;
if (!result.url.empty()) {
const bool templated = result.url.find("${") != std::string::npos;
result.kind = templated ? SourceKind::UrlTemplate
: SourceKind::ExplicitUrl;
result.url = expand_template(
std::move(result.url), context, resolved_version, arch, arch_alias);
}
}

for (auto& [region, url] : result.mirrors)
url = expand_template(
std::move(url), context, resolved_version, arch, arch_alias);

if (result.kind == SourceKind::None)
return std::unexpected(
"resource has neither url nor source for " + context.platform
+ "@" + resolved_version);
return result;
}

} // namespace mcpplibs::xpkg
15 changes: 13 additions & 2 deletions src/xpkg-loader.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,26 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) {

// Iterate platforms
int xpm_idx = lua::gettop(L);

// `source` is additive metadata: it supplies a default resource origin
// without changing the existing platform/version matrix.
xpm.source = get_str(L, xpm_idx, "source");

lua::pushnil(L);
while (lua::next(L, xpm_idx)) {
// key = platform name (at -2), value = version table (at -1)
std::string platform;
if (lua::type(L, -2) == lua::TSTRING)
platform = lua::tostring(L, -2);

if (!platform.empty() && lua::type(L, -1) == lua::TTABLE) {
if (!platform.empty() && platform != "source"
&& lua::type(L, -1) == lua::TTABLE) {
int plat_idx = lua::gettop(L);

auto platform_source = get_str(L, plat_idx, "source");
if (!platform_source.empty())
xpm.platform_sources[platform] = std::move(platform_source);

// Parse deps. Two accepted shapes:
//
// Legacy array form:
Expand Down Expand Up @@ -305,7 +315,8 @@ PlatformMatrix parse_xpm(lua::State* L, int pkg_idx) {
// (parsed above) that would otherwise leak in as a bogus
// version entry; `deps`/`inherits` likewise.
if (!version.empty() && version != "deps"
&& version != "inherits" && version != "exports") {
&& version != "inherits" && version != "exports"
&& version != "source") {
PlatformResource res;
if (lua::type(L, -1) == lua::TTABLE) {
int res_idx = lua::gettop(L);
Expand Down
5 changes: 5 additions & 0 deletions src/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ struct ExportsBlock {
};

struct PlatformMatrix {
// Optional resource defaults. `source` applies to every platform while a
// platform-specific value overrides it. Values are either "xlings-res"
// or a URL template; version entries keep the existing model unchanged.
std::string source;
std::unordered_map<std::string, std::string> platform_sources;
// platform -> version -> resource
std::unordered_map<std::string,
std::unordered_map<std::string, PlatformResource>> entries;
Expand Down
20 changes: 20 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2source_res.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package = {
spec = "2",
name = "v2source-res",
archs = {"x86_64", "aarch64"},
xpm = {
source = "xlings-res",
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
sha256 = {
x86_64 = "linux-amd64-hash",
aarch64 = "linux-arm64-hash",
},
},
},
macosx = {
["1.0.0"] = {},
},
},
}
27 changes: 27 additions & 0 deletions tests/fixtures/pkgindex/pkgs/v/v2source_template.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package = {
spec = "2",
name = "v2source-template",
xpm = {
source = "https://example.test/${name}/${version}/${name}-${os}-${arch}.${ext}",
linux = {
source = "https://linux.example.test/${version}/tool-${arch_alias}.tar.xz",
["1.0.0"] = {
sha256 = {
x86_64 = "linux-amd64-hash",
aarch64 = "linux-arm64-hash",
},
arch_alias = {
x86_64 = "amd64",
aarch64 = "arm64",
},
},
["custom"] = {
url = "https://override.test/custom.tar.gz",
sha256 = "custom-hash",
},
},
windows = {
["1.0.0"] = {},
},
},
}
Loading
Loading