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 { 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 @` 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& recipes, const lockfile::Lockfile& prior) -> lockfile::Lockfile { auto find_prior = [&](const std::string& name, const std::string& version) -> std::optional { 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 rev; if (auto p = find_prior(dep.name, dep.version_spec); p) { rev = p->nixpkgs_rev; } lock.packages.push_back(lockfile::LockfilePackage{ .name = dep.name, .version = dep.version_spec, .dependencies = {}, .nixpkgs_attr = rec.nixpkgs_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& cmake_args, std::string_view phase) -> util::Result { std::vector 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 target, std::optional overlay_path) -> util::Result { 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 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 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 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