diff --git a/README.md b/README.md index e9d15c3..6810ff9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,56 @@ Nix flake library that turns a user repository into the flake outputs that [Poin - `#pointy.projects` — project membership and ordering - `#pointy.srcFiles` — per-step source files - `#pointy.dependencies` — step dependency graph +- `#pointy.deps` — declared pointy repository dependencies: + `{ namespace = { input, repo? }; }` +- `#pointy.namespaces.` — each declared dependency's full pointy surface + mounted under a namespace: its stepDefs, stepConfig, templates, projects, + presets, srcFiles and deps, plus (resolved from the per-system package view) + its built `steps`, `projectOutPaths` and `autocomplete`. The namespace root + is a clean attrset. - per-system `#pointy.steps.` and `#pointy.projectOutPaths` — buildable derivations +## Depending on another pointy repo + +A repo imports another pointy user repo as a dependency by declaring a flake +input and listing it in `pointy.deps`: + +```nix +{ + # repo B's flake + inputs = { + a.url = "git@example.com:org/edit-tooling.git"; # A's flake (a pointy repo) + pointy-stdlib.url = "github:421anon/pointy-stdlib"; + }; + outputs = { pointy-stdlib, ... }@inputs: + pointy-stdlib.lib.mkFlake { inherit inputs; } { + pointy = { + # ... own stepDefs/templates/projects/srcFiles ... + deps = { + edit-tooling = { input = "a"; repo = "edit-tooling"; }; + }; + }; + }; +} +``` + +- `input` must name a flake input of this repo (never `self`); `repo` + (optional) is the dependency's pointy registry name and defaults to + resolving the input's locked URL against the registry. +- Because the per-system `packages..pointy` package shadows the + top-level `pointy` output for installable resolution, `#pointy.namespaces.` + resolves to the per-system merged surface: + - `#pointy.namespaces.edit-tooling.steps.` — the dependency's built steps + - `#pointy.namespaces.edit-tooling.stepDefs` etc. — its domain objects + (equivalently via the explicit per-system path + `#packages..pointy.namespaces.edit-tooling.steps.`). +- `#pointy.deps` is pure metadata of B's own committed state: evaluating it + does not force evaluation of dependency content. +- Unknown / non-pointy / `self` inputs fail with a descriptive error when the + namespace is forced. + +Dependencies are ordinary flake inputs, resolved from the dependent repo's +committed `flake.lock` at evaluation time; the backend evaluates the dependent +exactly as today. + See [Setting Up the User Repository](https://github.com/421anon/pointy/blob/main/docs/pages/user-repo-setup.md) for a minimal `flake.nix` and template examples. diff --git a/flake.nix b/flake.nix index a3bc1b2..73430f6 100644 --- a/flake.nix +++ b/flake.nix @@ -22,14 +22,47 @@ flakeModules.default = top: { options.pointy = { - stepDefs = top.lib.mkOption { type = top.lib.types.attrsOf pointyLib.types.pointy.stepDef; }; - templates = top.lib.mkOption { type = top.lib.types.attrs; }; + stepDefs = top.lib.mkOption { + type = top.lib.types.attrsOf pointyLib.types.pointy.stepDef; + default = { }; + }; + templates = top.lib.mkOption { + type = top.lib.types.attrs; + default = { }; + }; presets = top.lib.mkOption { type = top.lib.types.attrsOf pointyLib.types.pointy.preset; default = { }; }; - projects = top.lib.mkOption { type = top.lib.types.attrsOf pointyLib.types.pointy.project; }; - srcFiles = top.lib.mkOption { type = top.lib.types.raw; }; + projects = top.lib.mkOption { + type = top.lib.types.attrsOf pointyLib.types.pointy.project; + default = { }; + }; + srcFiles = top.lib.mkOption { + type = top.lib.types.raw; + default = { }; + }; + deps = top.lib.mkOption { + type = top.lib.types.attrsOf ( + top.lib.types.submodule { + options.input = top.lib.mkOption { + type = top.lib.types.str; + description = "The flake-input key in this repo's flake.nix that pins the dependency."; + }; + options.repo = top.lib.mkOption { + type = top.lib.types.nullOr top.lib.types.str; + default = null; + description = "The pointy registry name of the dependency. Optional; when absent Pointy resolves the input's locked URL against the registry."; + }; + } + ); + default = { }; + description = '' + Pointy repositories this repo depends on, keyed by namespace. + Each entry maps a namespace to `{ input, repo ? null }` where + `input` is the flake input key and `repo` the registry name. + ''; + }; }; config = @@ -39,6 +72,56 @@ type = "derivation"; name = ""; }; + + # Resolve a declared dependency to its flake input (the full input + # flake attrset); callers pick `.pointy` / `.packages..pointy` off it. + # Throws a helpful error when the input is unknown or not a pointy flake. + depOutput = + ns: decl: + let + inputName = decl.input; + in + # `self` is always injected into inputs by mkFlake's + # withDefaultNixpkgs; depending on it would recurse into this very + # flake's pointy output (stack overflow). + if inputName == "self" then + throw "pointy.deps: namespace `${ns}` may not depend on `self` (a repo cannot depend on its own pointy output)." + else if !(builtins.hasAttr inputName top.inputs) then + throw "pointy.deps: namespace `${ns}` refers to input `${inputName}` which is not declared in the flake's `inputs`." + else if !(top.inputs.${inputName} ? pointy) then + throw "pointy.deps: input `${inputName}` (namespace `${ns}`) is not a pointy flake: it has no `pointy` output." + else + top.inputs.${inputName}; + + # Backend contract (`#pointy.deps`): namespace -> { input, repo? }. + # Pure metadata of this repo's own committed state; does not force + # evaluation of dependency content. + depsContract = builtins.mapAttrs ( + _: decl: + { input = decl.input; } + // (if decl.repo != null then { inherit (decl) repo; } else { }) + ) cfg.deps; + + # Mount each declared dependency's pointy domain output (stepDefs, + # stepConfig, templates, projects, presets, srcFiles, deps) under a namespace. + domainNamespaces = builtins.mapAttrs (ns: decl: (depOutput ns decl).pointy) cfg.deps; + + # Per-system mount: also exposes dependency built steps / projectOutPaths / autocomplete. + # Strips the dependency's own fakeDrv package markers (type/name) from the + # namespace root so `#pointy.namespaces.` is a clean pointy surface. + perSystemNamespaces = + pkgs: + builtins.mapAttrs ( + ns: decl: + let + dep = depOutput ns decl; + in + (dep.pointy or { }) + // (builtins.removeAttrs (dep.packages.${pkgs.system}.pointy or { }) [ + "type" + "name" + ]) + ) cfg.deps; in { flake.pointy = with pointyLib; { @@ -48,6 +131,8 @@ stepDefs = evalStepDefs cfg; srcFiles = cfg.srcFiles; dependencies = evalDependencies cfg; + deps = depsContract; + namespaces = domainNamespaces; }; perSystem = { pkgs, ... }: @@ -61,6 +146,7 @@ steps = evalSteps <| cfg // { inherit pkgs; }; projectOutPaths = evalProjectOutPaths <| cfg // { inherit pkgs; }; autocomplete = evalAutocomplete <| cfg // { inherit pkgs; }; + namespaces = perSystemNamespaces pkgs; }; }; };