feat(dev-env): aiolabs dev environment — options, lib, config, presets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
02504f9fd3
commit
7960f82494
6 changed files with 1689 additions and 0 deletions
241
modules/dev-env/lib.nix
Normal file
241
modules/dev-env/lib.nix
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{ 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;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue