Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

getchoo/froyo

🍨 A tasteful way to organize your Nix code

getchoo/froyo.json
{
"createdAt": "2025-03-11T21:58:41Z",
"defaultBranch": "main",
"description": "🍨 A tasteful way to organize your Nix code",
"fullName": "getchoo/froyo",
"homepage": "",
"language": "Nix",
"name": "froyo",
"pushedAt": "2025-11-24T03:34:58Z",
"stargazersCount": 33,
"topics": [],
"updatedAt": "2025-11-09T21:05:14Z",
"url": "https://github.com/getchoo/froyo"
}

[!CAUTION] I made this on a whim in an afternoon. It’s not production ready and may never be. Here be dragons! 🐲

froyo brings the module system from NixOS right into your stable Nix code. This comes with a few big advantages, like:

  • No more manual importing of files
  • Improved re-usability of code
  • Self documenting interfaces
  • Less boilerplate
  • A lot of composability!

[!WARNING] froyo is best used with tools like npins and niv.

import <froyo> { } {
outputs = {
hello = "hi from froyo!";
};
}
Terminal window
$ nix-instantiate --eval --attr outputs.hello

By default, froyo will use it’s own Nixpkgs when applicable. However, it is highly recommended to maintain your own lockfile with Nixpkgs and pass that to froyo directly.

let
# In this example we use `npins`, but these could come from anything (even channels!)
inputs = import ./npins;
in
import inputs.froyo { inherit inputs; } {
outputs = {
hello = "hi from froyo!";
};
perTarget =
{ pkgs, ... }:
{
outputs = { inherit (pkgs) hello; };
};
}

You can then build this hello package:

Terminal window
$ nix-build --attr outputs.perTarget.x86_64-linux.hello

froyo projects can be boiled down to two things: inputs and outputs.

Inputs are sources (usually managed by tools like npins, niv, etc.) passed to froyo projects. They can be local (/nix/store) paths, instantiated package sets, or even flake inputs!

Outputs are pieces of your Nix code meant to be used by the outside world. This is represented by the outputs config option, which (along with inputs) are the only fields exported by froyo.

To assist in both common and more exotic workflows, froyo introduces the concept of “targets”. Targets are either:

  • A string describing a system recognized by Nixpkgs (i.e., "x86_64-linux" or "aarch64-darwin")
  • An attribute set describing a system (such as those created with lib.systems.elaborate)
  • An attribute set containing a buildPlatform and hostPlatform attribute (with a value of one of the above) for cross compilation

[!TIP] Sound familiar?! This is analogous to the system variable commonly used in flakes, but expanded upon to consider multiple systems

targets is the name-value pair option used to describe the targets froyo operates on by default, like so:

{ config, lib, ... }:
{
targets = {
x86_64-linux = lib.systems.elaborate "x86_64-linux";
inherit (lib.systems.examples) aarch64-darwin;
aarch64-cross = {
buildPlatform = config.targets.x86_64-linux;
hostPlatform = "aarch64-linux";
};
};
}

perTarget is a small helper option defined in [modules/per-target.nix]!(./modules/per-target.nix). It allows you to define attributes that will be applied to each target defined by the aforementioned targets option.

{
perTarget = { pkgs, ... }: {
# Similar to at the root level, you need to use `outputs` to export attributes
outputs = { inherit (pkgs) hello; };
};
}

This is then available in the final project as outputs.perTarget.<target name>.hello.

The biggest advantage of using target over system is the ability for froyo to describe cross-compilation configurations. You can do this by defining a buildPlatform and hostPlatform.

import inputs.froyo { } {
targets = {
riscv-cross = {
buildPlatform = "x86_64-linux";
hostPlatform = "riscv64-linux";
};
};
perTarget =
{ pkgs, ... }:
{
outputs = { inherit (pkgs) hello; };
};
}

Now, a cross-compiled hello will be available as outputs.perTarget.riscv-cross.hello.

froyo project can be extended - similar to using extendModules in NixOS configurations

let
# Assuming we're using the above example
my-froyo-project = import ./my-froyo-project;
extended = my-froyo-project.extend {
# This makes `perTarget` iterate over the new one defined here
targets = {
native-riscv = "riscv64-linux";
};
allSystems = {
my-other-cool-target =
{ pkgs, ... }:
{
target = "powerpc64-linux";
outputs = { inherit (pkgs) hello-go; };
};
};
};
in
{
# `native-riscv` is now in `perTarget`!
inherit (extended.outputs.perTarget.native-riscv) hello;
# `my-other-cool-target` now also exists, and has a special `hello-go` attribute exclusive to it
inherit (extended.outputs.perTarget.my-other-cool-target) hello-go;
}

As someone who primarily uses Flakes, one of my favorite parts of them for a while has been flake-parts. There isn’t much of an equivalent in stable Nix for its functions though, so after a couple social media posts and taking inspiration from previous work I’ve done, I came up with this