224 lines
7.5 KiB
C++
224 lines
7.5 KiB
C++
module cargoxx.cli;
|
|
|
|
import std;
|
|
import cargoxx.util;
|
|
import cargoxx.manifest;
|
|
import cargoxx.layout;
|
|
import cargoxx.linkdb;
|
|
import cargoxx.lockfile;
|
|
import cargoxx.codegen;
|
|
import cargoxx.exec;
|
|
|
|
namespace cargoxx::cli {
|
|
|
|
namespace {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
auto io_error(std::string msg, fs::path path) -> util::Error {
|
|
return util::Error{
|
|
util::ErrorCode::Internal, std::move(msg), "", std::move(path), std::nullopt,
|
|
};
|
|
}
|
|
|
|
auto write_text(const fs::path& path, std::string_view content) -> util::Result<void> {
|
|
std::ofstream out{path};
|
|
if (!out) {
|
|
return std::unexpected(
|
|
io_error(std::format("cannot open for writing: {}", path.string()), path));
|
|
}
|
|
out << content;
|
|
if (!out) {
|
|
return std::unexpected(io_error(std::format("write failed: {}", path.string()), path));
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// Builds the lockfile from the manifest + resolved recipes, **preserving**
|
|
// `nixpkgs_rev` for any (name, version) entry that already exists in
|
|
// `prior` with a matching key. This is what makes `cargoxx build`
|
|
// idempotent w.r.t. the lockfile — concrete pins written by
|
|
// `cargoxx add <pkg>@<ver>` survive subsequent rebuilds. New deps and
|
|
// version bumps get a null rev (today's behaviour); the dep tracks the
|
|
// shared `nixos-unstable` input until a future `cargoxx update`
|
|
// repopulates it.
|
|
auto merge_lockfile(const manifest::Manifest& m,
|
|
const std::vector<linkdb::Recipe>& recipes,
|
|
const lockfile::Lockfile& prior) -> lockfile::Lockfile {
|
|
auto find_prior = [&](const std::string& name, const std::string& version)
|
|
-> std::optional<lockfile::LockfilePackage> {
|
|
for (const auto& p : prior.packages) {
|
|
if (p.name == name && p.version == version) {
|
|
return p;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
lockfile::Lockfile lock;
|
|
lock.version = 1;
|
|
|
|
lockfile::LockfilePackage root{
|
|
.name = m.package.name,
|
|
.version = m.package.version,
|
|
.dependencies = {},
|
|
.nixpkgs_attr = std::nullopt,
|
|
.nixpkgs_rev = std::nullopt,
|
|
.linkdb_source = std::nullopt,
|
|
};
|
|
for (const auto& dep : m.dependencies) {
|
|
root.dependencies.push_back(std::format("{} {}", dep.name, dep.version_spec));
|
|
}
|
|
lock.packages.push_back(std::move(root));
|
|
|
|
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
|
const auto& dep = m.dependencies[i];
|
|
const auto& rec = recipes[i];
|
|
std::optional<std::string> rev;
|
|
// The recipe's nixpkgs_attr is correct for unpinned deps (it's
|
|
// curated against nixos-unstable). When the prior lockfile
|
|
// already carries an attr — written by `cargoxx add <pkg>@<v>`
|
|
// from devbox's authoritative attr_paths for the pinned rev —
|
|
// that one wins.
|
|
std::string attr = rec.nixpkgs_attr;
|
|
if (auto p = find_prior(dep.name, dep.version_spec); p) {
|
|
rev = p->nixpkgs_rev;
|
|
if (p->nixpkgs_attr && !p->nixpkgs_attr->empty()) {
|
|
attr = *p->nixpkgs_attr;
|
|
}
|
|
}
|
|
lock.packages.push_back(lockfile::LockfilePackage{
|
|
.name = dep.name,
|
|
.version = dep.version_spec,
|
|
.dependencies = {},
|
|
.nixpkgs_attr = std::move(attr),
|
|
.nixpkgs_rev = std::move(rev),
|
|
.linkdb_source = rec.source,
|
|
});
|
|
}
|
|
return lock;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
auto run_nix_cmake(const fs::path& project_root, const std::vector<std::string>& cmake_args,
|
|
std::string_view phase) -> util::Result<void> {
|
|
std::vector<std::string> args{"develop", "--command", "cmake"};
|
|
args.insert(args.end(), cmake_args.begin(), cmake_args.end());
|
|
|
|
auto r = exec::run("nix", args, exec::ExecOptions{
|
|
.cwd = project_root,
|
|
.env_overrides = {},
|
|
.timeout = std::nullopt,
|
|
.inherit_stdio = true,
|
|
});
|
|
if (!r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
if (r->exit_code != 0) {
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::BuildCmakeFailed,
|
|
std::format("cmake {} failed (exit {})", phase, r->exit_code),
|
|
"", std::nullopt, std::nullopt,
|
|
});
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|
std::optional<std::string> target,
|
|
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
|
auto manifest_path = project_root / "Cargoxx.toml";
|
|
auto m = manifest::parse(manifest_path);
|
|
if (!m) {
|
|
return std::unexpected(m.error());
|
|
}
|
|
|
|
auto layout_result = layout::discover(project_root, m->package.name);
|
|
if (!layout_result) {
|
|
return std::unexpected(layout_result.error());
|
|
}
|
|
|
|
auto db = linkdb::Database::open(std::move(overlay_path));
|
|
if (!db) {
|
|
return std::unexpected(db.error());
|
|
}
|
|
|
|
std::vector<linkdb::Recipe> recipes;
|
|
recipes.reserve(m->dependencies.size());
|
|
for (const auto& dep : m->dependencies) {
|
|
auto r = db->resolve(dep.name, dep.version_spec, dep.components);
|
|
if (!r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
recipes.push_back(std::move(*r));
|
|
}
|
|
|
|
lockfile::Lockfile prior;
|
|
if (std::error_code ec; std::filesystem::exists(project_root / "Cargoxx.lock", ec)) {
|
|
if (auto r = lockfile::parse(project_root / "Cargoxx.lock"); r) {
|
|
prior = std::move(*r);
|
|
}
|
|
// A malformed prior lockfile is non-fatal — we just rebuild from
|
|
// scratch. The user can re-pin by running `cargoxx add` again.
|
|
}
|
|
auto lock = merge_lockfile(*m, recipes, prior);
|
|
|
|
codegen::GenerateInputs in{*m, *layout_result, lock, recipes, project_root};
|
|
auto flake_text = codegen::flake_nix(in);
|
|
auto cmake_text = codegen::cmake_lists(in);
|
|
|
|
std::error_code ec;
|
|
fs::create_directories(project_root / "build", ec);
|
|
if (ec) {
|
|
return std::unexpected(io_error(
|
|
std::format("cannot create build directory: {}", ec.message()),
|
|
project_root / "build"));
|
|
}
|
|
|
|
if (auto r = write_text(project_root / "flake.nix", flake_text); !r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
if (auto r = write_text(project_root / "build" / "CMakeLists.txt", cmake_text); !r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
if (auto r = lockfile::write(lock, project_root / "Cargoxx.lock"); !r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
|
|
if (no_build) {
|
|
return {};
|
|
}
|
|
|
|
const std::string profile = release ? "release" : "debug";
|
|
const std::string profile_cap = release ? "Release" : "Debug";
|
|
const auto build_dir = std::format("build/{}", profile);
|
|
|
|
std::vector<std::string> configure_args{
|
|
"-B", build_dir,
|
|
"-S", "build",
|
|
"-G", "Ninja",
|
|
std::format("-DCMAKE_BUILD_TYPE={}", profile_cap),
|
|
};
|
|
if (auto r = run_nix_cmake(project_root, configure_args, "configure"); !r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
|
|
std::vector<std::string> build_args{"--build", build_dir};
|
|
if (target) {
|
|
build_args.push_back("--target");
|
|
build_args.push_back(*target);
|
|
}
|
|
if (auto r = run_nix_cmake(project_root, build_args, "build"); !r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
} // namespace cargoxx::cli
|