Files
cargoxx/src/cli/cmd_build.cpp

244 lines
8.0 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 std::vector<linkdb::Recipe>& dev_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));
}
for (const auto& dep : m.dev_dependencies) {
root.dependencies.push_back(std::format("{} {}", dep.name, dep.version_spec));
}
lock.packages.push_back(std::move(root));
auto emit_dep = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
std::optional<std::string> rev;
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,
});
};
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
emit_dep(m.dependencies[i], recipes[i]);
}
for (std::size_t i = 0; i < m.dev_dependencies.size(); ++i) {
emit_dep(m.dev_dependencies[i], dev_recipes[i]);
}
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());
}
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
-> util::Result<std::vector<linkdb::Recipe>> {
std::vector<linkdb::Recipe> out;
out.reserve(deps.size());
for (const auto& dep : deps) {
auto r = db->resolve(dep.name, dep.version_spec, dep.components);
if (!r) {
return std::unexpected(r.error());
}
out.push_back(std::move(*r));
}
return out;
};
auto recipes = resolve_list(m->dependencies);
if (!recipes) {
return std::unexpected(recipes.error());
}
auto dev_recipes = resolve_list(m->dev_dependencies);
if (!dev_recipes) {
return std::unexpected(dev_recipes.error());
}
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);
}
}
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior);
codegen::GenerateInputs in{
.manifest = *m,
.layout = *layout_result,
.lock = lock,
.recipes = *recipes,
.dev_recipes = *dev_recipes,
.project_root = 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