diff --git a/src/codegen/flake.cpp b/src/codegen/flake.cpp index 473c1c5..f0de7e5 100644 --- a/src/codegen/flake.cpp +++ b/src/codegen/flake.cpp @@ -18,6 +18,7 @@ struct DepBinding { std::string sanitized; // "nixpkgs_fmt_10_2_1" — input attr, // let-binding stem, lambda param std::optional rev; // pinned commit (null → unpinned) + bool libcxx_override = false; // wrap in .override { stdenv = libcxx } }; // Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe @@ -76,6 +77,7 @@ auto build_bindings(const GenerateInputs& in) -> std::vector { .nixpkgs_attr = std::move(attr), .sanitized = sanitize_input_attr(dep.name, dep.version_spec), .rev = std::move(ref.rev), + .libcxx_override = rec.requires_libcxx_override, }; out.push_back(std::move(b)); } @@ -138,20 +140,36 @@ auto emit_let_bindings(const std::vector& pinned) return out; } +auto base_expr(const DepBinding& b) -> std::string { + return b.rev && !b.rev->empty() + ? std::format("pkgs_{}.{}", b.sanitized, b.nixpkgs_attr) + : std::format("pkgs.{}", b.nixpkgs_attr); +} + auto emit_build_input_line(const DepBinding& b) -> std::string { - if (b.rev && !b.rev->empty()) { - return std::format(" pkgs_{}.{}\n", b.sanitized, b.nixpkgs_attr); + auto expr = base_expr(b); + if (b.libcxx_override) { + // Stdenv swap (libc++) plus a `doCheck = false` overrideAttrs + // to skip the package's test suite — without it, the rebuilt + // dep would re-run its full check phase under the new stdenv, + // adding minutes to hours of evaluation/build time. + expr = std::format( + "(({}.override {{ stdenv = llvmPkgs.libcxxStdenv; }})" + ".overrideAttrs (old: {{ doCheck = false; }}))", + expr); } - return std::format(" pkgs.{}\n", b.nixpkgs_attr); + return std::format(" {}\n", expr); } auto emit_build_inputs(const std::vector& bindings) -> std::string { std::set seen; std::string out; for (const auto& b : bindings) { - auto key = b.rev && !b.rev->empty() - ? std::format("pkgs_{}.{}", b.sanitized, b.nixpkgs_attr) - : std::format("pkgs.{}", b.nixpkgs_attr); + // Dedupe by base expression — two deps that resolve to the same + // (set, attr) but differ only in override are considered distinct + // because the dedup key includes the override flag. + auto key = std::format("{}{}", base_expr(b), + b.libcxx_override ? "@libcxx" : ""); if (seen.insert(key).second) { out += emit_build_input_line(b); } diff --git a/src/linkdb/curated.cpp b/src/linkdb/curated.cpp index 0d3799b..adfb74b 100644 --- a/src/linkdb/curated.cpp +++ b/src/linkdb/curated.cpp @@ -185,6 +185,7 @@ auto Database::resolve(const std::string& package, const std::string& version, .find_package = row.find_package, .targets = row.targets, .source = row.source, + .requires_libcxx_override = row.requires_libcxx_override, }; } } diff --git a/src/linkdb/linkdb.cppm b/src/linkdb/linkdb.cppm index e893383..7f50555 100644 --- a/src/linkdb/linkdb.cppm +++ b/src/linkdb/linkdb.cppm @@ -14,6 +14,12 @@ struct Recipe { std::string find_package; // post-substitution std::vector targets; // post-substitution std::string source; // 'curated' | 'manual' | etc. + // Set when the dep needs `.override { stdenv = llvmPkgs.libcxxStdenv; }` + // to link against the user's libc++-built project (e.g. nixpkgs ships + // it built against libstdc++ — abseil-cpp, llvm). verify_link toggles + // this on after a libstdc++/libc++ link failure. Codegen wraps the + // buildInputs entry in the override expression when true. + bool requires_libcxx_override = false; bool operator==(const Recipe&) const = default; }; @@ -37,6 +43,7 @@ struct OverlayRow { std::vector targets; std::string source; std::int64_t verified_at = 0; + bool requires_libcxx_override = false; }; // RAII wrapper for an open sqlite3 connection used by the overlay database. @@ -90,6 +97,13 @@ auto overlay_delete_recipe(OverlayState& state, const std::string& package, const std::string& source) -> cargoxx::util::Result; +// Toggle the libcxx-override flag for an existing row. Used by +// `verify_link` after a libstdc++/libc++ link mismatch. +auto overlay_set_libcxx_override(OverlayState& state, const std::string& package, + const std::string& version_range, + const std::string& source, bool value) + -> cargoxx::util::Result; + auto overlay_query(OverlayState& state, const std::string& package) -> cargoxx::util::Result>; @@ -125,6 +139,11 @@ class Database { const std::string& version_range, const std::string& source) -> util::Result; + auto set_libcxx_override(const std::string& package, + const std::string& version_range, + const std::string& source, bool value) + -> util::Result; + private: Database() = default; std::map> curated_; diff --git a/src/linkdb/overlay.cpp b/src/linkdb/overlay.cpp index d023025..3d17338 100644 --- a/src/linkdb/overlay.cpp +++ b/src/linkdb/overlay.cpp @@ -90,6 +90,25 @@ auto overlay_open(const std::filesystem::path& path) }); } + // Add the libcxx-override column to legacy overlay files. SQLite's + // ADD COLUMN errors when the column already exists; treat that as + // success (the column is present, which is all we need). + constexpr const char* MIGRATE = + "ALTER TABLE recipes ADD COLUMN requires_libcxx_override INTEGER NOT NULL DEFAULT 0"; + char* mig_err = nullptr; + if (sqlite3_exec(state->handle(), MIGRATE, nullptr, nullptr, &mig_err) != SQLITE_OK) { + if (mig_err && std::string_view{mig_err}.find("duplicate column") == + std::string_view::npos) { + std::string msg = std::format("cannot migrate overlay schema: {}", + mig_err ? mig_err : "?"); + sqlite3_free(mig_err); + return std::unexpected(util::Error{ + util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt, + }); + } + sqlite3_free(mig_err); + } + return state; } @@ -99,8 +118,8 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package, constexpr const char* SQL = "INSERT OR REPLACE INTO recipes " "(package, version_range, nixpkgs_attr, find_package, targets, components, source, " - " verified_at) " - "VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?)"; + " verified_at, requires_libcxx_override) " + "VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?, ?)"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; @@ -119,6 +138,7 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package, sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 6, now); + sqlite3_bind_int(stmt, 7, r.requires_libcxx_override ? 1 : 0); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); @@ -137,8 +157,8 @@ auto overlay_insert(OverlayState& state, const std::string& package, constexpr const char* SQL = "INSERT OR REPLACE INTO recipes " "(package, version_range, nixpkgs_attr, find_package, targets, components, source, " - " verified_at) " - "VALUES (?, ?, ?, ?, ?, NULL, ?, ?)"; + " verified_at, requires_libcxx_override) " + "VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; @@ -155,6 +175,7 @@ auto overlay_insert(OverlayState& state, const std::string& package, sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 7, verified_at); + sqlite3_bind_int(stmt, 8, r.requires_libcxx_override ? 1 : 0); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); @@ -223,7 +244,8 @@ auto overlay_delete_recipe(OverlayState& state, const std::string& package, auto overlay_query(OverlayState& state, const std::string& package) -> util::Result> { constexpr const char* SQL = - "SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at " + "SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at, " + " requires_libcxx_override " "FROM recipes WHERE package = ?"; sqlite3* db = state.handle(); @@ -254,6 +276,7 @@ auto overlay_query(OverlayState& state, const std::string& package) } row.source = column_text(stmt, 4); row.verified_at = sqlite3_column_int64(stmt, 5); + row.requires_libcxx_override = sqlite3_column_int(stmt, 6) != 0; out.push_back(std::move(row)); } sqlite3_finalize(stmt); @@ -278,6 +301,30 @@ auto overlay_is_fresh(const OverlayRow& row, std::int64_t now) -> bool { return (now - row.verified_at) < THIRTY_DAYS_SECONDS; } +auto overlay_set_libcxx_override(OverlayState& state, const std::string& package, + const std::string& version_range, + const std::string& source, bool value) + -> util::Result { + constexpr const char* SQL = + "UPDATE recipes SET requires_libcxx_override = ? " + "WHERE package = ? AND version_range = ? AND source = ?"; + sqlite3* db = state.handle(); + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { + return std::unexpected(sqlite_error(db, "prepare update libcxx_override")); + } + sqlite3_bind_int(stmt, 1, value ? 1 : 0); + sqlite3_bind_text(stmt, 2, package.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, version_range.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 4, source.c_str(), -1, SQLITE_TRANSIENT); + auto rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return std::unexpected(sqlite_error(db, "step update libcxx_override")); + } + return {}; +} + } // namespace detail auto Database::add_manual(const std::string& package, const std::string& version_range, @@ -333,4 +380,15 @@ auto Database::abort_provisional(const std::string& package, return detail::overlay_delete_recipe(*overlay_, package, version_range, source); } +auto Database::set_libcxx_override(const std::string& package, + const std::string& version_range, + const std::string& source, bool value) + -> util::Result { + if (auto ok = require_overlay(overlay_); !ok) { + return std::unexpected(ok.error()); + } + return detail::overlay_set_libcxx_override(*overlay_, package, version_range, + source, value); +} + } // namespace cargoxx::linkdb diff --git a/src/resolver/verify_link.cpp b/src/resolver/verify_link.cpp index 3917927..8f12841 100644 --- a/src/resolver/verify_link.cpp +++ b/src/resolver/verify_link.cpp @@ -142,12 +142,63 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) auto build_result = build_fn(proj_root); - // Always re-open the db to flip the provisional row's fate. + // First-pass success — confirm and we're done. + if (build_result) { + auto db = linkdb::Database::open(req.overlay_path); + if (!db) { + return std::unexpected(db.error()); + } + if (auto r = db->confirm_provisional(req.package_name, req.version_spec, + req.source); + !r) { + return std::unexpected(r.error()); + } + return {}; + } + + // First pass failed. Try once more with a libstdc++/libc++ ABI + // workaround: flip `requires_libcxx_override` on the overlay row, + // which makes flake codegen wrap the dep in + // `.override { stdenv = llvmPkgs.libcxxStdenv; }`. cmd_build + // re-resolves and regenerates the flake, so the second build_fn + // call sees the override. + std::cerr << std::format( + "verify_link: first build of '{}' failed; retrying with " + "libc++/doCheck=false override\n", + req.package_name); + { + auto db = linkdb::Database::open(req.overlay_path); + if (!db) { + return std::unexpected(db.error()); + } + if (auto r = db->set_libcxx_override(req.package_name, req.version_spec, + req.source, true); + !r) { + // Fall through to the abort path below — couldn't even mark. + (void)db->abort_provisional(req.package_name, req.version_spec, + req.source); + return std::unexpected(r.error()); + } + } + + // Wipe CMake's per-profile build dirs. The first pass's + // CMakeCache.txt has cached `LLVM_DIR` (and other find_package + // results) at the default-stdenv path; without clearing it, the + // retry's regenerated flake.nix has no effect on what + // find_package sees. + { + std::error_code wipe_ec; + for (auto* sub : {"debug", "release"}) { + std::filesystem::remove_all(proj_root / "build" / sub, wipe_ec); + } + } + + auto retry_result = build_fn(proj_root); auto db = linkdb::Database::open(req.overlay_path); if (!db) { return std::unexpected(db.error()); } - if (build_result) { + if (retry_result) { if (auto r = db->confirm_provisional(req.package_name, req.version_spec, req.source); !r) { @@ -156,7 +207,7 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) return {}; } (void)db->abort_provisional(req.package_name, req.version_spec, req.source); - return std::unexpected(build_result.error()); + return std::unexpected(retry_result.error()); } } // namespace cargoxx::resolver