Giter VIP home page Giter VIP logo

core's Introduction

Paisano Core

Organize your Flakes-based projects

Paisano implements an API for organizing Nix code into folders.

It then maps that folder structure into Flake outputs, once per system.

To type outputs, an output type system is provided and traits (i.e. "abstract behaviour") may be implemented on them.

Motivation

Nix flakes make it easy to create self-contained projects with a common top-level interface that has been standardized througout the Nix Community. However, as an upstream standard it was built (mostly) for the packaging use case of Nix in the context of small package-centric repositories. It is, therefore, less suited for a DevOps centric usage pattern.

With Paisano, we attempt to solve the following problems:

  • Internal code boundaries: Nix isn't typed and doesn't itself impose any opinion on how to organize code. It also doesn't define a module system in the same sense as a typical programming language, in which modules are often folder-based encapsulation of functionality.

    Therefore, Nix forces its users to make a significant upfront investment in designing their code layout. Unfortunately, due to the planning overhead that this entails, many users may just skip out on Nix for this reason alone. Over longer time horizons, Nix code seen in the wild tends, in our experience, to lose clarity and organization.

    Paisano addresses this problem by simple, clear, and extensible calling conventions on its module importer interface.

  • Ease of refactoring: In many a Nix codebase, a multiplicity of interaces, coupled with needlessly bespoke calling conventions make semantic code refactoring unnecessarily difficult.

    In contrast, Paisano enforces a single, but powerful, calling convention for its importer. As a result, Paisano makes semantic refactoring straightforward as your project naturally grows.

  • Shared mental structure: A custom code structure often comes with a hefty price tag in terms of context switching between projects. Yet, in a typical DevOps workflow, it is not uncommon to deal with multiple, diverse code bases.

    By encouraging basic organizational principles (at least) in your (Nix) code, a future "self" or present "other" will be able to significantly lower their cognitive load, leaving more time to get useful work done.

  • Take out the guesswork: The costs of the upfront design work required to effectively structure your project, along with the above mentioned hurdles in code refactoring are simply too high. And if that's not bad enough, the cost of making a mistake during this process is even higher, leading to more tedious work simply to make sense out of your existing code.

    As a consequence, many (Nix) projects evolve unguided, with the heavy price of refactoring postponed. This can quickly become a vicious cycle of ever growing spaghetti code, which is then more and more difficult to refactor as time goes on.

    Paisano's meta-structure alleviates the guesswork considerably, enabling the user to spend their time creating a meaningful project type system. It is this very system which allows time to focus on effectively solving your problem; the solution of which can then be mapped effortlessly over any related outputs, again and again.

    In short, considerable effort is expended to take the previously destructive feedback loop described above, and turn it into a highly productive one; allowing for quick and correct (i.e. well-typed) iteration.

  • Avoid level-creep: There is often a tension between depth and breadth when organizing the folder structure of your project. A deeply nested scheme may sometimes map the problem domain more efficiently, but it isn't necessarily optimized for human consumption.

    Paisano tries to find a nice balance, which readily accomodates every problem domain without compromising readability.

    We do this by offering an unambiguous model for structuring your code, which breaks down fairly simply as follows.

    When you want:

    • Breath → add a code block
    • Depth → compose flakes

    This creates a natural depth boundary at the repository level, since it is generally considered good practive to use one flake per project repository.

Terminology

  • Cells — they are the first level in the folder structure and group related functionality together. An example from the microserivce use case would be a backend cell or a frontend cell.

  • Block — the next level in the folder structure are (typed) blocks that implement a collection of similar functionality, i.e. code modules (in other languages). One could be labeled "tasks", another "packages", another "images", etc.

  • Targets — each block type can have one or more targets. These are your concrete packages, container images, scripts, etc.

  • Block Types & Actions — These are the types attached to the blocks mentioned above. They allow you to define arbitary actions which can be run over the specific targets contained in the associated block. This allows a platform or framework provider to implement shared functionality for their particular use case, such as a single action which describes how to "push" container images to their registry.

    This is really where Paisano breaks away from the simple packaging pattern of Nix and allows you to define what you actually want to do with those packages, artifacts, scripts, or images in a well-defined, boilerplate free way.

  • Registry — the registry extracts structured, yet json-serializable, data from our output type system and code structure. Consumers such as CI, a CLI/TUI, or even a UI can access and extract that data for a tight integration with the respective target use cases. For a concrete example of a consumer, see std-action.

