Skip to content

feat: Add Nix backend support#220

Open
maax3v3 wants to merge 4 commits into
ripytide:mainfrom
maax3v3:feature/nix-backend
Open

feat: Add Nix backend support#220
maax3v3 wants to merge 4 commits into
ripytide:mainfrom
maax3v3:feature/nix-backend

Conversation

@maax3v3
Copy link
Copy Markdown

@maax3v3 maax3v3 commented Feb 25, 2026

Related to #219

I took a first pass at Nix support in Metapac, using nix profile as the backend.

What’s in:

  • basic package lifecycle works: list, install (sync), remove (clean), update (update / update-all)
  • clean-cache for nix runs nix store gc
  • Nix packages in groups can use:
    • name (used as the stable package id in metapac)
    • optional options.installable (if missing, it falls back to nixpkgs#)
    • optional options.priority
  • backend config got a few practical knobs:
    • profile
    • impure
    • accept_flake_config

What’s not in (yet):

  • nix “repo-like” things (registries/channels/etc).

Why these choices:

  • nix profile is the modern CLI and works across distros, not just NixOS.
  • Using name as identity keeps metapac diffs predictable for sync/clean/unmanaged.
  • installable stays optional so common cases are still easy, but custom flakes/sources are supported.

Validation:

  • fmt/test/clippy all pass
  • I also ran manual end-to-end checks with cargo run -- ... + real nix commands:
    • install worked
    • upgrade worked
    • removal worked
    • unmanaged output looked correct

Happy to incorporate any feedback !

Copy link
Copy Markdown
Owner

@ripytide ripytide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty good PR, thanks for adding a changelog too.

Comment thread src/backends/nix.rs Outdated
Comment thread src/backends/nix.rs Outdated
Comment thread src/backends/nix.rs Outdated
}
}

fn append_profile_arg(args: &mut Vec<String>, config: &NixConfig) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't bother trying to make the code overly DRY, I'd again just inline this logic when it is needed, see other backends for examples.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still relevant I believe

Comment thread src/backends/nix.rs Outdated
config: &Self::Config,
) -> Result<()> {
for (name, options) in packages {
let mut args = vec!["nix".to_string(), "profile".to_string(), "add".to_string()];
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlining the profile logic may also mean you no longer need all these .to_string() calls, or at least if you do you can that after in a .map(|x| x.to_string()`

@maax3v3 maax3v3 force-pushed the feature/nix-backend branch from dbfaadd to d2c376b Compare April 6, 2026 20:56
@maax3v3
Copy link
Copy Markdown
Author

maax3v3 commented Apr 6, 2026

Thanks for the feedback. I've updated the PR according to suggested changes.

@maax3v3 maax3v3 requested a review from ripytide April 6, 2026 21:30
Comment thread src/backends/nix.rs
Comment on lines +123 to +132
if !packages.is_empty() {
let mut args = vec!["nix", "profile", "remove"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
if let Some(profile) = &config.profile {
args.extend(["--profile".to_string(), profile.clone()]);
}
args.extend(packages.iter().cloned());
run_command(args, Perms::Same)?;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to follow the patterns from other backends, mainly using iterator methods for building the arguments rather than a mutable vec. Here is an example from dnf.rs:

    fn install_packages(
        packages: &BTreeMap<String, Self::PackageOptions>,
        no_confirm: bool,
        _: &Self::Config,
    ) -> Result<()> {
        if !packages.is_empty() {
            run_command(
                ["dnf", "install"]
                    .into_iter()
                    .chain(Some("--assumeyes").filter(|_| no_confirm))
                    .chain(packages.keys().map(String::as_str)),
                Perms::Sudo,
            )?;
        }

        Ok(())
    }

@ripytide
Copy link
Copy Markdown
Owner

Here is an example of how I would rewrite your update_packages() method in case it helps:

    fn update_packages(packages: &BTreeSet<String>, _: bool, config: &Self::Config) -> Result<()> {
        if !packages.is_empty() {
            run_command(
                ["nix", "profile", "upgrade"]
                    .into_iter()
                    .chain(Some("--profile").filter(|_| config.profile.is_some()))
                    .chain(config.profile.as_deref())
                    .chain(Some("--impure").filter(|_| config.impure))
                    .chain(Some("--accept-flake-config").filter(|_| config.accept_flake_config))
                    .chain(packages.iter().map(String::as_str)),
                Perms::Same,
            )?;
        }

        Ok(())
    }

@maax3v3
Copy link
Copy Markdown
Author

maax3v3 commented May 12, 2026

I refactored the command building parts of concerned functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants