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
269 changes: 269 additions & 0 deletions src/pm/publisher.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// mcpp.pm.publisher — generate xpkg Lua entry from mcpp.toml + scanner.
//
// See docs/04-schema-xpkg-extension.md for the produced layout.

module;
#include <cstdio> // popen / pclose / fgets

export module mcpp.pm.publisher;

import std;
import mcpp.manifest;
import mcpp.modgraph.graph;

export namespace mcpp::pm {

struct ReleaseInfo {
std::string version; // tag/version, e.g. "0.1.0"

struct PerPlatform {
std::string url;
std::string sha256;
};
PerPlatform linux;
PerPlatform macosx;
PerPlatform windows;
};

// Generate the xpkg Lua content for a package.
std::string emit_xpkg(const mcpp::manifest::Manifest& manifest,
const mcpp::modgraph::Graph& graph,
const ReleaseInfo& release);

// Convenience: synthesize a placeholder ReleaseInfo for `mcpp emit xpkg --version V`
// before publish infrastructure exists. Uses {url, sha256} sentinels.
ReleaseInfo placeholder_release(std::string_view version);

// Compute the convention-based GitHub Release tarball URL for a package:
// "<repo>/releases/download/v<version>/<name>-<version>.tar.gz"
// Returns empty string if `repo` is empty or doesn't look like a https URL.
std::string release_tarball_url(std::string_view repo,
std::string_view name,
std::string_view version);

// Compute SHA-256 of `file` by shelling out to `sha256sum` (universally
// available on Linux). Returns empty string on failure.
std::string sha256_of_file(const std::filesystem::path& file);

// Pack the package source tree at `root` into a tarball at `output` using
// `git archive` (so .gitignore'd files are excluded automatically). The
// tarball uses prefix "<name>-<version>/" so unpacking yields a clean
// versioned directory.
//
// Requires the project to be in a git repo.
//
// Returns a non-empty error message on failure (empty on success).
std::string make_release_tarball(const std::filesystem::path& root,
std::string_view name,
std::string_view version,
const std::filesystem::path& output);

// Convenience: build a real ReleaseInfo for v0.0.3-style local publish
// where all three platforms point at the same source tarball. Caller has
// already produced the tarball + sha256 by other means.
ReleaseInfo make_release_info(std::string_view version,
std::string_view url,
std::string_view sha256);

} // namespace mcpp::pm

namespace mcpp::pm {

namespace {

// Quote `s` as a Lua double-quoted string literal: `"..."`.
//
// We deliberately use `"..."` (not the long-bracket `[[...]]` form)
// so the only meta-characters that need escaping are the standard set
// for `"` strings:
// - `"` and `\\` must be backslash-escaped
// - newline / carriage-return / NUL break the string literal
// - other control bytes are escaped numerically (`\xHH`) for safety
// so the emitted .lua is purely printable ASCII even when the input
// contains exotic bytes
//
// Long-bracket sequences like `]=]` are NOT a vector here because we
// never emit `[[`/`]=]` ourselves — the output is always `"..."`.
std::string lua_escape(std::string_view s) {
std::string out;
out.reserve(s.size() + 2);
out.push_back('"');
for (unsigned char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
case 0: out += "\\0"; break;
default:
if (c < 0x20 || c == 0x7f) {
// Other C0 controls + DEL — emit as \xHH to keep the
// .lua text purely printable.
char buf[5];
std::snprintf(buf, sizeof(buf), "\\x%02x", c);
out += buf;
} else {
out.push_back(static_cast<char>(c));
}
break;
}
}
out.push_back('"');
return out;
}

std::string platform_block(std::string_view version, const ReleaseInfo::PerPlatform& pp) {
return std::format(
" ['{0}'] = {{ url = {1}, sha256 = {2} }},\n",
version, lua_escape(pp.url), lua_escape(pp.sha256));
}

} // namespace