Regsitry Schema Spec

The current schema version is v0 (unstable).

The Jsonschema specification of the registry can be found inside ./registry.schema.json. It can be explored interactively with this link.

Usage

# flake.nix
{
  inputs.paisano.url = "github:divnix/paisano";
  inputs.paisano.inputs.nixpkgs.follows = "nixpkgs";

  outputs = { paisano, self }@inputs:
    paisano.growOn {
      /*
        the grow function needs `inputs` to desystemize
        them and make them available to your cell blocks
      */
      inherit inputs;
      /*
        sepcify from where to grow the cells
        `./nix` is a typical choice that signals
        to everyone where the nix code lies
      */
      cellsFrom = ./nix;
      /*
        These blocks may or may not be found in a particular cell.
        But blocks that aren't defined here, cannot exist in any cell.
      */
      cellBlocks = [
        {
          /*
            Because the name is `mycellblock`, paisano's importer
            will be hooking into any file `<cell>/mycellblock.nix`
            or `<cell>/mycellblock/default.nix`.
            Block tragets are exposed under:
            #<system>.<cell>.<block>.<target>
          */
          name = "mycellblock";
          type = "mytype";

          /*
            Optional

            Actions are exposed in paisano's "registry" under
            #__std.actions.<system>.<cell>.<block>.<target>.<action>
          */
          actions = {
            system,
            flake,
            fragment,
            fragmentRelPath,
          }: [
            {
              name = "build";
              description = "build this target";
              command = ''
                nix build ${flake}#${fragment}
              '';
            }
          ];
          /*
            Optional

            The CI registry flattens the targets and
            the actions to run for each target into a list
            so that downstream tooling can discover what to
            do in the CI. The invokable action is determined
            by the attribute name: ci.<action> = true

            #__std.ci.<system> = [ {...} ... ];
          */
          ci.build = true;
        }
      ];
    }
    {
      /* Soil */
      # Here, we make our domain layout compatible with the Nix CLI, among others
      devShells = paisano.harvest self [ "<cellname>" "<blockname>"];
      packages = paisano.winnow (n: v: n == "<targetname>" && v != null ) self [ "<cellname>" "<blockname>"];
      templates = paisano.pick self [ "<cellname>" "<blockname>"];
    };
}

core's People

Contributors

blaggacao avatar nrdxp avatar realityanomaly avatar gtrunsec avatar whs-dot-hk avatar

Stargazers

Juanjo Presa avatar Andrew Kvapil avatar  avatar  avatar aemogie. avatar Andrew Shebanow avatar Pascal Wittmann avatar  avatar Nikolaus Schlemm avatar dzmitry-lahoda avatar RyzeNGrind avatar Nick Dawbarn avatar Cameron Smith avatar 9glenda avatar vi|vi|vi avatar Matus Benko avatar Anouar avatar  avatar Josh Toft avatar YouSiki avatar  avatar Joshua W avatar Felx avatar Shayon avatar F. Emerson avatar  avatar luxus avatar Pendragonscode avatar anna avatar Argho basak avatar Henry Wong avatar Neil avatar Michael Hanley avatar Federico Damián Schonborn avatar Ilmari Vacklin avatar Gio d'Amelio avatar Jules Amonith avatar Will Leinweber avatar Daniel Kahlenberg avatar  avatar KlarkC avatar Harry Pray IV avatar  avatar  avatar  avatar chris montgomery avatar

Watchers

luxus avatar Juanjo Presa avatar chris montgomery avatar  avatar  avatar  avatar  avatar

core's Issues

