omnixient/modules/dev-env/lib.nix
Padreug 7960f82494 feat(dev-env): aiolabs dev environment — options, lib, config, presets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 09:59:05 +02:00

241 lines
7 KiB
Nix

{ config, lib, ... }:
let
inherit (lib) mkOption types;
cfg = config.dev-env;
# Parse a forgejo URL into { namespace, repo, ref }.
#
# Accepts:
# git+ssh://forgejo@host/org/repo.git?ref=branch
# git+ssh://forgejo@host/org/repo?ref=branch
# git+https://host/org/repo.git?ref=branch
# git+https://host/org/repo?ref=branch
#
# Returns null if the URL does not match a forgejo host.
#
# Implementation note: Nix's `builtins.match` uses POSIX ERE, which
# does NOT support non-greedy quantifiers (`+?`/`*?`). A naive regex
# like `([^?]+?)(\.git)?` lets the `+?` act greedy, producing a repo
# name of `webapp.git`. Instead we match everything before `?ref=`
# or end-of-string as the raw repo name, then strip `.git` after.
parseForgejoUrl =
url:
let
host = cfg.forgejo.host;
# One regex covering both schemes. Groups:
# 1: namespace
# 2: repo (possibly with .git suffix)
# 3: optional "?ref=..." (kept for completeness)
# 4: optional ref value
sshMatch = builtins.match "git\\+ssh://[^@]+@${host}/([^/]+)/([^?]+)(\\?ref=(.+))?" url;
httpsMatch = builtins.match "git\\+https://${host}/([^/]+)/([^?]+)(\\?ref=(.+))?" url;
m =
if sshMatch != null then
sshMatch
else if httpsMatch != null then
httpsMatch
else
null;
in
if m == null then
null
else
{
namespace = builtins.elemAt m 0;
repo = lib.removeSuffix ".git" (builtins.elemAt m 1);
ref = builtins.elemAt m 3;
};
# mkProject: shorthand constructor for `dev-env.projects.<name>`.
#
# Fills in conventional defaults so users write less boilerplate.
# The returned attrset matches the `projectType` submodule schema.
mkProject =
{
name ? null,
category ? null,
upstream ? null,
forgejoRepo ? null, # defaults to "${cfg.forgejo.org}/${name}"
originUrl ? null, # override origin URL (e.g. GitHub-only repos)
githubFork ? null, # defaults from github.forkUser
worktrees ? { },
isClone ? false,
deployFlakeInput ? null,
worktreeRoot ? null, # defaults to name
}:
{
forgejoRepo =
if forgejoRepo != null then
forgejoRepo
else if originUrl != null then
null
else if name != null then
"${cfg.forgejo.org}/${name}"
else
throw "dev-env.lib.mkProject: set forgejoRepo, originUrl, or name";
inherit
originUrl
upstream
category
isClone
deployFlakeInput
;
githubFork =
if githubFork != null then
githubFork
else if upstream != null && cfg.github.forkUser != null && name != null then
"git@github.com:${cfg.github.forkUser}/${name}.git"
else
null;
worktreeRoot =
if worktreeRoot != null then
worktreeRoot
else if name != null then
name
else
"";
inherit worktrees;
};
# deriveFromFlakeInput: parse one flake input url into a project entry
# (or null if the URL doesn't belong to our forgejo).
#
# Input: { name = "webapp-host5"; url = "git+ssh://forgejo@git.atitlan.io/aiolabs/webapp.git?ref=host5"; }
# Output: { namespace = "aiolabs"; repo = "webapp"; ref = "host5"; }
deriveFromFlakeInput =
{ inputName, url }:
let
parsed = parseForgejoUrl url;
in
if parsed == null then
null
else
{
inputName = inputName;
repo = parsed.repo;
namespace = parsed.namespace;
ref = parsed.ref;
};
# Collapse a list of `{ inputName, namespace, repo, ref }` into
# `{ <repo> = { forgejoRepo, worktrees = { <ref> = { branch = ref; }; ... }; }; }`.
#
# Multiple inputs can share the same repo with different refs; this is
# the common case for webapp + webapp-dev — both become worktrees of
# `webapp` (main and dev respectively).
groupDerivedProjects =
parsedList:
lib.foldl' (
acc: entry:
if entry == null || entry.ref == null then
acc
else
let
key = entry.repo;
existing =
acc.${key} or {
forgejoRepo = "${entry.namespace}/${entry.repo}";
originUrl = null;
worktreeRoot = entry.repo;
worktrees = { };
upstream = null;
githubFork = null;
category = null;
isClone = false;
deployFlakeInput = null;
};
newWorktree = {
branch = entry.ref;
path = null;
remote = "origin";
};
in
acc
// {
${key} = existing // {
worktrees = existing.worktrees // {
${entry.ref} = newWorktree;
};
# Record the LAST input name that contributed — good enough
# for --override-input wiring.
deployFlakeInput = entry.inputName;
};
}
) { } parsedList;
# Resolve worktree filesystem paths. Projects with category live under
# ${root}/${category}/${worktreeRoot}/<worktree>; otherwise
# ${root}/${worktreeRoot}/<worktree>.
worktreePath =
projectName: project: worktreeName:
let
root = cfg.root;
sub =
if project.category != null then
"${project.category}/${project.worktreeRoot}"
else
project.worktreeRoot;
leaf =
let
wt = project.worktrees.${worktreeName} or null;
in
if wt != null && wt.path != null then wt.path else worktreeName;
in
"${root}/${sub}/${leaf}";
# Bare repo path (always ${root}/repos/<basename>.git).
# Falls back to projectName when forgejoRepo is null (GitHub-only projects).
bareRepoPath =
projectName: project:
let
basename =
if project.forgejoRepo != null then
lib.last (lib.splitString "/" project.forgejoRepo)
else
projectName;
in
"${cfg.root}/repos/${basename}.git";
# Construct the remotes for a project. `origin` is `originUrl` if set,
# otherwise derived from the forgejo host + forgejoRepo. Projects with
# neither end up with no origin (filtered out below).
projectRemotes =
project:
lib.filterAttrs (_: v: v != null) {
origin =
if project.originUrl or null != null then
project.originUrl
else if project.forgejoRepo != null then
"${cfg.forgejo.sshUser}@${cfg.forgejo.host}:${project.forgejoRepo}.git"
else
null;
upstream = project.upstream;
github-fork = project.githubFork;
};
helpers = {
inherit
mkProject
deriveFromFlakeInput
parseForgejoUrl
groupDerivedProjects
worktreePath
bareRepoPath
projectRemotes
;
};
in
{
options.dev-env.lib = mkOption {
type = types.attrs;
internal = true;
description = "dev-env module helper functions (internal).";
};
config = {
dev-env.lib = helpers;
};
}