std::string emit_xpkg(const mcpp::manifest::Manifest& manifest,
const mcpp::modgraph::Graph& graph,
const ReleaseInfo& release)
{
std::string out;
out += "-- AUTO-GENERATED by `mcpp emit xpkg`. Do not edit by hand.\n";
out += std::format("-- Source: mcpp.toml @ v{}\n", release.version);
out += "package = {\n";
out += " spec = \"1\",\n";
out += std::format(" name = {},\n", lua_escape(manifest.package.name));
if (!manifest.package.description.empty())
out += std::format(" description = {},\n", lua_escape(manifest.package.description));
if (!manifest.package.license.empty())
out += std::format(" licenses = {{{}}},\n", lua_escape(manifest.package.license));
if (!manifest.package.repo.empty())
out += std::format(" repo = {},\n", lua_escape(manifest.package.repo));
out += " type = \"package\",\n\n";

out += " xpm = {\n";
out += " linux = {\n" + platform_block(release.version, release.linux) + " },\n";
out += " macosx = {\n" + platform_block(release.version, release.macosx) + " },\n";
out += " windows = {\n" + platform_block(release.version, release.windows) + " },\n";
out += " },\n\n";

out += " mcpp = {\n";
out += " schema = \"0.1\",\n";
out += std::format(" language = {},\n", lua_escape(manifest.language.standard));
out += std::format(" import_std = {},\n", manifest.language.importStd ? "true" : "false");

// Module list (from scanner)
out += " modules = {\n";
for (auto& u : graph.units) {
if (!u.provides) continue;
// Skip partition-only units: their logical name contains ':'
if (u.provides->logicalName.find(':') != std::string::npos) continue;
out += std::format(" {},\n", lua_escape(u.provides->logicalName));
}
out += " },\n";

// Dependencies (excluding dev-dependencies). Path-based deps are
// local-only and intentionally not exposed in the published xpkg
// descriptor; only version-based deps are emitted.
out += " deps = {\n";
for (auto& [k, v] : manifest.dependencies) {
if (v.isPath() || v.version.empty()) continue;
out += std::format(" [{}] = {},\n", lua_escape(k), lua_escape(v.version));
}
out += " },\n";

out += " manifest = \"mcpp.toml\",\n";
out += " },\n";
out += "}\n";
return out;
}

ReleaseInfo placeholder_release(std::string_view version) {
ReleaseInfo r;
r.version = std::string(version);
auto fill = [&](ReleaseInfo::PerPlatform& pp, std::string_view ext) {
pp.url = std::format("<TBD: release tarball URL>.{}", ext);
pp.sha256 = "<TBD: sha256>";
};
fill(r.linux, "tar.gz");
fill(r.macosx, "tar.gz");
fill(r.windows, "zip");
return r;
}

std::string release_tarball_url(std::string_view repo,
std::string_view name,
std::string_view version)
{
// Strip trailing ".git" if present.
std::string r{repo};
if (r.ends_with(".git")) r.resize(r.size() - 4);
if (r.empty()) return {};
if (!r.starts_with("https://") && !r.starts_with("http://")) return {};
return std::format("{}/releases/download/v{}/{}-{}.tar.gz",
r, version, name, version);
}

std::string sha256_of_file(const std::filesystem::path& file) {
if (!std::filesystem::exists(file)) return {};
auto cmd = std::format("sha256sum '{}' 2>/dev/null", file.string());
std::FILE* fp = ::popen(cmd.c_str(), "r");
if (!fp) return {};
std::array<char, 256> buf{};
std::string out;
while (std::fgets(buf.data(), buf.size(), fp))
out += buf.data();
int rc = ::pclose(fp);
if (rc != 0) return {};
// sha256sum format: "<64-hex> <filename>\n"
auto sp = out.find(' ');
if (sp == std::string::npos || sp != 64) return {};
return out.substr(0, 64);
}

std::string make_release_tarball(const std::filesystem::path& root,
std::string_view name,
std::string_view version,
const std::filesystem::path& output)
{
std::error_code ec;
std::filesystem::create_directories(output.parent_path(), ec);

auto cmd = std::format(
"git -C '{}' archive --format=tar.gz "
"--prefix='{}-{}/' "
"-o '{}' HEAD 2>&1",
root.string(), name, version, output.string());
std::FILE* fp = ::popen(cmd.c_str(), "r");
if (!fp) return std::format("popen failed for git archive: {}", cmd);

std::array<char, 4096> buf{};
std::string err;
while (std::fgets(buf.data(), buf.size(), fp))
err += buf.data();
int rc = ::pclose(fp);
if (rc != 0) {
return std::format("git archive failed (rc={}): {}", rc, err);
}
if (!std::filesystem::exists(output)) {
return std::format("git archive exited 0 but no tarball at '{}'",
output.string());
}
return {};
}

ReleaseInfo make_release_info(std::string_view version,
std::string_view url,
std::string_view sha256)
{
ReleaseInfo r;
r.version = std::string(version);
auto fill = [&](ReleaseInfo::PerPlatform& pp) {
pp.url = std::string(url);
pp.sha256 = std::string(sha256);
};
fill(r.linux);
fill(r.macosx);
fill(r.windows);
return r;
}

} // namespace mcpp::pm
Loading
Loading