Normalize actions signature (take 2)

From:

{
  currentSystem,
  fragment, # <system>.<cell>.<block>.<target>
  fragmentRelPath, # <cell>/<block>/<target>
  target,
}: []

To:

{
  root, # <cellsFrom>
  target, # any
  cursor, # ["<system>" "<cell>" "<block>" "<target>"]
  currentSystem,
}: []

Rationale:

  • root + cursor[1:] allows to reason and/or interact with the mutable repository layout
  • currentSystem abstracts that the action (not the target) must act on the host system This is not the same as cross-compilation!
  • cursor allows to construct a unique path inside the PRJ_* folders
  • cursor also allows to reason about the flake fragment

grow: delegate register initialization to external projects

I've been using the grow function in the paisano project and I've noticed that it initializes the ci register internally. However, I think that it would be more flexible and beneficial to delegate the initialization of registers to external projects.

Specifically, I suggest having the std project initialize the ci register, while other projects initialize different registers that they may need. This would allow users to customize the initialization process for different registers to suit their specific needs.

By delegating the register initialization to external projects, it would make the grow function more powerful and useful for a wider range of use cases.

I would love to hear your thoughts on this suggestion, and if there are any specific concerns you may have.

function to harvest all blocks by name across cells

I'm looking for an idiomatic approach to harvest (or, rather, pick) all (or some by filter) blocks of <name> across all cells.

Based on the function descriptions here, I have not deduced a way to transform:

system.cell.block.target -> block.target

pick comes close, but:

The transformation is: system.cell.block.target -> target

Abstract Example

Tree

.
├── bar
│  └── lib.nix
└── foo
   └── lib.nix

//foo/lib.nix

{
  inputs,
  cell
}: {
  alpha = a: b: a + b;
}

//bar/lib.nix

{
  inputs,
  cell
}: {
  # Should cause a conflict upon attrs merging:
  # alpha = a: b: a ++ b;

  beta = a: b: a // b;
}

Desired Invocation Result

{
  lib = {
    foo.alpha = <...>;
    bar.beta = <...>;
  };
}

Real Example

https://git.sr.ht/~montchr/apparat/tree/mitosis/item/flake.nix#L50-67

Wherein:

flake.lib =
  dmerge.merge
  (std.pick inputs.self [
    ["apparat" "lib"]
    ["apps" "lib"]
    ["lib" "functions"]
  ]) {
    # Retain cell name in output.
    types =
      l.foldl' (x: y: x // y) {}
      (l.map (v: {"${l.head v}" = std.pick inputs.self v;}) [
        ["filesystem" "types"]
        ["lib" "types"]
        ["networking" "types"]
        ["secrets" "types"]
        ["users" "types"]
      ]);
  };

Desired Output

Something resembling the well-known nixpkgs.lib structure, e.g. lib.types.filesystem, lib.types.users.

Given a set of cells maybe-with lib.nix and types.nix: plop types into place inside flake.lib.

(Obviously lib.types.lib is silly, just ignore for now I guess?)

Even better: negating the need to do this weird merge at all (it's not-even-fully-effective cf. resorting to use of foldl', but that's more due to my perhaps-misunderstanding of dmerge, which is out of scope here).

Types of actions

Glossary:

  • Stage 1: Reification of raw config data (e.g. provenient from the module system)
  • Stage 2: Scripting around the reified target

Problem:

  • Currently actions only where designed to accommodate stage 2
  • However, locating stage 1 at the cell level could be considered an implementation detail, if we regard cells as the intent-domain
  • Reuse it easier manipulating source artifacts (i.e. non-reified raw data)

One situation where those dynamics have caused weirdness is when working with devshell modules. Once, re-ified, you can't imports them anymore.

divnix/hive has been implementing collectors (similar to std.harvest) that also do the re-ification for similar reasons.

Solution:

  • optionally allow actions to include stage 1
  • mandate actions to include stage 1

Open questions: Impact on std-action when re-ification shifts into actions. @nrdxp

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.