Skip to content

Commit 396ff92

Browse files
domenkozarclaude
andcommitted
libstore-c: Add C FFI for garbage collection with closure support
Adds nix_store_collect_garbage to perform flexible garbage collection operations on the Nix store. Supports multiple GC modes including: - Returning or deleting live/dead paths - Closure-based deletion with safety checks respecting GC roots - Optional ignoreLiveness flag for force deletion - Per-operation byte limit Includes: - nix_gc_action enum with four action types - Callback-based result iteration - Support for local and closure-specific GC operations This enables PR NixOS#8417-style closure-based GC through the C FFI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 513b8ef commit 396ff92

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/libstore-c/nix_api_store.cc

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,82 @@ nix_err nix_store_compute_fs_closure(
389389
NIXC_CATCH_ERRS
390390
}
391391

392+
nix_err nix_store_collect_garbage(
393+
nix_c_context * context,
394+
Store * store,
395+
nix_gc_action action,
396+
StorePath ** paths_to_delete,
397+
size_t num_paths,
398+
bool ignore_liveness,
399+
uint64_t max_freed,
400+
nix_store_path_callback callback,
401+
void * user_data,
402+
uint64_t * bytes_freed)
403+
{
404+
if (context)
405+
context->last_err_code = NIX_OK;
406+
try {
407+
if (!store)
408+
return context ? context->last_err_code = NIX_ERR_KEY : NIX_ERR_KEY;
409+
410+
// Cast to GcStore
411+
auto gcStore = dynamic_cast<nix::GcStore *>(&*store->ptr);
412+
if (!gcStore)
413+
throw nix::Unsupported("Store does not support garbage collection");
414+
415+
// Build GCOptions
416+
nix::GCOptions options;
417+
418+
// Convert nix_gc_action to GCOptions::GCAction
419+
switch (action) {
420+
case NIX_GC_RETURN_LIVE:
421+
options.action = nix::GCOptions::gcReturnLive;
422+
break;
423+
case NIX_GC_RETURN_DEAD:
424+
options.action = nix::GCOptions::gcReturnDead;
425+
break;
426+
case NIX_GC_DELETE_DEAD:
427+
options.action = nix::GCOptions::gcDeleteDead;
428+
break;
429+
case NIX_GC_DELETE_SPECIFIC:
430+
options.action = nix::GCOptions::gcDeleteSpecific;
431+
break;
432+
default:
433+
return context ? context->last_err_code = NIX_ERR_KEY : NIX_ERR_KEY;
434+
}
435+
436+
options.ignoreLiveness = ignore_liveness;
437+
options.maxFreed = max_freed;
438+
439+
// Add paths to delete if provided and action is DELETE_SPECIFIC
440+
if (action == NIX_GC_DELETE_SPECIFIC && paths_to_delete && num_paths > 0) {
441+
for (size_t i = 0; i < num_paths; i++) {
442+
if (!paths_to_delete[i])
443+
return context ? context->last_err_code = NIX_ERR_KEY : NIX_ERR_KEY;
444+
options.pathsToDelete.insert(paths_to_delete[i]->path);
445+
}
446+
}
447+
448+
// Run garbage collection
449+
nix::GCResults results;
450+
gcStore->collectGarbage(options, results);
451+
452+
// Invoke callback for each path in the results
453+
if (callback) {
454+
for (const auto & pathStr : results.paths) {
455+
auto path = store->ptr->parseStorePath(pathStr);
456+
StorePath sp{path};
457+
callback(&sp, user_data);
458+
}
459+
}
460+
461+
// Return bytes freed if requested
462+
if (bytes_freed)
463+
*bytes_freed = results.bytesFreed;
464+
465+
return NIX_OK;
466+
}
467+
NIXC_CATCH_ERRS
468+
}
469+
392470
} // extern "C"

src/libstore-c/nix_api_store.h

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,22 @@ nix_err nix_store_add_indirect_root(nix_c_context * context, Store * store, cons
345345
*/
346346
nix_err nix_store_delete_path(nix_c_context * context, Store * store, const char * path, uint64_t * bytes_freed);
347347

348+
/**
349+
* @brief Garbage collection action types
350+
*
351+
* Specifies what the garbage collection operation should do.
352+
*/
353+
typedef enum {
354+
/** Return the set of live paths (reachable from roots) */
355+
NIX_GC_RETURN_LIVE,
356+
/** Return the set of dead paths (not reachable from roots) */
357+
NIX_GC_RETURN_DEAD,
358+
/** Delete all dead paths */
359+
NIX_GC_DELETE_DEAD,
360+
/** Delete only the specific paths provided (if they are dead) */
361+
NIX_GC_DELETE_SPECIFIC,
362+
} nix_gc_action;
363+
348364
/**
349365
* @brief Callback for iterating over store paths
350366
*
@@ -384,6 +400,49 @@ nix_err nix_store_compute_fs_closure(
384400
nix_store_path_callback callback,
385401
void * user_data);
386402

403+
/**
404+
* @brief Perform garbage collection on the store.
405+
*
406+
* This function provides flexible garbage collection with different modes:
407+
* - NIX_GC_RETURN_LIVE: Returns paths reachable from GC roots (live paths)
408+
* - NIX_GC_RETURN_DEAD: Returns paths not reachable from GC roots (dead paths)
409+
* - NIX_GC_DELETE_DEAD: Deletes all dead paths
410+
* - NIX_GC_DELETE_SPECIFIC: Deletes specific paths from the `paths_to_delete` array,
411+
* but only if they are not reachable from GC roots (respects liveness)
412+
*
413+
* When `ignore_liveness` is true, safety checks are bypassed (dangerous!).
414+
*
415+
* This only works with GcStore implementations (e.g., LocalStore).
416+
*
417+
* @param[out] context Optional, stores error information
418+
* @param[in] store Nix Store reference (must support GC)
419+
* @param[in] action The garbage collection action to perform
420+
* @param[in] paths_to_delete For NIX_GC_DELETE_SPECIFIC: paths to consider for deletion.
421+
* Can be NULL for other actions. Array is not modified.
422+
* @param[in] num_paths Number of paths in paths_to_delete (0 if paths_to_delete is NULL)
423+
* @param[in] ignore_liveness If true, ignore reachability from roots and delete even live paths.
424+
* Only has effect with NIX_GC_DELETE_SPECIFIC. Dangerous!
425+
* @param[in] max_freed Stop after freeing this many bytes. 0 means no limit.
426+
* @param[in] callback Optional callback function called for each path in the result set
427+
* (paths returned, deleted, or considered). Can be NULL.
428+
* @param[in] user_data Arbitrary data passed to the callback
429+
* @param[out] bytes_freed Optional pointer to uint64_t that will be set to the number of
430+
* bytes freed (for delete operations) or would be freed (for return operations).
431+
* Can be NULL if not needed.
432+
* @return NIX_OK on success, error code on failure
433+
*/
434+
nix_err nix_store_collect_garbage(
435+
nix_c_context * context,
436+
Store * store,
437+
nix_gc_action action,
438+
StorePath ** paths_to_delete,
439+
size_t num_paths,
440+
bool ignore_liveness,
441+
uint64_t max_freed,
442+
nix_store_path_callback callback,
443+
void * user_data,
444+
uint64_t * bytes_freed);
445+
387446
// cffi end
388447
#ifdef __cplusplus
389448
}

0 commit comments

Comments
 (0)